@@ -171,7 +171,7 @@ async def invoke(
171171 collected_sources : list [dict [str , str ]] = []
172172 seen_urls : set [str ] = set ()
173173
174- agent_chain = self ._build_agent_chain ()
174+ agent_chain = await self ._build_agent_chain ()
175175
176176 while step_index < max_iterations :
177177 logger .debug (f"Starting deep researcher { step_index + 1 } /{ max_iterations } step" )
@@ -202,17 +202,18 @@ async def invoke(
202202
203203 # Final answer branch
204204 if "final_answer" in parsed :
205- logger .debug ("Early-stopping deep research due to the final answer" )
206205 final_answer = str (parsed .get ("final_answer" , "" ))
207206 reasoning_traces .append (
208207 {
209208 "step" : f"iteration-{ step_index } " ,
210209 "model" : getattr (self .research_model , "model_name" , "unknown" ),
211210 "thought" : thought ,
212- "final_answer " : final_answer ,
211+ "observation " : final_answer ,
213212 }
214213 )
215- return final_answer , self .tool_history , reasoning_traces
214+ logger .debug ("Early-stopping deep research due to the final answer" )
215+ # return final_answer, self.tool_history, reasoning_traces
216+ break
216217
217218 # Action branch (only websearch supported)
218219 action = parsed .get ("action" ) or {}
@@ -255,7 +256,7 @@ async def invoke(
255256 "model" : getattr (self .research_model , "model_name" , "unknown" ),
256257 "thought" : thought ,
257258 "action" : {"tool" : "websearch" , "query" : query , "max_results" : max_results },
258- "observation" : observation_text [:1000 ],
259+ "observation" : observation_text [:1200 ],
259260 }
260261 )
261262 continue
@@ -289,7 +290,7 @@ async def invoke(
289290 "model" : getattr (self .research_model , "model_name" , "unknown" ),
290291 "thought" : thought ,
291292 "action" : {"tool" : "python_repl" , "code" : code [:1000 ]},
292- "observation" : observation_text [:1000 ],
293+ "observation" : observation_text [:1200 ],
293294 }
294295 )
295296 continue
@@ -305,19 +306,9 @@ async def invoke(
305306 )
306307 notes .append ("Agent returned an unsupported action. Use the websearch tool or provide final_answer." )
307308
308- # Fallback: if loop ends without final answer, ask final model to synthesize from notes
309+ # If loop ends without final answer, ask final model to synthesize from notes.
309310 logger .debug ("Generating final answer" )
310- final_prompt = PromptTemplate (
311- input_variables = ["question" , "notes" , "sources" ],
312- template = (
313- self ._FINAL_ANSWER_INST + "Do NOT use JSON, or any other structured data format.\n "
314- "Question:\n {question}\n \n "
315- "Notes:\n {notes}\n \n "
316- "Sources:\n {sources}\n \n "
317- "Research Report:"
318- ),
319- )
320- final_chain = final_prompt | self .final_model | StrOutputParser ()
311+ final_chain = await self ._build_final_chain ()
321312
322313 final_report : str = await self ._try_invoke (
323314 final_chain ,
@@ -336,6 +327,19 @@ async def invoke(
336327 )
337328 return final_report , self .tool_history , reasoning_traces
338329
330+ async def _build_final_chain (self ) -> RunnableSerializable [dict [str , Any ], str ]:
331+ final_prompt = PromptTemplate (
332+ input_variables = ["question" , "notes" , "sources" ],
333+ template = (
334+ self ._FINAL_ANSWER_INST + "Do NOT use JSON, or any other structured data format. Provide \n "
335+ "Question:\n {question}\n \n "
336+ "Notes:\n {notes}\n \n "
337+ "Sources:\n {sources}\n \n "
338+ "Research Report:"
339+ ),
340+ )
341+ return final_prompt | self .final_model | StrOutputParser ()
342+
339343 def _render_sources (self , collected_sources : list [dict [str , str ]], max_items : int = 12 ) -> str :
340344 if not collected_sources :
341345 return "(none)"
@@ -352,47 +356,49 @@ def _render_notes(self, notes: list[str], max_items: int = 8) -> str:
352356 clipped = notes [- max_items :]
353357 return "\n " .join (f"- { item } " for item in clipped )
354358
355- def _build_agent_chain (self ) -> RunnableSerializable [dict [str , Any ], str ]:
359+ async def _build_agent_chain (self ) -> RunnableSerializable [dict [str , Any ], str ]:
356360 prompt = PromptTemplate (
357361 input_variables = ["question" , "notes" , "sources" ],
358362 template = (
359363 "You are DeepResearcher, a meticulous, tool-using research agent.\n "
360364 "You can use exactly these tools: websearch, python_repl.\n \n "
361365 "Tool: websearch\n "
362- "- description: Search the web for relevant information.\n "
363- "- args: keys: 'query' (string), 'max_results' (integer <= 10)\n \n "
366+ " - description: Search the web for relevant information.\n "
367+ " - args: keys: 'query' (string), 'max_results' (integer <= 10)\n \n "
364368 "Tool: python_repl\n "
365- "- description: A Python shell for executing Python commands.\n "
366- "- note: Print values to see output, e.g., `print(...)`.\n "
367- "- args: keys: 'code' (string: valid python command).\n \n "
369+ " - description: A Python shell for executing Python commands.\n "
370+ " - note: Print values to see output, e.g., `print(...)`.\n "
371+ " - args: keys: 'code' (string: valid python command).\n \n "
368372 "Follow an iterative think-act-observe loop. "
369373 "Prefer rich internal reasoning over issuing many tool calls.\n "
370374 "Spend time thinking: produce substantial, explicit reasoning in each 'thought'.\n "
371375 "Avoid giving a final answer too early. Aim for at least 6 detailed thoughts before finalizing,\n "
372376 "unless the question is truly trivial. "
373377 "If no tool use is needed in a step, still provide a reflective 'thought'\n "
374378 "that evaluates evidence, identifies gaps, and plans the next step.\n \n "
375- "Always respond in strict JSON. Use one of the two schemas:\n \n "
376- "1) Action step (JSON keys shown with dot-paths):\n "
377- "- thought: string\n "
378- "- action.tool: 'websearch' | 'python_repl'\n "
379- "- action.input: for websearch -> {{query: string, max_results: integer}}\n "
380- "- action.input: for python_repl -> {{code: string}}\n \n "
381- "2) Final answer step:\n "
382- "- thought: string\n "
383- "- final_answer: string (use plain text for final answer, not a JSON)\n \n "
379+ "Always respond in strict JSON for deep research steps (do not use JSON for final answer). "
380+ "Use one of the two schemas:\n \n "
381+ "1. Action step (JSON keys shown with dot-paths):\n "
382+ " - thought: string\n "
383+ " - action.tool: 'websearch' | 'python_repl'\n "
384+ " - action.input: for websearch -> {{query: string, max_results: integer}}\n "
385+ " - action.input: for python_repl -> {{code: string}}\n \n "
386+ "2. Final answer step:\n "
387+ " - thought: string\n "
388+ " - final_answer: string\n \n "
384389 "In every step, make 'thought' a detailed paragraph (120-200 words) that:\n "
385- "- Summarizes what is known and unknown so far\n "
386- "- Justifies the chosen next action or decision not to act\n "
387- "- Evaluates evidence quality and cites source numbers when applicable\n "
388- "- Identifies risks, uncertainties, and alternative hypotheses\n \n "
390+ " - Summarizes what is known and unknown so far\n "
391+ " - Justifies the chosen next action or decision not to act\n "
392+ " - Evaluates evidence quality and cites source numbers when applicable\n "
393+ " - Identifies risks, uncertainties, and alternative hypotheses\n \n "
394+ "Respond with JSON only during deep research steps, "
395+ "final answer must be always in a plain text formatted as a research report, with sections:\n "
389396 "Executive Summary, Key Findings, Evidence, Limitations, Conclusion.\n "
390397 "Use inline numeric citations like [1], [2] that refer to Sources.\n "
391398 "Include a final section titled 'Sources' listing the numbered citations.\n \n "
392399 "Question:\n {question}\n \n "
393400 "Notes and observations so far:\n {notes}\n \n "
394401 "Sources (use these for citations):\n {sources}\n \n "
395- "Respond with JSON always, except for final_anwer (use plain text)."
396402 ),
397403 )
398404 return prompt | self .research_model | StrOutputParser ()
0 commit comments