mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-06 17:38:35 +00:00
style: revert unrelated Black-style formatting churn (#3220)
The earlier commits picked up a large amount of Black-style reformatting
(multi-line frozenset / keyword-arg wrapping / docstring blanks / removed
parens) on top of the actual guard fix. @chengyongru flagged it; the
first pass reverted some but not all.
This restores nanobot/providers/base.py, runner.py, heartbeat/service.py,
and utils/evaluator.py to origin/main and reapplies only the guard logic:
- base.py: add should_execute_tools property
- runner.py / heartbeat/service.py / utils/evaluator.py: route through it
+ log a warning when has_tool_calls but finish_reason is anomalous
Net diff vs main is now +87/-4 (was +211/-102) — roughly 30 lines of real
logic, which is what the PR is actually about.
Behavior unchanged from previous HEAD; full suite still 2014 passed.
Made-with: Cursor
This commit is contained in:
parent
9a569fdc6a
commit
14ee7cb121
@ -47,6 +47,7 @@ _COMPACTABLE_TOOLS = frozenset({
|
|||||||
_BACKFILL_CONTENT = "[Tool result unavailable — call was interrupted or lost]"
|
_BACKFILL_CONTENT = "[Tool result unavailable — call was interrupted or lost]"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True)
|
@dataclass(slots=True)
|
||||||
class AgentRunSpec:
|
class AgentRunSpec:
|
||||||
"""Configuration for a single agent execution."""
|
"""Configuration for a single agent execution."""
|
||||||
@ -119,7 +120,11 @@ class AgentRunner:
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Append injected user messages while preserving role alternation."""
|
"""Append injected user messages while preserving role alternation."""
|
||||||
for injection in injections:
|
for injection in injections:
|
||||||
if messages and injection.get("role") == "user" and messages[-1].get("role") == "user":
|
if (
|
||||||
|
messages
|
||||||
|
and injection.get("role") == "user"
|
||||||
|
and messages[-1].get("role") == "user"
|
||||||
|
):
|
||||||
merged = dict(messages[-1])
|
merged = dict(messages[-1])
|
||||||
merged["content"] = cls._merge_message_content(
|
merged["content"] = cls._merge_message_content(
|
||||||
merged.get("content"),
|
merged.get("content"),
|
||||||
@ -169,10 +174,7 @@ class AgentRunner:
|
|||||||
self._append_injected_messages(messages, injections)
|
self._append_injected_messages(messages, injections)
|
||||||
logger.info(
|
logger.info(
|
||||||
"Injected {} follow-up message(s) {} ({}/{})",
|
"Injected {} follow-up message(s) {} ({}/{})",
|
||||||
len(injections),
|
len(injections), phase, injection_cycles, _MAX_INJECTION_CYCLES,
|
||||||
phase,
|
|
||||||
injection_cycles,
|
|
||||||
_MAX_INJECTION_CYCLES,
|
|
||||||
)
|
)
|
||||||
return True, injection_cycles
|
return True, injection_cycles
|
||||||
|
|
||||||
@ -188,9 +190,12 @@ class AgentRunner:
|
|||||||
return []
|
return []
|
||||||
try:
|
try:
|
||||||
signature = inspect.signature(spec.injection_callback)
|
signature = inspect.signature(spec.injection_callback)
|
||||||
accepts_limit = "limit" in signature.parameters or any(
|
accepts_limit = (
|
||||||
parameter.kind is inspect.Parameter.VAR_KEYWORD
|
"limit" in signature.parameters
|
||||||
for parameter in signature.parameters.values()
|
or any(
|
||||||
|
parameter.kind is inspect.Parameter.VAR_KEYWORD
|
||||||
|
for parameter in signature.parameters.values()
|
||||||
|
)
|
||||||
)
|
)
|
||||||
if accepts_limit:
|
if accepts_limit:
|
||||||
items = await spec.injection_callback(limit=_MAX_INJECTIONS_PER_TURN)
|
items = await spec.injection_callback(limit=_MAX_INJECTIONS_PER_TURN)
|
||||||
@ -213,9 +218,7 @@ class AgentRunner:
|
|||||||
dropped = len(injected_messages) - _MAX_INJECTIONS_PER_TURN
|
dropped = len(injected_messages) - _MAX_INJECTIONS_PER_TURN
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"Injection callback returned {} messages, capping to {} ({} dropped)",
|
"Injection callback returned {} messages, capping to {} ({} dropped)",
|
||||||
len(injected_messages),
|
len(injected_messages), _MAX_INJECTIONS_PER_TURN, dropped,
|
||||||
_MAX_INJECTIONS_PER_TURN,
|
|
||||||
dropped,
|
|
||||||
)
|
)
|
||||||
injected_messages = injected_messages[:_MAX_INJECTIONS_PER_TURN]
|
injected_messages = injected_messages[:_MAX_INJECTIONS_PER_TURN]
|
||||||
return injected_messages
|
return injected_messages
|
||||||
@ -290,9 +293,7 @@ class AgentRunner:
|
|||||||
"model": spec.model,
|
"model": spec.model,
|
||||||
"assistant_message": assistant_message,
|
"assistant_message": assistant_message,
|
||||||
"completed_tool_results": [],
|
"completed_tool_results": [],
|
||||||
"pending_tool_calls": [
|
"pending_tool_calls": [tc.to_openai_tool_call() for tc in response.tool_calls],
|
||||||
tc.to_openai_tool_call() for tc in response.tool_calls
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -331,10 +332,7 @@ class AgentRunner:
|
|||||||
context.stop_reason = stop_reason
|
context.stop_reason = stop_reason
|
||||||
await hook.after_iteration(context)
|
await hook.after_iteration(context)
|
||||||
should_continue, injection_cycles = await self._try_drain_injections(
|
should_continue, injection_cycles = await self._try_drain_injections(
|
||||||
spec,
|
spec, messages, None, injection_cycles,
|
||||||
messages,
|
|
||||||
None,
|
|
||||||
injection_cycles,
|
|
||||||
phase="after tool error",
|
phase="after tool error",
|
||||||
)
|
)
|
||||||
if should_continue:
|
if should_continue:
|
||||||
@ -356,10 +354,7 @@ class AgentRunner:
|
|||||||
length_recovery_count = 0
|
length_recovery_count = 0
|
||||||
# Checkpoint 1: drain injections after tools, before next LLM call
|
# Checkpoint 1: drain injections after tools, before next LLM call
|
||||||
_drained, injection_cycles = await self._try_drain_injections(
|
_drained, injection_cycles = await self._try_drain_injections(
|
||||||
spec,
|
spec, messages, None, injection_cycles,
|
||||||
messages,
|
|
||||||
None,
|
|
||||||
injection_cycles,
|
|
||||||
phase="after tool execution",
|
phase="after tool execution",
|
||||||
)
|
)
|
||||||
if _drained:
|
if _drained:
|
||||||
@ -367,9 +362,9 @@ class AgentRunner:
|
|||||||
await hook.after_iteration(context)
|
await hook.after_iteration(context)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
elif response.has_tool_calls:
|
if response.has_tool_calls:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"Ignoring tool calls under finish_reason='%s' for %s",
|
"Ignoring tool calls under finish_reason='{}' for {}",
|
||||||
response.finish_reason,
|
response.finish_reason,
|
||||||
spec.session_key or "default",
|
spec.session_key or "default",
|
||||||
)
|
)
|
||||||
@ -418,13 +413,11 @@ class AgentRunner:
|
|||||||
)
|
)
|
||||||
if hook.wants_streaming():
|
if hook.wants_streaming():
|
||||||
await hook.on_stream_end(context, resuming=True)
|
await hook.on_stream_end(context, resuming=True)
|
||||||
messages.append(
|
messages.append(build_assistant_message(
|
||||||
build_assistant_message(
|
clean,
|
||||||
clean,
|
reasoning_content=response.reasoning_content,
|
||||||
reasoning_content=response.reasoning_content,
|
thinking_blocks=response.thinking_blocks,
|
||||||
thinking_blocks=response.thinking_blocks,
|
))
|
||||||
)
|
|
||||||
)
|
|
||||||
messages.append(build_length_recovery_message())
|
messages.append(build_length_recovery_message())
|
||||||
await hook.after_iteration(context)
|
await hook.after_iteration(context)
|
||||||
continue
|
continue
|
||||||
@ -441,10 +434,7 @@ class AgentRunner:
|
|||||||
# If injections are found we keep the stream alive (resuming=True)
|
# If injections are found we keep the stream alive (resuming=True)
|
||||||
# so streaming channels don't prematurely finalize the card.
|
# so streaming channels don't prematurely finalize the card.
|
||||||
should_continue, injection_cycles = await self._try_drain_injections(
|
should_continue, injection_cycles = await self._try_drain_injections(
|
||||||
spec,
|
spec, messages, assistant_message, injection_cycles,
|
||||||
messages,
|
|
||||||
assistant_message,
|
|
||||||
injection_cycles,
|
|
||||||
phase="after final response",
|
phase="after final response",
|
||||||
iteration=iteration,
|
iteration=iteration,
|
||||||
)
|
)
|
||||||
@ -468,10 +458,7 @@ class AgentRunner:
|
|||||||
context.stop_reason = stop_reason
|
context.stop_reason = stop_reason
|
||||||
await hook.after_iteration(context)
|
await hook.after_iteration(context)
|
||||||
should_continue, injection_cycles = await self._try_drain_injections(
|
should_continue, injection_cycles = await self._try_drain_injections(
|
||||||
spec,
|
spec, messages, None, injection_cycles,
|
||||||
messages,
|
|
||||||
None,
|
|
||||||
injection_cycles,
|
|
||||||
phase="after LLM error",
|
phase="after LLM error",
|
||||||
)
|
)
|
||||||
if should_continue:
|
if should_continue:
|
||||||
@ -488,10 +475,7 @@ class AgentRunner:
|
|||||||
context.stop_reason = stop_reason
|
context.stop_reason = stop_reason
|
||||||
await hook.after_iteration(context)
|
await hook.after_iteration(context)
|
||||||
should_continue, injection_cycles = await self._try_drain_injections(
|
should_continue, injection_cycles = await self._try_drain_injections(
|
||||||
spec,
|
spec, messages, None, injection_cycles,
|
||||||
messages,
|
|
||||||
None,
|
|
||||||
injection_cycles,
|
|
||||||
phase="after empty response",
|
phase="after empty response",
|
||||||
)
|
)
|
||||||
if should_continue:
|
if should_continue:
|
||||||
@ -499,14 +483,11 @@ class AgentRunner:
|
|||||||
continue
|
continue
|
||||||
break
|
break
|
||||||
|
|
||||||
messages.append(
|
messages.append(assistant_message or build_assistant_message(
|
||||||
assistant_message
|
clean,
|
||||||
or build_assistant_message(
|
reasoning_content=response.reasoning_content,
|
||||||
clean,
|
thinking_blocks=response.thinking_blocks,
|
||||||
reasoning_content=response.reasoning_content,
|
))
|
||||||
thinking_blocks=response.thinking_blocks,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
await self._emit_checkpoint(
|
await self._emit_checkpoint(
|
||||||
spec,
|
spec,
|
||||||
{
|
{
|
||||||
@ -542,10 +523,7 @@ class AgentRunner:
|
|||||||
# We ignore should_continue here because the for-loop has already
|
# We ignore should_continue here because the for-loop has already
|
||||||
# exhausted all iterations.
|
# exhausted all iterations.
|
||||||
drained_after_max_iterations, injection_cycles = await self._try_drain_injections(
|
drained_after_max_iterations, injection_cycles = await self._try_drain_injections(
|
||||||
spec,
|
spec, messages, None, injection_cycles,
|
||||||
messages,
|
|
||||||
None,
|
|
||||||
injection_cycles,
|
|
||||||
phase="after max_iterations",
|
phase="after max_iterations",
|
||||||
)
|
)
|
||||||
if drained_after_max_iterations:
|
if drained_after_max_iterations:
|
||||||
@ -597,7 +575,6 @@ class AgentRunner:
|
|||||||
tools=spec.tools.get_definitions(),
|
tools=spec.tools.get_definitions(),
|
||||||
)
|
)
|
||||||
if hook.wants_streaming():
|
if hook.wants_streaming():
|
||||||
|
|
||||||
async def _stream(delta: str) -> None:
|
async def _stream(delta: str) -> None:
|
||||||
await hook.on_stream(context, delta)
|
await hook.on_stream(context, delta)
|
||||||
|
|
||||||
@ -651,19 +628,13 @@ class AgentRunner:
|
|||||||
tool_results: list[tuple[Any, dict[str, str], BaseException | None]] = []
|
tool_results: list[tuple[Any, dict[str, str], BaseException | None]] = []
|
||||||
for batch in batches:
|
for batch in batches:
|
||||||
if spec.concurrent_tools and len(batch) > 1:
|
if spec.concurrent_tools and len(batch) > 1:
|
||||||
tool_results.extend(
|
tool_results.extend(await asyncio.gather(*(
|
||||||
await asyncio.gather(
|
self._run_tool(spec, tool_call, external_lookup_counts)
|
||||||
*(
|
for tool_call in batch
|
||||||
self._run_tool(spec, tool_call, external_lookup_counts)
|
)))
|
||||||
for tool_call in batch
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
for tool_call in batch:
|
for tool_call in batch:
|
||||||
tool_results.append(
|
tool_results.append(await self._run_tool(spec, tool_call, external_lookup_counts))
|
||||||
await self._run_tool(spec, tool_call, external_lookup_counts)
|
|
||||||
)
|
|
||||||
|
|
||||||
results: list[Any] = []
|
results: list[Any] = []
|
||||||
events: list[dict[str, str]] = []
|
events: list[dict[str, str]] = []
|
||||||
@ -711,11 +682,7 @@ class AgentRunner:
|
|||||||
"status": "error",
|
"status": "error",
|
||||||
"detail": prep_error.split(": ", 1)[-1][:120],
|
"detail": prep_error.split(": ", 1)[-1][:120],
|
||||||
}
|
}
|
||||||
return (
|
return prep_error + _HINT, event, RuntimeError(prep_error) if spec.fail_on_tool_error else None
|
||||||
prep_error + _HINT,
|
|
||||||
event,
|
|
||||||
RuntimeError(prep_error) if spec.fail_on_tool_error else None,
|
|
||||||
)
|
|
||||||
try:
|
try:
|
||||||
if tool is not None:
|
if tool is not None:
|
||||||
result = await tool.execute(**params)
|
result = await tool.execute(**params)
|
||||||
@ -777,11 +744,7 @@ class AgentRunner:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _append_model_error_placeholder(messages: list[dict[str, Any]]) -> None:
|
def _append_model_error_placeholder(messages: list[dict[str, Any]]) -> None:
|
||||||
if (
|
if messages and messages[-1].get("role") == "assistant" and not messages[-1].get("tool_calls"):
|
||||||
messages
|
|
||||||
and messages[-1].get("role") == "assistant"
|
|
||||||
and not messages[-1].get("tool_calls")
|
|
||||||
):
|
|
||||||
return
|
return
|
||||||
messages.append(build_assistant_message(_PERSISTED_MODEL_ERROR_PLACEHOLDER))
|
messages.append(build_assistant_message(_PERSISTED_MODEL_ERROR_PLACEHOLDER))
|
||||||
|
|
||||||
@ -871,15 +834,12 @@ class AgentRunner:
|
|||||||
insert_at = assistant_idx + 1 + offset
|
insert_at = assistant_idx + 1 + offset
|
||||||
while insert_at < len(updated) and updated[insert_at].get("role") == "tool":
|
while insert_at < len(updated) and updated[insert_at].get("role") == "tool":
|
||||||
insert_at += 1
|
insert_at += 1
|
||||||
updated.insert(
|
updated.insert(insert_at, {
|
||||||
insert_at,
|
"role": "tool",
|
||||||
{
|
"tool_call_id": call_id,
|
||||||
"role": "tool",
|
"name": name,
|
||||||
"tool_call_id": call_id,
|
"content": _BACKFILL_CONTENT,
|
||||||
"name": name,
|
})
|
||||||
"content": _BACKFILL_CONTENT,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
offset += 1
|
offset += 1
|
||||||
return updated
|
return updated
|
||||||
|
|
||||||
@ -938,13 +898,9 @@ class AgentRunner:
|
|||||||
if not messages or not spec.context_window_tokens:
|
if not messages or not spec.context_window_tokens:
|
||||||
return messages
|
return messages
|
||||||
|
|
||||||
provider_max_tokens = getattr(
|
provider_max_tokens = getattr(getattr(self.provider, "generation", None), "max_tokens", 4096)
|
||||||
getattr(self.provider, "generation", None), "max_tokens", 4096
|
max_output = spec.max_tokens if isinstance(spec.max_tokens, int) else (
|
||||||
)
|
provider_max_tokens if isinstance(provider_max_tokens, int) else 4096
|
||||||
max_output = (
|
|
||||||
spec.max_tokens
|
|
||||||
if isinstance(spec.max_tokens, int)
|
|
||||||
else (provider_max_tokens if isinstance(provider_max_tokens, int) else 4096)
|
|
||||||
)
|
)
|
||||||
budget = spec.context_block_limit or (
|
budget = spec.context_block_limit or (
|
||||||
spec.context_window_tokens - max_output - _SNIP_SAFETY_BUFFER
|
spec.context_window_tokens - max_output - _SNIP_SAFETY_BUFFER
|
||||||
@ -1027,3 +983,4 @@ class AgentRunner:
|
|||||||
if current:
|
if current:
|
||||||
batches.append(current)
|
batches.append(current)
|
||||||
return batches
|
return batches
|
||||||
|
|
||||||
|
|||||||
@ -93,18 +93,12 @@ class HeartbeatService:
|
|||||||
|
|
||||||
response = await self.provider.chat_with_retry(
|
response = await self.provider.chat_with_retry(
|
||||||
messages=[
|
messages=[
|
||||||
{
|
{"role": "system", "content": "You are a heartbeat agent. Call the heartbeat tool to report your decision."},
|
||||||
"role": "system",
|
{"role": "user", "content": (
|
||||||
"content": "You are a heartbeat agent. Call the heartbeat tool to report your decision.",
|
f"Current Time: {current_time_str(self.timezone)}\n\n"
|
||||||
},
|
"Review the following HEARTBEAT.md and decide whether there are active tasks.\n\n"
|
||||||
{
|
f"{content}"
|
||||||
"role": "user",
|
)},
|
||||||
"content": (
|
|
||||||
f"Current Time: {current_time_str(self.timezone)}\n\n"
|
|
||||||
"Review the following HEARTBEAT.md and decide whether there are active tasks.\n\n"
|
|
||||||
f"{content}"
|
|
||||||
),
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
tools=_HEARTBEAT_TOOL,
|
tools=_HEARTBEAT_TOOL,
|
||||||
model=self.model,
|
model=self.model,
|
||||||
@ -113,7 +107,7 @@ class HeartbeatService:
|
|||||||
if not response.should_execute_tools:
|
if not response.should_execute_tools:
|
||||||
if response.has_tool_calls:
|
if response.has_tool_calls:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"Ignoring tool calls under finish_reason='%s' in heartbeat",
|
"Ignoring heartbeat tool calls under finish_reason='{}'",
|
||||||
response.finish_reason,
|
response.finish_reason,
|
||||||
)
|
)
|
||||||
return "skip", ""
|
return "skip", ""
|
||||||
@ -177,10 +171,7 @@ class HeartbeatService:
|
|||||||
|
|
||||||
if response:
|
if response:
|
||||||
should_notify = await evaluate_response(
|
should_notify = await evaluate_response(
|
||||||
response,
|
response, tasks, self.provider, self.model,
|
||||||
tasks,
|
|
||||||
self.provider,
|
|
||||||
self.model,
|
|
||||||
)
|
)
|
||||||
if should_notify and self.on_notify:
|
if should_notify and self.on_notify:
|
||||||
logger.info("Heartbeat: completed, delivering response")
|
logger.info("Heartbeat: completed, delivering response")
|
||||||
|
|||||||
@ -18,7 +18,6 @@ from nanobot.utils.helpers import image_placeholder_text
|
|||||||
@dataclass
|
@dataclass
|
||||||
class ToolCallRequest:
|
class ToolCallRequest:
|
||||||
"""A tool call request from the LLM."""
|
"""A tool call request from the LLM."""
|
||||||
|
|
||||||
id: str
|
id: str
|
||||||
name: str
|
name: str
|
||||||
arguments: dict[str, Any]
|
arguments: dict[str, Any]
|
||||||
@ -41,16 +40,13 @@ class ToolCallRequest:
|
|||||||
if self.provider_specific_fields:
|
if self.provider_specific_fields:
|
||||||
tool_call["provider_specific_fields"] = self.provider_specific_fields
|
tool_call["provider_specific_fields"] = self.provider_specific_fields
|
||||||
if self.function_provider_specific_fields:
|
if self.function_provider_specific_fields:
|
||||||
tool_call["function"]["provider_specific_fields"] = (
|
tool_call["function"]["provider_specific_fields"] = self.function_provider_specific_fields
|
||||||
self.function_provider_specific_fields
|
|
||||||
)
|
|
||||||
return tool_call
|
return tool_call
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class LLMResponse:
|
class LLMResponse:
|
||||||
"""Response from an LLM provider."""
|
"""Response from an LLM provider."""
|
||||||
|
|
||||||
content: str | None
|
content: str | None
|
||||||
tool_calls: list[ToolCallRequest] = field(default_factory=list)
|
tool_calls: list[ToolCallRequest] = field(default_factory=list)
|
||||||
finish_reason: str = "stop"
|
finish_reason: str = "stop"
|
||||||
@ -73,7 +69,8 @@ class LLMResponse:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def should_execute_tools(self) -> bool:
|
def should_execute_tools(self) -> bool:
|
||||||
"""True only if tool_calls present AND finish_reason is a known-good signal (``tool_calls`` or ``stop``); blocks gateway-injected calls under ``refusal`` / ``content_filter`` / ``error``."""
|
"""Tools execute only when has_tool_calls AND finish_reason is ``tool_calls`` / ``stop``.
|
||||||
|
Blocks gateway-injected calls under ``refusal`` / ``content_filter`` / ``error`` (#3220)."""
|
||||||
if not self.has_tool_calls:
|
if not self.has_tool_calls:
|
||||||
return False
|
return False
|
||||||
return self.finish_reason in ("tool_calls", "stop")
|
return self.finish_reason in ("tool_calls", "stop")
|
||||||
@ -114,28 +111,24 @@ class LLMProvider(ABC):
|
|||||||
)
|
)
|
||||||
_RETRYABLE_STATUS_CODES = frozenset({408, 409, 429})
|
_RETRYABLE_STATUS_CODES = frozenset({408, 409, 429})
|
||||||
_TRANSIENT_ERROR_KINDS = frozenset({"timeout", "connection"})
|
_TRANSIENT_ERROR_KINDS = frozenset({"timeout", "connection"})
|
||||||
_NON_RETRYABLE_429_ERROR_TOKENS = frozenset(
|
_NON_RETRYABLE_429_ERROR_TOKENS = frozenset({
|
||||||
{
|
"insufficient_quota",
|
||||||
"insufficient_quota",
|
"quota_exceeded",
|
||||||
"quota_exceeded",
|
"quota_exhausted",
|
||||||
"quota_exhausted",
|
"billing_hard_limit_reached",
|
||||||
"billing_hard_limit_reached",
|
"insufficient_balance",
|
||||||
"insufficient_balance",
|
"credit_balance_too_low",
|
||||||
"credit_balance_too_low",
|
"billing_not_active",
|
||||||
"billing_not_active",
|
"payment_required",
|
||||||
"payment_required",
|
})
|
||||||
}
|
_RETRYABLE_429_ERROR_TOKENS = frozenset({
|
||||||
)
|
"rate_limit_exceeded",
|
||||||
_RETRYABLE_429_ERROR_TOKENS = frozenset(
|
"rate_limit_error",
|
||||||
{
|
"too_many_requests",
|
||||||
"rate_limit_exceeded",
|
"request_limit_exceeded",
|
||||||
"rate_limit_error",
|
"requests_limit_exceeded",
|
||||||
"too_many_requests",
|
"overloaded_error",
|
||||||
"request_limit_exceeded",
|
})
|
||||||
"requests_limit_exceeded",
|
|
||||||
"overloaded_error",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
_NON_RETRYABLE_429_TEXT_MARKERS = (
|
_NON_RETRYABLE_429_TEXT_MARKERS = (
|
||||||
"insufficient_quota",
|
"insufficient_quota",
|
||||||
"insufficient quota",
|
"insufficient quota",
|
||||||
@ -179,11 +172,7 @@ class LLMProvider(ABC):
|
|||||||
|
|
||||||
if isinstance(content, str) and not content:
|
if isinstance(content, str) and not content:
|
||||||
clean = dict(msg)
|
clean = dict(msg)
|
||||||
clean["content"] = (
|
clean["content"] = None if (msg.get("role") == "assistant" and msg.get("tool_calls")) else "(empty)"
|
||||||
None
|
|
||||||
if (msg.get("role") == "assistant" and msg.get("tool_calls"))
|
|
||||||
else "(empty)"
|
|
||||||
)
|
|
||||||
result.append(clean)
|
result.append(clean)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -357,7 +346,10 @@ class LLMProvider(ABC):
|
|||||||
def _is_retryable_429_response(cls, response: LLMResponse) -> bool:
|
def _is_retryable_429_response(cls, response: LLMResponse) -> bool:
|
||||||
type_token = cls._normalize_error_token(response.error_type)
|
type_token = cls._normalize_error_token(response.error_type)
|
||||||
code_token = cls._normalize_error_token(response.error_code)
|
code_token = cls._normalize_error_token(response.error_code)
|
||||||
semantic_tokens = {token for token in (type_token, code_token) if token is not None}
|
semantic_tokens = {
|
||||||
|
token for token in (type_token, code_token)
|
||||||
|
if token is not None
|
||||||
|
}
|
||||||
if any(token in cls._NON_RETRYABLE_429_ERROR_TOKENS for token in semantic_tokens):
|
if any(token in cls._NON_RETRYABLE_429_ERROR_TOKENS for token in semantic_tokens):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -511,13 +503,9 @@ class LLMProvider(ABC):
|
|||||||
streaming should override this method.
|
streaming should override this method.
|
||||||
"""
|
"""
|
||||||
response = await self.chat(
|
response = await self.chat(
|
||||||
messages=messages,
|
messages=messages, tools=tools, model=model,
|
||||||
tools=tools,
|
max_tokens=max_tokens, temperature=temperature,
|
||||||
model=model,
|
reasoning_effort=reasoning_effort, tool_choice=tool_choice,
|
||||||
max_tokens=max_tokens,
|
|
||||||
temperature=temperature,
|
|
||||||
reasoning_effort=reasoning_effort,
|
|
||||||
tool_choice=tool_choice,
|
|
||||||
)
|
)
|
||||||
if on_content_delta and response.content:
|
if on_content_delta and response.content:
|
||||||
await on_content_delta(response.content)
|
await on_content_delta(response.content)
|
||||||
@ -554,13 +542,9 @@ class LLMProvider(ABC):
|
|||||||
reasoning_effort = self.generation.reasoning_effort
|
reasoning_effort = self.generation.reasoning_effort
|
||||||
|
|
||||||
kw: dict[str, Any] = dict(
|
kw: dict[str, Any] = dict(
|
||||||
messages=messages,
|
messages=messages, tools=tools, model=model,
|
||||||
tools=tools,
|
max_tokens=max_tokens, temperature=temperature,
|
||||||
model=model,
|
reasoning_effort=reasoning_effort, tool_choice=tool_choice,
|
||||||
max_tokens=max_tokens,
|
|
||||||
temperature=temperature,
|
|
||||||
reasoning_effort=reasoning_effort,
|
|
||||||
tool_choice=tool_choice,
|
|
||||||
on_content_delta=on_content_delta,
|
on_content_delta=on_content_delta,
|
||||||
)
|
)
|
||||||
return await self._run_with_retry(
|
return await self._run_with_retry(
|
||||||
@ -600,13 +584,9 @@ class LLMProvider(ABC):
|
|||||||
reasoning_effort = self.generation.reasoning_effort
|
reasoning_effort = self.generation.reasoning_effort
|
||||||
|
|
||||||
kw: dict[str, Any] = dict(
|
kw: dict[str, Any] = dict(
|
||||||
messages=messages,
|
messages=messages, tools=tools, model=model,
|
||||||
tools=tools,
|
max_tokens=max_tokens, temperature=temperature,
|
||||||
model=model,
|
reasoning_effort=reasoning_effort, tool_choice=tool_choice,
|
||||||
max_tokens=max_tokens,
|
|
||||||
temperature=temperature,
|
|
||||||
reasoning_effort=reasoning_effort,
|
|
||||||
tool_choice=tool_choice,
|
|
||||||
)
|
)
|
||||||
return await self._run_with_retry(
|
return await self._run_with_retry(
|
||||||
self._safe_chat,
|
self._safe_chat,
|
||||||
@ -734,7 +714,7 @@ class LLMProvider(ABC):
|
|||||||
if response.finish_reason != "error":
|
if response.finish_reason != "error":
|
||||||
return response
|
return response
|
||||||
last_response = response
|
last_response = response
|
||||||
error_key = (response.content or "").strip().lower() or None
|
error_key = ((response.content or "").strip().lower() or None)
|
||||||
if error_key and error_key == last_error_key:
|
if error_key and error_key == last_error_key:
|
||||||
identical_error_count += 1
|
identical_error_count += 1
|
||||||
else:
|
else:
|
||||||
@ -776,7 +756,9 @@ class LLMProvider(ABC):
|
|||||||
(response.content or "")[:120].lower(),
|
(response.content or "")[:120].lower(),
|
||||||
)
|
)
|
||||||
if on_retry_wait:
|
if on_retry_wait:
|
||||||
await on_retry_wait(f"Model request failed after {attempt} retries, giving up.")
|
await on_retry_wait(
|
||||||
|
f"Model request failed after {attempt} retries, giving up."
|
||||||
|
)
|
||||||
break
|
break
|
||||||
|
|
||||||
base_delay = delays[min(attempt - 1, len(delays) - 1)]
|
base_delay = delays[min(attempt - 1, len(delays) - 1)]
|
||||||
|
|||||||
@ -39,7 +39,6 @@ _EVALUATE_TOOL = [
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
async def evaluate_response(
|
async def evaluate_response(
|
||||||
response: str,
|
response: str,
|
||||||
task_context: str,
|
task_context: str,
|
||||||
@ -56,15 +55,12 @@ async def evaluate_response(
|
|||||||
llm_response = await provider.chat_with_retry(
|
llm_response = await provider.chat_with_retry(
|
||||||
messages=[
|
messages=[
|
||||||
{"role": "system", "content": render_template("agent/evaluator.md", part="system")},
|
{"role": "system", "content": render_template("agent/evaluator.md", part="system")},
|
||||||
{
|
{"role": "user", "content": render_template(
|
||||||
"role": "user",
|
"agent/evaluator.md",
|
||||||
"content": render_template(
|
part="user",
|
||||||
"agent/evaluator.md",
|
task_context=task_context,
|
||||||
part="user",
|
response=response,
|
||||||
task_context=task_context,
|
)},
|
||||||
response=response,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
tools=_EVALUATE_TOOL,
|
tools=_EVALUATE_TOOL,
|
||||||
model=model,
|
model=model,
|
||||||
@ -75,7 +71,7 @@ async def evaluate_response(
|
|||||||
if not llm_response.should_execute_tools:
|
if not llm_response.should_execute_tools:
|
||||||
if llm_response.has_tool_calls:
|
if llm_response.has_tool_calls:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"evaluate_response: ignoring tool calls under finish_reason='%s', defaulting to notify",
|
"evaluate_response: ignoring tool calls under finish_reason='{}', defaulting to notify",
|
||||||
llm_response.finish_reason,
|
llm_response.finish_reason,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user