Compare commits

...

3 Commits

Author SHA1 Message Date
b517c6939f allow running commands in docker run
All checks were successful
Build and Push Agent Docker Image / build (push) Successful in 2m22s
Build and Push Web Docker Image / build (push) Successful in 3m26s
2025-12-13 06:36:16 +01:00
1f313f090a something is kinda working 2025-12-13 05:41:13 +01:00
955f992f8e use pydantic 2025-12-13 04:35:40 +01:00
27 changed files with 1324 additions and 5304 deletions

View File

@@ -1,9 +1,23 @@
Dockerfile
.dockerignore
# Dependencies
node_modules
agent/.venv
# Build outputs
.next
out
# Environment files
.env*
!.env.example
# Git
.git
.gitignore
# IDE
.vscode
.idea
# Misc
*.log
*.md
.env*.local
agent/

9
web/.env.example Normal file
View File

@@ -0,0 +1,9 @@
# Auth0 Configuration
AUTH0_SECRET=use-openssl-rand-hex-32-to-generate
AUTH0_DOMAIN=your-tenant.auth0.com
AUTH0_CLIENT_ID=your-client-id
AUTH0_CLIENT_SECRET=your-client-secret
APP_BASE_URL=https://your-domain.com
# Optional: Agent URL (defaults to http://localhost:8000/)
# AGENT_URL=http://localhost:8000/

21
web/.gitignore vendored
View File

@@ -30,8 +30,11 @@ yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# env files
.env
.env.local
.env.*.local
!.env.example
# vercel
.vercel
@@ -40,5 +43,15 @@ yarn-error.log*
*.tsbuildinfo
next-env.d.ts
# LangGraph API
.langgraph_api
.mastra/
# lock files
package-lock.json
yarn.lock
pnpm-lock.yaml
bun.lockb
# python
venv
.venv
__pycache__

View File

@@ -5,30 +5,38 @@ FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
# Install dependencies
COPY package.json package-lock.json* ./
RUN npm ci --ignore-scripts
# Build the application
# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Skip agent installation in Docker
ENV SKIP_AGENT_INSTALL=true
# Disable telemetry during build
ENV NEXT_TELEMETRY_DISABLED=1
RUN npm run build
# Production image
# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
# Set the correct permission for prerender cache
RUN mkdir .next
RUN chown nextjs:nodejs .next
# Automatically leverage output traces to reduce image size
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

View File

