Skip to content

Commit de2a854

Browse files
committed
fixes by review
1 parent ccfc970 commit de2a854

File tree

9 files changed

+189
-29
lines changed

9 files changed

+189
-29
lines changed

config.yaml.example

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,10 @@ agents:
107107
url: "https://mcp.deepwiki.com/mcp"
108108

109109
# Tools: names, or dicts with "name" and optional kwargs (e.g. search settings per tool)
110-
# Example: {"name": "web_search_tool", "max_results": 15, "max_searches": 6}
110+
# Example:
111+
# - web_search_tool:
112+
# max_results: 15
113+
# max_searches: 6
111114
tools:
112115
- "web_search_tool"
113116
- "extract_page_content_tool"

docs/ru/framework/configuration.md

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,68 @@ config = GlobalConfig.from_yaml("config.yaml")
7373

7474
**Ключевая особенность:** `AgentDefinition` наследует все параметры из `GlobalConfig` и переопределяет только те, которые указаны явно. Это позволяет создавать минималистичные конфигурации, указывая только необходимые изменения.
7575

76+
### Определения инструментов
77+
78+
Инструменты можно описывать в отдельной секции `tools:` в `config.yaml` или `agents.yaml`. Это позволяет:
79+
80+
- Задавать кастомные инструменты с нужными параметрами
81+
- Ссылаться на инструменты по имени в определениях агентов
82+
- Переопределять класс инструмента по умолчанию
83+
84+
**Формат определения инструмента:**
85+
86+
В глобальной секции `tools:` каждая запись может содержать:
87+
88+
- **base_class** (необязательно) — путь импорта или имя класса в реестре
89+
- **Любые другие ключи** — передаются в инструмент при вызове как kwargs (например, `max_results`, `max_searches`, `content_limit` для поисковых инструментов). Агенты, использующие инструмент по имени, получают эти параметры; конфиг в списке `tools` агента переопределяет глобальные значения.
90+
91+
```yaml
92+
tools:
93+
# Простое определение (base_class по умолчанию из ToolRegistry)
94+
reasoning_tool:
95+
# base_class по умолчанию: sgr_agent_core.tools.ReasoningTool
96+
97+
# Кастомный инструмент с явным base_class
98+
custom_tool:
99+
base_class: "tools.CustomTool" # Относительный путь
100+
101+
# Глобальные значения по умолчанию (все агенты с этим инструментом получат эти kwargs)
102+
web_search_tool:
103+
max_results: 12
104+
max_searches: 6
105+
```
106+
107+
**Использование инструментов в агентах:**
108+
109+
Каждый элемент списка `tools` может быть:
110+
111+
- **Строка** — имя инструмента (резолвится из секции `tools:` или `ToolRegistry`)
112+
- **Объект** — словарь с обязательным полем `"name"` и необязательными параметрами, передаваемыми в инструмент при вызове (например, настройки поиска)
113+
114+
```yaml
115+
agents:
116+
my_agent:
117+
base_class: "SGRToolCallingAgent"
118+
tools:
119+
- "web_search_tool"
120+
- "reasoning_tool"
121+
# Конфиг по инструменту: name + kwargs (например, настройки поиска)
122+
- name: "extract_page_content_tool"
123+
content_limit: 2000
124+
- name: "web_search_tool"
125+
max_results: 15
126+
max_searches: 6
127+
# tavily_api_key, max_searches и т.д. можно задать здесь вместо глобального search:
128+
```
129+
130+
Настройки поиска (`tavily_api_key`, `tavily_api_base_url`, `max_results`, `content_limit`, `max_searches`) можно задавать глобально в `search:` или в объекте инструмента. kwargs инструмента переопределяют агентский `search` для этого инструмента.
131+
132+
!!! note "Порядок резолва инструментов"
133+
При резолве инструментов система проверяет в порядке:
134+
1. Инструменты из секции `tools:` (по имени)
135+
2. Инструменты из `ToolRegistry` (по имени в snake_case — рекомендуется, или по имени класса в PascalCase для совместимости)
136+
3. Автопреобразование snake_case → PascalCase (например, `web_search_tool` → `WebSearchTool`) для совместимости
137+
76138
### Примеры конфигурации агентов
77139

