Basic skeleton for running an ag-ui demo using starlette and nextjs

This commit is contained in:
Yiorgis Gozadinos 2025-10-17 12:43:27 +03:00
parent b9c22e634c
commit e3b07db8b9
No known key found for this signature in database
26 changed files with 16682 additions and 0 deletions

View 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: ./haiku_rag.lancedb
DB_PATH=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

View file

@ -0,0 +1,47 @@
# Haiku.rag Interactive Research Assistant
Interactive research assistant powered by **Haiku.rag**, **Pydantic AI**, and **AG-UI** protocol. Ask complex questions and watch the multi-agent research process unfold in real-time with synchronized state between backend and frontend.
## Quick Start
### Prerequisites
- Docker and Docker Compose
- Ollama running on host (or configure another QA provider)
### Setup
1. **Clone the repository**
```bash
git clone <repository-url>
cd haiku.rag/examples/ag-ui-research
```
2. **Configure environment** (optional, defaults to Ollama with gpt-oss:latest)
```bash
cp .env.example .env
# Edit .env to customize provider/model or add API keys
```
See [haiku.rag configuration docs](https://ggozad.github.io/haiku.rag/configuration/) for provider setup.
3. **Prepare your knowledge base**
Create and populate a haiku.rag database:
```bash
# Create a data directory
mkdir -p data
# Add documents (requires haiku-rag installed locally)
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
```
4. **Start the application**
```bash
docker compose up --build
```
5. **Open the application**
- Frontend: http://localhost:3000

View file

@ -0,0 +1,12 @@
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
env/
venv/
.env
.venv
*.egg-info/
dist/
build/

View 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"]

View 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

View file

@ -0,0 +1,70 @@
"""Pydantic AI research agent for haiku.rag with AG-UI protocol."""
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.config import Config
from haiku.rag.graph.common import get_model
class ResearchState(BaseModel):
"""Shared state between research agent and frontend."""
question: str = ""
status: str = "idle"
current_iteration: int = 0
max_iterations: int = 2
confidence: float = 0.0
plan: list[dict] = []
findings: list[dict] = []
final_report: dict | None = None
def _as_state_snapshot(ctx: RunContext[StateDeps[ResearchState]]) -> StateSnapshotEvent:
"""Helper to create a state snapshot event for AG-UI."""
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[StateDeps[ResearchState], 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)
"""
agent = Agent(
model=get_model(qa_provider, qa_model),
deps_type=StateDeps[ResearchState],
instructions="""You are a research assistant powered by haiku.rag.
You help users conduct deep research on complex questions by:
- Breaking down questions into sub-questions
- Searching through a knowledge base
- Evaluating findings for completeness and confidence
- Synthesizing comprehensive reports with citations
The state is shared with the frontend application, showing research progress in real-time.
Currently, tools are placeholder stubs. Full integration with haiku.rag research pipeline
will be implemented in the next phase.""",
)
@agent.tool
async def get_research_status(ctx: RunContext[StateDeps[ResearchState]]) -> dict:
"""Get the current research state and progress."""
return {
"question": ctx.deps.state.question,
"status": ctx.deps.state.status,
"iteration": ctx.deps.state.current_iteration,
"max_iterations": ctx.deps.state.max_iterations,
"confidence": ctx.deps.state.confidence,
"has_plan": len(ctx.deps.state.plan) > 0,
"findings_count": len(ctx.deps.state.findings),
"has_report": ctx.deps.state.final_report is not None,
}
return agent

View file

@ -0,0 +1,63 @@
"""Main entry point for the haiku.rag AG-UI research assistant backend."""
from agent import ResearchState, create_agent
from pydantic_ai.ag_ui import StateDeps
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.config import Config
# Create research agent instance using haiku.rag config
agent = create_agent()
async def health(request):
"""Health check endpoint."""
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,
}
)
# Convert PydanticAI agent to AG-UI compatible ASGI app
ag_ui_app = agent.to_ag_ui(deps=StateDeps(ResearchState())) # type: ignore[arg-type]
# Mount the AG-UI app at /agent and add health endpoint
app = Starlette(
routes=[
Route("/health", health),
Mount("/agent", ag_ui_app),
],
middleware=[
Middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000", "http://frontend:3000"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
],
)
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,
)

View 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"

File diff suppressed because it is too large Load diff

View 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=${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
- ./data:/app/data
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

View file

@ -0,0 +1,7 @@
node_modules
.next
.git
.gitignore
README.md
npm-debug.log
.env*.local

View 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

View 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"]

View file

@ -0,0 +1,33 @@
import { HttpAgent } from "@ag-ui/client";
import {
CopilotRuntime,
copilotRuntimeNextJSAppRouterEndpoint,
ExperimentalEmptyAdapter,
} from "@copilotkit/runtime";
import type { NextRequest } from "next/server";
// Service adapter for multi-agent support (empty since we only have one agent)
const serviceAdapter = new ExperimentalEmptyAdapter();
// 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`,
}),
},
});
// 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);
}

View 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;
}

View 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>
);
}

View file

@ -0,0 +1,9 @@
import Agent from "@/components/Agent";
export default function Home() {
return (
<main>
<Agent />
</main>
);
}

View 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"
}
}
}
}

View file

@ -0,0 +1,122 @@
"use client";
import {
CopilotKit,
useCoAgent,
useCoAgentStateRender,
} from "@copilotkit/react-core";
import { CopilotSidebar } from "@copilotkit/react-ui";
import "@copilotkit/react-ui/styles.css";
import StateDisplay from "./StateDisplay";
interface ResearchState {
question: string;
status: string;
current_iteration: number;
max_iterations: number;
confidence: number;
plan: Array<Record<string, unknown>>;
findings: Array<Record<string, unknown>>;
final_report: Record<string, unknown> | null;
}
function AgentContent() {
// Use useCoAgent to sync state with the backend research agent
const { state } = useCoAgent<ResearchState>({
name: "research_agent",
initialState: {
question: "",
status: "idle",
current_iteration: 0,
max_iterations: 2,
confidence: 0.0,
plan: [],
findings: [],
final_report: null,
},
});
// Render state updates from the research agent
useCoAgentStateRender<ResearchState>({
name: "research_agent",
render: ({ state: newState }) => {
return (
<div
style={{
padding: "1rem",
background: "#e6f7ff",
borderRadius: "4px",
marginBottom: "0.5rem",
border: "1px solid #91d5ff",
}}
>
<strong>Research Update:</strong> Status: {newState.status},
Iteration: {newState.current_iteration}/{newState.max_iterations},
Confidence: {(newState.confidence * 100).toFixed(0)}%
</div>
);
},
});
return (
<div style={{ display: "flex", height: "100vh" }}>
<div
style={{
flex: 1,
padding: "2rem",
overflow: "auto",
}}
>
<div
style={{
maxWidth: "800px",
margin: "0 auto",
}}
>
<header style={{ marginBottom: "2rem" }}>
<h1
style={{
fontSize: "2.5rem",
fontWeight: "bold",
marginBottom: "0.5rem",
color: "#1a202c",
}}
>
Haiku.rag Research Assistant
</h1>
<p
style={{
fontSize: "1.125rem",
color: "#4a5568",
lineHeight: "1.6",
}}
>
Interactive research powered by <strong>Haiku.rag</strong>,{" "}
<strong>Pydantic AI</strong>, and <strong>AG-UI</strong>
</p>
</header>
<StateDisplay state={state} />
</div>
</div>
<CopilotSidebar
defaultOpen={true}
clickOutsideToClose={false}
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>
);
}
export default function Agent() {
return (
<CopilotKit runtimeUrl="/api/copilotkit" agent="research_agent">
<AgentContent />
</CopilotKit>
);
}

View file

@ -0,0 +1,277 @@
"use client";
interface ResearchState {
question: string;
status: string;
current_iteration: number;
max_iterations: number;
confidence: number;
plan: Array<Record<string, unknown>>;
findings: Array<Record<string, unknown>>;
final_report: Record<string, unknown> | null;
}
interface StateDisplayProps {
state: ResearchState;
}
export default function StateDisplay({ state }: StateDisplayProps) {
return (
<div
style={{
background: "white",
borderRadius: "8px",
padding: "1.5rem",
boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
marginTop: "2rem",
}}
>
<h2
style={{
fontSize: "1.5rem",
fontWeight: "600",
marginBottom: "1rem",
color: "#2d3748",
}}
>
Research State
</h2>
<p
style={{
color: "#4a5568",
lineHeight: "1.6",
marginBottom: "1rem",
fontSize: "0.875rem",
}}
>
This state is shared between the research agent and the frontend via the
AG-UI protocol.
</p>
<div
style={{
display: "grid",
gap: "1rem",
marginTop: "1rem",
}}
>
<div
style={{
padding: "1rem",
background: "#f7fafc",
borderRadius: "4px",
border: "1px solid #e2e8f0",
}}
>
<div
style={{
fontSize: "0.875rem",
color: "#718096",
marginBottom: "0.25rem",
}}
>
Question
</div>
<div
style={{
fontSize: "1.125rem",
fontWeight: "bold",
color: "#2d3748",
}}
>
{state.question || "No question yet"}
</div>
</div>
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(2, 1fr)",
gap: "1rem",
}}
>
<div
style={{
padding: "1rem",
background: "#f7fafc",
borderRadius: "4px",
border: "1px solid #e2e8f0",
}}
>
<div
style={{
fontSize: "0.875rem",
color: "#718096",
marginBottom: "0.25rem",
}}
>
Status
</div>
<div
style={{
fontSize: "1.125rem",
fontWeight: "bold",
color: state.status === "idle" ? "#718096" : "#38a169",
}}
>
{state.status}
</div>
</div>
<div
style={{
padding: "1rem",
background: "#f7fafc",
borderRadius: "4px",
border: "1px solid #e2e8f0",
}}
>
<div
style={{
fontSize: "0.875rem",
color: "#718096",
marginBottom: "0.25rem",
}}
>
Confidence
</div>
<div
style={{
fontSize: "1.5rem",
fontWeight: "bold",
color:
state.confidence > 0.8
? "#38a169"
: state.confidence > 0.5
? "#d69e2e"
: "#e53e3e",
}}
>
{(state.confidence * 100).toFixed(0)}%
</div>
</div>
</div>
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(3, 1fr)",
gap: "1rem",
}}
>
<div
style={{
padding: "1rem",
background: "#f7fafc",
borderRadius: "4px",
border: "1px solid #e2e8f0",
}}
>
<div
style={{
fontSize: "0.875rem",
color: "#718096",
marginBottom: "0.25rem",
}}
>
Progress
</div>
<div
style={{
fontSize: "1.125rem",
fontWeight: "bold",
color: "#2d3748",
}}
>
{state.current_iteration} / {state.max_iterations}
</div>
</div>
<div
style={{
padding: "1rem",
background: "#f7fafc",
borderRadius: "4px",
border: "1px solid #e2e8f0",
}}
>
<div
style={{
fontSize: "0.875rem",
color: "#718096",
marginBottom: "0.25rem",
}}
>
Plan Items
</div>
<div
style={{
fontSize: "1.125rem",
fontWeight: "bold",
color: "#2d3748",
}}
>
{state.plan.length}
</div>
</div>
<div
style={{
padding: "1rem",
background: "#f7fafc",
borderRadius: "4px",
border: "1px solid #e2e8f0",
}}
>
<div
style={{
fontSize: "0.875rem",
color: "#718096",
marginBottom: "0.25rem",
}}
>
Findings
</div>
<div
style={{
fontSize: "1.125rem",
fontWeight: "bold",
color: "#2d3748",
}}
>
{state.findings.length}
</div>
</div>
</div>
<div
style={{
padding: "1rem",
background: "#f7fafc",
borderRadius: "4px",
border: "1px solid #e2e8f0",
}}
>
<div
style={{
fontSize: "0.875rem",
color: "#718096",
marginBottom: "0.25rem",
}}
>
Final Report
</div>
<div
style={{
fontSize: "1.125rem",
fontWeight: "bold",
color: state.final_report ? "#38a169" : "#a0aec0",
}}
>
{state.final_report ? "Ready" : "Not ready"}
</div>
</div>
</div>
</div>
);
}

View file

@ -0,0 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: "standalone",
};
export default nextConfig;

File diff suppressed because it is too large Load diff

View 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"
}
}

View 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"]
}

View file

@ -0,0 +1,6 @@
{
"name": "haiku-ag-ui",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}