Spaces:
Runtime error
Runtime error
| """ | |
| ANALYST AGENT | |
| ============= | |
| Purpose: | |
| This agent takes raw research output and extracts structured insights. | |
| Why this agent exists: | |
| The researcher returns a pile of raw text from web + vector search. | |
| That raw text is noisy, unfiltered, and full of redundancy. | |
| The analyst's job is to: | |
| • identify patterns | |
| • extract key facts | |
| • run code if math/data analysis is needed | |
| • return clean, structured findings | |
| Why a separate agent (not just the writer)? | |
| Separation of concerns — analysis ≠ writing. | |
| An analyst focuses on WHAT is true. | |
| A writer focuses on HOW to communicate it. | |
| Splitting these produces far better final reports. | |
| """ | |
| # ========================= | |
| # Imports | |
| # ========================= | |
| from langchain_groq import ChatGroq | |
| from langchain_core.prompts import ChatPromptTemplate | |
| from pydantic import BaseModel | |
| from typing import List | |
| # Import the sandboxed code executor. | |
| # WHY: | |
| # If the planner flagged requires_code=True, the analyst | |
| # needs to run computations (e.g. averages, charts, statistics). | |
| # We never let the LLM invent numbers — we make it run real code. | |
| from tools.python_repl import python_repl | |
| # ========================= | |
| # 1️⃣ Structured Output Schema | |
| # ========================= | |
| # WHY: | |
| # We force the LLM to return structured JSON instead of free text. | |
| # This makes the analyst output reliably parseable by the writer. | |
| # A free-text response would require brittle string parsing. | |
| class AnalysisResult(BaseModel): | |
| # 3-5 bullet-point insights extracted from research | |
| key_insights: List[str] | |
| # A short paragraph summarizing the overall findings | |
| summary: str | |
| # Any code output if math/data was computed (empty string if not) | |
| code_output: str | |
| # How confident is the agent in these findings (low/medium/high) | |
| confidence: str | |
| # ========================= | |
| # 2️⃣ Analyst Function | |
| # ========================= | |
| def run_analyst( | |
| research_text: str, | |
| original_query: str, | |
| requires_code: bool = False, | |
| code_task: str = "" | |
| ) -> AnalysisResult: | |
| """ | |
| Analyze raw research output → structured insights. | |
| Parameters | |
| ---------- | |
| research_text : str | |
| Raw output from the researcher agent. | |
| original_query : str | |
| The user's original question (keeps analyst focused). | |
| requires_code : bool | |
| If True, the analyst will also run Python for computation. | |
| This flag comes directly from the planner's ResearchPlan. | |
| code_task : str | |
| Optional Python code to execute if requires_code is True. | |
| Generated by the LLM in a first pass before structured output. | |
| Returns | |
| ------- | |
| AnalysisResult | |
| Structured insights, summary, code output, confidence level. | |
| """ | |
| # ========================= | |
| # Step A: Optional Code Execution | |
| # ========================= | |
| # WHY: | |
| # If the planner flagged requires_code=True, we run any | |
| # computation BEFORE calling the LLM for analysis. | |
| # | |
| # This matters because: | |
| # • LLMs hallucinate numbers — code execution gives real results | |
| # • We inject the real output INTO the LLM prompt | |
| # • This grounds the analysis in verified data | |
| code_result = "" | |
| if requires_code and code_task: | |
| print("[Analyst] Running code task...") | |
| # python_repl is sandboxed — safe to run agent-generated code. | |
| # It only allows safe libraries and has a 3-second timeout. | |
| code_result = python_repl.run(code_task) | |
| print(f"[Analyst] Code output: {code_result}") | |
| # ========================= | |
| # Step B: LLM Setup | |
| # ========================= | |
| # WHY: | |
| # We use claude-sonnet-4-6 here because analysis requires | |
| # more reasoning depth than retrieval. | |
| # | |
| # with_structured_output() forces the LLM to return valid JSON | |
| # matching the AnalysisResult schema — no parsing errors. | |
| llm = ChatGroq( | |
| model="llama-3.3-70b-versatile" | |
| ).with_structured_output(AnalysisResult) | |
| # ========================= | |
| # Step C: Prompt Construction | |
| # ========================= | |
| # WHY: | |
| # We inject three things into the prompt: | |
| # 1. The original query → keeps the analysis on-topic | |
| # 2. The raw research text → the material to analyze | |
| # 3. Code output (if any) → grounds findings in real computation | |
| # | |
| # The system message instructs the LLM to act as a critical analyst, | |
| # not a summarizer. This produces sharper, more useful insights. | |
| prompt = ChatPromptTemplate.from_messages([ | |
| ( | |
| "system", | |
| "You are a critical research analyst. " | |
| "Your job is to extract 3–5 key insights from raw research. " | |
| "Be precise. Avoid repetition. " | |
| "If code output is provided, incorporate it into your insights. " | |
| "Rate your confidence (low/medium/high) based on source quality." | |
| ), | |
| ( | |
| "human", | |
| "Original question: {query}\n\n" | |
| "Research gathered:\n{research}\n\n" | |
| "Code execution output (if any):\n{code_output}" | |
| ) | |
| ]) | |
| # ========================= | |
| # Step D: Run the Chain | |
| # ========================= | |
| # WHY: | |
| # prompt | llm → LCEL pipeline (LangChain Expression Language) | |
| # This is the standard, composable way to chain components. | |
| # invoke() executes it synchronously and returns AnalysisResult. | |
| result = (prompt | llm).invoke({ | |
| "query": original_query, | |
| "research": research_text, | |
| "code_output": code_result if code_result else "No code was executed." | |
| }) | |
| # Attach the real code output to the result before returning. | |
| # WHY: | |
| # with_structured_output fills code_output from the LLM's JSON. | |
| # We overwrite it with the REAL executed output to ensure accuracy. | |
| # LLMs cannot fabricate execution results this way. | |
| result.code_output = code_result | |
| return result | |
| # ========================= | |
| # Example test | |
| # ========================= | |
| if __name__ == "__main__": | |
| # Simulated researcher output for offline testing | |
| sample_research = """ | |
| Title: AI Job Market 2024 | |
| Summary: AI engineer salaries range from $120k to $300k in the US. | |
| Major employers include Google, OpenAI, Microsoft, and Meta. | |
| Source: techcrunch.com | |
| Title: ML Engineer Demand | |
| Summary: Demand for ML roles grew 35% YoY. Python and PyTorch are top skills. | |
| Source: linkedin.com | |
| """ | |
| # Test with code execution enabled | |
| result = run_analyst( | |
| research_text=sample_research, | |
| original_query="Analyze AI job trends and salary ranges", | |
| requires_code=True, | |
| code_task="salaries = [120000, 180000, 220000, 300000]\nprint(f'Mean: {sum(salaries)/len(salaries)}')" | |
| ) | |
| print("\n=== Analysis Result ===") | |
| print("Key Insights:", result.key_insights) | |
| print("Summary:", result.summary) | |
| print("Code Output:", result.code_output) | |
| print("Confidence:", result.confidence) | |