返回 JoyAI-Echo
runtime.py
1 """Runtime-specific helper functions and constants."""
2
3 from __future__ import annotations
4
5 from typing import Any
6
7 from loguru import logger
8
9 from nanobot.utils.helpers import stringify_text_blocks
10
11 _MAX_REPEAT_EXTERNAL_LOOKUPS = 2
12
13 EMPTY_FINAL_RESPONSE_MESSAGE = (
14 "I completed the tool steps but couldn't produce a final answer. "
15 "Please try again or narrow the task."
16 )
17
18
19 def is_empty_final_response_message(content: str | None) -> bool:
20 """True when *content* is the runner's empty-final placeholder."""
21 if content is None:
22 return False
23 return content.strip() == EMPTY_FINAL_RESPONSE_MESSAGE
24
25 FINALIZATION_RETRY_PROMPT = (
26 "Please provide your response to the user based on the conversation above."
27 )
28
29 LENGTH_RECOVERY_PROMPT = (
30 "Output limit reached. Continue exactly where you left off "
31 "— no recap, no apology. Break remaining work into smaller steps if needed."
32 )
33
34
35 def empty_tool_result_message(tool_name: str) -> str:
36 """Short prompt-safe marker for tools that completed without visible output."""
37 return f"({tool_name} completed with no output)"
38
39
40 def ensure_nonempty_tool_result(tool_name: str, content: Any) -> Any:
41 """Replace semantically empty tool results with a short marker string."""
42 if content is None:
43 return empty_tool_result_message(tool_name)
44 if isinstance(content, str) and not content.strip():
45 return empty_tool_result_message(tool_name)
46 if isinstance(content, list):
47 if not content:
48 return empty_tool_result_message(tool_name)
49 text_payload = stringify_text_blocks(content)
50 if text_payload is not None and not text_payload.strip():
51 return empty_tool_result_message(tool_name)
52 return content
53
54
55 def is_blank_text(content: str | None) -> bool:
56 """True when *content* is missing or only whitespace."""
57 return content is None or not content.strip()
58
59
60 def build_finalization_retry_message() -> dict[str, str]:
61 """A short no-tools-allowed prompt for final answer recovery."""
62 return {"role": "user", "content": FINALIZATION_RETRY_PROMPT}
63
64
65 def build_length_recovery_message() -> dict[str, str]:
66 """Prompt the model to continue after hitting output token limit."""
67 return {"role": "user", "content": LENGTH_RECOVERY_PROMPT}
68
69
70 def external_lookup_signature(tool_name: str, arguments: dict[str, Any]) -> str | None:
71 """Stable signature for repeated external lookups we want to throttle."""
72 if tool_name == "web_fetch":
73 url = str(arguments.get("url") or "").strip()
74 if url:
75 return f"web_fetch:{url.lower()}"
76 if tool_name == "web_search":
77 query = str(arguments.get("query") or arguments.get("search_term") or "").strip()
78 if query:
79 return f"web_search:{query.lower()}"
80 return None
81
82
83 def repeated_external_lookup_error(
84 tool_name: str,
85 arguments: dict[str, Any],
86 seen_counts: dict[str, int],
87 ) -> str | None:
88 """Block repeated external lookups after a small retry budget."""
89 signature = external_lookup_signature(tool_name, arguments)
90 if signature is None:
91 return None
92 count = seen_counts.get(signature, 0) + 1
93 seen_counts[signature] = count
94 if count <= _MAX_REPEAT_EXTERNAL_LOOKUPS:
95 return None
96 logger.warning(
97 "Blocking repeated external lookup {} on attempt {}",
98 signature[:160],
99 count,
100 )
101 return (
102 "Error: repeated external lookup blocked. "
103 "Use the results you already have to answer, or try a meaningfully different source."
104 )
105
105 lines PYTHON