@@ -1,119 +1,104 @@
# Cavepedia Web
# Cavepedia
Next.js frontend with integrated PydanticAI agent for Cavepedia.
## Project Structure
```
web/
├── src/ # Next.js application
├── agent/ # PydanticAI agent (Python)
│ ├── main.py # Agent definition
│ ├── server.py # FastAPI server with AG-UI
│ └── pyproject.toml
└── ...
```
A caving assistant built with [PydanticAI](https://ai.pydantic.dev/), [CopilotKit](https://copilotkit.ai), and Gemini.
## Prerequisites
- Google API Key (for Gemini)
- Auth0 account with application configured
- Python 3.13+
- uv
- Node.js 24+
- Python 3.13
- npm
- Google AI API Key (for the PydanticAI agent)
## Environment Variables
### Web App (.env)
```
AUTH0_SECRET=<random-secret>
AUTH0_DOMAIN=<your-auth0-domain>
AUTH0_CLIENT_ID=<your-client-id>
AUTH0_CLIENT_SECRET=<your-client-secret>
APP_BASE_URL=https://your-domain.com
AGENT_URL=http://localhost:8000/
```
### Agent (agent/.env)
```
GOOGLE_API_KEY=<your-google-api-key>
```
## Development
### 1. Install dependencies
1. Install dependencies:
```bash
npm install
```
This also installs the agent's Python dependencies via the `install:agent` script.
### 2. Set up environment variables
```bash
# Agent environment
cp agent/.env.example agent/.env
# Edit agent/.env with your API keys
```
### 3. Start development servers
2. Start the development server:
```bash
npm run dev
```
This starts both the Next.js UI and PydanticAI agent servers concurrently.
This starts both the UI and agent servers concurrently.
## Agent Deployment
## Production
The agent can be containerized for production deployment.
### Environment Variables
| Variable | Required | Description |
|----------|----------|-------------|
| `GOOGLE_API_KEY` | Yes | Google AI API key for Gemini |
### Running in production
### Docker Build
**Web (Next.js):**
```bash
cd agent
uv run uvicorn server:app --host 0.0.0.0 --port 8000
docker build -t cavepedia-web .
docker run -p 3000:3000 \
-e AUTH0_SECRET=<secret> \
-e AUTH0_DOMAIN=<domain> \
-e AUTH0_CLIENT_ID=<client-id> \
-e AUTH0_CLIENT_SECRET=<client-secret> \
-e APP_BASE_URL=https://your-domain.com \
-e AGENT_URL=http://agent:8000/ \
cavepedia-web
```
## Web Deployment
**Agent (PydanticAI):**
```bash
cd agent
docker build -t cavepedia-agent .
docker run -p 8000:8000 \
-e GOOGLE_API_KEY=<api-key> \
cavepedia-agent
```
### Environment Variables
### Without Docker
| Variable | Required | Default | Description |
|----------|----------|---------|-------------|
| `LANGGRAPH_DEPLOYMENT_URL` | Yes | `http://localhost:8000` | URL to the agent |
| `AUTH0_SECRET` | Yes | - | Session encryption key (`openssl rand -hex 32`) |
| `AUTH0_DOMAIN` | Yes | - | Auth0 tenant domain |
| `AUTH0_CLIENT_ID` | Yes | - | Auth0 application client ID |
| `AUTH0_CLIENT_SECRET` | Yes | - | Auth0 application client secret |
| `APP_BASE_URL` | Yes | - | Public URL of the app |
### Docker Compose (Full Stack)
```yaml
services:
web:
image: git.seaturtle.pw/cavepedia/cavepediav2-web:latest
ports:
- "3000:3000"
environment:
LANGGRAPH_DEPLOYMENT_URL: http://agent:8000
AUTH0_SECRET: ${AUTH0_SECRET}
AUTH0_DOMAIN: ${AUTH0_DOMAIN}
AUTH0_CLIENT_ID: ${AUTH0_CLIENT_ID}
AUTH0_CLIENT_SECRET: ${AUTH0_CLIENT_SECRET}
APP_BASE_URL: ${APP_BASE_URL}
depends_on:
- agent
agent:
image: git.seaturtle.pw/cavepedia/cavepediav2-agent:latest
environment:
GOOGLE_API_KEY: ${GOOGLE_API_KEY}
```bash
npm run build
npm run start:all
```
## Available Scripts
- `dev` - Start both UI and agent servers
- `dev:ui` - Start only Next.js
- `dev:agent` - Start only PydanticAI agent
- `build` - Build Next.js for production
- `start` - Start production server
- `lint` - Run ESLint
- `install:agent` - Install agent Python dependencies
- `dev` - Starts both UI and agent servers in development mode
- `dev:ui` - Starts only the Next.js UI server
- `dev:agent` - Starts only the PydanticAI agent server
- `build` - Builds the Next.js application for production
- `start` - Starts the production Next.js server
- `start:agent` - Starts the production agent server
- `start:all` - Starts both servers in production mode
- `lint` - Runs ESLint
## References
## Troubleshooting
- [PydanticAI Documentation](https://ai.pydantic.dev/)
- [CopilotKit Documentation](https://docs.copilotkit.ai)
- [Next.js Documentation](https://nextjs.org/docs)
- [Auth0 Next.js SDK Examples](https://github.com/auth0/nextjs-auth0/blob/main/EXAMPLES.md)
### Agent Connection Issues
If you see connection errors:
1. Ensure the agent is running on port 8000
2. Check that GOOGLE_API_KEY is set correctly
3. Verify AGENT_URL has a trailing slash
### Python Dependencies
```bash
cd agent
uv sync
uv run uvicorn src.main:app --host 127.0.0.1 --port 8000
```

View File

@@ -1,14 +1,22 @@
Dockerfile
.dockerignore
# Virtual environment
.venv
# Environment files
.env
!.env.example
# Cache
__pycache__
*.pyc
# Git
.git
.gitignore
.env
.env.*
.venv/
venv/
__pycache__/
*.pyc
*.pyo
.langgraph_api/
.vercel/
# IDE
.vscode
.idea
# Misc
*.log
*.md

7
web/agent/.env.example Normal file
View File

@@ -0,0 +1,7 @@
# Google Gemini API Key (required)
GOOGLE_API_KEY=your-gemini-api-key
# Optional settings
# PORT=8000
# HOST=127.0.0.1
# DEBUG=false

View File

@@ -1,9 +0,0 @@
venv/
__pycache__/
*.pyc
.env
.vercel
# python
.venv/
.langgraph_api/

View File

@@ -0,0 +1 @@
3.13

View File

@@ -1,21 +1,28 @@
# syntax=docker/dockerfile:1
FROM python:3.13-slim
WORKDIR /app
# Install uv
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
WORKDIR /app
ENV PYTHONPATH=/app
# Copy dependency files
COPY pyproject.toml uv.lock ./
# Install dependencies
RUN uv sync --frozen --no-dev --no-install-project
RUN uv sync --frozen --no-dev
# Copy application code
COPY main.py server.py ./
COPY src ./src
# Create non-root user
RUN useradd --create-home --shell /bin/bash agent
USER agent
EXPOSE 8000
CMD ["uv", "run", "python", "server.py"]
ENV PORT=8000
ENV HOST="0.0.0.0"
CMD ["uv", "run", "uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"]

View File

@@ -1,26 +0,0 @@
"""
PydanticAI agent with MCP tools from Cavepedia server.
"""
from pydantic_ai import Agent
from pydantic_ai.models.google import GoogleModel
from pydantic_ai.mcp import MCPServerStreamableHTTP
# Create MCP server connection to Cavepedia
mcp_server = MCPServerStreamableHTTP(
url="https://mcp.caving.dev/mcp",
timeout=30.0,
)
# Create the agent with Google Gemini model
agent = Agent(
model=GoogleModel("gemini-2.5-pro"),
toolsets=[mcp_server],
instructions="""You are a helpful assistant with access to cave-related information through the Cavepedia MCP server. You can help users find information about caves, caving techniques, and related topics.
IMPORTANT RULES:
1. Always cite your sources at the end of each response. List the specific sources/documents you used.
2. If you cannot find information on a topic, say so clearly. Do NOT make up information or hallucinate facts.
3. If the MCP tools return no results, acknowledge that you couldn't find the information rather than guessing.""",
)

View File

@@ -1,11 +1,14 @@
[project]
name = "vpi-1000"
version = "1.0.0"
description = "VPI-1000"
requires-python = ">=3.13,<3.14"
name = "cavepedia-agent"
version = "0.1.0"
description = "Cavepedia AI Agent with MCP tools"
requires-python = ">=3.13"
dependencies = [
"pydantic-ai>=0.1.0",
"fastapi>=0.115.5,<1.0.0",
"uvicorn>=0.29.0,<1.0.0",
"python-dotenv>=1.0.0,<2.0.0",
"uvicorn",
"pydantic-ai",
"google-genai",
"mcp",
"ag-ui-protocol",
"python-dotenv",
"httpx",
]

View File

@@ -1,20 +0,0 @@
"""
Self-hosted PydanticAI agent server using AG-UI protocol.
"""
import os
import uvicorn
from dotenv import load_dotenv
from pydantic_ai.ui.ag_ui.app import AGUIApp
from main import agent
load_dotenv()
# Convert PydanticAI agent to ASGI app with AG-UI protocol
app = AGUIApp(agent)
if __name__ == "__main__":
port = int(os.getenv("PORT", "8000"))
uvicorn.run(app, host="0.0.0.0", port=port)

67
web/agent/src/agent.py Normal file
View File

@@ -0,0 +1,67 @@
"""
PydanticAI agent with MCP tools from Cavepedia server.
"""
import os
import logging
import httpx
from pydantic_ai import Agent
from pydantic_ai.models.google import GoogleModel
# Set up logging based on environment
log_level = logging.DEBUG if os.getenv("DEBUG") else logging.INFO
logging.basicConfig(
level=log_level,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__)
MCP_URL = "https://mcp.caving.dev/mcp"
logger.info("Initializing Cavepedia agent...")
def check_mcp_available(url: str, timeout: float = 5.0) -> bool:
"""Check if MCP server is reachable."""
try:
# Just check if we can connect - don't need a full MCP handshake
response = httpx.get(url, timeout=timeout, follow_redirects=True)
# Any response (even 4xx/5xx) means server is reachable
# 502 means upstream is down, so treat as unavailable
if response.status_code == 502:
logger.warning(f"MCP server returned 502 Bad Gateway")
return False
return True
except Exception as e:
logger.warning(f"MCP server not reachable: {e}")
return False
# Try to configure MCP if server is available
toolsets = []
if check_mcp_available(MCP_URL):
try:
from pydantic_ai.mcp import MCPServerStreamableHTTP
mcp_server = MCPServerStreamableHTTP(
url=MCP_URL,
timeout=30.0,
)
toolsets.append(mcp_server)
logger.info(f"MCP server configured: {MCP_URL}")
except Exception as e:
logger.warning(f"Could not configure MCP server: {e}")
else:
logger.info("MCP server unavailable - running without MCP tools")
# Create the agent with Google Gemini model
agent = Agent(
model=GoogleModel("gemini-2.5-pro"),
toolsets=toolsets if toolsets else None,
instructions="""You are a helpful caving assistant. Help users with all aspects of caving including cave exploration, safety, surveying techniques, cave locations, geology, equipment, history, conservation, and any other caving-related topics.
IMPORTANT RULES:
1. Always cite your sources at the end of each response when possible.
2. If you're not certain about information, say so clearly. Do NOT make up information or hallucinate facts.
3. Provide accurate, helpful, and safety-conscious information.""",
)
logger.info(f"Agent initialized successfully (MCP: {'enabled' if toolsets else 'disabled'})")

41
web/agent/src/main.py Normal file
View File

@@ -0,0 +1,41 @@
"""
Self-hosted PydanticAI agent server using AG-UI protocol.
"""
import os
import sys
import logging
from dotenv import load_dotenv
# Load environment variables BEFORE importing agent
load_dotenv()
# Set up logging based on environment
log_level = logging.DEBUG if os.getenv("DEBUG") else logging.INFO
logging.basicConfig(
level=log_level,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__)
# Validate required environment variables
if not os.getenv("GOOGLE_API_KEY"):
logger.error("GOOGLE_API_KEY environment variable is required")
sys.exit(1)
import uvicorn
from src.agent import agent
logger.info("Creating AG-UI app...")
# Convert PydanticAI agent to ASGI app with AG-UI protocol
debug_mode = os.getenv("DEBUG", "").lower() in ("true", "1", "yes")
app = agent.to_ag_ui(debug=debug_mode)
logger.info("AG-UI app created successfully")
if __name__ == "__main__":
port = int(os.getenv("PORT", "8000"))
host = os.getenv("HOST", "127.0.0.1")
uvicorn.run(app, host=host, port=port)

679
web/agent/uv.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,7 +11,6 @@ const eslintConfig = [
"out/**",
"build/**",
"next-env.d.ts",
"agent",
],
},
];

View File

@@ -1,7 +1,57 @@
import type { NextConfig } from "next";
const securityHeaders = [
{
key: "X-DNS-Prefetch-Control",
value: "on",
},
{
key: "Strict-Transport-Security",
value: "max-age=63072000; includeSubDomains; preload",
},
{
key: "X-Content-Type-Options",
value: "nosniff",
},
{
key: "X-Frame-Options",
value: "DENY",
},
{
key: "X-XSS-Protection",
value: "1; mode=block",
},
{
key: "Referrer-Policy",
value: "strict-origin-when-cross-origin",
},
{
key: "Permissions-Policy",
value: "camera=(), microphone=(), geolocation=()",
},
];
const nextConfig: NextConfig = {
serverExternalPackages: ["@copilotkit/runtime"],
// Enable standalone output for Docker
output: "standalone",
async headers() {
return [
{
source: "/(.*)",
headers: securityHeaders,
},
];
},
// Production optimizations
poweredByHeader: false,
reactStrictMode: true,
// Compress responses
compress: true,
};
export default nextConfig;

5335
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,14 +4,16 @@
"private": true,
"scripts": {
"dev": "concurrently \"npm run dev:ui\" \"npm run dev:agent\" --names ui,agent --prefix-colors blue,green --kill-others",
"dev:debug": "LOG_LEVEL=debug npm run dev",
"dev:agent": "cd agent && uv run uvicorn server:app --host 0.0.0.0 --port 8000 --reload",
"dev:agent": "./scripts/run-agent.sh",
"dev:ui": "next dev --turbopack -H 127.0.0.1",
"build": "next build --webpack",
"start": "next start -H 127.0.0.1",
"start": "next start",
"start:agent": "./scripts/start-agent.sh",
"start:all": "concurrently \"npm run start\" \"npm run start:agent\" --names ui,agent --prefix-colors blue,green",
"lint": "eslint .",
"install:agent": "sh ./scripts/setup-agent.sh || scripts\\setup-agent.bat",
"postinstall": "if [ -z \"$SKIP_AGENT_INSTALL\" ]; then npm run install:agent; fi"
"typecheck": "tsc --noEmit",
"install:agent": "./scripts/setup-agent.sh",
"postinstall": "npm run install:agent"
},
"dependencies": {
"@ag-ui/client": "^0.0.42",

7
web/scripts/run-agent.sh Executable file
View File

@@ -0,0 +1,7 @@
#!/bin/bash
# Navigate to the agent directory
cd "$(dirname "$0")/../agent" || exit 1
# Run the agent using uvicorn with reload for development
uv run uvicorn src.main:app --host 127.0.0.1 --port 8000 --reload

View File

@@ -1,6 +0,0 @@
@echo off
REM Navigate to the agent directory
cd /d "%~dp0\..\agent" || exit /b 1
REM Install dependencies using uv
uv sync

View File

@@ -3,5 +3,5 @@
# Navigate to the agent directory
cd "$(dirname "$0")/../agent" || exit 1
# Install dependencies using uv
# Install dependencies and create virtual environment using uv
uv sync

7
web/scripts/start-agent.sh Executable file
View File

@@ -0,0 +1,7 @@
#!/bin/bash
# Navigate to the agent directory
cd "$(dirname "$0")/../agent" || exit 1
# Run the agent in production mode
uv run uvicorn src.main:app --host 127.0.0.1 --port 8000 --workers 2

View File

@@ -12,7 +12,7 @@ const serviceAdapter = new ExperimentalEmptyAdapter();
const runtime = new CopilotRuntime({
agents: {
vpi_1000: new HttpAgent({
url: process.env.LANGGRAPH_DEPLOYMENT_URL || "http://localhost:8000",
url: process.env.AGENT_URL || "http://localhost:8000/",
}),
},
});
@@ -25,4 +25,4 @@ export const POST = async (req: NextRequest) => {
});
return handleRequest(req);
};
};

View File

@@ -1,6 +1,17 @@
import { Auth0Client, filterDefaultIdTokenClaims } from '@auth0/nextjs-auth0/server';
const isProduction = process.env.NODE_ENV === 'production';
export const auth0 = new Auth0Client({
session: {
rolling: true,
absoluteDuration: 60 * 60 * 24 * 7, // 7 days
inactivityDuration: 60 * 60 * 24, // 1 day
cookie: {
secure: isProduction,
sameSite: 'lax',
},
},
async beforeSessionSaved(session) {
return {
...session,

44
web/src/lib/env.ts Normal file
View File

@@ -0,0 +1,44 @@
// Environment variable validation for production
const requiredEnvVars = [
'AUTH0_SECRET',
'AUTH0_DOMAIN',
'AUTH0_CLIENT_ID',
'AUTH0_CLIENT_SECRET',
'APP_BASE_URL',
] as const;
const optionalEnvVars = [
'AGENT_URL',
] as const;
export function validateEnv() {
const missing: string[] = [];
for (const envVar of requiredEnvVars) {
if (!process.env[envVar]) {
missing.push(envVar);
}
}
if (missing.length > 0) {
throw new Error(
`Missing required environment variables: ${missing.join(', ')}\n` +
'Please check your .env.local file or environment configuration.'
);
}
}
export function getEnv() {
return {
auth0: {
secret: process.env.AUTH0_SECRET!,
domain: process.env.AUTH0_DOMAIN!,
clientId: process.env.AUTH0_CLIENT_ID!,
clientSecret: process.env.AUTH0_CLIENT_SECRET!,
},
appBaseUrl: process.env.APP_BASE_URL!,
agentUrl: process.env.AGENT_URL || 'http://localhost:8000/',
isProduction: process.env.NODE_ENV === 'production',
};
}