study-buddy / app /agents /report_agent.py
GitHub Actions
deploy d092bea3608b7a29952f16357fda39b7a29e399b
2e818da
Raw
History Blame Contribute Delete
5.52 kB
"""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