Last active 1791948304

ams-agent Raw
1Разобрался. Причина — в семантике @agent.tool в pydantic-ai, и в том, что AMS даёт в step один и тот же объект агента на каждой итерации.
2Почему падает
3@agent.tool → FunctionToolset.add_function() → add_tool(), а там жёсткая проверка:
4def 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
9step вызывается много раз, но 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=. Ничего не накапливается на агенте → конфликта нет по построению:
14import os
15from datetime import datetime
16from pydantic_ai import ModelSettings, RunContext, ModelRetry, FunctionToolset
17from pydantic_ai.workspaces import LocalWorkspaceBackend
18from pydantic_ai_harness.filesystem import FileSystem
19
20CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
21system_prompt = open(os.path.join(CURRENT_DIR, 'system_prompt.txt')).read()
22
23# инструмент объявлен на уровне модуля (один раз)
24async 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# создаётся ОДИН раз
42TOOLSET = FunctionToolset(tools=[read_file_lines])
43WORKSPACE = LocalWorkspaceBackend('./logs')
44
45def 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 — в процессе может жить несколько агентов):
67def _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 вызывает по ключевым словам.