View documents, group search results under questions

This commit is contained in:
Yiorgis Gozadinos 2025-10-17 15:38:29 +03:00
parent cc3d1c7e43
commit a3e37dce5a
No known key found for this signature in database
3 changed files with 527 additions and 232 deletions

View file

@ -21,21 +21,30 @@ class ResearchState(BaseModel):
phase: str = "idle" # idle|planning|searching|analyzing|evaluating|done phase: str = "idle" # idle|planning|searching|analyzing|evaluating|done
status: str = "" # Human-readable message status: str = "" # Human-readable message
# Research plan # Research plan with embedded search results
plan: list[dict] = [] # [{id, question, status: pending|searching|done}] plan: list[
dict
] = [] # [{id, question, status: pending|searching|done, search_results: {type, results: [...]}}]
current_question_index: int = 0 current_question_index: int = 0
# Search results (live updates)
current_search: dict | None = (
None # {query, type, results: [{chunk, score, expanded}]}
)
# Accumulated findings # Accumulated findings
insights: list[dict] = [] # [{summary, confidence, sources}] insights: list[
dict
] = [] # [{summary, confidence, source_refs: [{chunk_id, document_uri, document_title, chunk_position}]}]
# Document registry - tracks all referenced documents
document_registry: dict[
str, dict
] = {} # {doc_uri: {title, chunks_referenced: [chunk_id]}}
# Document viewer state
current_document: dict | None = None # {uri, title, content, total_chunks}
# Final output # Final output
confidence: float = 0.0 confidence: float = 0.0
final_report: dict | None = None final_report: dict | None = (
None # {title, summary, findings, conclusions, citations: [{document_uri, document_title, chunk_ids}]}
)
@dataclass @dataclass
@ -69,17 +78,28 @@ You work step-by-step with the user to conduct deep research on complex question
Your workflow: Your workflow:
1. When user asks a question, propose a research plan (3-5 sub-questions) 1. When user asks a question, propose a research plan (3-5 sub-questions)
2. Wait for user approval before proceeding 2. Wait for user approval before proceeding
3. For each sub-question: 3. For each sub-question IN ORDER (0, 1, 2, etc.):
- Announce what you're searching for - Announce what you're searching for
- Execute search and show results with scores - Execute search_question for that question ID
- Extract insights from the results - IMMEDIATELY extract insights using extract_insights_from_results with the SAME question ID
- Ask user if they want to continue to next question - Ask user if they want to continue to next question
4. Evaluate overall confidence in your findings 4. Evaluate overall confidence in your findings
5. Ask user if confident enough or should search more 5. Ask user if confident enough or should search more
6. Synthesize final report with citations 6. Synthesize final report with complete citations
CRITICAL RULES:
- ALWAYS call search_question BEFORE extract_insights_from_results for each question
- Process questions in sequence: search Q0 extract Q0 search Q1 extract Q1, etc.
- NEVER skip ahead to extract insights for a question you haven't searched yet
Document Viewing:
- Users can request to view the full content of any cited document
- When a user asks to "show document X" or "view source Y", use the get_full_document tool
- Document URIs are tracked automatically as you search
- The final report includes structured citations linking back to source documents
Be transparent: always announce what you're doing before you do it. Be transparent: always announce what you're doing before you do it.
Show search scores, explain your reasoning, and involve the user in decisions. Show search scores, explain your reasoning, cite your sources, and involve the user in decisions.
""", """,
) )
@ -181,31 +201,56 @@ Return ONLY a JSON array of sub-questions, like: ["Question 1?", "Question 2?",
else: else:
expanded_map = {} expanded_map = {}
# Process results # Process results and update document registry
results = [] results = []
for chunk, score in search_results: for chunk, score in search_results:
# Update document registry
doc_uri = chunk.document_uri or "unknown"
doc_title = chunk.document_title or chunk.document_uri or "Unknown"
if doc_uri not in ctx.deps.state.document_registry:
ctx.deps.state.document_registry[doc_uri] = {
"title": doc_title,
"chunks_referenced": [],
}
if (
chunk.id
not in ctx.deps.state.document_registry[doc_uri]["chunks_referenced"]
):
ctx.deps.state.document_registry[doc_uri]["chunks_referenced"].append(
chunk.id
)
# Check if this chunk was expanded # Check if this chunk was expanded
if chunk.id in expanded_map: if chunk.id in expanded_map:
expanded_chunk, _ = expanded_map[chunk.id] expanded_chunk, _ = expanded_map[chunk.id]
result_data = { result_data = {
"chunk": expanded_chunk.content[:500], # Truncate for display "chunk": expanded_chunk.content[:500], # Truncate for display
"chunk_id": chunk.id,
"document_uri": doc_uri,
"document_title": doc_title,
"chunk_position": chunk.order,
"full_chunk_content": expanded_chunk.content,
"score": round(score, 3), "score": round(score, 3),
"source": chunk.document_title or chunk.document_uri or "Unknown",
"expanded": True, "expanded": True,
} }
else: else:
result_data = { result_data = {
"chunk": chunk.content[:500], # Truncate for display "chunk": chunk.content[:500], # Truncate for display
"chunk_id": chunk.id,
"document_uri": doc_uri,
"document_title": doc_title,
"chunk_position": chunk.order,
"full_chunk_content": chunk.content,
"score": round(score, 3), "score": round(score, 3),
"source": chunk.document_title or chunk.document_uri or "Unknown",
"expanded": False, "expanded": False,
} }
results.append(result_data) results.append(result_data)
# Update state # Store search results in the plan item
ctx.deps.state.current_search = { plan[question_id]["search_results"] = {
"query": question,
"type": search_type, "type": search_type,
"results": results, "results": results,
} }
@ -218,29 +263,52 @@ Return ONLY a JSON array of sub-questions, like: ["Question 1?", "Question 2?",
@agent.tool @agent.tool
async def extract_insights_from_results( async def extract_insights_from_results(
ctx: RunContext[ResearchDeps], ctx: RunContext[ResearchDeps],
question_id: int,
) -> StateSnapshotEvent: ) -> StateSnapshotEvent:
"""Extract key insights from current search results.""" """Extract key insights from search results for a specific question.
current_search = ctx.deps.state.current_search
if not current_search: IMPORTANT: You must call search_question for this question_id BEFORE calling this tool.
raise ValueError("No current search results to analyze") This tool requires that search results already exist for the given question.
Args:
question_id: ID of the question whose results to analyze
"""
plan = ctx.deps.state.plan
if question_id >= len(plan):
raise ValueError(f"Question ID {question_id} not found in plan")
question_item = plan[question_id]
if "search_results" not in question_item:
raise ValueError(
f"No search results found for question ID {question_id}. "
f"You must call search_question(question_id={question_id}) first before extracting insights."
)
search_results = question_item["search_results"]
# Update state # Update state
ctx.deps.state.phase = "analyzing" ctx.deps.state.phase = "analyzing"
ctx.deps.state.status = "Extracting insights from results..." ctx.deps.state.status = "Extracting insights from results..."
# Build context from results # Build context from results with chunk IDs for reference
context = "\n\n".join( context_parts = []
[f"[Source: {r['source']}] {r['chunk']}" for r in current_search["results"]] for idx, r in enumerate(search_results["results"]):
) context_parts.append(
f"[Result {idx}] [Source: {r['document_title']}] {r['full_chunk_content']}"
)
context = "\n\n".join(context_parts)
# Use LLM to extract insights # Use LLM to extract insights
extract_prompt = f"""Analyze these search results and extract 1-3 key insights that help answer the question: "{current_search["query"]}" question_text = question_item["question"]
extract_prompt = f"""Analyze these search results and extract 1-3 key insights that help answer the question: "{question_text}"
Search Results: Search Results:
{context} {context}
For each insight, reference which result numbers (0, 1, 2, etc.) support it.
Return a JSON array of insights with format: Return a JSON array of insights with format:
[{{"summary": "brief insight", "confidence": 0.0-1.0, "sources": ["source1", "source2"]}}]""" [{{"summary": "brief insight", "confidence": 0.0-1.0, "result_indices": [0, 1, ...]}}]"""
response = await ctx.deps.client.ask(extract_prompt) response = await ctx.deps.client.ask(extract_prompt)
@ -248,22 +316,48 @@ Return a JSON array of insights with format:
import json import json
try: try:
new_insights = json.loads(response) raw_insights = json.loads(response)
except json.JSONDecodeError: except json.JSONDecodeError:
# Fallback: create simple insight # Fallback: create simple insight referencing all results
new_insights = [ raw_insights = [
{ {
"summary": response[:200], "summary": response[:200],
"confidence": 0.7, "confidence": 0.7,
"sources": [r["source"] for r in current_search["results"][:3]], "result_indices": list(
range(min(3, len(search_results["results"])))
),
} }
] ]
# Convert result indices to structured source references
new_insights = []
for insight in raw_insights:
result_indices = insight.get("result_indices", [])
source_refs = []
for idx in result_indices:
if 0 <= idx < len(search_results["results"]):
result = search_results["results"][idx]
source_refs.append(
{
"chunk_id": result["chunk_id"],
"document_uri": result["document_uri"],
"document_title": result["document_title"],
"chunk_position": result["chunk_position"],
}
)
new_insights.append(
{
"summary": insight["summary"],
"confidence": insight.get("confidence", 0.7),
"source_refs": source_refs,
}
)
# Add to accumulated insights # Add to accumulated insights
ctx.deps.state.insights.extend(new_insights) ctx.deps.state.insights.extend(new_insights)
# Clear current search
ctx.deps.state.current_search = None
ctx.deps.state.status = f"Extracted {len(new_insights)} insights" ctx.deps.state.status = f"Extracted {len(new_insights)} insights"
print("[AGENT] Insights extracted, sending state snapshot") print("[AGENT] Insights extracted, sending state snapshot")
@ -336,17 +430,28 @@ Return JSON: {{"confidence": 0.0-1.0, "gaps": ["gap1", "gap2"], "recommendation"
ctx.deps.state.phase = "synthesizing" ctx.deps.state.phase = "synthesizing"
ctx.deps.state.status = "Generating final report..." ctx.deps.state.status = "Generating final report..."
# Build summary of insights with source information
insights_summary = []
for i in insights:
source_titles = [ref["document_title"] for ref in i.get("source_refs", [])]
unique_sources = list(
dict.fromkeys(source_titles)
) # Preserve order, remove duplicates
insights_summary.append(
f"- {i['summary']} (sources: {', '.join(unique_sources[:2])})"
)
# Build report prompt # Build report prompt
report_prompt = f"""Generate a comprehensive research report answering: "{ctx.deps.state.question}" report_prompt = f"""Generate a comprehensive research report answering: "{ctx.deps.state.question}"
Based on these insights: Based on these insights:
{chr(10).join([f"- {i['summary']} (sources: {', '.join(i.get('sources', [])[:2])})" for i in insights])} {chr(10).join(insights_summary)}
Create a structured report with: Create a structured report with:
- Executive Summary (2-3 sentences) - Executive Summary (2-3 sentences)
- Main Findings (bullet points) - Main Findings (bullet points)
- Conclusions - Conclusions
- Sources - Sources (list the document titles mentioned above)
Return JSON with format: Return JSON with format:
{{ {{
@ -371,11 +476,23 @@ Return JSON with format:
"summary": response[:300], "summary": response[:300],
"findings": [i["summary"] for i in insights], "findings": [i["summary"] for i in insights],
"conclusions": ["See findings above"], "conclusions": ["See findings above"],
"sources": list( "sources": [],
set([s for i in insights for s in i.get("sources", [])])
),
} }
# Build structured citations from document registry
citations = []
for doc_uri, doc_info in ctx.deps.state.document_registry.items():
citations.append(
{
"document_uri": doc_uri,
"document_title": doc_info["title"],
"chunk_ids": doc_info["chunks_referenced"],
}
)
# Add citations to report
report["citations"] = citations
# Update state # Update state
ctx.deps.state.final_report = report ctx.deps.state.final_report = report
ctx.deps.state.phase = "done" ctx.deps.state.phase = "done"
@ -384,4 +501,54 @@ Return JSON with format:
return _as_state_snapshot(ctx) return _as_state_snapshot(ctx)
@agent.tool
async def get_full_document(
ctx: RunContext[ResearchDeps],
document_uri: str,
) -> StateSnapshotEvent:
"""Retrieve and display the full content of a document by its URI.
Args:
document_uri: The URI identifier of the document to retrieve
"""
# Update state
ctx.deps.state.status = f"Retrieving document: {document_uri}"
# Get document from haiku.rag
document = await ctx.deps.client.get_document_by_uri(document_uri)
if document is None:
ctx.deps.state.status = f"Document not found: {document_uri}"
ctx.deps.state.current_document = {
"uri": document_uri,
"title": "Not Found",
"content": f"Document with URI '{document_uri}' was not found in the database.",
"total_chunks": 0,
}
else:
# Get all chunks for this document to count them
all_chunks = await ctx.deps.client.search(
query="", # Empty query to get all chunks
limit=1000,
search_type="fts",
)
chunks_for_doc = [
c for c, _ in all_chunks if c.document_uri == document_uri
]
ctx.deps.state.current_document = {
"uri": document.uri or document_uri,
"title": document.title or "Untitled",
"content": document.content,
"total_chunks": len(chunks_for_doc),
"metadata": document.metadata,
}
ctx.deps.state.status = (
f"Retrieved document: {document.title or document_uri}"
)
print(f"[AGENT] Document retrieved: {document_uri}")
return _as_state_snapshot(ctx)
return agent return agent

View file

@ -9,31 +9,55 @@ import { CopilotChat } from "@copilotkit/react-ui";
import "@copilotkit/react-ui/styles.css"; import "@copilotkit/react-ui/styles.css";
import StateDisplay from "./StateDisplay"; import StateDisplay from "./StateDisplay";
interface SourceRef {
chunk_id: string;
document_uri: string;
document_title: string;
chunk_position: number;
}
interface ResearchState { interface ResearchState {
question: string; question: string;
phase: string; // idle|planning|searching|analyzing|evaluating|done phase: string;
status: string; status: string;
plan: Array<{ plan: Array<{
id: number; id: number;
question: string; question: string;
status: string; // pending|searching|done status: string;
search_results?: {
type: string;
results: Array<{
chunk: string;
chunk_id: string;
document_uri: string;
document_title: string;
chunk_position: number;
full_chunk_content: string;
score: number;
expanded: boolean;
}>;
};
}>; }>;
current_question_index: number; current_question_index: number;
current_search: {
query: string;
type: string;
results?: Array<{
chunk: string;
score: number;
source: string;
expanded: boolean;
}>;
} | null;
insights: Array<{ insights: Array<{
summary: string; summary: string;
confidence: number; confidence: number;
sources: string[]; source_refs: SourceRef[];
}>; }>;
document_registry: Record<
string,
{
title: string;
chunks_referenced: string[];
}
>;
current_document: {
uri: string;
title: string;
content: string;
total_chunks: number;
metadata?: Record<string, unknown>;
} | null;
confidence: number; confidence: number;
final_report: { final_report: {
title: string; title: string;
@ -41,6 +65,11 @@ interface ResearchState {
findings: string[]; findings: string[];
conclusions: string[]; conclusions: string[];
sources: string[]; sources: string[];
citations: Array<{
document_uri: string;
document_title: string;
chunk_ids: string[];
}>;
} | null; } | null;
} }
@ -54,8 +83,9 @@ function AgentContent() {
status: "", status: "",
plan: [], plan: [],
current_question_index: 0, current_question_index: 0,
current_search: null,
insights: [], insights: [],
document_registry: {},
current_document: null,
confidence: 0.0, confidence: 0.0,
final_report: null, final_report: null,
}, },
@ -76,9 +106,7 @@ function AgentContent() {
phaseMessage = "Planning research..."; phaseMessage = "Planning research...";
break; break;
case "searching": case "searching":
phaseMessage = newState.current_search phaseMessage = "Searching...";
? `Searching: ${newState.current_search.query}`
: "Searching...";
break; break;
case "analyzing": case "analyzing":
phaseMessage = "Extracting insights..."; phaseMessage = "Extracting insights...";

View file

@ -2,6 +2,13 @@
import { useState } from "react"; import { useState } from "react";
interface SourceRef {
chunk_id: string;
document_uri: string;
document_title: string;
chunk_position: number;
}
interface ResearchState { interface ResearchState {
question: string; question: string;
phase: string; phase: string;
@ -10,23 +17,40 @@ interface ResearchState {
id: number; id: number;
question: string; question: string;
status: string; status: string;
search_results?: {
type: string;
results: Array<{
chunk: string;
chunk_id: string;
document_uri: string;
document_title: string;
chunk_position: number;
full_chunk_content: string;
score: number;
expanded: boolean;
}>;
};
}>; }>;
current_question_index: number; current_question_index: number;
current_search: {
query: string;
type: string;
results?: Array<{
chunk: string;
score: number;
source: string;
expanded: boolean;
}>;
} | null;
insights: Array<{ insights: Array<{
summary: string; summary: string;
confidence: number; confidence: number;
sources: string[]; source_refs: SourceRef[];
}>; }>;
document_registry: Record<
string,
{
title: string;
chunks_referenced: string[];
}
>;
current_document: {
uri: string;
title: string;
content: string;
total_chunks: number;
metadata?: Record<string, unknown>;
} | null;
confidence: number; confidence: number;
final_report: { final_report: {
title: string; title: string;
@ -34,6 +58,11 @@ interface ResearchState {
findings: string[]; findings: string[];
conclusions: string[]; conclusions: string[];
sources: string[]; sources: string[];
citations: Array<{
document_uri: string;
document_title: string;
chunk_ids: string[];
}>;
} | null; } | null;
} }
@ -46,11 +75,15 @@ export default function StateDisplay({ state }: StateDisplayProps) {
Record<string, boolean> Record<string, boolean>
>({ >({
plan: true, plan: true,
search: true,
insights: true, insights: true,
report: true, report: true,
document: true,
}); });
const [expandedQuestions, setExpandedQuestions] = useState<
Record<number, boolean>
>({});
const toggleSection = (section: string) => { const toggleSection = (section: string) => {
setExpandedSections((prev) => ({ setExpandedSections((prev) => ({
...prev, ...prev,
@ -58,6 +91,13 @@ export default function StateDisplay({ state }: StateDisplayProps) {
})); }));
}; };
const toggleQuestion = (questionId: number) => {
setExpandedQuestions((prev) => ({
...prev,
[questionId]: !prev[questionId],
}));
};
// Phase indicator // Phase indicator
const phases = [ const phases = [
"idle", "idle",
@ -271,174 +311,170 @@ export default function StateDisplay({ state }: StateDisplayProps) {
<div <div
key={item.id} key={item.id}
style={{ style={{
padding: "0.75rem", marginBottom: "0.5rem",
background: "white", background: "white",
borderRadius: "4px", borderRadius: "4px",
marginBottom: "0.5rem",
border: "1px solid #e2e8f0", border: "1px solid #e2e8f0",
display: "flex", overflow: "hidden",
gap: "0.75rem",
}} }}
> >
<div <button
type="button"
onClick={() => toggleQuestion(item.id)}
style={{ style={{
fontSize: "1.25rem", width: "100%",
color: display: "flex",
item.status === "done" gap: "0.75rem",
? "#48bb78" padding: "0.75rem",
: item.status === "searching" background: "white",
? "#4299e1" border: "none",
: "#a0aec0", cursor: "pointer",
textAlign: "left",
alignItems: "center",
}} }}
> >
{item.status === "done"
? "✓"
: item.status === "searching"
? "🔍"
: "⏳"}
</div>
<div style={{ flex: 1 }}>
<div <div
style={{ style={{
fontSize: "0.875rem", fontSize: "1.25rem",
color: "#4a5568", color:
item.status === "done"
? "#48bb78"
: item.status === "searching"
? "#4299e1"
: "#a0aec0",
flexShrink: 0,
}} }}
> >
{item.question} {item.status === "done"
? "✓"
: item.status === "searching"
? "🔍"
: "⏳"}
</div> </div>
</div> <div style={{ flex: 1 }}>
</div>
))}
</div>
)}
</div>
)}
{/* Current Search Results */}
{state.current_search && (
<div
style={{
background: "white",
borderRadius: "8px",
boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
overflow: "hidden",
}}
>
<button
type="button"
onClick={() => toggleSection("search")}
style={{
width: "100%",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
padding: "0.75rem",
background: "#edf2f7",
border: "1px solid #e2e8f0",
borderRadius: "4px",
cursor: "pointer",
fontSize: "1rem",
fontWeight: "600",
color: "#2d3748",
}}
>
<span>
Search Results: {state.current_search.query.substring(0, 50)}...
</span>
<span>{expandedSections.search ? "▼" : "▶"}</span>
</button>
{expandedSections.search && (
<div
style={{
padding: "1rem",
background: "#f7fafc",
border: "1px solid #e2e8f0",
borderTop: "none",
borderRadius: "0 0 4px 4px",
}}
>
{state.current_search.results && (
<div>
<div
style={{
fontSize: "0.875rem",
color: "#718096",
marginBottom: "0.5rem",
}}
>
Type: {state.current_search.type} |{" "}
{state.current_search.results.length} results
</div>
{state.current_search.results.map((result, idx) => (
<div
key={`${result.source}-${idx}`}
style={{
padding: "0.75rem",
background: "white",
borderRadius: "4px",
marginBottom: "0.5rem",
border: "1px solid #e2e8f0",
}}
>
<div
style={{
display: "flex",
justifyContent: "space-between",
marginBottom: "0.5rem",
}}
>
<span
style={{
fontSize: "0.875rem",
fontWeight: "600",
color: "#4a5568",
}}
>
{result.source}
</span>
<div style={{ display: "flex", gap: "0.5rem" }}>
{result.expanded && (
<span
style={{
fontSize: "0.75rem",
padding: "0.125rem 0.5rem",
background: "#bee3f8",
color: "#2c5282",
borderRadius: "4px",
}}
>
Expanded
</span>
)}
<span
style={{
fontSize: "0.875rem",
fontWeight: "bold",
color:
result.score > 0.8
? "#48bb78"
: result.score > 0.6
? "#ed8936"
: "#a0aec0",
}}
>
{result.score.toFixed(2)}
</span>
</div>
</div>
<div <div
style={{ style={{
fontSize: "0.875rem", fontSize: "0.875rem",
color: "#718096", color: "#4a5568",
lineHeight: "1.4",
}} }}
> >
{result.chunk}... {item.question}
</div> </div>
{item.search_results && (
<div
style={{
fontSize: "0.75rem",
color: "#718096",
marginTop: "0.25rem",
}}
>
{item.search_results.results.length} results
</div>
)}
</div> </div>
))} {item.search_results && (
<span
style={{
fontSize: "0.875rem",
color: "#718096",
}}
>
{expandedQuestions[item.id] ? "▼" : "▶"}
</span>
)}
</button>
{/* Search Results nested inside question */}
{expandedQuestions[item.id] && item.search_results && (
<div
style={{
padding: "1rem",
background: "#f7fafc",
borderTop: "1px solid #e2e8f0",
}}
>
<div
style={{
fontSize: "0.75rem",
color: "#718096",
marginBottom: "0.5rem",
fontWeight: "600",
}}
>
Search Type: {item.search_results.type}
</div>
{item.search_results.results.map((result, idx) => (
<div
key={`${result.chunk_id}-${idx}`}
style={{
padding: "0.75rem",
background: "white",
borderRadius: "4px",
marginBottom: "0.5rem",
border: "1px solid #e2e8f0",
}}
>
<div
style={{
display: "flex",
justifyContent: "space-between",
marginBottom: "0.5rem",
}}
>
<span
style={{
fontSize: "0.875rem",
fontWeight: "600",
color: "#2d3748",
}}
>
{result.document_title}
</span>
<div style={{ display: "flex", gap: "0.5rem" }}>
{result.expanded && (
<span
style={{
fontSize: "0.75rem",
padding: "0.125rem 0.5rem",
background: "#bee3f8",
color: "#2c5282",
borderRadius: "4px",
}}
>
Expanded
</span>
)}
<span
style={{
fontSize: "0.875rem",
fontWeight: "bold",
color:
result.score > 0.8
? "#48bb78"
: result.score > 0.6
? "#ed8936"
: "#a0aec0",
}}
>
{result.score.toFixed(2)}
</span>
</div>
</div>
<div
style={{
fontSize: "0.875rem",
color: "#718096",
lineHeight: "1.4",
}}
>
{result.chunk}...
</div>
</div>
))}
</div>
)}
</div> </div>
)} ))}
</div> </div>
)} )}
</div> </div>
@ -520,7 +556,7 @@ export default function StateDisplay({ state }: StateDisplayProps) {
color: "#718096", color: "#718096",
}} }}
> >
{insight.sources.length} sources {insight.source_refs?.length || 0} sources
</span> </span>
</div> </div>
<div <div
@ -528,10 +564,30 @@ export default function StateDisplay({ state }: StateDisplayProps) {
fontSize: "0.875rem", fontSize: "0.875rem",
color: "#2d3748", color: "#2d3748",
lineHeight: "1.5", lineHeight: "1.5",
marginBottom: "0.5rem",
}} }}
> >
{insight.summary} {insight.summary}
</div> </div>
{insight.source_refs && insight.source_refs.length > 0 && (
<div
style={{
fontSize: "0.75rem",
color: "#718096",
marginTop: "0.5rem",
}}
>
<span style={{ fontWeight: "600" }}>Sources: </span>
{insight.source_refs.map((ref, refIdx) => (
<span key={ref.chunk_id}>
{refIdx > 0 && ", "}
<span style={{ fontSize: "0.75rem" }}>
{ref.document_title}
</span>
</span>
))}
</div>
)}
</div> </div>
))} ))}
</div> </div>
@ -672,21 +728,65 @@ export default function StateDisplay({ state }: StateDisplayProps) {
marginBottom: "0.5rem", marginBottom: "0.5rem",
}} }}
> >
Sources Citations
</h4> </h4>
<div {state.final_report.citations &&
style={{ state.final_report.citations.length > 0 ? (
fontSize: "0.75rem", <div
color: "#718096", style={{
lineHeight: "1.4", display: "flex",
}} flexDirection: "column",
> gap: "0.5rem",
{state.final_report.sources.map((source) => ( }}
<div key={source} style={{ marginBottom: "0.25rem" }}> >
{source} {state.final_report.citations.map((citation) => (
</div> <div
))} key={citation.document_uri}
</div> style={{
padding: "0.5rem",
background: "#f7fafc",
borderRadius: "4px",
border: "1px solid #e2e8f0",
}}
>
<div
style={{
fontSize: "0.875rem",
fontWeight: "600",
color: "#2d3748",
marginBottom: "0.25rem",
}}
>
{citation.document_title}
</div>
<div
style={{
fontSize: "0.75rem",
color: "#718096",
}}
>
{citation.chunk_ids.length} chunk
{citation.chunk_ids.length !== 1 ? "s" : ""}{" "}
referenced
</div>
</div>
))}
</div>
) : (
<div
style={{
fontSize: "0.75rem",
color: "#718096",
lineHeight: "1.4",
}}
>
{state.final_report.sources?.map((source) => (
<div key={source} style={{ marginBottom: "0.25rem" }}>
{source}
</div>
))}
</div>
)}
</div> </div>
</div> </div>
)} )}