"""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), }