Spaces:
Running
Running
File size: 5,709 Bytes
7f94735 95d6832 7f94735 a170f20 7f94735 a170f20 7f94735 470138f 7f94735 470138f 7f94735 a170f20 7f94735 6802438 7f94735 a170f20 8e3a1fd 6802438 a170f20 8e3a1fd 7f94735 6802438 a170f20 7f94735 a170f20 7f94735 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 | """The manual agent loop: the LLM drives the three tools within budgets.
Each step, a planner call returns a JSON action (search_docs / read_page /
ask_source / answer). We execute it, append a short observation, and repeat
until the model declares it can answer or a budget trips β then generate the
grounded Answer from everything accumulated. agent/graph.py is a LangGraph
twin of this loop that shares the same tools, budgets, and planner.
"""
from __future__ import annotations
import ast
import json
import re
from agent.schemas import Answer, Referral
BUDGETS = {"search_docs": 6, "read_page": 2, "ask_source": 1}
MAX_STEPS = sum(BUDGETS.values()) + 2
PLANNER_SYSTEM = (
"You are planning how to answer a PyTorch question using tools. Each turn, "
"reply with ONE json object and nothing else:\n"
' {"action":"search_docs","query":"english keywords","library":null,"kind":null}\n'
' {"action":"read_page","url":"<a url from a previous search result>"}\n'
' {"action":"ask_source","question":"..."} (ONLY for source-code internals)\n'
' {"action":"answer"} (when the gathered context can answer the question)\n'
'kind picks the content space: "api" = reference pages (use for catalog '
'questions like "what loss functions exist?" or to find a specific class), '
'"tutorial" or "guide" = walkthroughs; null = everything. If a search '
"returned only tutorials but you need the actual API, search again with "
'kind "api" and the likely symbol names as the query.\n'
"Guidance: decompose multi-part questions into several search_docs calls "
"with different queries. Use read_page to open a promising page in full. "
"Use ask_source only when the answer needs implementation details the docs "
"don't cover. Answer as soon as you have enough β don't waste tool calls."
)
def _plan(question: str, transcript: list[str], budgets: dict, provider, client) -> dict:
from agent.llm import GenerationError, _raw_completion
state = "\n".join(transcript) if transcript else "(no tools used yet)"
remaining = ", ".join(f"{t}:{n}" for t, n in budgets.items())
prompt = (
f"Question: {question}\n\nTool calls so far:\n{state}\n\n"
f"Remaining tool budget: {remaining}\nYour next action as one json object:"
)
try:
raw = _raw_completion(prompt, system=PLANNER_SYSTEM, provider=provider, client=client)
except GenerationError:
return {"action": "answer"} # planner unreachable β answer with what we have
return _parse_action(raw)
def _parse_action(raw: str) -> dict:
start, end = raw.find("{"), raw.rfind("}")
if start == -1 or end == -1:
return {"action": "answer"}
try:
action = json.loads(raw[start : end + 1])
except json.JSONDecodeError:
return {"action": "answer"}
if action.get("action") not in BUDGETS and action.get("action") != "answer":
return {"action": "answer"}
return action
def _humanize(line: str) -> str:
"""Turn a transcript step into a short grey trace line for the web UI.
The transcript entries are terse function-call records (see tools_exec.py);
this renders them as the model's visible reasoning. Falls back to the raw
line on any shape it doesn't recognise, so a format change degrades to a
still-useful trace rather than crashing the answer path.
"""
m = re.match(r"search_docs\((.+?)\) β (\[.*\])$", line)
if m:
try:
query = ast.literal_eval(m.group(1))
titles = [str(t).split(" > ")[-1] for t in ast.literal_eval(m.group(2))]
except (ValueError, SyntaxError):
return f"π {line}"
shown = " Β· ".join(t for t in titles[:4] if t)
return f"π searched β{query}β" + (f" β {shown}" if shown else "")
if line.startswith("read_page"):
return "π read a full page"
if line.startswith("ask_source"):
return "π checked the source-code references"
return f"β’ {line}"
def answer_agentic(
question: str, provider: str | None = None, client=None, progress=None
) -> Answer:
"""Run the tool loop and return a grounded Answer."""
from agent.grounded import answer_from_sections
from agent.tools_exec import do_search, execute_tool
budgets = dict(BUDGETS)
sections: list[dict] = []
referrals: list[Referral] = []
seen_urls: set[str] = set()
transcript: list[str] = []
def emit_last(): # surface the step we just recorded, in the UI's grey trace
if progress and transcript:
progress(_humanize(transcript[-1]))
# always retrieve once for the raw question β never depend on a (possibly
# rate-limited) planner call just to do the obvious first search
budgets["search_docs"] -= 1
do_search(question, None, sections=sections, seen_urls=seen_urls, transcript=transcript)
emit_last()
for _ in range(MAX_STEPS):
action = _plan(question, transcript, budgets, provider, client)
name = action.get("action")
if name == "answer" or all(v == 0 for v in budgets.values()):
break
if budgets.get(name, 0) == 0:
transcript.append(f"{name}: budget exhausted, pick another action or answer")
continue
budgets[name] -= 1
execute_tool(
name, action, question,
sections=sections, referrals=referrals,
seen_urls=seen_urls, transcript=transcript,
)
emit_last()
if progress:
progress("βοΈ writing the answer")
return answer_from_sections(
question, sections, referrals=referrals, provider=provider, client=client
)
|