Skip to content
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
f9d6fb4
Refactor agent structure and add tool-calling agents
virrius Sep 10, 2025
78926b1
Refactor agent and tool architecture, remove legacy schemas
virrius Sep 11, 2025
58d7eb4
agent class docstrings
virrius Sep 11, 2025
3a26e69
Rename ResearchCompletionTool to CompletionTool
virrius Sep 12, 2025
2cf7b85
Refactor agent architecture and tool handling
virrius Sep 12, 2025
190c664
Update sgr_tools_agent.py
virrius Sep 12, 2025
40008c3
Refactor agent imports and update __init__ modules
virrius Sep 12, 2025
12328f7
Add agent model selection and model listing endpoint
virrius Sep 12, 2025
6db5030
Remove debug print from agent state loop
virrius Sep 12, 2025
5e1b227
linting fixes
virrius Sep 12, 2025
94149ba
Add SGRSOToolCallingResearchAgent
virrius Sep 12, 2025
279569f
Update sgr_so_tools_agent.py
virrius Sep 12, 2025
a041fdc
Update Docker volumes and remove agent test code
virrius Sep 12, 2025
f66f5cf
Update docstrings and agent ID naming for clarity
virrius Sep 12, 2025
1d3803d
Rename agent.py to base_agent.py and update imports
virrius Sep 13, 2025
ff6ab7e
Update sgr_deep_research/api/models.py
virrius Sep 13, 2025
9296545
Update sgr_deep_research/core/agents/tools_agent.py
virrius Sep 13, 2025
f1b9559
Update sgr_deep_research/core/agents/base_agent.py
virrius Sep 13, 2025
90fed3e
Translate comments and fields to English
virrius Sep 13, 2025
72f7b37
Refactor tool imports for consistency and clarity
virrius Sep 13, 2025
abbdcc1
Update endpoints.py
virrius Sep 13, 2025
3a0f9a8
Update model name in README examples
virrius Sep 13, 2025
edd8d00
Update docker-compose.yml
virrius Sep 13, 2025
6d64224
Note about /models added
EvilFreelancer Sep 13, 2025
8f22ec9
Table of agents added
EvilFreelancer Sep 13, 2025
96648c2
Tunes
EvilFreelancer Sep 13, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ client = OpenAI(

# Make research request
response = client.chat.completions.create(
model="sgr-research",
model="sgr-agent",
messages=[{"role": "user", "content": "Research BMW X6 2025 prices in Russia"}],
stream=True,
temperature=0.4,
Expand All @@ -119,7 +119,7 @@ client = OpenAI(base_url="http://localhost:8010/v1", api_key="dummy")
# Step 1: Initial research request
print("Starting research...")
response = client.chat.completions.create(
model="sgr-research",
model="sgr-agent",
messages=[{"role": "user", "content": "Research AI market trends"}],
stream=True,
temperature=0,
Expand Down Expand Up @@ -195,7 +195,7 @@ The system provides a fully OpenAI-compatible API with advanced agent interrupti
curl -X POST "http://localhost:8010/v1/chat/completions" \
-H "Content-Type: application/json" \
-d '{
"model": "sgr-research",
"model": "sgr-agent",
"messages": [{"role": "user", "content": "Research BMW X6 2025 prices in Russia"}],
"stream": true,
"max_tokens": 1500,
Expand All @@ -213,7 +213,7 @@ When the agent needs clarification, it returns a unique agent ID in the streamin
curl -X POST "http://localhost:8010/v1/chat/completions" \
-H "Content-Type: application/json" \
-d '{
"model": "sgr-research",
"model": "sgr-agent",
"messages": [{"role": "user", "content": "Research AI market trends"}],
"stream": true,
"max_tokens": 1500,
Expand Down Expand Up @@ -293,7 +293,7 @@ sequenceDiagram

Note over Client, Tools: SGR Deep Research - Agent Workflow

Client->>API: POST /v1/chat/completions<br/>{"model": "sgr-research", "messages": [...]}
Client->>API: POST /v1/chat/completions<br/>{"model": "sgr-agent", "messages": [...]}

API->>Agent: Create new SGR Agent<br/>with unique ID
Note over Agent: State: INITED
Expand Down
2 changes: 2 additions & 0 deletions config.yaml.example
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ openai:
# Tavily Search Configuration
tavily:
api_key: "your-tavily-api-key-here" # Required: Your Tavily API key
api_base_url: "https://api.tavily.com" # Tavily API base URL

# Search Settings
search:
Expand All @@ -29,6 +30,7 @@ scraping:
execution:
max_steps: 6 # Maximum number of execution steps
reports_dir: "reports" # Directory for saving reports
logs_dir: "logs" # Directory for saving reports

# Prompts Settings
prompts:
Expand Down
2 changes: 2 additions & 0 deletions services/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ services:
volumes:
- ../sgr_deep_research:/app/sgr_deep_research:ro
- ../config.yaml:/app/config.yaml:ro
- ./logs:/app/logs
- ./reports:/app/reports

healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8010/health"]
Expand Down
61 changes: 53 additions & 8 deletions sgr_deep_research/api/endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,23 @@
from fastapi.responses import StreamingResponse

from sgr_deep_research.api.models import (
AGENT_MODEL_MAPPING,
AgentListItem,
AgentListResponse,
AgentModel,
AgentStateResponse,
ChatCompletionRequest,
HealthResponse,
)
from sgr_deep_research.core.agent import SGRResearchAgent
from sgr_deep_research.core.agents import BaseAgent
from sgr_deep_research.core.models import AgentStatesEnum

logger = logging.getLogger(__name__)

app = FastAPI(title="SGR Deep Research API", version="1.0.0")

# ToDo: better to move to a separate service
agents_storage: dict[str, SGRResearchAgent] = {}
agents_storage: dict[str, BaseAgent] = {}


@app.get("/health", response_model=HealthResponse)
Expand All @@ -35,8 +37,8 @@ async def get_agent_state(agent_id: str):
agent = agents_storage[agent_id]

current_state_dict = None
if agent._context.current_state:
current_state_dict = agent._context.current_state.model_dump()
if agent._context.current_state_reasoning:
current_state_dict = agent._context.current_state_reasoning.model_dump()

return AgentStateResponse(
agent_id=agent.id,
Expand All @@ -58,6 +60,18 @@ async def get_agents_list():
return AgentListResponse(agents=agents_list, total=len(agents_list))


@app.get("/v1/models")
async def get_available_models():
"""Get list of available agent models."""
return {
"data": [
{"id": model.value, "object": "model", "created": 1234567890, "owned_by": "sgr-deep-research"}
for model in AgentModel
],
"object": "list",
}


def extract_user_content_from_messages(messages):
for message in reversed(messages):
if message.role == "user":
Expand Down Expand Up @@ -96,22 +110,52 @@ async def provide_clarification(agent_id: str, request: ChatCompletionRequest):
raise HTTPException(status_code=500, detail="str(e)")


def _is_agent_id(model_str: str) -> bool:
"""Check if model string is an agent ID (contains underscore and UUID-like
format)."""
return "_" in model_str and len(model_str) > 20


@app.post("/v1/chat/completions")
async def create_chat_completion(request: ChatCompletionRequest):
if not request.stream:
raise HTTPException(status_code=501, detail="Only streaming responses are supported. Set 'stream=true'")

# Check if this is a clarification request for an existing agent
if (
request.model
and isinstance(request.model, str)
and _is_agent_id(request.model)
and request.model in agents_storage
and agents_storage[request.model].state == AgentStatesEnum.WAITING_FOR_CLARIFICATION
and agents_storage[request.model]._context.state == AgentStatesEnum.WAITING_FOR_CLARIFICATION
):
return await provide_clarification(request.model, request)

try:
task = extract_user_content_from_messages(request.messages)
agent = SGRResearchAgent(task=task)

# Determine agent model type
agent_model = request.model
if isinstance(agent_model, str) and _is_agent_id(agent_model):
# If it's an agent ID but not found in storage, use default
agent_model = AgentModel.SGR_AGENT
elif agent_model is None:
agent_model = AgentModel.SGR_AGENT
elif isinstance(agent_model, str):
# Try to convert string to AgentModel enum
try:
agent_model = AgentModel(agent_model)
except ValueError:
raise HTTPException(
status_code=400,
detail=f"Invalid model '{agent_model}'. Available models: {[m.value for m in AgentModel]}",
)

# Create agent using mapping
agent_class = AGENT_MODEL_MAPPING[agent_model]
agent = agent_class(task=task)
agents_storage[agent.id] = agent
logger.info(f"Agent {agent.id} created and stored for task: {task[:100]}...")
logger.info(f"Agent {agent.id} ({agent_model.value}) created and stored for task: {task[:100]}...")

_ = asyncio.create_task(agent.execute())
return StreamingResponse(
Expand All @@ -121,11 +165,12 @@ async def create_chat_completion(request: ChatCompletionRequest):
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"X-Agent-ID": str(agent.id),
"X-Agent-Model": agent_model.value,
},
)

except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error(f"Error completion: {e}")
raise HTTPException(status_code=500, detail="str(e)")
raise HTTPException(status_code=500, detail=str(e))
97 changes: 64 additions & 33 deletions sgr_deep_research/api/models.py
Original file line number Diff line number Diff line change
@@ -1,69 +1,100 @@
"""OpenAI-совместимые модели для API endpoints."""
"""OpenAI-compatible models for API endpoints."""

from enum import Enum
from typing import Any, Dict, List, Literal

from pydantic import BaseModel, Field

from sgr_deep_research.core.agents import (
SGRAutoToolCallingResearchAgent,
SGRResearchAgent,
SGRSOToolCallingResearchAgent,
SGRToolCallingResearchAgent,
ToolCallingResearchAgent,
)


class AgentModel(str, Enum):
"""Available agent models for chat completion."""

SGR_AGENT = "sgr-agent"
SGR_TOOLS_AGENT = "sgr-tools-agent"
SGR_AUTO_TOOLS_AGENT = "sgr-auto-tools-agent"
SGR_SO_TOOLS_AGENT = "sgr-so-tools-agent"
TOOLS_AGENT = "tools-agent"


# Mapping of agent types to their classes
AGENT_MODEL_MAPPING = {
AgentModel.SGR_AGENT: SGRResearchAgent,
AgentModel.SGR_TOOLS_AGENT: SGRToolCallingResearchAgent,
AgentModel.SGR_AUTO_TOOLS_AGENT: SGRAutoToolCallingResearchAgent,
AgentModel.SGR_SO_TOOLS_AGENT: SGRSOToolCallingResearchAgent,
AgentModel.TOOLS_AGENT: ToolCallingResearchAgent,
}


class ChatMessage(BaseModel):
"""Сообщение в чате."""
"""Chat message."""

role: Literal["system", "user", "assistant", "tool"] = Field(default="user", description="Роль отправителя")
content: str = Field(description="Содержимое сообщения")
role: Literal["system", "user", "assistant", "tool"] = Field(default="user", description="Sender role")
content: str = Field(description="Message content")


class ChatCompletionRequest(BaseModel):
"""Запрос на создание chat completion."""
"""Request for creating chat completion."""

model: str | None = Field(
default=None, description="Идентификатор агента", example="sgr_agent_35702b10-4d4e-426f-9b33-b170032e37df"
default=AgentModel.SGR_AGENT,
description="Agent type or existing agent identifier",
example="sgr-agent",
)
messages: List[ChatMessage] = Field(description="Список сообщений")
stream: bool = Field(default=True, description="Включить потоковый режим")
max_tokens: int | None = Field(default=1500, description="Максимальное количество токенов")
temperature: float | None = Field(default=0, description="Температура генерации")
messages: List[ChatMessage] = Field(description="List of messages")
stream: bool = Field(default=True, description="Enable streaming mode")
max_tokens: int | None = Field(default=1500, description="Maximum number of tokens")
temperature: float | None = Field(default=0, description="Generation temperature")


class ChatCompletionChoice(BaseModel):
"""Выбор в ответе chat completion."""
"""Choice in chat completion response."""

index: int = Field(description="Индекс выбора")
message: ChatMessage = Field(description="Сообщение ответа")
finish_reason: str | None = Field(description="Причина завершения")
index: int = Field(description="Choice index")
message: ChatMessage = Field(description="Response message")
finish_reason: str | None = Field(description="Finish reason")


class ChatCompletionResponse(BaseModel):
"""Ответ chat completion (не потоковый)."""
"""Chat completion response (non-streaming)."""

id: str = Field(description="ID ответа")
id: str = Field(description="Response ID")
object: Literal["chat.completion"] = "chat.completion"
created: int = Field(description="Время создания")
model: str = Field(description="Использованная модель")
choices: List[ChatCompletionChoice] = Field(description="Список выборов")
usage: Dict[str, int] | None = Field(default=None, description="Информация об использовании")
created: int = Field(description="Creation time")
model: str = Field(description="Model used")
choices: List[ChatCompletionChoice] = Field(description="List of choices")
usage: Dict[str, int] | None = Field(default=None, description="Usage information")


class HealthResponse(BaseModel):
status: Literal["healthy"] = "healthy"
service: str = Field(default="SGR Deep Research API", description="Название сервиса")
service: str = Field(default="SGR Deep Research API", description="Service name")


class AgentStateResponse(BaseModel):
agent_id: str = Field(description="ID агента")
task: str = Field(description="Задача агента")
state: str = Field(description="Текущее состояние агента")
searches_used: int = Field(description="Количество выполненных поисков")
clarifications_used: int = Field(description="Количество запрошенных уточнений")
sources_count: int = Field(description="Количество найденных источников")
current_state: Dict[str, Any] | None = Field(default=None, description="Текущий шаг агента")
agent_id: str = Field(description="Agent ID")
task: str = Field(description="Agent task")
state: str = Field(description="Current agent state")
searches_used: int = Field(description="Number of searches performed")
clarifications_used: int = Field(description="Number of clarifications requested")
sources_count: int = Field(description="Number of sources found")
current_state: Dict[str, Any] | None = Field(default=None, description="Current agent step")


class AgentListItem(BaseModel):
agent_id: str = Field(description="ID агента")
task: str = Field(description="Задача агента")
state: str = Field(description="Текущее состояние агента")
agent_id: str = Field(description="Agent ID")
task: str = Field(description="Agent task")
state: str = Field(description="Current agent state")


class AgentListResponse(BaseModel):
agents: List[AgentListItem] = Field(description="Список агентов")
total: int = Field(description="Общее количество агентов")
agents: List[AgentListItem] = Field(description="List of agents")
total: int = Field(description="Total number of agents")
18 changes: 16 additions & 2 deletions sgr_deep_research/core/__init__.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,32 @@
"""Core modules for SGR Deep Research."""

from sgr_deep_research.core.agent import SGRResearchAgent
from sgr_deep_research.core.agents import ( # noqa: F403
BaseAgent,
SGRAutoToolCallingResearchAgent,
SGRResearchAgent,
SGRSOToolCallingResearchAgent,
SGRToolCallingResearchAgent,
ToolCallingResearchAgent,
)
from sgr_deep_research.core.models import AgentStatesEnum, ResearchContext, SearchResult, SourceData
from sgr_deep_research.core.prompts import PromptLoader
from sgr_deep_research.core.reasoning_schemas import * # noqa: F403
from sgr_deep_research.core.stream import OpenAIStreamingGenerator
from sgr_deep_research.core.tools import * # noqa: F403

__all__ = [
# Agents
"BaseAgent",
"SGRResearchAgent",
"SGRToolCallingResearchAgent",
"SGRAutoToolCallingResearchAgent",
"ToolCallingResearchAgent",
"SGRSOToolCallingResearchAgent",
# Models
"AgentStatesEnum",
"ResearchContext",
"SearchResult",
"SourceData",
# Other core modules
"PromptLoader",
"OpenAIStreamingGenerator",
]
Loading