Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 6 additions & 3 deletions gui_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@
SimpleAnswerStep,
GetCurrentDatetimeStep,
)
from tool_schemas import get_all_tools, make_tool_choice_generate_reasoning # noqa: E402
from tool_schemas import ( # noqa: E402
get_all_tools,
make_tool_choice_generate_reasoning,
)
from sgr_agent import ( # noqa: E402
load_config,
load_prompts,
Expand Down Expand Up @@ -84,8 +87,8 @@ async def display_reasoning_step(rs: ReasoningStep) -> None:
- Enough data: {"✅" if rs.enough_data else "❌"}
- Task completed: {"✅" if rs.task_completed else "❌"}

**📝 Remaining Steps:**
{chr(10).join([f"→ {step}" for step in rs.remaining_steps]) if rs.remaining_steps else "No remaining steps"}
**📝 Next Steps:**
{chr(10).join([f"→ {step}" for step in rs.next_steps]) if rs.next_steps else "No next steps"}
"""

msg = cl.Message(
Expand Down
6 changes: 3 additions & 3 deletions models.py
Original file line number Diff line number Diff line change
Expand Up @@ -314,12 +314,12 @@ class ReasoningStep(BaseModel):
# Task completion
task_completed: bool = Field(description="Is the research task finished?")

# Remaining work
remaining_steps: Annotated[
# Planned next steps
next_steps: Annotated[
List[str],
Field(
min_length=0,
max_length=3,
description="0-3 remaining steps (empty when task completed)",
description="0-3 next steps (empty when task completed)",
),
]
6 changes: 3 additions & 3 deletions prompts.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ structured_output_reasoning:
- next_action: What should be done next (from available actions above)
- action_reasoning: WHY this action is needed now
- task_completed: Boolean completion status
- remaining_steps: What still needs to be done (0-3 items)
- next_steps: What should be done next (0-3 items)

EXAMPLE OUTPUT FORMAT:

Expand All @@ -235,7 +235,7 @@ structured_output_reasoning:
"next_action": "search",
"action_reasoning": "Нужно найти актуальную информацию по запросу",
"task_completed": false,
"remaining_steps": ["Поиск данных", "Анализ результатов"]
"next_steps": ["Поиск данных", "Анализ результатов"]
}

CONTEXT CONTINUATION REQUEST (e.g., "цены по годам" after BMW topic):
Expand All @@ -248,5 +248,5 @@ structured_output_reasoning:
"next_action": "search",
"action_reasoning": "Объединяю текущий запрос 'цены по годам' с предыдущим контекстом BMW для поиска",
"task_completed": false,
"remaining_steps": ["Поиск цен BMW по годам", "Анализ данных"]
"next_steps": ["Поиск цен BMW по годам", "Анализ данных"]
}
141 changes: 141 additions & 0 deletions settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Centralized application settings using Pydantic Settings (v2).

Loads configuration in the following precedence (highest first):
1) Environment variables
2) .env file (if present)
3) config.yaml (if present)
4) Defaults in the model

Exports:
- settings: AppSettings instance
- CONFIG: dict view of settings for legacy access (CONFIG["key"]).
"""

from __future__ import annotations

from typing import Any, Dict
import os

import yaml
from pydantic_settings import (
BaseSettings,
SettingsConfigDict,
PydanticBaseSettingsSource,
)


class YamlConfigSettingsSource(PydanticBaseSettingsSource):
"""Custom settings source that reads from config.yaml file."""

def get_field_value(self, field_info, field_name: str):
# Not used in this implementation
return None

def prepare_field_value(self, field_name: str, value, value_is_complex: bool):
return value

def __call__(self) -> Dict[str, Any]:
"""Read config from config.yaml if it exists and map to flat settings
keys."""
path = os.path.join(os.getcwd(), "config.yaml")
if not os.path.exists(path):
return {}

try:
with open(path, "r", encoding="utf-8") as f:
y = yaml.safe_load(f) or {}
except Exception:
return {}

result: Dict[str, Any] = {}

# OpenAI block
if isinstance(y.get("openai"), dict):
o = y["openai"]
if "api_key" in o:
result["openai_api_key"] = o.get("api_key")
if "base_url" in o:
result["openai_base_url"] = o.get("base_url", "")
if "model" in o:
result["openai_model"] = o.get("model", "gpt-4o-mini")
if "max_tokens" in o:
result["max_tokens"] = int(o.get("max_tokens", 6000))
if "temperature" in o:
result["temperature"] = float(o.get("temperature", 0.3))

# Tavily block
if isinstance(y.get("tavily"), dict):
t = y["tavily"]
if "api_key" in t:
result["tavily_api_key"] = t.get("api_key")

# Search block
if isinstance(y.get("search"), dict):
s = y["search"]
if "max_results" in s:
result["max_search_results"] = int(s.get("max_results", 10))

# Execution block
if isinstance(y.get("execution"), dict):
ex = y["execution"]
if "max_rounds" in ex:
result["max_rounds"] = int(ex.get("max_rounds", 8))
if "reports_dir" in ex:
result["reports_directory"] = ex.get("reports_dir", "reports")
if "max_searches_total" in ex:
result["max_searches_total"] = int(ex.get("max_searches_total", 6))

# Note: so_temperature is controlled via env (SO_TEMPERATURE) or default below
return result


class AppSettings(BaseSettings):
# OpenAI
openai_api_key: str = ""
openai_base_url: str = ""
openai_model: str = "gpt-4o-mini"
max_tokens: int = 6000
temperature: float = 0.3

# Tavily
tavily_api_key: str = ""

# Search/Execution
max_search_results: int = 10
max_rounds: int = 8
reports_directory: str = "reports"
max_searches_total: int = 6
so_temperature: float = 0.1

model_config = SettingsConfigDict(
env_file=".env",
env_prefix="",
extra="ignore",
# Allow env vars to match our uppercase names exactly
env_parse_none_str="", # treat empty strings as valid empty values
)

@classmethod
def settings_customise_sources(
cls,
settings_cls,
init_settings,
env_settings,
dotenv_settings,
file_secret_settings,
):
# Order: env > dotenv > yaml > init > file secrets
return (
env_settings,
dotenv_settings,
YamlConfigSettingsSource(settings_cls),
init_settings,
file_secret_settings,
)


# Instantiate settings and a legacy dict view for minimal code changes
settings = AppSettings()
CONFIG: Dict[str, Any] = settings.model_dump()
72 changes: 9 additions & 63 deletions sgr_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
"""

import json
import os
import yaml
import asyncio
from typing import Any, Dict, List
Expand Down Expand Up @@ -37,6 +36,7 @@
GetCurrentDatetimeStep,
)
from tool_schemas import get_all_tools, make_tool_choice_generate_reasoning
from settings import CONFIG as SETTINGS_CONFIG
from executors import get_executors


Expand All @@ -46,64 +46,8 @@


def load_config() -> Dict[str, Any]:
"""Load configuration from environment and config file."""
cfg = {
"openai_api_key": os.getenv("OPENAI_API_KEY", ""),
"openai_base_url": os.getenv("OPENAI_BASE_URL", ""),
"openai_model": os.getenv("OPENAI_MODEL", "gpt-4o-mini"),
"max_tokens": int(os.getenv("MAX_TOKENS", "6000")),
"temperature": float(os.getenv("TEMPERATURE", "0.3")),
"tavily_api_key": os.getenv("TAVILY_API_KEY", ""),
"max_search_results": int(os.getenv("MAX_SEARCH_RESULTS", "10")),
"max_rounds": int(os.getenv("MAX_ROUNDS", "8")),
"reports_directory": os.getenv("REPORTS_DIRECTORY", "reports"),
"max_searches_total": int(os.getenv("MAX_SEARCHES_TOTAL", "6")),
"so_temperature": float(os.getenv("SO_TEMPERATURE", "0.1")),
}

if os.path.exists("config.yaml"):
try:
with open("config.yaml", "r", encoding="utf-8") as f:
y = yaml.safe_load(f) or {}

# Update from YAML config
if "openai" in y:
oc = y["openai"]
cfg.update(
{
k: oc.get(k.split("_", 1)[1], v)
for k, v in cfg.items()
if k.startswith("openai_")
}
)

if "tavily" in y:
cfg["tavily_api_key"] = y["tavily"].get(
"api_key", cfg["tavily_api_key"]
)

if "search" in y:
cfg["max_search_results"] = y["search"].get(
"max_results", cfg["max_search_results"]
)

if "execution" in y:
ex = y["execution"]
cfg.update(
{
"max_rounds": ex.get("max_rounds", cfg["max_rounds"]),
"reports_directory": ex.get(
"reports_dir", cfg["reports_directory"]
),
"max_searches_total": ex.get(
"max_searches_total", cfg["max_searches_total"]
),
}
)
except Exception as e:
print(f"[yellow]Warning: could not load config.yaml: {e}[/yellow]")

return cfg
"""Legacy shim: return dict view of Pydantic settings."""
return SETTINGS_CONFIG


def load_prompts() -> Dict[str, Any]:
Expand Down Expand Up @@ -228,9 +172,11 @@ def update_global_context(
task_summary = {
"user_request": user_requests[-1], # Последний запрос пользователя
"actions_performed": [],
"files_created": [task_context.get("created_file_path")]
if task_context.get("created_file_path")
else [],
"files_created": (
[task_context.get("created_file_path")]
if task_context.get("created_file_path")
else []
),
"searches_done": task_context.get("searches_total", 0),
}

Expand Down Expand Up @@ -328,7 +274,7 @@ def pretty_print_reasoning(rs: ReasoningStep) -> None:
table.add_row("Reasoning steps", " • ".join(rs.reasoning_steps))
table.add_row("Next action", f"[bold cyan]{rs.next_action}[/bold cyan]")
table.add_row("Action reasoning", rs.action_reasoning)
table.add_row("Remaining steps", " → ".join(rs.remaining_steps))
table.add_row("Next steps", " → ".join(rs.next_steps))
table.add_row("Searches done", str(rs.searches_done))
table.add_row("Enough data", str(rs.enough_data))
table.add_row("Task completed", str(rs.task_completed))
Expand Down