Skip to content

Commit e4eb794

Browse files
veegee82claude
andcommitted
fix: budget tracking, system prompt loading, and reasoning model support
- Track tool_calls_used in delegation loop budget (was always 0) - Add max_tool_calls check to can_continue() and budget_fraction_remaining - Load system prompt from agent config path before convention path fallback - Add robust JSON extraction from LLM responses with text wrapping - Support reasoning models (Nemotron) that return content in reasoning field - Add E2E integration test notebook (07) covering all AWP modalities - Update notebook defaults to nvidia/nemotron-3-super-120b-a12b:free Tested end-to-end with OpenRouter + Nemotron 3 Super: 9/9 tests pass (LLM connectivity, tool calling, DAG A0/A1, delegation loop A2/A3, AgentWorkflow with DataFrame/tool creation A4). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 5f16faf commit e4eb794

7 files changed

Lines changed: 291 additions & 24 deletions

File tree

‎examples/jupyter/tutorials/04_dag_workflow_e2e.ipynb‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
"\n",
4545
"# --- OpenRouter (cloud) ---------------------------------------\n",
4646
"OPENROUTER_API_KEY = \"\" # paste your key or set env var OPENROUTER_API_KEY\n",
47-
"OPENROUTER_MODEL = \"openrouter/google/gemini-2.5-flash\"\n",
47+
"OPENROUTER_MODEL = \"nvidia/nemotron-3-super-120b-a12b:free\"\n",
4848
"\n",
4949
"# --- Custom OpenAI-compatible API -----------------------------\n",
5050
"CUSTOM_API_KEY = \"\" # paste your key or set env var LLM_API_KEY\n",
@@ -1055,4 +1055,4 @@
10551055
},
10561056
"nbformat": 4,
10571057
"nbformat_minor": 5
1058-
}
1058+
}

‎examples/jupyter/tutorials/05_delegation_loop_e2e.ipynb‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@
5858
"\n",
5959
"# --- OpenRouter (cloud) ---------------------------------------\n",
6060
"OPENROUTER_API_KEY = \"\" # paste your key or set env var OPENROUTER_API_KEY\n",
61-
"OPENROUTER_MODEL = \"openrouter/google/gemini-2.5-flash\"\n",
61+
"OPENROUTER_MODEL = \"nvidia/nemotron-3-super-120b-a12b:free\"\n",
6262
"\n",
6363
"# --- Custom OpenAI-compatible API -----------------------------\n",
6464
"CUSTOM_API_KEY = \"\" # paste your key or set env var LLM_API_KEY\n",
@@ -1460,4 +1460,4 @@
14601460
},
14611461
"nbformat": 4,
14621462
"nbformat_minor": 5
1463-
}
1463+
}

‎examples/jupyter/tutorials/06_full_showcase.ipynb‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@
5757
"\n",
5858
"# --- OpenRouter (cloud) ---------------------------------------\n",
5959
"OPENROUTER_API_KEY = \"\" # paste your key or set env var OPENROUTER_API_KEY\n",
60-
"OPENROUTER_MODEL = \"openrouter/google/gemini-2.5-flash\"\n",
60+
"OPENROUTER_MODEL = \"nvidia/nemotron-3-super-120b-a12b:free\"\n",
6161
"\n",
6262
"# --- Custom OpenAI-compatible API -----------------------------\n",
6363
"CUSTOM_API_KEY = \"\"\n",
@@ -11587,7 +11587,7 @@
1158711587
" result = AgentWorkflow(\n",
1158811588
" inputs={\"data\": df, \"config\": {...}},\n",
1158911589
" task=\"Analyze trends and create a report\",\n",
11590-
" model=\"openrouter/google/gemini-2.5-flash\",\n",
11590+
" model=\"nvidia/nemotron-3-super-120b-a12b:free\",\n",
1159111591
" max_loops=5,\n",
1159211592
" code_mode=True,\n",
1159311593
" tool_creation=True,\n",
@@ -11742,4 +11742,4 @@
1174211742
},
1174311743
"nbformat": 4,
1174411744
"nbformat_minor": 4
11745-
}
11745+
}

‎examples/jupyter/tutorials/07_e2e_integration_test.ipynb‎

Lines changed: 211 additions & 0 deletions
Large diffs are not rendered by default.

‎reference/python/src/awp/runtime/agent.py‎

Lines changed: 54 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -157,14 +157,55 @@ def _run_simple(self, messages: list[dict], **kwargs: Any) -> dict[str, Any]:
157157
try:
158158
return self._llm.chat_json(messages, **kwargs)
159159
except json.JSONDecodeError:
160-
# Retry with explicit JSON instruction
160+
# Try to extract JSON from within the response text
161161
text = self._llm.chat_text(messages, **kwargs)
162+
extracted = self._extract_json(text)
163+
if extracted is not None:
164+
return extracted
162165
logger.warning("Agent %s: non-JSON response, wrapping", self.name)
163-
return {"result": text, "confidence": 0.3}
166+
return {"result": text, "confidence": 0.5}
164167
except Exception as exc:
165168
logger.error("Agent %s LLM error: %s", self.name, exc)
166169
return {"error": str(exc), "confidence": 0.0}
167170

