agstack 2.1.0__tar.gz → 2.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {agstack-2.1.0 → agstack-2.2.0}/PKG-INFO +1 -1
- {agstack-2.1.0 → agstack-2.2.0}/agstack/llm/flow/agent.py +118 -84
- {agstack-2.1.0 → agstack-2.2.0}/agstack.egg-info/PKG-INFO +1 -1
- {agstack-2.1.0 → agstack-2.2.0}/agstack.egg-info/SOURCES.txt +1 -0
- {agstack-2.1.0 → agstack-2.2.0}/pyproject.toml +1 -1
- agstack-2.2.0/tests/test_agent_request_overrides.py +50 -0
- {agstack-2.1.0 → agstack-2.2.0}/LICENSE +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/README.md +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/agstack/__init__.py +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/agstack/cache/__init__.py +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/agstack/cache/base.py +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/agstack/cache/memory.py +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/agstack/cache/redis.py +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/agstack/config/__init__.py +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/agstack/config/logger.py +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/agstack/config/manager.py +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/agstack/config/types.py +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/agstack/contexts.py +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/agstack/decorators.py +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/agstack/events.py +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/agstack/exceptions.py +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/agstack/fastapi/__init__.py +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/agstack/fastapi/exception.py +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/agstack/fastapi/middleware.py +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/agstack/fastapi/offline.py +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/agstack/fastapi/sse.py +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/agstack/infra/db/__init__.py +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/agstack/infra/es/__init__.py +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/agstack/infra/kg/__init__.py +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/agstack/infra/mq/__init__.py +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/agstack/llm/__init__.py +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/agstack/llm/client.py +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/agstack/llm/flow/__init__.py +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/agstack/llm/flow/context.py +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/agstack/llm/flow/event.py +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/agstack/llm/flow/exceptions.py +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/agstack/llm/flow/factory.py +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/agstack/llm/flow/flow.py +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/agstack/llm/flow/loader.py +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/agstack/llm/flow/nodes/__init__.py +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/agstack/llm/flow/nodes/agent_node.py +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/agstack/llm/flow/nodes/base.py +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/agstack/llm/flow/nodes/detect_node.py +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/agstack/llm/flow/nodes/echo_node.py +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/agstack/llm/flow/nodes/iterator_node.py +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/agstack/llm/flow/nodes/llm_chat_node.py +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/agstack/llm/flow/nodes/llm_embed_node.py +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/agstack/llm/flow/nodes/llm_rerank_node.py +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/agstack/llm/flow/nodes/python_node.py +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/agstack/llm/flow/nodes/subflow_node.py +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/agstack/llm/flow/nodes/switch_node.py +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/agstack/llm/flow/nodes/tool_node.py +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/agstack/llm/flow/records.py +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/agstack/llm/flow/registry.py +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/agstack/llm/flow/sandbox.py +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/agstack/llm/flow/state.py +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/agstack/llm/flow/tool.py +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/agstack/llm/flow/trace.py +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/agstack/llm/prompts.py +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/agstack/llm/token.py +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/agstack/messagebus/__init__.py +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/agstack/messagebus/base.py +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/agstack/messagebus/memory.py +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/agstack/messagebus/redis.py +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/agstack/schema.py +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/agstack/security/__init__.py +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/agstack/security/casbin.py +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/agstack/security/crypt.py +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/agstack/status.py +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/agstack.egg-info/dependency_links.txt +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/agstack.egg-info/requires.txt +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/agstack.egg-info/top_level.txt +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/setup.cfg +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/tests/test_agent_parallel_tools.py +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/tests/test_cache_memory.py +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/tests/test_cache_redis.py +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/tests/test_flow_cancellation.py +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/tests/test_flow_error_semantics.py +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/tests/test_flow_io.py +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/tests/test_flow_iterator.py +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/tests/test_flow_switch_subflow.py +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/tests/test_llm_usage_callback.py +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/tests/test_messagebus_memory.py +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/tests/test_messagebus_redis.py +0 -0
- {agstack-2.1.0 → agstack-2.2.0}/tests/test_tool_hooks.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agstack
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: Production-ready toolkit for building FastAPI and LLM applications
|
|
5
5
|
Author-email: XtraVisions <gitadmin@xtravisions.com>, Chen Hao <chenhao@xtravisions.com>
|
|
6
6
|
Maintainer-email: XtraVisions <gitadmin@xtravisions.com>, Chen Hao <chenhao@xtravisions.com>
|
|
@@ -34,6 +34,7 @@ class Agent:
|
|
|
34
34
|
*,
|
|
35
35
|
tool_choice: str = "auto",
|
|
36
36
|
on_max_turns: str = "finalize",
|
|
37
|
+
retry_empty_response: bool = False,
|
|
37
38
|
label: str | None = None,
|
|
38
39
|
echo: bool = False,
|
|
39
40
|
):
|
|
@@ -47,6 +48,8 @@ class Agent:
|
|
|
47
48
|
:param max_tokens: 最大 token 数
|
|
48
49
|
:param max_turns: 最大轮次
|
|
49
50
|
:param on_max_turns: max_turns 耗尽时的行为,"finalize"(降级输出并标记 truncated)或 "error"(抛出异常)
|
|
51
|
+
:param retry_empty_response: 一轮既无文字也无 tool_calls 时(如推理模型把输出预算耗尽在 reasoning 上)
|
|
52
|
+
以 ``request_overrides(..., retry=True)`` 的覆盖参数重试一次
|
|
50
53
|
:param label: 面向用户的展示名称(控制 STEP 进度事件可见性)
|
|
51
54
|
:param echo: 是否转发 TEXT_MESSAGE 给用户
|
|
52
55
|
"""
|
|
@@ -59,6 +62,7 @@ class Agent:
|
|
|
59
62
|
self.max_turns = max_turns
|
|
60
63
|
self.tool_choice = tool_choice
|
|
61
64
|
self.on_max_turns = on_max_turns
|
|
65
|
+
self.retry_empty_response = retry_empty_response
|
|
62
66
|
self.label = label
|
|
63
67
|
self.echo = echo
|
|
64
68
|
|
|
@@ -70,6 +74,27 @@ class Agent:
|
|
|
70
74
|
"""获取工具 schema"""
|
|
71
75
|
return [tool.to_openai_tool() for tool in self.tools]
|
|
72
76
|
|
|
77
|
+
def request_overrides(self, context: "FlowContext", turn: int, *, retry: bool = False) -> dict[str, Any]:
|
|
78
|
+
"""按轮覆盖本次模型请求参数的钩子(子类实现,默认不覆盖)
|
|
79
|
+
|
|
80
|
+
返回值合并进 ``client.chat`` 的 kwargs:``extra_body`` 按键合并,其余键直接覆盖。
|
|
81
|
+
典型用法:决策轮 / 作答轮分别设置 ``extra_body={"enable_thinking": ...}`` 与 ``max_tokens``;
|
|
82
|
+
``retry=True`` 表示上一次请求空响应后的重试。
|
|
83
|
+
|
|
84
|
+
:param turn: 本 agent 本次运行内的轮次,从 1 起
|
|
85
|
+
"""
|
|
86
|
+
return {}
|
|
87
|
+
|
|
88
|
+
@staticmethod
|
|
89
|
+
def _apply_overrides(kwargs: dict[str, Any], overrides: dict[str, Any]) -> None:
|
|
90
|
+
for key, value in overrides.items():
|
|
91
|
+
if key == "extra_body" and isinstance(value, dict):
|
|
92
|
+
merged = dict(kwargs.get("extra_body") or {})
|
|
93
|
+
merged.update(value)
|
|
94
|
+
kwargs["extra_body"] = merged
|
|
95
|
+
else:
|
|
96
|
+
kwargs[key] = value
|
|
97
|
+
|
|
73
98
|
def get_tool_by_name(self, name: str) -> "Tool | None":
|
|
74
99
|
"""根据名称获取工具"""
|
|
75
100
|
for tool in self.tools:
|
|
@@ -265,7 +290,7 @@ class Agent:
|
|
|
265
290
|
|
|
266
291
|
# Agent 循环
|
|
267
292
|
assistant_content = ""
|
|
268
|
-
for
|
|
293
|
+
for turn in range(1, self.max_turns + 1):
|
|
269
294
|
# 协作式取消检查点:不再开始新的 LLM 轮次
|
|
270
295
|
if context.is_cancelled:
|
|
271
296
|
if not context.get_variable("_cancel_emitted"):
|
|
@@ -275,96 +300,105 @@ class Agent:
|
|
|
275
300
|
|
|
276
301
|
context.increment_turn()
|
|
277
302
|
|
|
278
|
-
#
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
303
|
+
# 调用模型;空响应(无文字无 tool_calls)且开启 retry_empty_response 时以重试覆盖参数再请求一次
|
|
304
|
+
attempt = 0
|
|
305
|
+
while True:
|
|
306
|
+
assistant_content = ""
|
|
307
|
+
tool_calls: list[dict[str, Any]] = []
|
|
308
|
+
tool_calls_buffer: dict[int, dict[str, Any]] = {}
|
|
309
|
+
|
|
310
|
+
try:
|
|
311
|
+
kwargs: dict[str, Any] = {
|
|
312
|
+
"messages": messages,
|
|
313
|
+
"model": self.model,
|
|
314
|
+
"temperature": self.temperature,
|
|
315
|
+
}
|
|
282
316
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
kwargs["max_tokens"] = self.max_tokens
|
|
292
|
-
|
|
293
|
-
if tools_schema:
|
|
294
|
-
kwargs["tools"] = tools_schema
|
|
295
|
-
kwargs["tool_choice"] = self.tool_choice
|
|
296
|
-
|
|
297
|
-
stream = await client.chat(stream=True, **kwargs)
|
|
298
|
-
|
|
299
|
-
async for chunk in stream:
|
|
300
|
-
if not chunk.choices:
|
|
301
|
-
continue
|
|
302
|
-
|
|
303
|
-
choice = chunk.choices[0]
|
|
304
|
-
delta = choice.delta
|
|
305
|
-
|
|
306
|
-
# 内容增量 - AG-UI: TEXT_MESSAGE_CONTENT
|
|
307
|
-
if delta.content:
|
|
308
|
-
assistant_content += delta.content
|
|
309
|
-
yield event.text_message_content(
|
|
310
|
-
message_id=msg_id,
|
|
311
|
-
delta=delta.content,
|
|
312
|
-
)
|
|
313
|
-
|
|
314
|
-
# 工具调用
|
|
315
|
-
if delta.tool_calls:
|
|
316
|
-
for tool_call_delta in delta.tool_calls:
|
|
317
|
-
idx = tool_call_delta.index # noqa
|
|
318
|
-
if idx not in tool_calls_buffer:
|
|
319
|
-
tool_calls_buffer[idx] = {
|
|
320
|
-
"id": tool_call_delta.id or "", # noqa
|
|
321
|
-
"name": "",
|
|
322
|
-
"arguments": "",
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
if tool_call_delta.id: # noqa
|
|
326
|
-
tool_calls_buffer[idx]["id"] = tool_call_delta.id # noqa
|
|
327
|
-
if tool_call_delta.function and tool_call_delta.function.name: # noqa
|
|
328
|
-
tool_calls_buffer[idx]["name"] = tool_call_delta.function.name # noqa
|
|
329
|
-
if tool_call_delta.function and tool_call_delta.function.arguments: # noqa
|
|
330
|
-
tool_calls_buffer[idx]["arguments"] += tool_call_delta.function.arguments # noqa
|
|
331
|
-
|
|
332
|
-
# 完成
|
|
333
|
-
if choice.finish_reason:
|
|
334
|
-
# AG-UI: 工具调用事件
|
|
335
|
-
for tool_call_data in tool_calls_buffer.values():
|
|
336
|
-
tool_calls.append(tool_call_data)
|
|
337
|
-
|
|
338
|
-
# TOOL_CALL_START
|
|
339
|
-
yield event.tool_call_start(
|
|
340
|
-
tool_call_id=tool_call_data["id"],
|
|
341
|
-
tool_call_name=tool_call_data["name"],
|
|
342
|
-
)
|
|
317
|
+
if self.max_tokens:
|
|
318
|
+
kwargs["max_tokens"] = self.max_tokens
|
|
319
|
+
|
|
320
|
+
if tools_schema:
|
|
321
|
+
kwargs["tools"] = tools_schema
|
|
322
|
+
kwargs["tool_choice"] = self.tool_choice
|
|
323
|
+
|
|
324
|
+
self._apply_overrides(kwargs, self.request_overrides(context, turn, retry=attempt > 0) or {})
|
|
343
325
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
326
|
+
stream = await client.chat(stream=True, **kwargs)
|
|
327
|
+
|
|
328
|
+
async for chunk in stream:
|
|
329
|
+
if not chunk.choices:
|
|
330
|
+
continue
|
|
331
|
+
|
|
332
|
+
choice = chunk.choices[0]
|
|
333
|
+
delta = choice.delta
|
|
334
|
+
|
|
335
|
+
# 内容增量 - AG-UI: TEXT_MESSAGE_CONTENT
|
|
336
|
+
if delta.content:
|
|
337
|
+
assistant_content += delta.content
|
|
338
|
+
yield event.text_message_content(
|
|
339
|
+
message_id=msg_id,
|
|
340
|
+
delta=delta.content,
|
|
348
341
|
)
|
|
349
342
|
|
|
350
|
-
|
|
351
|
-
|
|
343
|
+
# 工具调用
|
|
344
|
+
if delta.tool_calls:
|
|
345
|
+
for tool_call_delta in delta.tool_calls:
|
|
346
|
+
idx = tool_call_delta.index # noqa
|
|
347
|
+
if idx not in tool_calls_buffer:
|
|
348
|
+
tool_calls_buffer[idx] = {
|
|
349
|
+
"id": tool_call_delta.id or "", # noqa
|
|
350
|
+
"name": "",
|
|
351
|
+
"arguments": "",
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if tool_call_delta.id: # noqa
|
|
355
|
+
tool_calls_buffer[idx]["id"] = tool_call_delta.id # noqa
|
|
356
|
+
if tool_call_delta.function and tool_call_delta.function.name: # noqa
|
|
357
|
+
tool_calls_buffer[idx]["name"] = tool_call_delta.function.name # noqa
|
|
358
|
+
if tool_call_delta.function and tool_call_delta.function.arguments: # noqa
|
|
359
|
+
tool_calls_buffer[idx]["arguments"] += tool_call_delta.function.arguments # noqa
|
|
360
|
+
|
|
361
|
+
# 完成
|
|
362
|
+
if choice.finish_reason:
|
|
363
|
+
# AG-UI: 工具调用事件
|
|
364
|
+
for tool_call_data in tool_calls_buffer.values():
|
|
365
|
+
tool_calls.append(tool_call_data)
|
|
366
|
+
|
|
367
|
+
# TOOL_CALL_START
|
|
368
|
+
yield event.tool_call_start(
|
|
369
|
+
tool_call_id=tool_call_data["id"],
|
|
370
|
+
tool_call_name=tool_call_data["name"],
|
|
371
|
+
)
|
|
352
372
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
prompt_tokens=chunk.usage.prompt_tokens or 0,
|
|
358
|
-
completion_tokens=chunk.usage.completion_tokens or 0,
|
|
359
|
-
total_tokens=chunk.usage.total_tokens or 0,
|
|
373
|
+
# TOOL_CALL_ARGS
|
|
374
|
+
yield event.tool_call_args(
|
|
375
|
+
tool_call_id=tool_call_data["id"],
|
|
376
|
+
delta=tool_call_data["arguments"],
|
|
360
377
|
)
|
|
361
|
-
)
|
|
362
378
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
379
|
+
# TOOL_CALL_END
|
|
380
|
+
yield event.tool_call_end(tool_call_id=tool_call_data["id"])
|
|
381
|
+
|
|
382
|
+
# 更新 usage
|
|
383
|
+
if hasattr(chunk, "usage") and chunk.usage:
|
|
384
|
+
context.add_usage(
|
|
385
|
+
Usage(
|
|
386
|
+
prompt_tokens=chunk.usage.prompt_tokens or 0,
|
|
387
|
+
completion_tokens=chunk.usage.completion_tokens or 0,
|
|
388
|
+
total_tokens=chunk.usage.total_tokens or 0,
|
|
389
|
+
)
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
except Exception as e:
|
|
393
|
+
error_msg = str(e)
|
|
394
|
+
# AG-UI: RUN_ERROR
|
|
395
|
+
yield event.run_error(message=error_msg)
|
|
396
|
+
raise FlowError("AGENT_EXECUTION_FAILED", 500, {"error": error_msg}) from e
|
|
397
|
+
|
|
398
|
+
if self.retry_empty_response and attempt == 0 and not tool_calls and not assistant_content.strip():
|
|
399
|
+
attempt = 1
|
|
400
|
+
continue
|
|
401
|
+
break
|
|
368
402
|
|
|
369
403
|
# 保存 assistant 消息(tool_calls 转为 OpenAI 标准格式)
|
|
370
404
|
if tool_calls:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agstack
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: Production-ready toolkit for building FastAPI and LLM applications
|
|
5
5
|
Author-email: XtraVisions <gitadmin@xtravisions.com>, Chen Hao <chenhao@xtravisions.com>
|
|
6
6
|
Maintainer-email: XtraVisions <gitadmin@xtravisions.com>, Chen Hao <chenhao@xtravisions.com>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Agent.request_overrides 按轮覆盖请求参数 + retry_empty_response 空响应重试"""
|
|
2
|
+
|
|
3
|
+
from unittest.mock import patch
|
|
4
|
+
|
|
5
|
+
from agstack.llm.flow.agent import Agent
|
|
6
|
+
from agstack.llm.flow.context import FlowContext
|
|
7
|
+
from agstack.llm.flow.event import EventType
|
|
8
|
+
from tests.test_flow_error_semantics import FakeStreamClient, _collect, _finish_chunk, _run, _text_chunk
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class _TurnAwareAgent(Agent):
|
|
12
|
+
def request_overrides(self, context, turn, *, retry=False):
|
|
13
|
+
if retry:
|
|
14
|
+
return {"extra_body": {"enable_thinking": False}, "max_tokens": 64}
|
|
15
|
+
return {"extra_body": {"enable_thinking": turn == 1}, "temperature": 0.1}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TestRequestOverrides:
|
|
19
|
+
@patch("agstack.llm.flow.agent.get_llm_client")
|
|
20
|
+
def test_overrides_merged_per_turn(self, mock_get_client):
|
|
21
|
+
client = FakeStreamClient([[_text_chunk("hi"), _finish_chunk()]])
|
|
22
|
+
mock_get_client.return_value = client
|
|
23
|
+
agent = _TurnAwareAgent(name="a", model="m")
|
|
24
|
+
_run(_collect(agent.stream(FlowContext(), {"input": "q"})))
|
|
25
|
+
assert len(client.requests) == 1
|
|
26
|
+
assert client.requests[0]["extra_body"] == {"enable_thinking": True}
|
|
27
|
+
assert client.requests[0]["temperature"] == 0.1
|
|
28
|
+
|
|
29
|
+
@patch("agstack.llm.flow.agent.get_llm_client")
|
|
30
|
+
def test_empty_response_retried_once_with_retry_overrides(self, mock_get_client):
|
|
31
|
+
client = FakeStreamClient([[_finish_chunk()], [_text_chunk("answer"), _finish_chunk()]])
|
|
32
|
+
mock_get_client.return_value = client
|
|
33
|
+
agent = _TurnAwareAgent(name="a", model="m", retry_empty_response=True)
|
|
34
|
+
ctx = FlowContext()
|
|
35
|
+
events = _run(_collect(agent.stream(ctx, {"input": "q"})))
|
|
36
|
+
assert len(client.requests) == 2
|
|
37
|
+
assert client.requests[1]["extra_body"] == {"enable_thinking": False}
|
|
38
|
+
assert client.requests[1]["max_tokens"] == 64
|
|
39
|
+
assert ctx.outputs["a"]["result"] == "answer"
|
|
40
|
+
assert [e["delta"] for e in events if e["type"] == EventType.TEXT_MESSAGE_CONTENT] == ["answer"]
|
|
41
|
+
|
|
42
|
+
@patch("agstack.llm.flow.agent.get_llm_client")
|
|
43
|
+
def test_empty_response_not_retried_by_default(self, mock_get_client):
|
|
44
|
+
client = FakeStreamClient([[_finish_chunk()]])
|
|
45
|
+
mock_get_client.return_value = client
|
|
46
|
+
agent = Agent(name="a", model="m")
|
|
47
|
+
ctx = FlowContext()
|
|
48
|
+
_run(_collect(agent.stream(ctx, {"input": "q"})))
|
|
49
|
+
assert len(client.requests) == 1
|
|
50
|
+
assert ctx.outputs["a"]["result"] == ""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|