55from .signatures import PlanOrAct , Finalize , PlanWithTools
66from .tools import TOOLS , run_tool , safe_eval_math , to_dspy_tools
77from .runtime import parse_decision_text
8+ from .costs import estimate_tokens , estimate_cost_usd
89
910class MicroAgent (dspy .Module ):
1011 """
@@ -151,7 +152,7 @@ def used_tool(state, name: str) -> bool:
151152
152153 state : List [Dict [str , Any ]] = []
153154
154- def _accumulate_usage ():
155+ def _accumulate_usage (input_text : str = "" , output_text : str = "" ):
155156 # Pull new usage entries from dspy.settings.trace
156157 try :
157158 for _ , _ , out in dspy .settings .trace [- 1 :]:
@@ -164,6 +165,22 @@ def _accumulate_usage():
164165 total_out_tokens += int (usage .get ("output_tokens" , 0 ) or 0 )
165166 except Exception :
166167 pass
168+ # Heuristic fallback: estimate tokens from input/output texts and compute cost via env prices
169+ try :
170+ if input_text :
171+ it = estimate_tokens (input_text , getattr (self .lm , "model" , "" ))
172+ else :
173+ it = 0
174+ if output_text :
175+ ot = estimate_tokens (output_text , getattr (self .lm , "model" , "" ))
176+ else :
177+ ot = 0
178+ if it or ot :
179+ total_in_tokens += it
180+ total_out_tokens += ot
181+ total_cost += estimate_cost_usd (it , ot , getattr (self .lm , "model" , "" ), self ._provider or "" )
182+ except Exception :
183+ pass
167184
168185 # Path A: OpenAI-native tool calling using DSPy signatures/adapters.
169186 if self ._use_tool_calls :
@@ -184,6 +201,17 @@ def _accumulate_usage():
184201 total_out_tokens += int (usage .get ('output_tokens' , 0 ) or 0 )
185202 except Exception :
186203 pass
204+ # Heuristic fallback: estimate using a reconstructed prompt & result
205+ try :
206+ approx_prompt = self ._decision_prompt (
207+ question = question ,
208+ state_json = json .dumps (state , ensure_ascii = False ),
209+ tools_json = json .dumps (self ._tool_list , ensure_ascii = False ),
210+ )
211+ approx_out = getattr (pred , 'final' , None ) or (str (getattr (pred , 'tool_calls' , '' )))
212+ _accumulate_usage (approx_prompt , approx_out )
213+ except Exception :
214+ pass
187215
188216 # If tool calls are proposed, execute them.
189217 calls = getattr (pred , 'tool_calls' , None )
@@ -302,6 +330,11 @@ def _accumulate_usage():
302330 # Path B: Ollama-friendly loop via raw LM completions and robust JSON parsing.
303331 for _ in range (self .max_steps ):
304332 lm_calls += 1
333+ prompt_text = self ._decision_prompt (
334+ question = question ,
335+ state_json = json .dumps (state , ensure_ascii = False ),
336+ tools_json = json .dumps (self ._tool_list , ensure_ascii = False ),
337+ )
305338 raw = self .lm (
306339 prompt = self ._decision_prompt (
307340 question = question ,
@@ -310,13 +343,18 @@ def _accumulate_usage():
310343 )
311344 )
312345 decision_text = raw [0 ] if isinstance (raw , list ) else (raw if isinstance (raw , str ) else str (raw ))
313- _accumulate_usage ()
346+ _accumulate_usage (prompt_text , decision_text )
314347
315348 # Extract and parse JSON; if malformed, try a flexible parser and one self-correction retry.
316349 try :
317350 decision = parse_decision_text (decision_text )
318351 except Exception :
319352 lm_calls += 1
353+ prompt_text = self ._decision_prompt (
354+ question = question ,
355+ state_json = json .dumps (state , ensure_ascii = False ),
356+ tools_json = json .dumps (self ._tool_list , ensure_ascii = False ),
357+ )
320358 raw = self .lm (
321359 prompt = self ._decision_prompt (
322360 question = question ,
@@ -325,7 +363,7 @@ def _accumulate_usage():
325363 )
326364 )
327365 decision_text = raw [0 ] if isinstance (raw , list ) else (raw if isinstance (raw , str ) else str (raw ))
328- _accumulate_usage ()
366+ _accumulate_usage (prompt_text , decision_text )
329367 try :
330368 decision = parse_decision_text (decision_text )
331369 except Exception :
@@ -413,14 +451,18 @@ def _accumulate_usage():
413451 ans = " | " .join (parts ) if parts else ""
414452 else :
415453 lm_calls += 1
454+ finalize_prompt = (
455+ "Given the question and the trace of tool observations, write the final answer.\n \n "
456+ f"Question: { question } \n \n Trace: { json .dumps (state , ensure_ascii = False )} \n \n "
457+ "Answer succinctly."
458+ )
416459 raw = self .lm (
417460 prompt = (
418- "Given the question and the trace of tool observations, write the final answer.\n \n "
419- f"Question: { question } \n \n Trace: { json .dumps (state , ensure_ascii = False )} \n \n "
420- "Answer succinctly."
461+ finalize_prompt
421462 )
422463 )
423464 ans = raw [0 ] if isinstance (raw , list ) else (raw if isinstance (raw , str ) else str (raw ))
465+ _accumulate_usage (finalize_prompt , ans )
424466 p = dspy .Prediction (answer = ans , trace = state )
425467 p .usage = {
426468 "lm_calls" : lm_calls ,
0 commit comments