feat(agents): research loop with budget guards, compaction, depth nudge, finalize
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016DvdRZkyCy9NJpHYDdPqmS
This commit is contained in:
1 parent
63085113ed
commit
b2ae3d6ced
3 files changed
+439
No files matched your search
@@ -0,0 +1,90 @@
|
||||
"""Phase-1 GATE: does the local model reliably emit tool calls over MULTIPLE rounds?
|
||||
|
||||
This is the single load-bearing risk of the whole design. A single tool call often works;
|
||||
the failure mode is multi-turn (the model stops calling tools, or emits malformed calls).
|
||||
Run this with LM Studio up and a model loaded:
|
||||
|
||||
lms server start && lms load gpt-oss-20b
|
||||
uv run python scripts/smoke_toolcalling.py
|
||||
|
||||
PASS = the model chained >=3 real tool calls and produced a final answer.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
||||
|
||||
from langchain_core.messages import AIMessage, HumanMessage, SystemMessage, ToolMessage
|
||||
|
||||
from llm.client import build_llm, list_loaded_models, resolve_model_id
|
||||
from tools.market_data import get_quote
|
||||
from tools.python_exec import run_python
|
||||
from tools.web import web_search
|
||||
|
||||
TOOLS = [run_python, web_search, get_quote]
|
||||
TOOLS_BY_NAME = {t.name: t for t in TOOLS}
|
||||
|
||||
SYSTEM = (
|
||||
"You are a test agent. Use a tool for EVERY factual or numeric step — never compute "
|
||||
"in your head. Call tools one step at a time, then give a final summary."
|
||||
)
|
||||
TASK = (
|
||||
"Do these steps, each via a tool call, then summarize the results:\n"
|
||||
"1) Get the last price of AAPL.\n"
|
||||
"2) With run_python, compute a 7% stop-loss price (price * 0.93).\n"
|
||||
"3) With run_python, compute a 15% take-profit price (price * 1.15).\n"
|
||||
"4) web_search for one recent Apple (AAPL) news headline.\n"
|
||||
"Report the stop price, take price, and the headline."
|
||||
)
|
||||
MAX_ROUNDS = 10
|
||||
|
||||
|
||||
def main() -> int:
|
||||
loaded = list_loaded_models()
|
||||
if not loaded:
|
||||
print("FAIL: LM Studio not reachable at /v1/models.")
|
||||
print("Start it: lms server start then lms load <model>")
|
||||
return 2
|
||||
print("Loaded models :", loaded)
|
||||
print("Resolved model:", resolve_model_id())
|
||||
print("-" * 60)
|
||||
|
||||
llm = build_llm(TOOLS)
|
||||
messages: list = [SystemMessage(content=SYSTEM), HumanMessage(content=TASK)]
|
||||
tool_calls_made = 0
|
||||
rounds = 0
|
||||
|
||||
while rounds < MAX_ROUNDS:
|
||||
rounds += 1
|
||||
ai: AIMessage = llm.invoke(messages)
|
||||
messages.append(ai)
|
||||
if not ai.tool_calls:
|
||||
print(f"\n[round {rounds}] FINAL:\n{ai.content}\n")
|
||||
break
|
||||
for tc in ai.tool_calls:
|
||||
name, args = tc["name"], tc["args"]
|
||||
print(f"[round {rounds}] call {name}({args})")
|
||||
tool = TOOLS_BY_NAME.get(name)
|
||||
if tool is None:
|
||||
result = f"ERROR: unknown tool {name}"
|
||||
else:
|
||||
try:
|
||||
result = tool.invoke(args)
|
||||
except Exception as e:
|
||||
result = f"ERROR: {type(e).__name__}: {e}"
|
||||
tool_calls_made += 1
|
||||
print(f" -> {str(result)[:180]}")
|
||||
messages.append(ToolMessage(content=str(result), tool_call_id=tc["id"]))
|
||||
|
||||
print("=" * 60)
|
||||
print(f"rounds={rounds} tool_calls_executed={tool_calls_made}")
|
||||
ok = tool_calls_made >= 3
|
||||
print("RESULT:", "PASS — multi-turn tool calling works" if ok else "FAIL — tool calling unreliable")
|
||||
return 0 if ok else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in new issue
Block a user