Sync from GitHub 4e3ca94
Browse files- models/minicpm_agent.py +34 -15
- pipelines/agent_ask.py +15 -7
models/minicpm_agent.py
CHANGED
|
@@ -50,24 +50,32 @@ SYSTEM_PROMPT = (
|
|
| 50 |
"a page viewer. You do NOT answer questions or explain β the manual does the "
|
| 51 |
"talking. Your only job is to FIND the right page and POINT at things on it.\n\n"
|
| 52 |
"Each step, choose exactly ONE tool and reply with ONLY its JSON object β no "
|
| 53 |
-
"prose, no markdown, nothing else
|
| 54 |
-
|
|
|
|
|
|
|
| 55 |
' {"tool": "search", "query": "<focused search phrase>"}\n'
|
| 56 |
-
"- Jump to a section
|
| 57 |
' {"tool": "go_to_section", "section": <number>}\n'
|
| 58 |
"- Circle something on the CURRENT page (its full text is given to you):\n"
|
| 59 |
' {"tool": "circle", "target": "<short name of the thing to circle>"}\n'
|
| 60 |
"- Finish β nothing more to do, or it isn't in the manual:\n"
|
| 61 |
' {"tool": "done", "message": "<one short line for the mechanic>"}\n\n'
|
| 62 |
-
"
|
| 63 |
-
|
| 64 |
-
"
|
| 65 |
-
"
|
| 66 |
-
"
|
| 67 |
-
"
|
|
|
|
| 68 |
"- Use the conversation history only to resolve what they mean (e.g. "
|
| 69 |
-
'"circle the other one"); never restate earlier answers.\n'
|
| 70 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
)
|
| 72 |
|
| 73 |
|
|
@@ -96,12 +104,16 @@ def state_message(
|
|
| 96 |
if page_text
|
| 97 |
else f"CURRENT PAGE: {where} (no text available)"
|
| 98 |
)
|
|
|
|
|
|
|
|
|
|
| 99 |
return {
|
| 100 |
"role": "user",
|
| 101 |
"content": (
|
| 102 |
-
f"
|
| 103 |
f"TABLE OF CONTENTS:\n{toc_lines}\n\n"
|
| 104 |
-
f"{
|
|
|
|
| 105 |
),
|
| 106 |
}
|
| 107 |
|
|
@@ -177,7 +189,7 @@ def _parse_tool(raw: str) -> dict | None:
|
|
| 177 |
tool = obj.get("tool")
|
| 178 |
if tool == "search":
|
| 179 |
query = str(obj.get("query") or "").strip()
|
| 180 |
-
return {"tool": "search", "query": query} if query else None
|
| 181 |
if tool == "go_to_section":
|
| 182 |
try:
|
| 183 |
return {"tool": "go_to_section", "section": int(obj.get("section"))}
|
|
@@ -185,12 +197,19 @@ def _parse_tool(raw: str) -> dict | None:
|
|
| 185 |
return None
|
| 186 |
if tool == "circle":
|
| 187 |
target = str(obj.get("target") or "").strip()
|
| 188 |
-
return {"tool": "circle", "target": target} if target else None
|
| 189 |
if tool == "done":
|
| 190 |
return {"tool": "done", "message": str(obj.get("message") or "").strip()}
|
| 191 |
return None
|
| 192 |
|
| 193 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 194 |
def decide(messages: list[dict]) -> tuple[dict | None, str]:
|
| 195 |
"""One agentic step. messages is the running conversation the pipeline
|
| 196 |
maintains (system + past turns + this turn's state and any tool results).
|
|
|
|
| 50 |
"a page viewer. You do NOT answer questions or explain β the manual does the "
|
| 51 |
"talking. Your only job is to FIND the right page and POINT at things on it.\n\n"
|
| 52 |
"Each step, choose exactly ONE tool and reply with ONLY its JSON object β no "
|
| 53 |
+
"prose, no markdown, nothing else. Replace every <...> placeholder with the "
|
| 54 |
+
"real value β NEVER output the angle brackets.\n\n"
|
| 55 |
+
"Tools:\n"
|
| 56 |
+
'- Search the manual for a part/topic/procedure:\n'
|
| 57 |
' {"tool": "search", "query": "<focused search phrase>"}\n'
|
| 58 |
+
"- Jump to a section β use its number from the TABLE OF CONTENTS:\n"
|
| 59 |
' {"tool": "go_to_section", "section": <number>}\n'
|
| 60 |
"- Circle something on the CURRENT page (its full text is given to you):\n"
|
| 61 |
' {"tool": "circle", "target": "<short name of the thing to circle>"}\n'
|
| 62 |
"- Finish β nothing more to do, or it isn't in the manual:\n"
|
| 63 |
' {"tool": "done", "message": "<one short line for the mechanic>"}\n\n'
|
| 64 |
+
"How to choose:\n"
|
| 65 |
+
'- They say "go to" / "take me to" / name a section β go_to_section.\n'
|
| 66 |
+
"- The thing they want is on the CURRENT page β circle it. The target MUST "
|
| 67 |
+
"be what the mechanic asked for β match it to the page's exact wording if it "
|
| 68 |
+
"appears there; NEVER circle a different component.\n"
|
| 69 |
+
"- Otherwise β search. After a search shows a page, that page becomes the "
|
| 70 |
+
"CURRENT page, so circle on it or search again.\n"
|
| 71 |
"- Use the conversation history only to resolve what they mean (e.g. "
|
| 72 |
+
'"circle the other one"); never restate earlier answers.\n\n'
|
| 73 |
+
"Examples (copy the FORMAT, not the values):\n"
|
| 74 |
+
'Mechanic: "go to the cooling system" β {"tool": "go_to_section", "section": 5}\n'
|
| 75 |
+
'Mechanic: "where do I replace the fuel filter" (not on this page) β '
|
| 76 |
+
'{"tool": "search", "query": "fuel filter replacement"}\n'
|
| 77 |
+
'Mechanic: "circle the bleeder screw" (it is on this page) β '
|
| 78 |
+
'{"tool": "circle", "target": "bleeder screw"}'
|
| 79 |
)
|
| 80 |
|
| 81 |
|
|
|
|
| 104 |
if page_text
|
| 105 |
else f"CURRENT PAGE: {where} (no text available)"
|
| 106 |
)
|
| 107 |
+
# The request goes LAST (after the long page text) so it stays freshest β
|
| 108 |
+
# otherwise the page block dominates and the agent acts on the page instead
|
| 109 |
+
# of what was asked.
|
| 110 |
return {
|
| 111 |
"role": "user",
|
| 112 |
"content": (
|
| 113 |
+
f"{page_block}\n\n"
|
| 114 |
f"TABLE OF CONTENTS:\n{toc_lines}\n\n"
|
| 115 |
+
f"The mechanic said: {request!r}\n"
|
| 116 |
+
"Choose ONE tool and reply with ONLY its JSON object."
|
| 117 |
),
|
| 118 |
}
|
| 119 |
|
|
|
|
| 189 |
tool = obj.get("tool")
|
| 190 |
if tool == "search":
|
| 191 |
query = str(obj.get("query") or "").strip()
|
| 192 |
+
return {"tool": "search", "query": query} if _real(query) else None
|
| 193 |
if tool == "go_to_section":
|
| 194 |
try:
|
| 195 |
return {"tool": "go_to_section", "section": int(obj.get("section"))}
|
|
|
|
| 197 |
return None
|
| 198 |
if tool == "circle":
|
| 199 |
target = str(obj.get("target") or "").strip()
|
| 200 |
+
return {"tool": "circle", "target": target} if _real(target) else None
|
| 201 |
if tool == "done":
|
| 202 |
return {"tool": "done", "message": str(obj.get("message") or "").strip()}
|
| 203 |
return None
|
| 204 |
|
| 205 |
|
| 206 |
+
def _real(value: str) -> bool:
|
| 207 |
+
"""A usable arg, not an echoed placeholder. Small models sometimes copy the
|
| 208 |
+
schema example verbatim ("<short name of the thing to circle>") β angle
|
| 209 |
+
brackets are the tell; reject so the loop re-asks for a real value."""
|
| 210 |
+
return bool(value) and "<" not in value and ">" not in value
|
| 211 |
+
|
| 212 |
+
|
| 213 |
def decide(messages: list[dict]) -> tuple[dict | None, str]:
|
| 214 |
"""One agentic step. messages is the running conversation the pipeline
|
| 215 |
maintains (system + past turns + this turn's state and any tool results).
|
pipelines/agent_ask.py
CHANGED
|
@@ -112,12 +112,16 @@ def agent_events(
|
|
| 112 |
tool, raw = minicpm_agent.decide(messages)
|
| 113 |
log.info("step %d: tool=%s | raw=%r", step, tool, raw[:200])
|
| 114 |
if tool is None:
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
messages.append(minicpm_agent.assistant_action_message(tool))
|
| 122 |
|
| 123 |
if tool["tool"] == "go_to_section":
|
|
@@ -169,7 +173,11 @@ def agent_events(
|
|
| 169 |
minicpm_agent.tool_result_message(
|
| 170 |
f"Search showed p.{best_page}. It is now the CURRENT page.\n"
|
| 171 |
f"CURRENT PAGE (p.{best_page}) β full text:\n"
|
| 172 |
-
f"{page_text(best_page) or '(no text available)'}"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 173 |
)
|
| 174 |
)
|
| 175 |
continue
|
|
|
|
| 112 |
tool, raw = minicpm_agent.decide(messages)
|
| 113 |
log.info("step %d: tool=%s | raw=%r", step, tool, raw[:200])
|
| 114 |
if tool is None:
|
| 115 |
+
# Unusable reply (bad JSON, or an echoed placeholder target). Correct
|
| 116 |
+
# it and let the agent try again rather than abandon the turn.
|
| 117 |
+
messages.append(
|
| 118 |
+
minicpm_agent.tool_result_message(
|
| 119 |
+
"That was not a valid tool call. Reply with ONLY one JSON "
|
| 120 |
+
'object like {"tool": "circle", "target": "drain plug"} β '
|
| 121 |
+
"fill in the real value."
|
| 122 |
+
)
|
| 123 |
+
)
|
| 124 |
+
continue
|
| 125 |
messages.append(minicpm_agent.assistant_action_message(tool))
|
| 126 |
|
| 127 |
if tool["tool"] == "go_to_section":
|
|
|
|
| 173 |
minicpm_agent.tool_result_message(
|
| 174 |
f"Search showed p.{best_page}. It is now the CURRENT page.\n"
|
| 175 |
f"CURRENT PAGE (p.{best_page}) β full text:\n"
|
| 176 |
+
f"{page_text(best_page) or '(no text available)'}\n\n"
|
| 177 |
+
f"The mechanic asked for: {request!r}. If THAT is on this page, "
|
| 178 |
+
"circle it (use its exact name as printed on the page). "
|
| 179 |
+
"Otherwise search again or go to a section. Do not circle a "
|
| 180 |
+
"different component."
|
| 181 |
)
|
| 182 |
)
|
| 183 |
continue
|