Merge pull request #111 from ggozad/feat/ag-ui-example
Full AG-UI integration example featuring an interactive frontend doing research
This commit is contained in:
commit
c7e84aa2da
29 changed files with 17892 additions and 0 deletions
|
|
@ -163,6 +163,14 @@ The A2A agent provides:
|
|||
- Source citations with titles and URIs
|
||||
- Full document retrieval on request
|
||||
|
||||
## Examples
|
||||
|
||||
See the [examples directory](examples/) for working examples:
|
||||
|
||||
- **[Interactive Research Assistant](examples/ag-ui-research/)** - Full-stack research assistant with Pydantic AI and AG-UI featuring human-in-the-loop approval and real-time state synchronization
|
||||
- **[Docker Setup](examples/docker/)** - Complete Docker deployment with file monitoring, MCP server, and A2A agent
|
||||
- **[A2A Security](examples/a2a-security/)** - Authentication examples (API key, OAuth2, GitHub)
|
||||
|
||||
## Documentation
|
||||
|
||||
Full documentation at: https://ggozad.github.io/haiku.rag/
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ Three agentic flows are provided by haiku.rag:
|
|||
- Deep QA Agent — multi-agent question decomposition for complex questions
|
||||
- Research Multi‑Agent — a multi‑step, analyzable research workflow
|
||||
|
||||
For an interactive example using Pydantic AI and AG-UI, see the [Interactive Research Assistant](https://github.com/ggozad/haiku.rag/tree/main/examples/ag-ui-research) example ([demo video](https://vimeo.com/1128874386)). The demo uses a knowledge base containing haiku.rag's code and documentation.
|
||||
|
||||
|
||||
### Simple QA Agent
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,19 @@
|
|||
|
||||
This directory contains example scripts demonstrating various features of haiku.rag.
|
||||
|
||||
## Interactive Research Assistant
|
||||
|
||||
**Directory:** `ag-ui-research/`
|
||||
|
||||
Full-stack research assistant with interactive UI powered by Pydantic AI and AG-UI:
|
||||
- Multi-step research workflow with question decomposition
|
||||
- Human-in-the-loop approval for research plans
|
||||
- Real-time state synchronization between backend and frontend
|
||||
- Context expansion and insight extraction
|
||||
- Structured research reports with citations
|
||||
|
||||
See `ag-ui-research/README.md` for setup instructions.
|
||||
|
||||
## Docker Example
|
||||
|
||||
**Directory:** `docker/`
|
||||
|
|
|
|||
23
examples/ag-ui-research/.env.example
Normal file
23
examples/ag-ui-research/.env.example
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
# QA Provider for the research agent (ollama, openai, anthropic, etc.)
|
||||
QA_PROVIDER=ollama
|
||||
|
||||
# QA Model name
|
||||
QA_MODEL=gpt-oss:latest
|
||||
|
||||
# Ollama base URL (only needed if using ollama provider)
|
||||
# For Docker: http://host.docker.internal:11434
|
||||
# For local development: http://localhost:11434
|
||||
OLLAMA_BASE_URL=http://host.docker.internal:11434
|
||||
|
||||
# Path to the LanceDB database
|
||||
# For Docker: /app/data/haiku_rag.lancedb
|
||||
# For local development: Use absolute path to existing database
|
||||
DB_PATH=~/SOME_FOLDER/haiku.rag.lancedb
|
||||
|
||||
# API keys (set as needed for your QA provider)
|
||||
# OPENAI_API_KEY=your-key-here
|
||||
# ANTHROPIC_API_KEY=your-key-here
|
||||
|
||||
# Embedding provider configuration (optional, defaults will be used)
|
||||
# EMBEDDING_PROVIDER=openai
|
||||
# EMBEDDING_MODEL=text-embedding-3-small
|
||||
80
examples/ag-ui-research/README.md
Normal file
80
examples/ag-ui-research/README.md
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
# Interactive Research Assistant
|
||||
|
||||
Research assistant powered by [haiku.rag](https://ggozad.github.io/haiku.rag/), [Pydantic AI](https://ai.pydantic.dev/), and [AG-UI](https://docs.ag-ui.com/). Ask complex questions and watch the research process unfold in real-time.
|
||||
|
||||
[Watch demo video](https://vimeo.com/1128874386)
|
||||
|
||||
## Features
|
||||
|
||||
- **Multi-step research workflow**: Question decomposition, search, analysis, and synthesis
|
||||
- **Human-in-the-loop**: Approve or revise research plans before execution
|
||||
- **Live state synchronization**: Real-time updates of research progress between backend and frontend
|
||||
- **Context expansion**: Automatically expands top search results for better context
|
||||
- **Rich reporting**: Generates structured reports with findings, conclusions, and citations
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Docker and Docker Compose
|
||||
- A haiku.rag database with indexed documents
|
||||
- Ollama (or configure another LLM provider)
|
||||
|
||||
### Setup
|
||||
|
||||
1. **Prepare your knowledge base**
|
||||
```bash
|
||||
mkdir -p data
|
||||
haiku-rag add "Your documents here" --db data/haiku_rag.lancedb
|
||||
# Or add from files
|
||||
haiku-rag add-src document.pdf --db data/haiku_rag.lancedb
|
||||
```
|
||||
|
||||
2. **Configure environment** (optional)
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# Edit .env to customize provider/model
|
||||
```
|
||||
See [haiku.rag configuration](https://ggozad.github.io/haiku.rag/configuration/) for details.
|
||||
|
||||
3. **Start the application**
|
||||
```bash
|
||||
docker compose up --build
|
||||
```
|
||||
|
||||
4. **Access the interface**
|
||||
- Frontend: http://localhost:3000
|
||||
- Backend health: http://localhost:8000/health
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **Ask a question**: Type your research question in the chat
|
||||
2. **Review the plan**: The agent decomposes your question into 3 sub-questions
|
||||
3. **Approve or revise**: Choose to approve the plan or request changes
|
||||
4. **Watch it work**: The agent automatically:
|
||||
- Searches the knowledge base for each sub-question
|
||||
- Extracts key insights from search results
|
||||
- Evaluates overall confidence in findings
|
||||
5. **Get your report**: Receive a structured research report with citations
|
||||
|
||||
## Architecture
|
||||
|
||||
- **Backend** (Python): Pydantic AI agent with haiku.rag integration
|
||||
- `agent.py`: Research agent with tool definitions
|
||||
- `main.py`: Starlette app serving AG-UI protocol
|
||||
|
||||
- **Frontend** (Next.js): CopilotKit/AG-UI interface
|
||||
- Real-time state synchronization with backend
|
||||
- Interactive approval workflow
|
||||
- Collapsible research plan and insights display
|
||||
|
||||
## Configuration
|
||||
|
||||
Environment variables (see `.env.example`):
|
||||
|
||||
- `DB_PATH`: Path to haiku.rag database (default: `haiku_rag.lancedb`)
|
||||
- `QA_PROVIDER`: LLM provider (default: `ollama`)
|
||||
- `QA_MODEL`: Model name (default: `gpt-oss:latest`)
|
||||
- `OLLAMA_BASE_URL`: Ollama endpoint (default: `http://host.docker.internal:11434`)
|
||||
|
||||
For other providers (OpenAI, Anthropic, etc.), see [haiku.rag configuration docs](https://ggozad.github.io/haiku.rag/configuration/).
|
||||
12
examples/ag-ui-research/backend/.gitignore
vendored
Normal file
12
examples/ag-ui-research/backend/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
env/
|
||||
venv/
|
||||
.env
|
||||
.venv
|
||||
*.egg-info/
|
||||
dist/
|
||||
build/
|
||||
27
examples/ag-ui-research/backend/Dockerfile
Normal file
27
examples/ag-ui-research/backend/Dockerfile
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Enable bytecode compilation
|
||||
ENV UV_COMPILE_BYTECODE=1
|
||||
|
||||
# Copy from the cache instead of linking since it's a mounted volume
|
||||
ENV UV_LINK_MODE=copy
|
||||
|
||||
# Install dependencies
|
||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
--mount=type=bind,source=uv.lock,target=uv.lock \
|
||||
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
|
||||
uv sync --frozen --no-install-project --no-dev
|
||||
|
||||
# Copy the project into the image
|
||||
COPY . .
|
||||
|
||||
# Sync the project
|
||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
uv sync --frozen --no-dev
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
# Run with uv
|
||||
CMD ["uv", "run", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
|
||||
17
examples/ag-ui-research/backend/README.md
Normal file
17
examples/ag-ui-research/backend/README.md
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# Haiku.rag Research Assistant Backend
|
||||
|
||||
FastAPI backend for the haiku.rag interactive research assistant, using Pydantic AI with AG-UI protocol support.
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
uv sync
|
||||
uv run python main.py
|
||||
```
|
||||
|
||||
The server starts on `http://localhost:8000` and uses [haiku.rag configuration](https://ggozad.github.io/haiku.rag/configuration/).
|
||||
|
||||
## Endpoints
|
||||
|
||||
- `GET /health` - Health check
|
||||
- `POST /agent` - AG-UI protocol endpoint
|
||||
431
examples/ag-ui-research/backend/agent.py
Normal file
431
examples/ag-ui-research/backend/agent.py
Normal file
|
|
@ -0,0 +1,431 @@
|
|||
import json
|
||||
from dataclasses import dataclass
|
||||
|
||||
from ag_ui.core import EventType, StateSnapshotEvent
|
||||
from pydantic import BaseModel
|
||||
from pydantic_ai import Agent, RunContext
|
||||
from pydantic_ai.ag_ui import StateDeps
|
||||
|
||||
from haiku.rag.client import HaikuRAG
|
||||
from haiku.rag.config import Config
|
||||
from haiku.rag.graph.common import get_model
|
||||
|
||||
|
||||
class ResearchState(BaseModel):
|
||||
"""Shared state between research agent and frontend."""
|
||||
|
||||
question: str = ""
|
||||
phase: str = "idle"
|
||||
status: str = ""
|
||||
plan: list[dict] = []
|
||||
current_question_index: int = 0
|
||||
insights: list[dict] = []
|
||||
document_registry: dict[str, dict] = {}
|
||||
current_document: dict | None = None
|
||||
confidence: float = 0.0
|
||||
final_report: dict | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class ResearchDeps(StateDeps[ResearchState]):
|
||||
"""Dependencies for the research agent with HaikuRAG client."""
|
||||
|
||||
client: HaikuRAG
|
||||
|
||||
|
||||
def _as_state_snapshot(ctx: RunContext[ResearchDeps]) -> StateSnapshotEvent:
|
||||
return StateSnapshotEvent(type=EventType.STATE_SNAPSHOT, snapshot=ctx.deps.state)
|
||||
|
||||
|
||||
def create_agent(
|
||||
qa_provider: str = Config.QA_PROVIDER, qa_model: str = Config.QA_MODEL
|
||||
) -> Agent[ResearchDeps, str]:
|
||||
"""Create and configure the research agent.
|
||||
|
||||
Args:
|
||||
qa_provider: QA provider for the agent (default: from Config.QA_PROVIDER)
|
||||
qa_model: Model name to use (default: from Config.QA_MODEL)
|
||||
"""
|
||||
print(f"[AGENT SETUP] Creating agent with provider={qa_provider}, model={qa_model}")
|
||||
agent = Agent(
|
||||
model=get_model(qa_provider, qa_model),
|
||||
deps_type=ResearchDeps,
|
||||
instructions="""You are a research co-pilot powered by haiku.rag.
|
||||
|
||||
Your workflow MUST follow these exact steps in order:
|
||||
1. Call propose_research_plan with the user's question
|
||||
2. After propose_research_plan completes, IMMEDIATELY call approve_research_plan (with no arguments)
|
||||
3. WAIT for approve_research_plan to return:
|
||||
- If it returns "APPROVED", proceed to step 4
|
||||
- If it returns "REVISE", ask the user "How would you like me to revise the research plan?" and wait for their response
|
||||
- Once you receive their revision feedback, revise the plan and go back to step 1
|
||||
4. Once approved, process questions ONE AT A TIME:
|
||||
- Call search_question(question_id=0) and WAIT for it to complete
|
||||
- Then call extract_insights_from_results(question_id=0) and WAIT for it to complete
|
||||
- Then call search_question(question_id=1) and WAIT for it to complete
|
||||
- Then call extract_insights_from_results(question_id=1) and WAIT for it to complete
|
||||
- Then call search_question(question_id=2) and WAIT for it to complete
|
||||
- Then call extract_insights_from_results(question_id=2) and WAIT for it to complete
|
||||
5. After all questions are processed, call evaluate_research_confidence
|
||||
6. Ask user if they want to finalize or continue researching
|
||||
7. When user approves, call synthesize_final_report
|
||||
|
||||
CRITICAL RULES:
|
||||
- MANDATORY: Call approve_research_plan immediately after propose_research_plan - NO EXCEPTIONS
|
||||
- If approve_research_plan returns "REVISE", ask the user for revision feedback naturally in chat
|
||||
- Call ONE tool at a time - wait for each tool to return before calling the next
|
||||
- NEVER call extract_insights_from_results until search_question has completed and returned results
|
||||
- DO NOT explain what you're about to do - just call the tool
|
||||
- The state updates will show the user what's happening - you don't need to narrate
|
||||
- Process all 3 questions automatically without asking for approval between them
|
||||
|
||||
Document Viewing:
|
||||
- When user asks to "show document X", call get_full_document with the document_uri
|
||||
|
||||
Remember: Call tools ONE AT A TIME in sequence. Each tool must complete before calling the next.
|
||||
""",
|
||||
)
|
||||
|
||||
@agent.tool
|
||||
async def propose_research_plan(
|
||||
ctx: RunContext[ResearchDeps], question: str
|
||||
) -> StateSnapshotEvent:
|
||||
"""Propose a research plan by decomposing the question into sub-questions."""
|
||||
ctx.deps.state.question = question
|
||||
ctx.deps.state.phase = "planning"
|
||||
ctx.deps.state.status = "Decomposing question into sub-questions..."
|
||||
|
||||
decompose_prompt = f"""Break down this research question into exactly 3 specific sub-questions that would help answer it comprehensively.
|
||||
|
||||
Research Question: {question}
|
||||
|
||||
Return ONLY a JSON array of sub-questions, like: ["Question 1?", "Question 2?", "Question 3?"]"""
|
||||
|
||||
response = await ctx.deps.client.ask(decompose_prompt)
|
||||
|
||||
try:
|
||||
sub_questions = json.loads(response)
|
||||
except json.JSONDecodeError:
|
||||
sub_questions = [
|
||||
q.strip().lstrip("0123456789.-) ")
|
||||
for q in response.split("\n")
|
||||
if q.strip()
|
||||
][:3]
|
||||
|
||||
plan = [
|
||||
{"id": i, "question": q, "status": "pending"}
|
||||
for i, q in enumerate(sub_questions)
|
||||
]
|
||||
|
||||
ctx.deps.state.plan = plan
|
||||
ctx.deps.state.current_question_index = 0
|
||||
ctx.deps.state.status = f"Proposed plan with {len(plan)} sub-questions"
|
||||
|
||||
return _as_state_snapshot(ctx)
|
||||
|
||||
@agent.tool
|
||||
async def search_question(
|
||||
ctx: RunContext[ResearchDeps],
|
||||
question_id: int,
|
||||
search_type: str = "hybrid",
|
||||
) -> StateSnapshotEvent:
|
||||
"""Execute search for a specific sub-question."""
|
||||
plan = ctx.deps.state.plan
|
||||
if question_id >= len(plan):
|
||||
raise ValueError(f"Question ID {question_id} not found in plan")
|
||||
|
||||
question = plan[question_id]["question"]
|
||||
ctx.deps.state.phase = "searching"
|
||||
ctx.deps.state.current_question_index = question_id
|
||||
ctx.deps.state.status = f"Searching: {question}"
|
||||
plan[question_id]["status"] = "searching"
|
||||
|
||||
search_results = await ctx.deps.client.search(
|
||||
question, limit=5, search_type=search_type
|
||||
)
|
||||
|
||||
expanded_map = {}
|
||||
if search_results:
|
||||
expanded_results = await ctx.deps.client.expand_context(
|
||||
search_results[:3], radius=2
|
||||
)
|
||||
expanded_map = {
|
||||
chunk.id: (chunk, score) for chunk, score in expanded_results
|
||||
}
|
||||
|
||||
results = []
|
||||
for chunk, score in search_results:
|
||||
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
|
||||
)
|
||||
|
||||
expanded_chunk, _ = (
|
||||
expanded_map[chunk.id] if chunk.id in expanded_map else (chunk, score)
|
||||
)
|
||||
result_data = {
|
||||
"chunk": expanded_chunk.content[:500],
|
||||
"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),
|
||||
"expanded": chunk.id in expanded_map,
|
||||
}
|
||||
results.append(result_data)
|
||||
|
||||
plan[question_id]["search_results"] = {
|
||||
"type": search_type,
|
||||
"results": results,
|
||||
}
|
||||
plan[question_id]["status"] = "searched"
|
||||
ctx.deps.state.status = f"Found {len(results)} results"
|
||||
|
||||
return _as_state_snapshot(ctx)
|
||||
|
||||
@agent.tool
|
||||
async def extract_insights_from_results(
|
||||
ctx: RunContext[ResearchDeps],
|
||||
question_id: int,
|
||||
) -> StateSnapshotEvent:
|
||||
"""Extract key insights from search results for a specific question."""
|
||||
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."
|
||||
)
|
||||
|
||||
search_results = question_item["search_results"]
|
||||
ctx.deps.state.phase = "analyzing"
|
||||
ctx.deps.state.status = "Extracting insights from results..."
|
||||
|
||||
context_parts = [
|
||||
f"[Result {idx}] [Source: {r['document_title']}] {r['full_chunk_content']}"
|
||||
for idx, r in enumerate(search_results["results"])
|
||||
]
|
||||
context = "\n\n".join(context_parts)
|
||||
|
||||
class InsightResult(BaseModel):
|
||||
summary: str
|
||||
confidence: float
|
||||
result_indices: list[int]
|
||||
|
||||
class InsightsList(BaseModel):
|
||||
insights: list[InsightResult]
|
||||
|
||||
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:
|
||||
{context}
|
||||
|
||||
For each insight, reference which result numbers (0, 1, 2, etc.) support it."""
|
||||
|
||||
insight_agent: Agent[None, InsightsList] = Agent(
|
||||
ctx.model,
|
||||
output_type=InsightsList,
|
||||
)
|
||||
|
||||
result = await insight_agent.run(extract_prompt)
|
||||
raw_insights = [
|
||||
{
|
||||
"summary": insight.summary,
|
||||
"confidence": insight.confidence,
|
||||
"result_indices": insight.result_indices,
|
||||
}
|
||||
for insight in result.output.insights
|
||||
]
|
||||
|
||||
new_insights = []
|
||||
for insight in raw_insights:
|
||||
source_refs = []
|
||||
for idx in insight.get("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,
|
||||
}
|
||||
)
|
||||
|
||||
ctx.deps.state.insights.extend(new_insights)
|
||||
plan[question_id]["status"] = "done"
|
||||
ctx.deps.state.status = f"Extracted {len(new_insights)} insights"
|
||||
|
||||
return _as_state_snapshot(ctx)
|
||||
|
||||
@agent.tool
|
||||
async def evaluate_research_confidence(
|
||||
ctx: RunContext[ResearchDeps],
|
||||
) -> StateSnapshotEvent:
|
||||
"""Evaluate overall confidence in the research findings."""
|
||||
insights = ctx.deps.state.insights
|
||||
if not insights:
|
||||
raise ValueError("No insights collected yet")
|
||||
|
||||
ctx.deps.state.phase = "evaluating"
|
||||
ctx.deps.state.status = "Evaluating research confidence..."
|
||||
|
||||
confidences = [i.get("confidence", 0.5) for i in insights]
|
||||
overall_confidence = sum(confidences) / len(confidences) if confidences else 0
|
||||
|
||||
eval_prompt = f"""Evaluate if these insights provide a confident answer to: "{ctx.deps.state.question}"
|
||||
|
||||
Insights collected:
|
||||
{chr(10).join([f"- {i['summary']}" for i in insights])}
|
||||
|
||||
Assess:
|
||||
1. Do we have enough information to answer the question?
|
||||
2. What gaps remain?
|
||||
3. Overall confidence (0.0-1.0)
|
||||
|
||||
Return JSON: {{"confidence": 0.0-1.0, "gaps": ["gap1", "gap2"], "recommendation": "continue" or "finalize"}}"""
|
||||
|
||||
response = await ctx.deps.client.ask(eval_prompt)
|
||||
|
||||
try:
|
||||
evaluation = json.loads(response)
|
||||
overall_confidence = evaluation.get("confidence", overall_confidence)
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
ctx.deps.state.confidence = overall_confidence
|
||||
ctx.deps.state.status = f"Confidence: {overall_confidence:.0%}"
|
||||
|
||||
return _as_state_snapshot(ctx)
|
||||
|
||||
@agent.tool
|
||||
async def synthesize_final_report(
|
||||
ctx: RunContext[ResearchDeps],
|
||||
) -> StateSnapshotEvent:
|
||||
"""Generate final research report with citations."""
|
||||
insights = ctx.deps.state.insights
|
||||
if not insights:
|
||||
raise ValueError("No insights to synthesize")
|
||||
|
||||
ctx.deps.state.phase = "synthesizing"
|
||||
ctx.deps.state.status = "Generating final report..."
|
||||
|
||||
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))
|
||||
insights_summary.append(
|
||||
f"- {i['summary']} (sources: {', '.join(unique_sources[:2])})"
|
||||
)
|
||||
|
||||
report_prompt = f"""Generate a comprehensive research report answering: "{ctx.deps.state.question}"
|
||||
|
||||
Based on these insights:
|
||||
{chr(10).join(insights_summary)}
|
||||
|
||||
Create a structured report with:
|
||||
- Executive Summary (2-3 sentences)
|
||||
- Main Findings (bullet points)
|
||||
- Conclusions
|
||||
- Sources (list the document titles mentioned above)
|
||||
|
||||
Return JSON with format:
|
||||
{{
|
||||
"title": "...",
|
||||
"summary": "...",
|
||||
"findings": ["finding1", "finding2", ...],
|
||||
"conclusions": ["conclusion1", ...],
|
||||
"sources": ["source1", "source2", ...]
|
||||
}}"""
|
||||
|
||||
response = await ctx.deps.client.ask(report_prompt)
|
||||
|
||||
try:
|
||||
report = json.loads(response)
|
||||
except json.JSONDecodeError:
|
||||
report = {
|
||||
"title": ctx.deps.state.question,
|
||||
"summary": response[:300],
|
||||
"findings": [i["summary"] for i in insights],
|
||||
"conclusions": ["See findings above"],
|
||||
"sources": [],
|
||||
}
|
||||
|
||||
citations = [
|
||||
{
|
||||
"document_uri": doc_uri,
|
||||
"document_title": doc_info["title"],
|
||||
"chunk_ids": doc_info["chunks_referenced"],
|
||||
}
|
||||
for doc_uri, doc_info in ctx.deps.state.document_registry.items()
|
||||
]
|
||||
report["citations"] = citations
|
||||
|
||||
ctx.deps.state.final_report = report
|
||||
ctx.deps.state.phase = "done"
|
||||
ctx.deps.state.status = "Research complete"
|
||||
|
||||
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."""
|
||||
ctx.deps.state.status = f"Retrieving document: {document_uri}"
|
||||
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.",
|
||||
"total_chunks": 0,
|
||||
}
|
||||
else:
|
||||
all_chunks = await ctx.deps.client.search(
|
||||
query="", 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.title or document_uri}"
|
||||
|
||||
return _as_state_snapshot(ctx)
|
||||
|
||||
return agent
|
||||
111
examples/ag-ui-research/backend/main.py
Normal file
111
examples/ag-ui-research/backend/main.py
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
import logging
|
||||
import os
|
||||
from contextlib import asynccontextmanager
|
||||
from pathlib import Path
|
||||
|
||||
from agent import ResearchDeps, ResearchState, create_agent
|
||||
from starlette.applications import Starlette
|
||||
from starlette.middleware import Middleware
|
||||
from starlette.middleware.cors import CORSMiddleware
|
||||
from starlette.responses import JSONResponse
|
||||
from starlette.routing import Mount, Route
|
||||
|
||||
from haiku.rag.client import HaikuRAG
|
||||
from haiku.rag.config import Config
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
client: HaikuRAG | None = None
|
||||
ag_ui_app = None
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app):
|
||||
global client
|
||||
db_path_str = os.getenv("DB_PATH", "haiku_rag.lancedb")
|
||||
db_path = Path(db_path_str)
|
||||
|
||||
if not db_path.exists():
|
||||
logger.error(f"Database not found at {db_path}")
|
||||
logger.error("Run: haiku-rag add <path-to-documents>")
|
||||
raise RuntimeError(f"Database not found: {db_path}")
|
||||
|
||||
logger.info(f"Initializing HaikuRAG client with database: {db_path}")
|
||||
client = HaikuRAG(db_path)
|
||||
logger.info("Research assistant backend ready")
|
||||
logger.info(f"QA Provider: {Config.QA_PROVIDER}, Model: {Config.QA_MODEL}")
|
||||
|
||||
yield
|
||||
|
||||
if client:
|
||||
logger.info("Closing HaikuRAG client")
|
||||
client.close()
|
||||
|
||||
|
||||
agent = create_agent()
|
||||
|
||||
|
||||
async def health(request):
|
||||
db_path_str = os.getenv("DB_PATH", "haiku_rag.lancedb")
|
||||
return JSONResponse(
|
||||
{
|
||||
"status": "healthy",
|
||||
"agent_model": str(agent.model),
|
||||
"qa_provider": Config.QA_PROVIDER,
|
||||
"qa_model": Config.QA_MODEL,
|
||||
"ollama_base_url": Config.OLLAMA_BASE_URL,
|
||||
"db_path": db_path_str,
|
||||
"db_exists": Path(db_path_str).exists(),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def get_ag_ui_app():
|
||||
global ag_ui_app
|
||||
if ag_ui_app is None and client is not None:
|
||||
research_deps = ResearchDeps(client=client, state=ResearchState())
|
||||
logger.info("Creating AG-UI app")
|
||||
ag_ui_app = agent.to_ag_ui(deps=research_deps)
|
||||
return ag_ui_app
|
||||
|
||||
|
||||
async def agent_endpoint(scope, receive, send):
|
||||
app = get_ag_ui_app()
|
||||
if app is None:
|
||||
response = JSONResponse({"error": "Client not initialized"}, status_code=503)
|
||||
await response(scope, receive, send)
|
||||
return
|
||||
await app(scope, receive, send)
|
||||
|
||||
|
||||
app = Starlette(
|
||||
routes=[
|
||||
Route("/health", health),
|
||||
Mount("/agent", agent_endpoint),
|
||||
],
|
||||
middleware=[
|
||||
Middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["http://localhost:3000", "http://frontend:3000"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
],
|
||||
lifespan=lifespan,
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
print("Starting haiku.rag research assistant backend...")
|
||||
print(f"Agent model: {agent.model}")
|
||||
print(f"QA provider: {Config.QA_PROVIDER}")
|
||||
print(f"QA model: {Config.QA_MODEL}")
|
||||
|
||||
uvicorn.run(
|
||||
"main:app",
|
||||
host="0.0.0.0",
|
||||
port=8000,
|
||||
reload=True,
|
||||
)
|
||||
26
examples/ag-ui-research/backend/pyproject.toml
Normal file
26
examples/ag-ui-research/backend/pyproject.toml
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
[project]
|
||||
name = "haiku-rag-research-assistant"
|
||||
version = "0.1.0"
|
||||
description = "Haiku.rag research assistant with AG-UI protocol support"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = [
|
||||
"starlette>=0.45.2",
|
||||
"uvicorn[standard]>=0.34.2",
|
||||
"pydantic-ai-slim[ag-ui,openai]>=1.1.0",
|
||||
"python-dotenv>=1.0.1",
|
||||
"haiku-rag>=0.12.1",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"pyright>=1.1.406",
|
||||
"ruff>=0.13.0",
|
||||
]
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["."]
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
4011
examples/ag-ui-research/backend/uv.lock
Normal file
4011
examples/ag-ui-research/backend/uv.lock
Normal file
File diff suppressed because it is too large
Load diff
50
examples/ag-ui-research/docker-compose.yml
Normal file
50
examples/ag-ui-research/docker-compose.yml
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
services:
|
||||
backend:
|
||||
build:
|
||||
context: ./backend
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "8000:8000"
|
||||
environment:
|
||||
- QA_PROVIDER=${QA_PROVIDER:-ollama}
|
||||
- QA_MODEL=${QA_MODEL:-gpt-oss:latest}
|
||||
- DB_PATH=/app/data/haiku.rag.lancedb
|
||||
- OLLAMA_BASE_URL=${OLLAMA_BASE_URL:-http://host.docker.internal:11434}
|
||||
- OPENAI_API_KEY=${OPENAI_API_KEY}
|
||||
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
|
||||
volumes:
|
||||
- ./backend:/app
|
||||
- /app/.venv
|
||||
- ${DB_PATH}:/app/data/haiku.rag.lancedb
|
||||
networks:
|
||||
- ag-ui-network
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
frontend:
|
||||
build:
|
||||
context: ./frontend
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
- BACKEND_URL=http://backend:8000
|
||||
volumes:
|
||||
- ./frontend:/app
|
||||
- /app/node_modules
|
||||
- /app/.next
|
||||
depends_on:
|
||||
- backend
|
||||
networks:
|
||||
- ag-ui-network
|
||||
restart: unless-stopped
|
||||
|
||||
networks:
|
||||
ag-ui-network:
|
||||
driver: bridge
|
||||
7
examples/ag-ui-research/frontend/.dockerignore
Normal file
7
examples/ag-ui-research/frontend/.dockerignore
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
node_modules
|
||||
.next
|
||||
.git
|
||||
.gitignore
|
||||
README.md
|
||||
npm-debug.log
|
||||
.env*.local
|
||||
39
examples/ag-ui-research/frontend/.gitignore
vendored
Normal file
39
examples/ag-ui-research/frontend/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# env files
|
||||
.env*.local
|
||||
.env
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
12
examples/ag-ui-research/frontend/Dockerfile
Normal file
12
examples/ag-ui-research/frontend/Dockerfile
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
# Development Dockerfile for Next.js frontend
|
||||
FROM node:22-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json package-lock.json* ./
|
||||
RUN npm ci
|
||||
COPY . .
|
||||
EXPOSE 3000
|
||||
|
||||
# Run in development mode with hot reload
|
||||
CMD ["npm", "run", "dev"]
|
||||
33
examples/ag-ui-research/frontend/app/api/copilotkit/route.ts
Normal file
33
examples/ag-ui-research/frontend/app/api/copilotkit/route.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { HttpAgent } from "@ag-ui/client";
|
||||
import {
|
||||
CopilotRuntime,
|
||||
copilotRuntimeNextJSAppRouterEndpoint,
|
||||
ExperimentalEmptyAdapter,
|
||||
} from "@copilotkit/runtime";
|
||||
import type { NextRequest } from "next/server";
|
||||
|
||||
// Connect CopilotKit to PydanticAI via HttpAgent
|
||||
// The HttpAgent creates a bridge between the Next.js frontend and the Python backend
|
||||
// It communicates with the server created by agent.to_ag_ui()
|
||||
const runtime = new CopilotRuntime({
|
||||
agents: {
|
||||
// "research_agent" maps to the agent name used in useCoAgent() on the frontend
|
||||
research_agent: new HttpAgent({
|
||||
url: `${process.env.BACKEND_URL || "http://backend:8000"}/agent`,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
// Service adapter for multi-agent support (empty since we only have one agent)
|
||||
const serviceAdapter = new ExperimentalEmptyAdapter();
|
||||
|
||||
// Next.js API route handler that proxies requests between frontend and backend
|
||||
export async function POST(request: NextRequest) {
|
||||
const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
|
||||
runtime,
|
||||
serviceAdapter,
|
||||
endpoint: "/api/copilotkit",
|
||||
});
|
||||
|
||||
return handleRequest(request);
|
||||
}
|
||||
24
examples/ag-ui-research/frontend/app/globals.css
Normal file
24
examples/ag-ui-research/frontend/app/globals.css
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
* {
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
max-width: 100vw;
|
||||
overflow-x: hidden;
|
||||
font-family:
|
||||
system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||
"Helvetica Neue", Arial, sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
background: linear-gradient(to bottom, #f8f9fa, #e9ecef);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
20
examples/ag-ui-research/frontend/app/layout.tsx
Normal file
20
examples/ag-ui-research/frontend/app/layout.tsx
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import type { Metadata } from "next";
|
||||
import "./globals.css";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Haiku.rag Research Assistant",
|
||||
description:
|
||||
"Interactive research powered by Haiku.rag, Pydantic AI, and AG-UI",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
9
examples/ag-ui-research/frontend/app/page.tsx
Normal file
9
examples/ag-ui-research/frontend/app/page.tsx
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import Agent from "@/components/Agent";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<main>
|
||||
<Agent />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
34
examples/ag-ui-research/frontend/biome.json
Normal file
34
examples/ag-ui-research/frontend/biome.json
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.2.6/schema.json",
|
||||
"vcs": {
|
||||
"enabled": false,
|
||||
"clientKind": "git",
|
||||
"useIgnoreFile": false
|
||||
},
|
||||
"files": {
|
||||
"ignoreUnknown": false
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"indentStyle": "tab"
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true
|
||||
}
|
||||
},
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"quoteStyle": "double"
|
||||
}
|
||||
},
|
||||
"assist": {
|
||||
"enabled": true,
|
||||
"actions": {
|
||||
"source": {
|
||||
"organizeImports": "on"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
282
examples/ag-ui-research/frontend/components/Agent.tsx
Normal file
282
examples/ag-ui-research/frontend/components/Agent.tsx
Normal file
|
|
@ -0,0 +1,282 @@
|
|||
"use client";
|
||||
|
||||
import {
|
||||
CopilotKit,
|
||||
useCoAgent,
|
||||
useCoAgentStateRender,
|
||||
useCopilotAction,
|
||||
} from "@copilotkit/react-core";
|
||||
import { CopilotChat } from "@copilotkit/react-ui";
|
||||
import "@copilotkit/react-ui/styles.css";
|
||||
import StateDisplay from "./StateDisplay";
|
||||
|
||||
interface SourceRef {
|
||||
chunk_id: string;
|
||||
document_uri: string;
|
||||
document_title: string;
|
||||
chunk_position: number;
|
||||
}
|
||||
|
||||
interface ResearchState {
|
||||
question: string;
|
||||
phase: string;
|
||||
status: string;
|
||||
plan: Array<{
|
||||
id: number;
|
||||
question: 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;
|
||||
insights: Array<{
|
||||
summary: string;
|
||||
confidence: number;
|
||||
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;
|
||||
final_report: {
|
||||
title: string;
|
||||
summary: string;
|
||||
findings: string[];
|
||||
conclusions: string[];
|
||||
sources: string[];
|
||||
citations: Array<{
|
||||
document_uri: string;
|
||||
document_title: string;
|
||||
chunk_ids: string[];
|
||||
}>;
|
||||
} | null;
|
||||
}
|
||||
|
||||
function AgentContent() {
|
||||
const { state } = useCoAgent<ResearchState>({
|
||||
name: "research_agent",
|
||||
initialState: {
|
||||
question: "",
|
||||
phase: "idle",
|
||||
status: "",
|
||||
plan: [],
|
||||
current_question_index: 0,
|
||||
insights: [],
|
||||
document_registry: {},
|
||||
current_document: null,
|
||||
confidence: 0.0,
|
||||
final_report: null,
|
||||
},
|
||||
});
|
||||
|
||||
useCopilotAction({
|
||||
name: "approve_research_plan",
|
||||
description:
|
||||
"Request user approval for the research plan. Returns 'APPROVED' if approved or 'REVISE' if user wants to revise.",
|
||||
parameters: [],
|
||||
renderAndWaitForResponse: ({ respond, status }) => (
|
||||
<div
|
||||
style={{
|
||||
padding: "1.5rem",
|
||||
background: "white",
|
||||
borderRadius: "8px",
|
||||
border: "2px solid #4299e1",
|
||||
marginBottom: "1rem",
|
||||
boxShadow: "0 2px 8px rgba(0,0,0,0.1)",
|
||||
}}
|
||||
>
|
||||
<h3
|
||||
style={{
|
||||
fontSize: "1.25rem",
|
||||
fontWeight: "bold",
|
||||
marginBottom: "1rem",
|
||||
color: "#2d3748",
|
||||
}}
|
||||
>
|
||||
Research Plan Approval
|
||||
</h3>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "0.875rem",
|
||||
color: "#4a5568",
|
||||
marginBottom: "1rem",
|
||||
}}
|
||||
>
|
||||
Please review the research plan in the right pane.
|
||||
</p>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: "1rem",
|
||||
}}
|
||||
className={status !== "executing" ? "hidden" : ""}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => respond?.("REVISE")}
|
||||
disabled={status !== "executing"}
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: "0.75rem",
|
||||
background: "white",
|
||||
border: "2px solid #e2e8f0",
|
||||
borderRadius: "6px",
|
||||
fontSize: "0.875rem",
|
||||
fontWeight: "600",
|
||||
cursor: status === "executing" ? "pointer" : "not-allowed",
|
||||
opacity: status === "executing" ? 1 : 0.5,
|
||||
}}
|
||||
>
|
||||
Revise Plan
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => respond?.("APPROVED")}
|
||||
disabled={status !== "executing"}
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: "0.75rem",
|
||||
background: "#4299e1",
|
||||
color: "white",
|
||||
border: "none",
|
||||
borderRadius: "6px",
|
||||
fontSize: "0.875rem",
|
||||
fontWeight: "600",
|
||||
cursor: status === "executing" ? "pointer" : "not-allowed",
|
||||
opacity: status === "executing" ? 1 : 0.5,
|
||||
}}
|
||||
>
|
||||
Approve & Start Research
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
});
|
||||
|
||||
useCoAgentStateRender<ResearchState>({
|
||||
name: "research_agent",
|
||||
render: ({ state: newState }) => {
|
||||
const phaseMessages: Record<string, string> = {
|
||||
planning: "Planning research...",
|
||||
searching: "Searching...",
|
||||
analyzing: "Extracting insights...",
|
||||
evaluating: `Evaluating confidence: ${(newState.confidence * 100).toFixed(0)}%`,
|
||||
synthesizing: "Generating final report...",
|
||||
done: "Research complete!",
|
||||
};
|
||||
const phaseMessage =
|
||||
phaseMessages[newState.phase] || newState.status || "Ready";
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
padding: "1rem",
|
||||
background: "#e6f7ff",
|
||||
borderRadius: "4px",
|
||||
marginBottom: "0.5rem",
|
||||
border: "1px solid #91d5ff",
|
||||
}}
|
||||
>
|
||||
<strong>Research Update:</strong> {phaseMessage}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<style>{`
|
||||
.chat-container {
|
||||
width: 50%;
|
||||
height: 100vh;
|
||||
border-right: 1px solid #e2e8f0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.chat-container > * {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
`}</style>
|
||||
<div style={{ display: "flex", height: "100vh" }}>
|
||||
{/* Chat on the left */}
|
||||
<div className="chat-container">
|
||||
<CopilotChat
|
||||
labels={{
|
||||
title: "Research Assistant",
|
||||
initial:
|
||||
"Hello! I can help you conduct deep research on complex questions using the haiku.rag knowledge base. Ask me anything!",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* State display on the right */}
|
||||
<div
|
||||
style={{
|
||||
width: "50%",
|
||||
height: "100vh",
|
||||
overflow: "auto",
|
||||
background: "#f7fafc",
|
||||
}}
|
||||
>
|
||||
<div style={{ padding: "2rem" }}>
|
||||
<header style={{ marginBottom: "2rem" }}>
|
||||
<h1
|
||||
style={{
|
||||
fontSize: "2rem",
|
||||
fontWeight: "bold",
|
||||
marginBottom: "0.5rem",
|
||||
color: "#1a202c",
|
||||
}}
|
||||
>
|
||||
Research State
|
||||
</h1>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "0.875rem",
|
||||
color: "#4a5568",
|
||||
lineHeight: "1.6",
|
||||
}}
|
||||
>
|
||||
Live updates from the research agent
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<StateDisplay state={state} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Agent() {
|
||||
return (
|
||||
<CopilotKit runtimeUrl="/api/copilotkit" agent="research_agent">
|
||||
<AgentContent />
|
||||
</CopilotKit>
|
||||
);
|
||||
}
|
||||
862
examples/ag-ui-research/frontend/components/StateDisplay.tsx
Normal file
862
examples/ag-ui-research/frontend/components/StateDisplay.tsx
Normal file
|
|
@ -0,0 +1,862 @@
|
|||
"use client";
|
||||
|
||||
import { Markdown } from "@copilotkit/react-ui";
|
||||
import { useState } from "react";
|
||||
|
||||
interface SourceRef {
|
||||
chunk_id: string;
|
||||
document_uri: string;
|
||||
document_title: string;
|
||||
chunk_position: number;
|
||||
}
|
||||
|
||||
interface ResearchState {
|
||||
question: string;
|
||||
phase: string;
|
||||
status: string;
|
||||
plan: Array<{
|
||||
id: number;
|
||||
question: 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;
|
||||
insights: Array<{
|
||||
summary: string;
|
||||
confidence: number;
|
||||
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;
|
||||
final_report: {
|
||||
title: string;
|
||||
summary: string;
|
||||
findings: string[];
|
||||
conclusions: string[];
|
||||
sources: string[];
|
||||
citations: Array<{
|
||||
document_uri: string;
|
||||
document_title: string;
|
||||
chunk_ids: string[];
|
||||
}>;
|
||||
} | null;
|
||||
}
|
||||
|
||||
interface StateDisplayProps {
|
||||
state: ResearchState;
|
||||
}
|
||||
|
||||
export default function StateDisplay({ state }: StateDisplayProps) {
|
||||
const [expandedSections, setExpandedSections] = useState<
|
||||
Record<string, boolean>
|
||||
>({
|
||||
plan: true,
|
||||
insights: true,
|
||||
report: true,
|
||||
document: true,
|
||||
});
|
||||
|
||||
const [expandedQuestions, setExpandedQuestions] = useState<
|
||||
Record<number, boolean>
|
||||
>({});
|
||||
|
||||
const toggleSection = (section: string) => {
|
||||
setExpandedSections((prev) => ({
|
||||
...prev,
|
||||
[section]: !prev[section],
|
||||
}));
|
||||
};
|
||||
|
||||
const toggleQuestion = (questionId: number) => {
|
||||
setExpandedQuestions((prev) => ({
|
||||
...prev,
|
||||
[questionId]: !prev[questionId],
|
||||
}));
|
||||
};
|
||||
|
||||
// Calculate research progress
|
||||
const completedQuestions = state.plan.filter(
|
||||
(q) => q.status === "done",
|
||||
).length;
|
||||
const totalQuestions = state.plan.length;
|
||||
const researchProgress =
|
||||
totalQuestions > 0 ? (completedQuestions / totalQuestions) * 100 : 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "1rem",
|
||||
}}
|
||||
>
|
||||
{/* Current Phase & Status */}
|
||||
<div
|
||||
style={{
|
||||
background: "white",
|
||||
borderRadius: "8px",
|
||||
padding: "1.5rem",
|
||||
boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "0.875rem",
|
||||
color: "#718096",
|
||||
marginBottom: "0.5rem",
|
||||
}}
|
||||
>
|
||||
Current Phase
|
||||
</div>
|
||||
<div style={{ display: "flex", gap: "0.75rem", alignItems: "center" }}>
|
||||
<div
|
||||
style={{
|
||||
padding: "0.5rem 1rem",
|
||||
background:
|
||||
state.phase === "idle"
|
||||
? "#e2e8f0"
|
||||
: state.phase === "planning"
|
||||
? "#fef3c7"
|
||||
: state.phase === "searching"
|
||||
? "#dbeafe"
|
||||
: state.phase === "analyzing"
|
||||
? "#e0e7ff"
|
||||
: state.phase === "evaluating"
|
||||
? "#fce7f3"
|
||||
: "#d1fae5",
|
||||
color:
|
||||
state.phase === "idle"
|
||||
? "#718096"
|
||||
: state.phase === "planning"
|
||||
? "#92400e"
|
||||
: state.phase === "searching"
|
||||
? "#1e40af"
|
||||
: state.phase === "analyzing"
|
||||
? "#3730a3"
|
||||
: state.phase === "evaluating"
|
||||
? "#9f1239"
|
||||
: "#065f46",
|
||||
borderRadius: "6px",
|
||||
fontSize: "1rem",
|
||||
fontWeight: "700",
|
||||
textTransform: "capitalize",
|
||||
}}
|
||||
>
|
||||
{state.phase}
|
||||
</div>
|
||||
{state.status && (
|
||||
<div
|
||||
style={{
|
||||
fontSize: "0.875rem",
|
||||
color: "#4a5568",
|
||||
}}
|
||||
>
|
||||
{state.status}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* Research Progress Bar */}
|
||||
{totalQuestions > 0 && state.phase !== "idle" && (
|
||||
<div style={{ marginTop: "1rem" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginBottom: "0.5rem",
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
fontSize: "0.75rem",
|
||||
color: "#718096",
|
||||
}}
|
||||
>
|
||||
Research Progress
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
fontSize: "0.75rem",
|
||||
fontWeight: "600",
|
||||
color: "#2d3748",
|
||||
}}
|
||||
>
|
||||
{completedQuestions}/{totalQuestions} questions
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
height: "0.5rem",
|
||||
background: "#e2e8f0",
|
||||
borderRadius: "4px",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: `${researchProgress}%`,
|
||||
height: "100%",
|
||||
background: "#48bb78",
|
||||
transition: "width 0.3s ease",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Question */}
|
||||
{state.question && (
|
||||
<div
|
||||
style={{
|
||||
background: "white",
|
||||
borderRadius: "8px",
|
||||
padding: "1.5rem",
|
||||
boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "0.875rem",
|
||||
color: "#718096",
|
||||
marginBottom: "0.25rem",
|
||||
}}
|
||||
>
|
||||
Question
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "1.125rem",
|
||||
fontWeight: "bold",
|
||||
color: "#2d3748",
|
||||
}}
|
||||
>
|
||||
{state.question}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Confidence Meter */}
|
||||
{state.confidence > 0 && (
|
||||
<div
|
||||
style={{
|
||||
background: "white",
|
||||
borderRadius: "8px",
|
||||
padding: "1.5rem",
|
||||
boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "0.875rem",
|
||||
color: "#718096",
|
||||
marginBottom: "0.5rem",
|
||||
}}
|
||||
>
|
||||
Confidence
|
||||
</div>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "1rem" }}>
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
height: "1rem",
|
||||
background: "#e2e8f0",
|
||||
borderRadius: "4px",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: `${state.confidence * 100}%`,
|
||||
height: "100%",
|
||||
background:
|
||||
state.confidence > 0.8
|
||||
? "#48bb78"
|
||||
: state.confidence > 0.5
|
||||
? "#ed8936"
|
||||
: "#f56565",
|
||||
transition: "width 0.3s ease",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "1.5rem",
|
||||
fontWeight: "bold",
|
||||
color:
|
||||
state.confidence > 0.8
|
||||
? "#48bb78"
|
||||
: state.confidence > 0.5
|
||||
? "#ed8936"
|
||||
: "#f56565",
|
||||
}}
|
||||
>
|
||||
{(state.confidence * 100).toFixed(0)}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Research Plan */}
|
||||
{state.plan.length > 0 && (
|
||||
<div
|
||||
style={{
|
||||
background: "white",
|
||||
borderRadius: "8px",
|
||||
boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleSection("plan")}
|
||||
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>Research Plan ({state.plan.length} questions)</span>
|
||||
<span>{expandedSections.plan ? "▼" : "▶"}</span>
|
||||
</button>
|
||||
{expandedSections.plan && (
|
||||
<div
|
||||
style={{
|
||||
padding: "1rem",
|
||||
background: "#f7fafc",
|
||||
border: "1px solid #e2e8f0",
|
||||
borderTop: "none",
|
||||
borderRadius: "0 0 4px 4px",
|
||||
}}
|
||||
>
|
||||
{state.plan.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
style={{
|
||||
marginBottom: "0.5rem",
|
||||
background: "white",
|
||||
borderRadius: "4px",
|
||||
border: "1px solid #e2e8f0",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleQuestion(item.id)}
|
||||
style={{
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
gap: "0.75rem",
|
||||
padding: "0.75rem",
|
||||
background: "white",
|
||||
border: "none",
|
||||
cursor: "pointer",
|
||||
textAlign: "left",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "1.25rem",
|
||||
color:
|
||||
item.status === "done"
|
||||
? "#48bb78"
|
||||
: item.status === "searching" ||
|
||||
item.status === "searched"
|
||||
? "#4299e1"
|
||||
: "#a0aec0",
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
{item.status === "done"
|
||||
? "✓"
|
||||
: item.status === "searching"
|
||||
? "🔍"
|
||||
: item.status === "searched"
|
||||
? "📊"
|
||||
: "⏳"}
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "0.875rem",
|
||||
color: "#4a5568",
|
||||
}}
|
||||
>
|
||||
<Markdown content={item.question} />
|
||||
</div>
|
||||
{item.search_results && (
|
||||
<div
|
||||
style={{
|
||||
fontSize: "0.75rem",
|
||||
color: "#718096",
|
||||
marginTop: "0.25rem",
|
||||
}}
|
||||
>
|
||||
{item.search_results.results.length} results
|
||||
</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",
|
||||
}}
|
||||
>
|
||||
<Markdown content={`${result.chunk}...`} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Insights */}
|
||||
{state.insights.length > 0 && (
|
||||
<div
|
||||
style={{
|
||||
background: "white",
|
||||
borderRadius: "8px",
|
||||
boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleSection("insights")}
|
||||
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>Key Insights ({state.insights.length})</span>
|
||||
<span>{expandedSections.insights ? "▼" : "▶"}</span>
|
||||
</button>
|
||||
{expandedSections.insights && (
|
||||
<div
|
||||
style={{
|
||||
padding: "1rem",
|
||||
background: "#f7fafc",
|
||||
border: "1px solid #e2e8f0",
|
||||
borderTop: "none",
|
||||
borderRadius: "0 0 4px 4px",
|
||||
}}
|
||||
>
|
||||
{state.insights.map((insight, idx) => (
|
||||
<div
|
||||
key={`${insight.summary.substring(0, 30)}-${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.75rem",
|
||||
padding: "0.125rem 0.5rem",
|
||||
background: "#c6f6d5",
|
||||
color: "#22543d",
|
||||
borderRadius: "4px",
|
||||
}}
|
||||
>
|
||||
{(insight.confidence * 100).toFixed(0)}% confidence
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
fontSize: "0.75rem",
|
||||
color: "#718096",
|
||||
}}
|
||||
>
|
||||
{insight.source_refs?.length || 0} sources
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "0.875rem",
|
||||
color: "#2d3748",
|
||||
lineHeight: "1.5",
|
||||
marginBottom: "0.5rem",
|
||||
}}
|
||||
>
|
||||
<Markdown content={insight.summary} />
|
||||
</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>
|
||||
)}
|
||||
|
||||
{/* Final Report */}
|
||||
{state.final_report && (
|
||||
<div
|
||||
style={{
|
||||
background: "white",
|
||||
borderRadius: "8px",
|
||||
boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleSection("report")}
|
||||
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>Final Report</span>
|
||||
<span>{expandedSections.report ? "▼" : "▶"}</span>
|
||||
</button>
|
||||
{expandedSections.report && (
|
||||
<div
|
||||
style={{
|
||||
padding: "1.5rem",
|
||||
background: "white",
|
||||
border: "1px solid #e2e8f0",
|
||||
borderTop: "none",
|
||||
borderRadius: "0 0 4px 4px",
|
||||
}}
|
||||
>
|
||||
<h3
|
||||
style={{
|
||||
fontSize: "1.25rem",
|
||||
fontWeight: "600",
|
||||
marginBottom: "1rem",
|
||||
color: "#2d3748",
|
||||
}}
|
||||
>
|
||||
{state.final_report.title}
|
||||
</h3>
|
||||
<div style={{ marginBottom: "1.5rem" }}>
|
||||
<h4
|
||||
style={{
|
||||
fontSize: "0.875rem",
|
||||
fontWeight: "600",
|
||||
color: "#718096",
|
||||
marginBottom: "0.5rem",
|
||||
}}
|
||||
>
|
||||
Executive Summary
|
||||
</h4>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "0.875rem",
|
||||
color: "#4a5568",
|
||||
lineHeight: "1.6",
|
||||
}}
|
||||
>
|
||||
<Markdown content={state.final_report.summary} />
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ marginBottom: "1.5rem" }}>
|
||||
<h4
|
||||
style={{
|
||||
fontSize: "0.875rem",
|
||||
fontWeight: "600",
|
||||
color: "#718096",
|
||||
marginBottom: "0.5rem",
|
||||
}}
|
||||
>
|
||||
Main Findings
|
||||
</h4>
|
||||
<ul
|
||||
style={{
|
||||
paddingLeft: "1.5rem",
|
||||
fontSize: "0.875rem",
|
||||
color: "#4a5568",
|
||||
lineHeight: "1.6",
|
||||
}}
|
||||
>
|
||||
{state.final_report.findings.map((finding, idx) => (
|
||||
<li
|
||||
key={`finding-${idx}-${finding.substring(0, 30)}`}
|
||||
style={{ marginBottom: "0.5rem" }}
|
||||
>
|
||||
<Markdown content={finding} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div style={{ marginBottom: "1.5rem" }}>
|
||||
<h4
|
||||
style={{
|
||||
fontSize: "0.875rem",
|
||||
fontWeight: "600",
|
||||
color: "#718096",
|
||||
marginBottom: "0.5rem",
|
||||
}}
|
||||
>
|
||||
Conclusions
|
||||
</h4>
|
||||
<ul
|
||||
style={{
|
||||
paddingLeft: "1.5rem",
|
||||
fontSize: "0.875rem",
|
||||
color: "#4a5568",
|
||||
lineHeight: "1.6",
|
||||
}}
|
||||
>
|
||||
{state.final_report.conclusions.map((conclusion, idx) => (
|
||||
<li
|
||||
key={`conclusion-${idx}-${conclusion.substring(0, 30)}`}
|
||||
style={{ marginBottom: "0.5rem" }}
|
||||
>
|
||||
<Markdown content={conclusion} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h4
|
||||
style={{
|
||||
fontSize: "0.875rem",
|
||||
fontWeight: "600",
|
||||
color: "#718096",
|
||||
marginBottom: "0.5rem",
|
||||
}}
|
||||
>
|
||||
Citations
|
||||
</h4>
|
||||
{state.final_report.citations &&
|
||||
state.final_report.citations.length > 0 ? (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "0.5rem",
|
||||
}}
|
||||
>
|
||||
{state.final_report.citations.map((citation) => (
|
||||
<div
|
||||
key={citation.document_uri}
|
||||
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>
|
||||
);
|
||||
}
|
||||
7
examples/ag-ui-research/frontend/next.config.ts
Normal file
7
examples/ag-ui-research/frontend/next.config.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: "standalone",
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
11690
examples/ag-ui-research/frontend/package-lock.json
generated
Normal file
11690
examples/ag-ui-research/frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
29
examples/ag-ui-research/frontend/package.json
Normal file
29
examples/ag-ui-research/frontend/package.json
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"name": "ag-ui-frontend",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"check": "biome check app components",
|
||||
"format": "biome check --write app components"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ag-ui/client": "^0.0.40",
|
||||
"@copilotkit/react-core": "^1.10.6",
|
||||
"@copilotkit/react-ui": "^1.10.6",
|
||||
"@copilotkit/runtime": "^1.10.6",
|
||||
"next": "15.5.5",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.2.6",
|
||||
"@types/node": "^22",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
0
examples/ag-ui-research/frontend/public/.gitkeep
Normal file
0
examples/ag-ui-research/frontend/public/.gitkeep
Normal file
27
examples/ag-ui-research/frontend/tsconfig.json
Normal file
27
examples/ag-ui-research/frontend/tsconfig.json
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
6
examples/ag-ui-research/package-lock.json
generated
Normal file
6
examples/ag-ui-research/package-lock.json
generated
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "haiku-ag-ui",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
||||
Loading…
Reference in a new issue