Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
51 changes: 44 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ https://github.com/user-attachments/assets/a5e34116-7853-43c2-ba93-2db811b8584a

Production-ready open-source system for automated research using Schema-Guided Reasoning (SGR). Features real-time streaming responses, OpenAI-compatible API, and comprehensive research capabilities with agent interruption support.

## 📊 Summary Table of Agents

| Agent | SGR Implementation | ReasoningTool | Tools | API Requests | Selection Mechanism |
| ----------------------- | ------------------ | -------------------- | --------------------- | ------------ | ------------------- |
| **1. SGR-Agent** | Structured Output | ❌ Built into schema | 6 basic | 1 | SO Union Type |
| **2. FCAgent** | ❌ Absent | ❌ Absent | 6 basic | 1 | FC "required" |
| **3. HybridSGRAgent** | FC Tool enforced | ✅ First step FC | 7 (6 + ReasoningTool) | 2 | FC → FC |
| **4. OptionalSGRAgent** | FC Tool optional | ✅ At model’s choice | 7 (6 + ReasoningTool) | 1–2 | FC "auto" |
| **5. ReasoningFC_SO** | FC → SO → FC auto | ✅ FC enforced | 7 (6 + ReasoningTool) | 3 | FC → SO → FC auto |

## 👥 Open-Source Development Team

This project is built by the community with pure enthusiasm as an open-source initiative:
Expand Down Expand Up @@ -94,7 +104,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 +129,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 +205,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 +223,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 +303,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 Expand Up @@ -453,17 +463,23 @@ cp config.yaml.example config.yaml
2. **Configure API keys:**

```yaml
# SGR Research Agent - Configuration Template
# Production-ready configuration for Schema-Guided Reasoning
# Copy this file to config.yaml and fill in your API keys

# OpenAI API Configuration
openai:
api_key: "your-openai-api-key-here" # Required: Your OpenAI API key
base_url: "" # Optional: Alternative URL (e.g., for proxy LiteLLM/vLLM)
model: "gpt-4o-mini" # Model to use
max_tokens: 8000 # Maximum number of tokens
temperature: 0.4 # Generation temperature (0.0-1.0)
proxy: "" # Example: "socks5://127.0.0.1:1081" or "http://127.0.0.1:8080" or leave empty for no proxy

# 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 @@ -479,12 +495,13 @@ 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:
prompts_dir: "prompts" # Directory with prompts
prompts_dir: "prompts" # Directory with prompts
tool_function_prompt_file: "tool_function_prompt.txt" # Tool function prompt file
system_prompt_file: "system_prompt.txt" # System prompt file
system_prompt_file: "system_prompt.txt" # System prompt file
```

### Server Configuration
Expand All @@ -494,6 +511,26 @@ prompts:
python sgr_deep_research --host 127.0.0.1 --port 8080
```

## 🤖 Available Agent Models

### Agent Types Overview

| Agent Model | Description |
| ---------------------- | ---------------------------------- |
| `sgr-agent` | Pure SGR (Schema-Guided Reasoning) |
| `sgr-tools-agent` | SGR + Function Calling hybrid |
| `sgr-auto-tools-agent` | SGR + Auto Function Calling |
| `sgr-so-tools-agent` | SGR + Structured Output |
| `tools-agent` | Pure Function Calling |

### Models Endpoint

Get the list of available agent models:

```bash
curl http://localhost:8010/v1/models
```

## 📝 Reports

Research reports are automatically saved to the `reports/` directory in Markdown format:
Expand Down
6 changes: 4 additions & 2 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,9 +30,10 @@ 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:
prompts_dir: "prompts" # Directory with prompts
prompts_dir: "prompts" # Directory with prompts
tool_function_prompt_file: "tool_function_prompt.txt" # Tool function prompt file
system_prompt_file: "system_prompt.txt" # System prompt file
system_prompt_file: "system_prompt.txt" # System prompt file
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))
Loading