171+
@staticmethod
172+
def _extract_json(text: str) -> dict[str, Any] | None:
173+
"""Try to extract a JSON object from text that may contain extra content."""
174+
if not text:
175+
return None
176+
# Try the full text first
177+
stripped = text.strip()
178+
# Strip markdown fences
179+
if stripped.startswith("```"):
180+
lines = stripped.split("\n")
181+
lines = [ln for ln in lines if not ln.strip().startswith("```")]
182+
stripped = "\n".join(lines).strip()
183+
try:
184+
result = json.loads(stripped)
185+
if isinstance(result, dict):
186+
return result
187+
except (json.JSONDecodeError, ValueError):
188+
pass
189+
# Find first { ... } block
190+
start = text.find("{")
191+
if start == -1:
192+
return None
193+
depth = 0
194+
for i in range(start, len(text)):
195+
if text[i] == "{":
196+
depth += 1
197+
elif text[i] == "}":
198+
depth -= 1
199+
if depth == 0:
200+
try:
201+
result = json.loads(text[start : i + 1])
202+
if isinstance(result, dict):
203+
return result
204+
except (json.JSONDecodeError, ValueError):
205+
pass
206+
break
207+
return None
208+
168209
def _run_with_tools(
169210
self, messages: list[dict], tool_defs: list[dict], **kwargs: Any
170211
) -> dict[str, Any]:
@@ -193,17 +234,11 @@ def _run_with_tools(
193234
if not content:
194235
return {"result": "No response", "confidence": 0.0}
195236

196-
# Parse as JSON
197-
cleaned = content.strip()
198-
if cleaned.startswith("```"):
199-
lines = cleaned.split("\n")
200-
lines = [line for line in lines if not line.strip().startswith("```")]
201-
cleaned = "\n".join(lines).strip()
202-
203-
try:
204-
return json.loads(cleaned)
205-
except json.JSONDecodeError:
206-
return {"result": content, "confidence": 0.3}
237+
# Parse as JSON (try robust extraction)
238+
extracted = self._extract_json(content)
239+
if extracted is not None:
240+
return extracted
241+
return {"result": content, "confidence": 0.5}
207242

208243
except Exception as exc:
209244
logger.error("Agent %s tool loop error: %s", self.name, exc)
@@ -215,8 +250,12 @@ def _build_system_prompt(self) -> str:
215250
"""Assemble system prompt: base + skills + memory."""
216251
parts: list[str] = []
217252

218-
# 1. Base system prompt
219-
base = self._load_file("workflow/instructions/SYSTEM_PROMPT.md")
253+
# 1. Base system prompt — try config path first, then convention path
254+
base = ""
255+
if self._config.prompt.system:
256+
base = self._load_file(self._config.prompt.system)
257+
if not base:
258+
base = self._load_file("workflow/instructions/SYSTEM_PROMPT.md")
220259
if base:
221260
parts.append(base)
222261
else:

‎reference/python/src/awp/runtime/delegation_loop_runner.py‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@ def budget_fraction_remaining(self) -> float:
9696
1 - (self.workers_spawned / max(self.max_total_workers, 1)),
9797
1 - (self.wall_time_elapsed / max(self.max_wall_time, 1)),
9898
]
99+
if self.max_tool_calls:
100+
fractions.append(1 - (self.tool_calls_used / self.max_tool_calls))
99101
return max(0.0, min(fractions))
100102

101103
def can_continue(self) -> tuple[bool, str]:
@@ -106,6 +108,8 @@ def can_continue(self) -> tuple[bool, str]:
106108
return False, "max_total_workers reached"
107109
if self.wall_time_elapsed >= self.max_wall_time:
108110
return False, "max_wall_time exceeded"
111+
if self.max_tool_calls and self.tool_calls_used >= self.max_tool_calls:
112+
return False, "max_tool_calls reached"
109113
return True, "ok"
110114

111115
def to_dict(self) -> dict[str, Any]:
@@ -1348,6 +1352,7 @@ def _tracking_call(name: str, arguments: dict) -> dict:
13481352
"result": result,
13491353
}
13501354
)
1355+
self._budget.tool_calls_used += 1
13511356
return result
13521357

13531358
final_msg = llm.chat_with_tools(

‎reference/python/src/awp/runtime/llm.py‎

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -381,12 +381,24 @@ def chat_text(
381381
messages: list[dict[str, Any]],
382382
**kwargs: Any,
383383
) -> str:
384-
"""Return just the assistant's text content."""
384+
"""Return just the assistant's text content.
385+
386+
Handles reasoning models that may put output in the ``reasoning``
387+
field when ``content`` is null or empty.
388+
"""
385389
data = self.chat(messages, **kwargs)
386390
choices = data.get("choices", [])
387391
if not choices:
388392
return ""
389-
return choices[0].get("message", {}).get("content", "") or ""
393+
msg = choices[0].get("message", {})
394+
content = msg.get("content", "") or ""
395+
if not content:
396+
# Reasoning models (e.g. Nemotron) may return content in
397+
# the reasoning field when max_tokens is constrained
398+
reasoning = msg.get("reasoning", "")
399+
if reasoning:
400+
content = reasoning
401+
return content
390402

391403
def chat_json(
392404
self,

0 commit comments

Comments
 (0)