"""Report Agent -> streams textbook-style report sections from highlighted context. Used by the Live-Compiling Research Report Canvas. Each highlighted passage becomes a formatted section; a background pass may attach a grounded visual to it. """ from __future__ import annotations from typing import Any, Dict, List, Optional from pydantic import BaseModel, Field from app.agents.cerebras_client import CerebrasClient class NoteInsight(BaseModel): concept: str = Field(description="The concept/topic this note is about (≤8 words)") summary: str = Field(description="What the student captured, grounded in the source (2-4 sentences)") key_points: List[str] = Field(default_factory=list, description="0-4 atomic facts worth keeping") class ReportAgent: def __init__(self, client: Optional[CerebrasClient] = None) -> None: self._client = client or CerebrasClient() # ------------------------------------------------------------------ # # Notes → report: process each note statelessly, then synthesize # # ------------------------------------------------------------------ # def process_note( self, note_text: str, snippet_text: str, source_context: str, familiarity: str, extracted_content: str = "", ) -> NoteInsight: """One stateless pass over a single note → a grounded insight (pooled later). This is the per-note unit a per-PDF agent runs over every annotation; the output is what gets pooled (and, at scale, persisted to the PDF's Cognee cluster). """ messages = [ { "role": "system", "content": ( "You are extracting a clean, grounded insight from ONE of a student's notes on a " "research paper. Use the student's note and the surrounding source context. " "Capture what the note is really about -> do not invent beyond the source. " f"Tailor wording to the {familiarity} level." ), }, { "role": "user", "content": ( f"STUDENT NOTE:\n{note_text or '(no written note -> the student marked this region)'}\n\n" f"HIGHLIGHTED PASSAGE:\n{snippet_text[:1200]}\n\n" f"PINNED/EXTRACTED CONTENT:\n{extracted_content[:800]}\n\n" f"SURROUNDING SOURCE CONTEXT:\n{source_context[:2000]}" ), }, ] return self._client.structured_complete(messages, NoteInsight) async def synthesize_report( self, insights: List[NoteInsight], topic: str, toc_labels: List[str], familiarity: str, knowledge_mode: str = "content_only", edit_instruction: str = "", web_context: str = "", ): """Stream the final report, amalgamating all pooled note-insights into one document.""" pooled = "\n\n".join( f"### {ins.concept}\n{ins.summary}\n" + "".join(f"- {p}\n" for p in ins.key_points) for ins in insights ) toc = "\n".join(f"- {t}" for t in toc_labels) if toc_labels else "(none)" if knowledge_mode == "net_support": grounding = ( "Ground the report strictly in the pooled notes and the WEB CONTEXT provided below -> " "you may reorganize and reword for clarity, but never add a fact, figure, or claim " "that is not present in the notes or web context." ) else: grounding = "Base the report strictly on the pooled notes and their source. Never fabricate." edit_note = f"REVISION REQUEST (apply to the whole report): {edit_instruction}\n\n" if edit_instruction else "" web_block = f"\n\nWEB CONTEXT:\n{web_context}" if web_context else "" system = ( "You are compiling a student's personal research report from THEIR OWN notes on a paper. " "You are given pooled insights (one per note) and the paper's auto-generated table of " f"contents. {grounding} Tailor depth to the {familiarity} level.\n\n" "Your job: amalgamate, de-duplicate, reorganize, and reword the pooled insights into a " "single coherent, textbook-style report that follows the table of contents where it helps. " "It should read as the student's distilled understanding, not a list of notes.\n\n" "FORMAT (Markdown):\n" "- A '# ' title, then '## ' sections.\n" "- Flowing prose with **bold** key terms and bullet lists where useful.\n" "- LaTeX for math: $...$ and $$...$$.\n" "- You MAY include a ```mermaid diagram (plain-text labels, quote labels with parentheses) " "or a ```plotly JSON block for real data from the notes.\n" "- No [Source: ...] citations." ) user = ( f"{edit_note}" f"PAPER TOPIC: {topic or '(derive from the notes)'}\n\n" f"TABLE OF CONTENTS:\n{toc}\n\n" f"POOLED NOTE INSIGHTS:\n{pooled or '(no notes yet)'}{web_block}\n\n" "Write the full report now." ) messages = [ {"role": "system", "content": system}, {"role": "user", "content": user}, ] async for token in self._client.stream_complete(messages): yield token