Files
llm-wiki/scripts/deep-research/deep_research/pipeline.py
T

131 lines
6.3 KiB
Python

"""asyncio 오케스트레이션. 원본 pipeline(무배리어)/parallel(배리어) 의미 재현."""
import asyncio
from deep_research.config import Config
from deep_research.core import Deduper, host_of, rank_claims, tally, build_synth_blocks, build_stats
from deep_research.schemas import Scope, Search, Extract, Verdict, Report
from deep_research import prompts
async def run_research(question: str, backend, *, config: Config) -> dict:
q = (question or "").strip()
if not q:
return {"error": "No research question provided."}
scope = await backend.run_agent(prompts.scope_prompt(q), Scope, label="scope")
if scope is None:
return {"error": "Scope agent returned no result — cannot decompose the question."}
deduper = Deduper(config)
sem = asyncio.Semaphore(config.CONCURRENCY)
dedup_lock = asyncio.Lock()
async def search_and_fetch(angle) -> list[dict]:
async with sem:
sr = await backend.run_agent(
prompts.search_prompt(q, angle), Search, label="search:" + angle.label)
if sr is None:
return []
async with dedup_lock: # 공유 상태(seen/fetch_slots) 임계구역 직렬화
novel = deduper.filter_novel(angle.label, [r.model_dump() for r in sr.results])
async def fetch_one(source: dict):
async with sem:
ext = await backend.run_agent(
prompts.fetch_prompt(q, source, angle.label),
Extract, label="fetch:" + host_of(source["url"]))
if ext is None:
return None
return {
"url": source["url"], "title": source["title"], "angle": angle.label,
"sourceQuality": ext.sourceQuality, "publishDate": ext.publishDate,
"claims": [{**c.model_dump(), "sourceUrl": source["url"], "sourceQuality": ext.sourceQuality}
for c in ext.claims],
}
fetched = await asyncio.gather(*[fetch_one(s) for s in novel])
return [f for f in fetched if f is not None]
per_angle = await asyncio.gather(*[search_and_fetch(a) for a in scope.angles])
all_sources = [s for sub in per_angle for s in sub]
all_claims = [c for s in all_sources for c in s["claims"]]
ranked = rank_claims(all_claims, config.MAX_VERIFY_CLAIMS)
def _sources_out():
return [{"url": s["url"], "quality": s["sourceQuality"], "angle": s["angle"],
"claimCount": len(s["claims"])} for s in all_sources]
if not ranked:
return {
"question": q,
"summary": f"No claims extracted. {len(all_sources)} sources fetched, all empty/failed.",
"findings": [], "refuted": [], "sources": _sources_out(),
"stats": {"angles": len(scope.angles), "sources": len(all_sources),
"claims": 0, "dupes": len(deduper.dupes)},
}
# ── Verify (배리어) ──
async def verify_claim(claim: dict) -> dict:
async def one_vote(v: int):
async with sem:
return await backend.run_agent(
prompts.verify_prompt(q, claim, v, config.VOTES_PER_CLAIM, config.REFUTATIONS_REQUIRED),
Verdict, label="v" + str(v) + ":" + claim["claim"][:40])
verdicts = await asyncio.gather(*[one_vote(v) for v in range(config.VOTES_PER_CLAIM)])
t = tally([vd.model_dump() if vd is not None else None for vd in verdicts],
config.VOTES_PER_CLAIM, config.REFUTATIONS_REQUIRED)
return {**claim, **t}
voted = await asyncio.gather(*[verify_claim(c) for c in ranked])
confirmed = [c for c in voted if c["survives"]]
killed = [c for c in voted if not c["survives"]]
def _refuted_out():
return [{"claim": c["claim"],
"vote": str(len(c["valid"]) - c["refutedVotes"]) + "-" + str(c["refutedVotes"]),
"source": c["sourceUrl"]} for c in killed]
if not confirmed:
return {
"question": q,
"summary": f"All {len(voted)} claims refuted by adversarial verification. Research inconclusive.",
"findings": [], "refuted": _refuted_out(), "sources": _sources_out(),
"stats": build_stats(angles=len(scope.angles), sources=len(all_sources),
claims=len(all_claims), voted=len(voted), confirmed=0,
killed=len(killed), after_synth=0, dupes=len(deduper.dupes),
budget_dropped=len(deduper.budget_dropped),
votes_per_claim=config.VOTES_PER_CLAIM),
}
# ── Synthesize ──
block, killed_block = build_synth_blocks(confirmed, killed)
report = await backend.run_agent(
prompts.synth_prompt(q, block, killed_block, len(confirmed), config.VOTES_PER_CLAIM),
Report, label="synthesize")
if report is None:
return {
"question": q,
"summary": f"Synthesis step was skipped or failed — returning {len(confirmed)} verified claims unmerged.",
"findings": [],
"confirmed": [{"claim": c["claim"], "source": c["sourceUrl"], "quote": c["quote"],
"vote": str(len(c["valid"]) - c["refutedVotes"]) + "-" + str(c["refutedVotes"])}
for c in confirmed],
"refuted": _refuted_out(), "sources": _sources_out(),
"stats": build_stats(angles=len(scope.angles), sources=len(all_sources),
claims=len(all_claims), voted=len(voted), confirmed=len(confirmed),
killed=len(killed), after_synth=0, dupes=len(deduper.dupes),
budget_dropped=len(deduper.budget_dropped),
votes_per_claim=config.VOTES_PER_CLAIM),
}
return {
"question": q,
**report.model_dump(),
"refuted": _refuted_out(), "sources": _sources_out(),
"stats": build_stats(angles=len(scope.angles), sources=len(all_sources),
claims=len(all_claims), voted=len(voted), confirmed=len(confirmed),
killed=len(killed), after_synth=len(report.findings),
dupes=len(deduper.dupes), budget_dropped=len(deduper.budget_dropped),
votes_per_claim=config.VOTES_PER_CLAIM),
}