Last active 1791948304

en2zmax's Avatar en2zmax revised this gist 1791948304. Go to revision

1 file changed, 77 insertions

ams-agent(file created)

@@ -0,0 +1,77 @@
1 + Разобрался. Причина — в семантике @agent.tool в pydantic-ai, и в том, что AMS даёт в step один и тот же объект агента на каждой итерации.
2 + Почему падает
3 + @agent.tool → FunctionToolset.add_function() → add_tool(), а там жёсткая проверка:
4 + def add_tool(self, tool):
5 + if tool.name in self.tools:
6 + raise UserError(f'Tool name conflicts with existing tool: {tool.name!r}')
7 + ...
8 + self.tools[tool.name] = tool
9 + step вызывается много раз, но agent переиспользуется (именно поэтому agent.history и set_system_prompt имеют смысл — история копится в одном объекте). Значит на второй итерации регистрация read_file_lines с тем же именем бросает UserError: Tool name conflicts with existing tool: 'read_file_lines' (или молча затирается — в зависимости от версии, в 2.33 именно исключение).
10 + Плюс AMS-обёртка StatefulAgent (это Cython-класс в /opt/ams/app/processors/tools/stateful_agent.so) проксирует .tool на внутренний pydantic_ai.Agent через __getattr__ — так что «re-register» бьёт прямо по тулсету внутреннего агента.
11 + Как правильно (рекомендую вариант 1)
12 + Вариант 1 — вообще не трогать агент, передавать тулсет на каждый run
13 + Тулсет создаётся один раз при импорте модуля, а в ран передаётся через toolsets=. Ничего не накапливается на агенте → конфликта нет по построению:
14 + import os
15 + from datetime import datetime
16 + from pydantic_ai import ModelSettings, RunContext, ModelRetry, FunctionToolset
17 + from pydantic_ai.workspaces import LocalWorkspaceBackend
18 + from pydantic_ai_harness.filesystem import FileSystem
19 +
20 + CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
21 + system_prompt = open(os.path.join(CURRENT_DIR, 'system_prompt.txt')).read()
22 +
23 + # инструмент объявлен на уровне модуля (один раз)
24 + async def read_file_lines(ctx: RunContext, path: str,
25 + start_line: int = 1, max_lines: int = 50) -> str:
26 + """Read a specific range of lines from a text file..."""
27 + if start_line < 1:
28 + raise ModelRetry('start_line must be 1 or greater.')
29 + try:
30 + content = await ctx.workspace.read_text(path)
31 + except FileNotFoundError:
32 + raise ModelRetry(f"File '{path}' does not exist.")
33 + lines = content.splitlines()
34 + start_idx = start_line - 1
35 + end_idx = start_idx + max_lines
36 + chunk = lines[start_idx:end_idx]
37 + if not chunk:
38 + return f"EOF: No lines found in range {start_line}-{end_idx}."
39 + return f"[Lines {start_line}-{min(end_idx, len(lines))} of {len(lines)}]:\n" + "\n".join(chunk)
40 +
41 + # создаётся ОДИН раз
42 + TOOLSET = FunctionToolset(tools=[read_file_lines])
43 + WORKSPACE = LocalWorkspaceBackend('./logs')
44 +
45 + def step(agent, state, params):
46 + dt = datetime.now().strftime("%d.%m.%Y %H:%M")
47 + agent.set_system_prompt(system_prompt)
48 +
49 + model_settings = ModelSettings(**params['sampling_params'])
50 + prompt = params['prompt']
51 + # ... ваш блок с params['file'] ...
52 +
53 + if not prompt:
54 + return
55 +
56 + with agent.run_stream_sync(
57 + prompt,
58 + model_settings=model_settings,
59 + toolsets=[TOOLSET], # <-- тул приходит на этот ран, не мутируя агента
60 + capabilities=[FileSystem()],
61 + workspace=WORKSPACE,
62 + ) as stream:
63 + for chunk in stream.stream_text(delta=True):
64 + yield {'type': 'chunk', 'data': chunk}
65 + Вариант 2 — оставить @agent.tool, но регистрировать идемпотентно
66 + Если хочется именно закрепить тул на агенте — вынесите функцию из step и защититесь флагом на объекте агента (не глобальным bool — в процессе может жить несколько агентов):
67 + def _register_tools_once(agent):
68 + if getattr(agent, "_file_tools_registered", False):
69 + return
70 + agent.tool(read_file_lines)
71 + agent._file_tools_registered = True
72 + и в начале step: _register_tools_once(agent).
73 + Отличие: вариант 1 не зависит от того, переиспользуется ли объект агента/обёртки; вариант 2 надёжен, только пока AMS передаёт тот же самый объект agent. Поэтому вариант 1 безопаснее.
74 + Ещё два момента
75 + - capabilities=[FileSystem()] уже даёт файловые тулы (read_file, edit_file, list_files, grep, с параметрами max_read_lines/max_read_chars и т.п.). Так что ваш read_file_lines может быть избыточен — если нужен именно построчный диапазон + ModelRetry, оставляйте, но дублирующиеся имена (read_file) не создавайте.
76 + - Проверьте окружение на сервере. На 192.168.102.120 в вене AMS стоит pydantic_ai 2.33.0, а в нём нет pydantic_ai.workspaces, не установлен pydantic_ai_harness, и параметра workspace= у run_stream_sync нет (есть deps, toolsets, capabilities). Актуальные версии — pydantic-ai 2.56.0 / pydantic-ai-harness 0.56.0. То есть этот handler.py на текущем серверном AMS не заимпортится — нужен либо апгрейд/установка harness, либо адаптация под 2.33 (capabilities=[FileSystem()] там есть, а workspace-механику придётся заменить).
77 + То же самое с сигнатурой: в хелпе AMS (/opt/ams/app/help/agents/index.html) контракт — step(params, state, agent), но «имена аргументов являются частью контракта», поэтому ваш порядок (agent, state, params) сработает, если AMS вызывает по ключевым словам.
Newer Older