78140
Агенты определяются в файле `agents.yaml` или могут быть загружены программно:
@@ -238,6 +300,31 @@ agents:
238300
- "final_answer_tool"
239301
```
240302

303+
#### Пример 5: С определениями инструментов
304+
305+
Агент, использующий определения инструментов из секции `tools`:
306+
307+
```yaml
308+
# Определяем инструменты в секции tools
309+
tools:
310+
reasoning_tool:
311+
# По умолчанию: sgr_agent_core.tools.ReasoningTool
312+
custom_file_tool:
313+
base_class: "tools.CustomFileTool" # Кастомный инструмент из локального модуля
314+
315+
agents:
316+
file_agent:
317+
base_class: "SGRToolCallingAgent"
318+
319+
llm:
320+
model: "gpt-4o-mini"
321+
322+
# Ссылаемся на инструменты по имени из секции tools или ToolRegistry
323+
tools:
324+
- "reasoning_tool" # Из секции tools
325+
- "custom_file_tool" # Из секции tools
326+
- "final_answer_tool" # Из ToolRegistry
327+
```
241328

242329
## Рекомендации
243330

@@ -246,5 +333,5 @@ agents:
246333
В production окружении рекомендуется использовать ENV переменные вместо хардкода ключей в YAML
247334

248335
- **Используйте минимальные переопределения** - указывайте только то, что отличается от GlobalConfig
249-
- **Храните Definitions, a не Agents** - Агенты создаются под непосредственное выполнение задачи,
336+
- **Храните Definitions, а не Agents** - Агенты создаются под непосредственное выполнение задачи,
250337
их definitions можно добавлять/удалять/изменять в любое время

examples/sgr_deep_research/agents.py

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,8 @@ async def _prepare_tools(self) -> Type[NextStepToolStub]:
6161
tools -= {
6262
ClarificationTool,
6363
}
64-
max_searches = self.tool_configs.get(WebSearchTool.tool_name, {}).get("max_searches") or (
65-
self.config.search.max_searches if self.config.search else 4
66-
)
67-
if self._context.searches_used >= max_searches:
64+
search_config = self.get_tool_config(WebSearchTool)
65+
if self._context.searches_used >= search_config.max_searches:
6866
tools -= {
6967
WebSearchTool,
7068
}
@@ -105,10 +103,8 @@ async def _prepare_tools(self) -> list[ChatCompletionFunctionToolParam]:
105103
tools -= {
106104
ClarificationTool,
107105
}
108-
max_searches = self.tool_configs.get(WebSearchTool.tool_name, {}).get("max_searches") or (
109-
self.config.search.max_searches if self.config.search else 4
110-
)
111-
if self._context.searches_used >= max_searches:
106+
search_config = self.get_tool_config(WebSearchTool)
107+
if self._context.searches_used >= search_config.max_searches:
112108
tools -= {
113109
WebSearchTool,
114110
}
@@ -150,10 +146,8 @@ async def _prepare_tools(self) -> list[ChatCompletionFunctionToolParam]:
150146
tools -= {
151147
ClarificationTool,
152148
}
153-
max_searches = self.tool_configs.get(WebSearchTool.tool_name, {}).get("max_searches") or (
154-
self.config.search.max_searches if self.config.search else 4
155-
)
156-
if self._context.searches_used >= max_searches:
149+
search_config = self.get_tool_config(WebSearchTool)
150+
if self._context.searches_used >= search_config.max_searches:
157151
tools -= {
158152
WebSearchTool,
159153
}

examples/sgr_deep_research_without_reporting/agents.py

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,8 @@ async def _prepare_tools(self) -> Type[NextStepToolStub]:
6161
tools -= {
6262
ClarificationTool,
6363
}
64-
max_searches = self.tool_configs.get(WebSearchTool.tool_name, {}).get("max_searches") or (
65-
self.config.search.max_searches if self.config.search else 4
66-
)
67-
if self._context.searches_used >= max_searches:
64+
search_config = self.get_tool_config(WebSearchTool)
65+
if self._context.searches_used >= search_config.max_searches:
6866
tools -= {
6967
WebSearchTool,
7068
}
@@ -106,10 +104,8 @@ async def _prepare_tools(self) -> list[ChatCompletionFunctionToolParam]:
106104
tools -= {
107105
ClarificationTool,
108106
}
109-
max_searches = self.tool_configs.get(WebSearchTool.tool_name, {}).get("max_searches") or (
110-
self.config.search.max_searches if self.config.search else 4
111-
)
112-
if self._context.searches_used >= max_searches:
107+
search_config = self.get_tool_config(WebSearchTool)
108+
if self._context.searches_used >= search_config.max_searches:
113109
tools -= {
114110
WebSearchTool,
115111
}
@@ -152,10 +148,8 @@ async def _prepare_tools(self) -> list[ChatCompletionFunctionToolParam]:
152148
tools -= {
153149
ClarificationTool,
154150
}
155-
max_searches = self.tool_configs.get(WebSearchTool.tool_name, {}).get("max_searches") or (
156-
self.config.search.max_searches if self.config.search else 4
157-
)
158-
if self._context.searches_used >= max_searches:
151+
search_config = self.get_tool_config(WebSearchTool)
152+
if self._context.searches_used >= search_config.max_searches:
159153
tools -= {
160154
WebSearchTool,
161155
}

sgr_agent_core/base_agent.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@
55
import traceback
66
import uuid
77
from datetime import datetime
8-
from typing import Type
8+
from typing import Any, Type
99

1010
from openai import AsyncOpenAI, pydantic_function_tool
1111
from openai.types.chat import ChatCompletionFunctionToolParam, ChatCompletionMessageParam
12+
from pydantic import BaseModel
1213

1314
from sgr_agent_core.agent_definition import AgentConfig
1415
from sgr_agent_core.models import AgentContext, AgentStatesEnum
@@ -20,6 +21,7 @@
2021
ClarificationTool,
2122
ReasoningTool,
2223
)
24+
from sgr_agent_core.utils import config_from_kwargs
2325

2426

2527
class AgentRegistryMixin:
@@ -62,6 +64,22 @@ def __init__(
6264

6365
self._execute_task: asyncio.Task | None = None
6466

67+
def get_tool_config(self, tool_class: Type[BaseTool]) -> BaseModel | dict[str, Any]:
68+
"""Return resolved config for a tool as a Pydantic model or raw dict.
69+
70+
If the tool defines config_model (and optionally
71+
base_config_attr), merges agent-level base config with
72+
tool_configs and returns a validated instance. Otherwise returns
73+
the raw dict from tool_configs.
74+
"""
75+
raw = self.tool_configs.get(tool_class.tool_name, {})
76+
config_model = getattr(tool_class, "config_model", None)
77+
if config_model is None:
78+
return raw
79+
base_attr = getattr(tool_class, "base_config_attr", None)
80+
base = getattr(self.config, base_attr, None) if base_attr and self.config else None
81+
return config_from_kwargs(config_model, base, raw)
82+
6583
async def provide_clarification(self, messages: list[ChatCompletionMessageParam]):
6684
"""Receive clarification from an external source (e.g. user input) in
6785
OpenAI messages format."""

sgr_agent_core/base_tool.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
from sgr_agent_core.agent_definition import AgentConfig
1515
from sgr_agent_core.models import AgentContext
1616

17-
1817
logger = logging.getLogger(__name__)
1918

2019

@@ -30,6 +29,10 @@ class BaseTool(BaseModel, ToolRegistryMixin):
3029

3130
tool_name: ClassVar[str] = None
3231
description: ClassVar[str] = None
32+
# Optional: Pydantic model for this tool's config; agent.get_tool_config(tool_class) returns it
33+
config_model: ClassVar[type[BaseModel] | None] = None
34+
# If set, agent config attribute to merge as base (e.g. "search") when resolving tool config
35+
base_config_attr: ClassVar[str | None] = None
3336

3437
async def __call__(self, context: AgentContext, config: AgentConfig, **kwargs) -> str:
3538
"""The result should be a string or dumped JSON."""

sgr_agent_core/tools/extract_page_content_tool.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ class ExtractPageContentTool(BaseTool):
3434
- For date/number questions, cross-check extracted values with search snippets
3535
"""
3636

37+
config_model = SearchConfig
38+
base_config_attr = "search"
39+
3740
reasoning: str = Field(description="Why extract these specific pages")
3841
urls: list[str] = Field(description="List of URLs to extract full content from", min_length=1, max_length=5)
3942

sgr_agent_core/tools/web_search_tool.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ class WebSearchTool(BaseTool):
4444
- If the snippet directly answers the question, you may not need to extract the full page
4545
"""
4646

47+
config_model = SearchConfig
48+
base_config_attr = "search"
49+
4750
reasoning: str = Field(description="Why this search is needed and what to expect")
4851
query: str = Field(description="Search query in same language as user request")
4952
max_results: int = Field(

tests/test_base_agent.py

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@
1111

1212
import pytest
1313

14+
from sgr_agent_core.agent_definition import AgentConfig, ExecutionConfig, LLMConfig, PromptsConfig, SearchConfig
1415
from sgr_agent_core.base_agent import BaseAgent
1516
from sgr_agent_core.models import AgentContext, AgentStatesEnum
16-
from sgr_agent_core.tools import BaseTool, ReasoningTool
17+
from sgr_agent_core.tools import BaseTool, ReasoningTool, WebSearchTool
1718
from tests.conftest import create_test_agent
1819

1920

@@ -331,6 +332,60 @@ class TestToolWithEnum(BaseTool):
331332
assert tool_context["status"] == "done"
332333

333334

335+
class TestBaseAgentGetToolConfig:
336+
"""Tests for get_tool_config resolution."""
337+
338+
def test_get_tool_config_returns_model_when_tool_has_config_model(self):
339+
"""get_tool_config returns Pydantic model instance for tools with
340+
config_model."""
341+
agent = create_test_agent(
342+
BaseAgent,
343+
task_messages=[{"role": "user", "content": "Test"}],
344+
toolkit=[WebSearchTool],
345+
)
346+
agent.tool_configs = {"websearchtool": {"max_searches": 6}}
347+
agent.config = AgentConfig(
348+
llm=LLMConfig(api_key="k", base_url="https://api.openai.com/v1"),
349+
prompts=PromptsConfig(system_prompt_str="p", initial_user_request_str="p", clarification_response_str="p"),
350+
execution=ExecutionConfig(),
351+
search=None,
352+
)
353+
out = agent.get_tool_config(WebSearchTool)
354+
assert isinstance(out, SearchConfig)
355+
assert out.max_searches == 6
356+
357+
def test_get_tool_config_merges_base_from_agent_config(self):
358+
"""get_tool_config merges base from base_config_attr (e.g.
359+
config.search)."""
360+
agent = create_test_agent(
361+
BaseAgent,
362+
task_messages=[{"role": "user", "content": "Test"}],
363+
toolkit=[WebSearchTool],
364+
)
365+
agent.tool_configs = {"websearchtool": {}}
366+
agent.config = AgentConfig(
367+
llm=LLMConfig(api_key="k", base_url="https://api.openai.com/v1"),
368+
prompts=PromptsConfig(system_prompt_str="p", initial_user_request_str="p", clarification_response_str="p"),
369+
execution=ExecutionConfig(),
370+
search=SearchConfig(tavily_api_key="key", max_searches=10),
371+
)
372+
out = agent.get_tool_config(WebSearchTool)
373+
assert isinstance(out, SearchConfig)
374+
assert out.max_searches == 10
375+
assert out.tavily_api_key == "key"
376+
377+
def test_get_tool_config_returns_dict_when_tool_has_no_config_model(self):
378+
"""get_tool_config returns raw dict when tool has no config_model."""
379+
agent = create_test_agent(
380+
BaseAgent,
381+
task_messages=[{"role": "user", "content": "Test"}],
382+
toolkit=[ReasoningTool],
383+
)
384+
agent.tool_configs = {"reasoningtool": {"foo": "bar"}}
385+
out = agent.get_tool_config(ReasoningTool)
386+
assert out == {"foo": "bar"}
387+
388+
334389
class TestBaseAgentAbstractMethods:
335390
"""Tests for abstract methods that must be implemented by subclasses."""
336391

0 commit comments

Comments
 (0)