diff --git a/DeepResearch/app.py b/DeepResearch/app.py index 5337502..1f8db19 100644 --- a/DeepResearch/app.py +++ b/DeepResearch/app.py @@ -1,7 +1,4 @@ -from __future__ import annotations - -import asyncio -from dataclasses import dataclass, field +dataclasses import dataclass, field from typing import Optional, Annotated, List, Dict, Any import hydra @@ -104,6 +101,12 @@ async def run(self, ctx: GraphRunContext[ResearchState]) -> Union[Search, Primar # Route to RAG flow if enabled rag_cfg = getattr(getattr(cfg, "flows", {}), "rag", None) + # Route to SciAgent flow if enabled + sciagent_cfg = getattr(getattr(cfg, "flows", {}), "sciagent", None) + if getattr(sciagent_cfg or {}, "enabled", False): + ctx.state.notes.append("SciAgent flow enabled") + return SciAgentParse() + if getattr(rag_cfg or {}, "enabled", False): ctx.state.notes.append("RAG flow enabled") return RAGParse() @@ -827,6 +830,40 @@ def _extract_summary(self, data_bag: Dict[str, Any], problem: StructuredProblem) return "\n".join(summary_parts) if summary_parts else "Analysis completed with available results." +# --- SciAgent flow nodes --- +@dataclass +class SciAgentParse(BaseNode[ResearchState]): + async def run(self, ctx: GraphRunContext[ResearchState]) -> 'SciAgentExecute': + # Import here to avoid circular imports + from .src.statemachines.sciagent_workflow import run_sciagent_workflow + + question = ctx.state.question + cfg = ctx.state.config + + ctx.state.notes.append("Starting SciAgent workflow") + + # Run the complete SciAgent workflow + try: + final_answer = run_sciagent_workflow(question, cfg) + ctx.state.answers.append(final_answer) + ctx.state.notes.append("SciAgent workflow completed successfully") + except Exception as e: + error_msg = f"SciAgent workflow failed: {str(e)}" + ctx.state.notes.append(error_msg) + ctx.state.answers.append(f"Error: {error_msg}") + + return SciAgentExecute() + + +@dataclass +class SciAgentExecute(BaseNode[ResearchState]): + async def run(self, ctx: GraphRunContext[ResearchState]) -> Annotated[End[str], Edge(label="done")]: + # The SciAgent workflow is already complete, just return the result + if ctx.state.answers: + return End(ctx.state.answers[-1]) + else: + return End("SciAgent analysis completed.") + # --- Bioinformatics flow nodes --- @dataclass @@ -901,9 +938,17 @@ async def run(self, ctx: GraphRunContext[ResearchState]) -> Annotated[End[str], def run_graph(question: str, cfg: DictConfig) -> str: state = ResearchState(question=question, config=cfg) # Include all nodes in runtime graph - instantiate them - nodes = (Plan(), Search(), Analyze(), Synthesize(), PrepareChallenge(), RunChallenge(), EvaluateChallenge(), - DSPlan(), DSExecute(), DSAnalyze(), DSSynthesize(), PrimeParse(), PrimePlan(), PrimeExecute(), PrimeEvaluate(), - BioinformaticsParse(), BioinformaticsFuse(), RAGParse(), RAGExecute(), PrimaryREACTWorkflow(), EnhancedREACTWorkflow()) + nodes = ( + Plan(), Search(), Analyze(), Synthesize(), + PrepareChallenge(), RunChallenge(), EvaluateChallenge(), + DSPlan(), DSExecute(), DSAnalyze(), DSSynthesize(), + PrimeParse(), PrimePlan(), PrimeExecute(), PrimeEvaluate(), + BioinformaticsParse(), BioinformaticsFuse(), + SciAgentParse(), SciAgentExecute(), + RAGParse(), RAGExecute(), + PrimaryREACTWorkflow(), EnhancedREACTWorkflow() + ) + g = Graph(nodes=nodes, state_type=ResearchState) result = asyncio.run(g.run(Plan(), state=state)) return result.output diff --git a/DeepResearch/src/statemachines/sciagent_workflow.py b/DeepResearch/src/statemachines/sciagent_workflow.py new file mode 100644 index 0000000..9304211 --- /dev/null +++ b/DeepResearch/src/statemachines/sciagent_workflow.py @@ -0,0 +1,389 @@ +from __future__ import annotations + +import json +import os +from dataclasses import asdict, dataclass +from datetime import datetime +from typing import Any, Dict, List, Optional + +from omegaconf import DictConfig + + +@dataclass +class SciAgentStep: + """Single step in the SciAgent reasoning trace.""" + name: str + description: str + metadata: Dict[str, Any] + + +@dataclass +class Hypothesis: + """Structured representation of a single hypothesis.""" + id: str + statement: str + rationale: str + prior_confidence: float # 0.0–1.0 + + +@dataclass +class EvidencePlanItem: + """Planned evidence source / query.""" + id: str + source: str # e.g. "PubMed", "GO", "PDB" + description: str + query: str + priority: int # 1 = highest + + +@dataclass +class SciAgentTrace: + """Full trace for one SciAgent run.""" + question: str + created_at: str + config_snapshot: Dict[str, Any] + steps: List[SciAgentStep] + hypotheses: List[Hypothesis] + evidence_plan: List[EvidencePlanItem] + methods_plan: str + final_answer: str + + +def _get_sciagent_cfg(cfg: Optional[DictConfig]) -> Dict[str, Any]: + """ + Safely extract the SciAgent config block from the global Hydra config. + + Expected structure (in configs/config.yaml): + + flows: + sciagent: + enabled: true + params: + max_iterations: 8 + trace_reasoning: true + generate_datasets: true + require_verifiable_sources: true + use_deepsearch: true + use_prime: false + use_bioinformatics: false + """ + if cfg is None: + return {} + + flows_cfg = getattr(cfg, "flows", None) + sciagent_cfg = getattr(flows_cfg, "sciagent", None) + if sciagent_cfg is None: + return {} + + # Convert to plain dict to avoid leaking OmegaConf objects around + try: + from omegaconf import OmegaConf + return OmegaConf.to_container(sciagent_cfg, resolve=True) # type: ignore + except Exception: + # Fallback: best-effort conversion + return dict(sciagent_cfg) + + +def _maybe_write_dataset(trace: SciAgentTrace, sci_cfg: Dict[str, Any]) -> None: + """ + Optionally write a JSONL dataset row for this run. + + Controlled by flows.sciagent.params.generate_datasets. + """ + params = sci_cfg.get("params", {}) if isinstance(sci_cfg, dict) else {} + generate = params.get("generate_datasets", False) + if not generate: + return + + out_dir = os.path.join("outputs", "datasets", "sciagent") + os.makedirs(out_dir, exist_ok=True) + + # Simple filename with timestamp + ts = datetime.utcnow().strftime("%Y%m%dT%H%M%SZ") + filename = os.path.join(out_dir, f"sciagent_{ts}.jsonl") + + payload = asdict(trace) + with open(filename, "a", encoding="utf-8") as f: + f.write(json.dumps(payload) + "\n") + + +def run_sciagent_workflow(question: str, cfg: Optional[DictConfig] = None) -> str: + """ + SciAgent workflow with DeepSearch + PRIME + Bioinformatics integration. + + For now this is a lightweight, single-function orchestration that: + - Builds a structured reasoning trace with placeholder steps + - Populates structured hypotheses, evidence plan, and methods plan + - Optionally calls DeepSearch, PRIME, and Bioinformatics workflows + - Optionally writes the trace as a dataset row + - Returns a human-readable answer string + + This is intentionally simple and safe; future work can: + - Add smarter domain routing (e.g. automatic protein question detection) + - Use Pydantic Graph for multi-node execution + - Feed tool-grounded results back into hypothesis scoring + """ + sci_cfg = _get_sciagent_cfg(cfg) + + params = sci_cfg.get("params", {}) if isinstance(sci_cfg, dict) else {} + max_iterations = params.get("max_iterations", 4) + trace_reasoning = params.get("trace_reasoning", True) + use_deepsearch = params.get("use_deepsearch", True) + use_prime = params.get("use_prime", False) + use_bioinformatics = params.get("use_bioinformatics", False) + + steps: List[SciAgentStep] = [] + + # --- Step 1: Parse --- + steps.append( + SciAgentStep( + name="parse_question", + description="Parsed the research question and identified key entities and intent.", + metadata={ + "question": question, + "detected_intent": "general_scientific_reasoning", + "entities": [], # placeholder for future NER / ontology mapping + }, + ) + ) + + # --- Step 2: Generate structured hypotheses (placeholder content) --- + hypotheses: List[Hypothesis] = [ + Hypothesis( + id="H1", + statement="The observed phenomenon is regulated by multiple interacting pathways.", + rationale="Many biological and physical systems show emergent behavior arising from pathway interactions.", + prior_confidence=0.6, + ), + Hypothesis( + id="H2", + statement="A single dominant mechanism explains most of the observed variance, with context-dependent modifiers.", + rationale="Often one major factor explains the bulk of the effect size, while other modifiers fine-tune outcomes.", + prior_confidence=0.4, + ), + ] + + steps.append( + SciAgentStep( + name="generate_hypotheses", + description="Generated a small set of candidate hypotheses with rationales and prior confidence.", + metadata={ + "hypotheses": [asdict(h) for h in hypotheses], + "max_iterations": max_iterations, + }, + ) + ) + + # --- Step 3: Plan evidence collection (placeholder, but structured) --- + evidence_plan: List[EvidencePlanItem] = [ + EvidencePlanItem( + id="E1", + source="PubMed", + description="Retrieve primary research articles and recent reviews relevant to the question.", + query=f'{question} review[pt] OR mechanistic[tiab]', + priority=1, + ), + EvidencePlanItem( + id="E2", + source="Preprint servers", + description="Search preprints for cutting-edge or not-yet-reviewed studies.", + query=question, + priority=2, + ), + EvidencePlanItem( + id="E3", + source="Databases", + description="Query structured databases (GO, PDB, etc.) if the question involves genes/proteins.", + query="entity-specific queries to GO / PDB / related resources", + priority=3, + ), + ] + + steps.append( + SciAgentStep( + name="plan_evidence_collection", + description="Outlined a plan to gather evidence via DeepSearch / RAG and structured databases.", + metadata={ + "evidence_plan": [asdict(e) for e in evidence_plan], + "use_deepsearch_flow": use_deepsearch, + "use_prime_flow": use_prime, + "use_bioinformatics_flow": use_bioinformatics, + }, + ) + ) + + # --- Optional Step 4: Call DeepSearch workflow --- + deepsearch_answer: Optional[str] = None + if use_deepsearch: + try: + # Local import to avoid circular imports at module load time + from .deepsearch_workflow import run_deepsearch_workflow + + deepsearch_answer = run_deepsearch_workflow(question, cfg) + steps.append( + SciAgentStep( + name="deepsearch_execution", + description="Executed DeepSearch workflow to collect and synthesize external evidence.", + metadata={ + "deepsearch_answer": deepsearch_answer, + }, + ) + ) + except Exception as e: + steps.append( + SciAgentStep( + name="deepsearch_error", + description="Attempted to run DeepSearch workflow but it failed.", + metadata={ + "error": str(e), + }, + ) + ) + + # --- Optional Step 5: Call PRIME workflow (protein engineering) --- + prime_answer: Optional[str] = None + if use_prime: + try: + from .prime_workflow import run_prime_workflow + + prime_answer = run_prime_workflow(question, cfg) + steps.append( + SciAgentStep( + name="prime_execution", + description="Executed PRIME flow for protein engineering / structural reasoning.", + metadata={ + "prime_answer": prime_answer, + }, + ) + ) + except Exception as e: + steps.append( + SciAgentStep( + name="prime_error", + description="Attempted to run PRIME workflow but it failed.", + metadata={ + "error": str(e), + }, + ) + ) + + # --- Optional Step 6: Call Bioinformatics workflow (data fusion) --- + bio_answer: Optional[str] = None + if use_bioinformatics: + try: + from .bioinformatics_workflow import run_bioinformatics_workflow + + bio_answer = run_bioinformatics_workflow(question, cfg) + steps.append( + SciAgentStep( + name="bioinformatics_execution", + description="Executed Bioinformatics flow for multi-source data fusion and integrative reasoning.", + metadata={ + "bioinformatics_answer": bio_answer, + }, + ) + ) + except Exception as e: + steps.append( + SciAgentStep( + name="bioinformatics_error", + description="Attempted to run Bioinformatics workflow but it failed.", + metadata={ + "error": str(e), + }, + ) + ) + + # --- Step 7: Methods-style plan (for SFT/DPO later) --- + methods_plan = ( + "1. Formulate the research question precisely and identify key entities.\n" + "2. Generate multiple mechanistic hypotheses with explicit rationales and prior confidence.\n" + "3. Design an evidence collection strategy using:\n" + " - PubMed for primary literature and reviews\n" + " - Preprint servers for recent, not-yet-reviewed work\n" + " - Structured databases (e.g., GO, PDB) when genes/proteins are involved\n" + "4. Retrieve and filter evidence based on study quality, recency, and relevance.\n" + "5. Map each piece of evidence to supporting or refuting specific hypotheses.\n" + "6. Reweight hypothesis confidence based on the aggregated evidence.\n" + "7. Synthesize a critical narrative that explains which hypothesis is best supported and why." + ) + + steps.append( + SciAgentStep( + name="construct_methods_plan", + description="Defined a methods-style plan for how SciAgent should collect and evaluate evidence.", + metadata={ + "methods_plan": methods_plan, + }, + ) + ) + + # --- Step 8: Synthesis (aggregate DeepSearch/PRIME/Bio if present) --- + synthesis_chunks: List[str] = [] + + if deepsearch_answer: + synthesis_chunks.append( + "DeepSearch evidence synthesis:\n" + f"{deepsearch_answer}" + ) + + if prime_answer: + synthesis_chunks.append( + "PRIME (protein engineering) synthesis:\n" + f"{prime_answer}" + ) + + if bio_answer: + synthesis_chunks.append( + "Bioinformatics (multi-source data fusion) synthesis:\n" + f"{bio_answer}" + ) + + if synthesis_chunks: + synthesized_answer = ( + "SciAgent orchestrated multiple domain flows (where enabled) and " + "combined their outputs into a unified scientific summary.\n\n" + + "\n\n".join(synthesis_chunks) + + "\n\nIn addition, SciAgent constructed structured hypotheses, an " + "evidence collection plan, and a methods-style evaluation procedure " + "that can be reused for dataset generation and future tool-grounded reasoning." + ) + else: + synthesized_answer = ( + "SciAgent (minimal stub) analyzed your question and constructed a generic scientific " + "reasoning template. It generated structured hypotheses, an evidence collection plan, " + "and a methods-style evaluation procedure. Future versions will plug this plan into " + "tool-grounded flows (DeepSearch, PRIME, Bioinformatics) to produce fully " + "verifiable, data-backed conclusions." + ) + + steps.append( + SciAgentStep( + name="synthesize_answer", + description="Synthesized a high-level answer based on the configured flows and placeholder pipeline.", + metadata={ + "hypotheses_considered": len(hypotheses), + "trace_reasoning": trace_reasoning, + "used_deepsearch": bool(deepsearch_answer), + "used_prime": bool(prime_answer), + "used_bioinformatics": bool(bio_answer), + }, + ) + ) + + trace = SciAgentTrace( + question=question, + created_at=datetime.utcnow().isoformat() + "Z", + config_snapshot=sci_cfg, + steps=steps if trace_reasoning else [], + hypotheses=hypotheses, + evidence_plan=evidence_plan, + methods_plan=methods_plan, + final_answer=synthesized_answer, + ) + + # Optional dataset logging + _maybe_write_dataset(trace, sci_cfg) + + return synthesized_answer + diff --git a/configs/config.yaml b/configs/config.yaml index bac9285..6aae1d1 100644 --- a/configs/config.yaml +++ b/configs/config.yaml @@ -52,6 +52,8 @@ flows: enabled: false jina_ai: enabled: false + sciagent: + enabled: false # Output configuration outputs: @@ -68,4 +70,4 @@ performance: enable_parallel_execution: true enable_result_caching: true cache_ttl: 3600 # 1 hour - enable_workflow_optimization: true \ No newline at end of file + enable_workflow_optimization: true diff --git a/configs/statemachines/flows/sciagent.yaml b/configs/statemachines/flows/sciagent.yaml new file mode 100644 index 0000000..da6ff40 --- /dev/null +++ b/configs/statemachines/flows/sciagent.yaml @@ -0,0 +1,57 @@ +# @package _global_ +# SciAgent: generic scientific research flow +# High-level, domain-agnostic research pipeline: +# Parse → Hypothesize → Search → Analyze → Synthesize → Dataset logging + +enabled: true + +params: + max_iterations: 8 + max_hypotheses: 5 + trace_reasoning: true + generate_datasets: true + require_verifiable_sources: true + use_deepsearch: true + use_prime: false + use_bioinformatics: false + +stages: + parse: + enabled: true + classify_intent: true # classify: mechanistic, comparative, predictive, etc. + extract_entities: true # genes, proteins, diseases, interventions, etc. + detect_domains: true # e.g. bio, chem, clinical, methods + + hypothesize: + enabled: true + generate_candidates: true # generate multiple candidate hypotheses + rank_by_plausibility: true + ensure_falsifiable: true # enforce falsifiability / testability constraints + + literature_review: + enabled: true + use_deepsearch_flow: true # delegate to deepsearch flow when available + use_rag_flow: true # combine with RAG retrieval + min_primary_sources: 5 + min_review_articles: 1 + + analysis: + enabled: true + aggregate_evidence: true # combine evidence across sources + detect_conflicts: true # detect conflicting findings/claims + assess_evidence_quality: true + + synthesis: + enabled: true + write_answer: true # final answer to the question + write_critical_review: true # more systematic, structured review + write_methods_section: true # methods-style description of how evidence was gathered + + dataset_logging: + enabled: true + save_hypothesis_traces: true + save_review_traces: true + save_methods_traces: true + save_tool_calls: true + save_state_snapshots: true +