Skip to content

Commit d263a60

Browse files
committed
Add retries, debug logs
1 parent e7ac5bc commit d263a60

File tree

2 files changed

+105
-79
lines changed

2 files changed

+105
-79
lines changed

apex/services/deep_research/deep_research_langchain.py

Lines changed: 104 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import asyncio
12
from typing import Any
23

4+
import tenacity
35
from langchain.prompts import PromptTemplate
46
from langchain.retrievers import ContextualCompressionRetriever
57
from langchain.retrievers.document_compressors import LLMChainFilter
@@ -12,6 +14,8 @@
1214
from langchain_experimental.utilities import PythonREPL
1315
from langchain_openai import ChatOpenAI
1416
from langchain_text_splitters import RecursiveCharacterTextSplitter
17+
from loguru import logger
18+
from openai import RateLimitError
1519

1620
from apex.common.config import Config
1721
from apex.services.deep_research.deep_research_base import DeepResearchBase
@@ -77,7 +81,7 @@ def __init__(
7781
openai_api_base=final_base_url if final_base_url is not None else base_url,
7882
max_retries=3,
7983
temperature=0.01,
80-
max_tokens=1600,
84+
max_tokens=2000,
8185
)
8286
# Caution: PythonREPL can execute arbitrary code on the host machine.
8387
# Use with caution and consider sandboxing for untrusted inputs.
@@ -118,7 +122,7 @@ def _create_research_chain(self) -> RunnableSerializable[dict[str, Any], str]:
118122
- Executive Summary
119123
- Key Findings
120124
- Evidence (quote or paraphrase context with attributions)
121-
- Limitations and Uncertainties
125+
- Limitations
122126
- Conclusion
123127
124128
Explain reasoning explicitly in prose. Prefer depth over breadth.
@@ -141,9 +145,9 @@ async def invoke(
141145

142146
# Seed notes with any provided documents
143147
notes: list[str] = []
144-
if body and "documents" in body and body["documents"]:
148+
if body is not None and "documents" in body:
145149
for doc in body["documents"]:
146-
if doc and "page_content" in doc and doc["page_content"] is not None:
150+
if doc and doc.get("page_content") is not None:
147151
content = str(doc["page_content"])[:1000]
148152
notes.append(f"Provided document snippet: {content}")
149153

@@ -155,79 +159,19 @@ async def invoke(
155159
collected_sources: list[dict[str, str]] = []
156160
seen_urls: set[str] = set()
157161

158-
def _render_sources(max_items: int = 12) -> str:
159-
if not collected_sources:
160-
return "(none)"
161-
lines: list[str] = []
162-
for i, src in enumerate(collected_sources[:max_items], start=1):
163-
title = src.get("title") or "untitled"
164-
url = src.get("url") or ""
165-
lines.append(f"[{i}] {title} - {url}")
166-
return "\n".join(lines)
167-
168-
def _render_notes(max_items: int = 8) -> str:
169-
if not notes:
170-
return "(none yet)"
171-
clipped = notes[-max_items:]
172-
return "\n".join(f"- {item}" for item in clipped)
173-
174-
def _build_agent_chain() -> RunnableSerializable[dict[str, Any], str]:
175-
prompt = PromptTemplate(
176-
input_variables=["question", "notes", "sources"],
177-
template=(
178-
"You are DeepResearcher, a meticulous, tool-using research agent.\n"
179-
"You can use exactly these tools: websearch, python_repl.\n\n"
180-
"Tool: websearch\n"
181-
"- description: Search the web for relevant information.\n"
182-
"- args: keys: 'query' (string), 'max_results' (integer <= 10)\n\n"
183-
"Tool: python_repl\n"
184-
"- description: A Python shell for executing Python commands.\n"
185-
"- note: Print values to see output, e.g., `print(...)`.\n"
186-
"- args: keys: 'code' (string: valid python command).\n\n"
187-
"Follow an iterative think-act-observe loop. "
188-
"Prefer rich internal reasoning over issuing many tool calls.\n"
189-
"Spend time thinking: produce substantial, explicit reasoning in each 'thought'.\n"
190-
"Avoid giving a final answer too early. Aim for at least 6 detailed thoughts before finalizing,\n"
191-
"unless the question is truly trivial. "
192-
"If no tool use is needed in a step, still provide a reflective 'thought'\n"
193-
"that evaluates evidence, identifies gaps, and plans the next step.\n\n"
194-
"Always respond in strict JSON. Use one of the two schemas:\n\n"
195-
"1) Action step (JSON keys shown with dot-paths):\n"
196-
"- thought: string\n"
197-
"- action.tool: 'websearch' | 'python_repl'\n"
198-
"- action.input: for websearch -> {{query: string, max_results: integer}}\n"
199-
"- action.input: for python_repl -> {{code: string}}\n\n"
200-
"2) Final answer step:\n"
201-
"- thought: string\n"
202-
"- final_answer: string\n\n"
203-
"In every step, make 'thought' a detailed paragraph (120-200 words) that:\n"
204-
"- Summarizes what is known and unknown so far\n"
205-
"- Justifies the chosen next action or decision not to act\n"
206-
"- Evaluates evidence quality and cites source numbers when applicable\n"
207-
"- Identifies risks, uncertainties, and alternative hypotheses\n\n"
208-
"Executive Summary, Key Findings, Evidence, Limitations, Conclusion.\n"
209-
"Use inline numeric citations like [1], [2] that refer to Sources.\n"
210-
"Include a final section titled 'Sources' listing the numbered citations.\n\n"
211-
"Question:\n{question}\n\n"
212-
"Notes and observations so far:\n{notes}\n\n"
213-
"Sources (use these for citations):\n{sources}\n\n"
214-
"Respond with JSON only."
215-
),
216-
)
217-
return prompt | self.research_model | StrOutputParser()
218-
219-
agent_chain = _build_agent_chain()
162+
agent_chain = self._build_agent_chain()
220163

221164
while step_index < max_iterations:
165+
logger.debug(f"Starting deep researcher {step_index + 1}/{max_iterations} step")
222166
step_index += 1
223-
agent_output: str = await agent_chain.ainvoke(
167+
agent_output: str = await self._try_invoke(
168+
agent_chain,
224169
{
225170
"question": question,
226-
"notes": _render_notes(),
227-
"sources": _render_sources(),
228-
}
171+
"notes": self._render_notes(notes=notes),
172+
"sources": self._render_sources(collected_sources=collected_sources),
173+
},
229174
)
230-
231175
parsed = self._safe_parse_json(agent_output)
232176
if parsed is None:
233177
reasoning_traces.append(
@@ -246,6 +190,7 @@ def _build_agent_chain() -> RunnableSerializable[dict[str, Any], str]:
246190

247191
# Final answer branch
248192
if "final_answer" in parsed:
193+
logger.debug("Early-stopping deep research due to the final answer")
249194
final_answer = str(parsed.get("final_answer", ""))
250195
reasoning_traces.append(
251196
{
@@ -274,7 +219,7 @@ def _build_agent_chain() -> RunnableSerializable[dict[str, Any], str]:
274219
observations: list[str] = []
275220
for idx, website in enumerate(websites[:max_results]):
276221
if website.content:
277-
snippet = str(website.content)[:500]
222+
snippet = str(website.content)[:1000]
278223
observations.append(
279224
f"Result {idx + 1}: {website.title or website.url or 'untitled'}\n{snippet}"
280225
)
@@ -304,8 +249,10 @@ def _build_agent_chain() -> RunnableSerializable[dict[str, Any], str]:
304249
continue
305250

306251
if action.get("tool") == "python_repl":
252+
logger.debug(f"Applying tool: {action.get('tool')}")
307253
action_input = action.get("input") or {}
308254
code = str(action_input.get("code", "")).strip()
255+
logger.debug(f"Code to be executed:\n{code}")
309256
# Record the tool use (truncate long code for history)
310257
self.tool_history.append({"tool": "python_repl", "args": code[:200]})
311258

@@ -316,17 +263,19 @@ def _build_agent_chain() -> RunnableSerializable[dict[str, Any], str]:
316263
# PythonREPL returns only printed output (may include trailing newline)
317264
repl_output = self.python_repl.run(code)
318265
observation_text = repl_output if repl_output else "(no output)"
266+
logger.debug(f"Code execution result:\n{observation_text}")
319267
except Exception as e: # noqa: BLE001
320268
observation_text = f"Error while executing code: {e}"
321269

322270
notes.append(f"Thought: {thought}")
271+
logger.debug(f"Thought: {thought}")
323272
notes.append(f"Observation from python_repl:\n{observation_text}")
324273
reasoning_traces.append(
325274
{
326275
"step": f"iteration-{step_index}",
327276
"model": getattr(self.research_model, "model_name", "unknown"),
328277
"thought": thought,
329-
"action": {"tool": "python_repl", "code": code[:500]},
278+
"action": {"tool": "python_repl", "code": code[:1000]},
330279
"observation": observation_text[:1000],
331280
}
332281
)
@@ -344,26 +293,33 @@ def _build_agent_chain() -> RunnableSerializable[dict[str, Any], str]:
344293
notes.append("Agent returned an unsupported action. Use the websearch tool or provide final_answer.")
345294

346295
# Fallback: if loop ends without final answer, ask final model to synthesize from notes
296+
logger.debug("Generating final answer")
347297
final_prompt = PromptTemplate(
348298
input_variables=["question", "notes", "sources"],
349299
template=(
350-
"You are a senior researcher. Write a research report with sections:\n"
300+
"You are a senior interdisciplinary researcher with expertise across "
301+
"science, technology, humanities, and social sciences.\n"
302+
"Provide report only in plain text using natural language.\n"
303+
"Write your response in the form of a well-structured research report with sections:\n"
351304
"Executive Summary, Key Findings, Evidence, Limitations, Conclusion.\n"
352305
"Use inline numeric citations like [1], [2] that refer to Sources.\n"
353306
"At the end, include a 'Sources' section listing the numbered citations.\n\n"
307+
"Do NOT use JSON, or any other structured data format.\n"
354308
"Question:\n{question}\n\n"
355309
"Notes:\n{notes}\n\n"
356310
"Sources:\n{sources}\n\n"
357311
"Research Report:"
358312
),
359313
)
360314
final_chain = final_prompt | self.final_model | StrOutputParser()
361-
final_report: str = await final_chain.ainvoke(
315+
316+
final_report: str = await self._try_invoke(
317+
final_chain,
362318
{
363319
"question": question,
364-
"notes": _render_notes(12),
365-
"sources": _render_sources(20),
366-
}
320+
"notes": self._render_notes(notes=notes, max_items=12),
321+
"sources": self._render_sources(collected_sources=collected_sources, max_items=20),
322+
},
367323
)
368324
reasoning_traces.append(
369325
{
@@ -374,6 +330,76 @@ def _build_agent_chain() -> RunnableSerializable[dict[str, Any], str]:
374330
)
375331
return final_report, self.tool_history, reasoning_traces
376332

333+
def _render_sources(self, collected_sources: list[dict[str, str]], max_items: int = 12) -> str:
334+
if not collected_sources:
335+
return "(none)"
336+
lines: list[str] = []
337+
for i, src in enumerate(collected_sources[:max_items], start=1):
338+
title = src.get("title") or "untitled"
339+
url = src.get("url") or ""
340+
lines.append(f"[{i}] {title} - {url}")
341+
return "\n".join(lines)
342+
343+
def _render_notes(self, notes: list[str], max_items: int = 8) -> str:
344+
if not notes:
345+
return "(none yet)"
346+
clipped = notes[-max_items:]
347+
return "\n".join(f"- {item}" for item in clipped)
348+
349+
def _build_agent_chain(self) -> RunnableSerializable[dict[str, Any], str]:
350+
prompt = PromptTemplate(
351+
input_variables=["question", "notes", "sources"],
352+
template=(
353+
"You are DeepResearcher, a meticulous, tool-using research agent.\n"
354+
"You can use exactly these tools: websearch, python_repl.\n\n"
355+
"Tool: websearch\n"
356+
"- description: Search the web for relevant information.\n"
357+
"- args: keys: 'query' (string), 'max_results' (integer <= 10)\n\n"
358+
"Tool: python_repl\n"
359+
"- description: A Python shell for executing Python commands.\n"
360+
"- note: Print values to see output, e.g., `print(...)`.\n"
361+
"- args: keys: 'code' (string: valid python command).\n\n"
362+
"Follow an iterative think-act-observe loop. "
363+
"Prefer rich internal reasoning over issuing many tool calls.\n"
364+
"Spend time thinking: produce substantial, explicit reasoning in each 'thought'.\n"
365+
"Avoid giving a final answer too early. Aim for at least 6 detailed thoughts before finalizing,\n"
366+
"unless the question is truly trivial. "
367+
"If no tool use is needed in a step, still provide a reflective 'thought'\n"
368+
"that evaluates evidence, identifies gaps, and plans the next step.\n\n"
369+
"Always respond in strict JSON. Use one of the two schemas:\n\n"
370+
"1) Action step (JSON keys shown with dot-paths):\n"
371+
"- thought: string\n"
372+
"- action.tool: 'websearch' | 'python_repl'\n"
373+
"- action.input: for websearch -> {{query: string, max_results: integer}}\n"
374+
"- action.input: for python_repl -> {{code: string}}\n\n"
375+
"2) Final answer step:\n"
376+
"- thought: string\n"
377+
"- final_answer: string (use plain text for final answer, not a JSON)\n\n"
378+
"In every step, make 'thought' a detailed paragraph (120-200 words) that:\n"
379+
"- Summarizes what is known and unknown so far\n"
380+
"- Justifies the chosen next action or decision not to act\n"
381+
"- Evaluates evidence quality and cites source numbers when applicable\n"
382+
"- Identifies risks, uncertainties, and alternative hypotheses\n\n"
383+
"Executive Summary, Key Findings, Evidence, Limitations, Conclusion.\n"
384+
"Use inline numeric citations like [1], [2] that refer to Sources.\n"
385+
"Include a final section titled 'Sources' listing the numbered citations.\n\n"
386+
"Question:\n{question}\n\n"
387+
"Notes and observations so far:\n{notes}\n\n"
388+
"Sources (use these for citations):\n{sources}\n\n"
389+
"Respond with JSON always, except for final_anwer (use plain text)."
390+
),
391+
)
392+
return prompt | self.research_model | StrOutputParser()
393+
394+
@tenacity.retry(
395+
retry=tenacity.retry_if_exception_type(RateLimitError),
396+
stop=tenacity.stop_after_attempt(5),
397+
wait=tenacity.wait_fixed(10),
398+
reraise=True,
399+
)
400+
async def _try_invoke(self, chain: RunnableSerializable[dict[str, Any], str], inputs: dict[str, Any]) -> str:
401+
return await chain.ainvoke(inputs)
402+
377403
def _safe_parse_json(self, text: str) -> dict[str, Any] | None:
378404
"""Attempt to parse a JSON object from model output.
379405

apex/validator/generate_reference.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ async def generate_reference(
2929
"content": (
3030
f"Research Question: {query}\n\n"
3131
"Please think through the answer carefully, annotate each step with citations like [1], [2], etc., "
32-
'and conclude with a "References:" list mapping each [n] to its source URL or title.'
32+
'and conclude with a "Sources:" list mapping each [n] to its source URL or title.'
3333
),
3434
}
3535

0 commit comments

Comments
 (0)