shenmali's picture
Upload folder using huggingface_hub
2bc64b6 verified
Raw
History Blame Contribute Delete
4.1 kB
import json
import time
from collections.abc import Callable, Iterator
from _core.llm import LLMClient
from _core.models import estimate_cost
from _core.tracer import Step
PLAN_PROMPT = (
"Break the research topic into 2-4 specific sub-questions that together cover it. "
"Output ONLY a JSON array of strings."
)
SYNTH_PROMPT = (
"Write a concise research brief answering the topic. Cite sources inline as [n] "
"using the provided numbered source list."
)
SearchFn = Callable[[str], list[dict]]
class DeepResearchAgent:
def __init__(self, llm: LLMClient, search_fn: SearchFn, max_subquestions: int = 4):
self.llm = llm
self.search_fn = search_fn
self.max_subquestions = max_subquestions
def _ask(self, system: str, user: str):
start = time.monotonic()
resp = self.llm.chat(
[{"role": "system", "content": system}, {"role": "user", "content": user}]
)
latency = int((time.monotonic() - start) * 1000)
cost = estimate_cost(self.llm.model, resp.prompt_tokens, resp.completion_tokens)
return resp, latency, cost
def run(self, topic: str) -> Iterator[Step]:
resp, lat, cost = self._ask(PLAN_PROMPT, topic)
try:
parsed = json.loads(resp.content or "[]")
except json.JSONDecodeError:
parsed = []
subqs = parsed[: self.max_subquestions] if isinstance(parsed, list) else []
if not subqs:
subqs = [topic]
yield Step(
kind="thought",
content="Sub-questions:\n" + "\n".join(f"- {q}" for q in subqs),
tokens=resp.prompt_tokens + resp.completion_tokens,
cost_usd=cost,
latency_ms=lat,
)
sources: list[dict] = []
for q in subqs:
yield Step(kind="action", content=f"search: {q}")
results = self.search_fn(q)
ids = []
for r in results:
sources.append(r)
ids.append(len(sources))
yield Step(
kind="observation",
content="\n".join(
f"[{i}] {sources[i - 1].get('title', '')}: {sources[i - 1].get('body', '')}"
for i in ids
)
or "No results.",
)
source_list = "\n".join(
f"[{i + 1}] {s.get('title', '')}{s.get('url', '')}" for i, s in enumerate(sources)
)
fresp, flat, fcost = self._ask(
SYNTH_PROMPT, f"Topic: {topic}\nNumbered sources:\n{source_list}"
)
yield Step(
kind="final",
content=(fresp.content or "") + "\n\n### Sources\n" + source_list,
tokens=fresp.prompt_tokens + fresp.completion_tokens,
cost_usd=fcost,
latency_ms=flat,
)
def make_search_fn(tavily_key: str | None = None, max_results: int = 3) -> SearchFn:
if tavily_key:
import requests
def search(query: str) -> list[dict]:
resp = requests.post(
"https://api.tavily.com/search",
json={"api_key": tavily_key, "query": query, "max_results": max_results},
timeout=30,
)
resp.raise_for_status()
data = resp.json()
return [
{"title": r.get("title", ""), "body": r.get("content", ""), "url": r.get("url", "")}
for r in data.get("results", [])
]
return search
def search(query: str) -> list[dict]:
try:
from ddgs import DDGS
except ImportError:
from duckduckgo_search import DDGS # type: ignore
out: list[dict] = []
with DDGS() as ddgs:
for r in ddgs.text(query, max_results=max_results):
out.append(
{
"title": r.get("title", ""),
"body": r.get("body", ""),
"url": r.get("href", ""),
}
)
return out
return search