pass roles + debugging
All checks were successful
Build and Push Agent Docker Image / build (push) Successful in 1m3s
Build and Push Web Docker Image / build (push) Successful in 3m59s

This commit is contained in:
2025-12-13 17:23:52 +01:00
parent 0e24515303
commit c808f51eb7
9 changed files with 123 additions and 88 deletions

View File

@@ -25,4 +25,4 @@ EXPOSE 8021
ENV PORT=8021 ENV PORT=8021
ENV HOST="0.0.0.0" ENV HOST="0.0.0.0"
CMD ["uv", "run", "uvicorn", "server:app", "--host", "0.0.0.0", "--port", "8021"] CMD ["uv", "run", "--frozen", "uvicorn", "server:app", "--host", "0.0.0.0", "--port", "8021"]

View File

@@ -1,48 +0,0 @@
from pgvector.psycopg import register_vector, Bit
from psycopg.rows import dict_row
from urllib.parse import unquote
import anthropic
import cohere
import dotenv
import datetime
import json
import minio
import numpy as np
import os
import psycopg
import time
dotenv.load_dotenv('/home/paul/scripts-private/lech/cavepedia-v2/poller.env')
COHERE_API_KEY = os.getenv('COHERE_API_KEY')
co = cohere.ClientV2(COHERE_API_KEY)
conn = psycopg.connect(
host='127.0.0.1',
port=4010,
dbname='cavepediav2_db',
user='cavepediav2_user',
password='cavepediav2_pw',
row_factory=dict_row,
)
def embed(text, input_type):
resp = co.embed(
texts=[text],
model='embed-v4.0',
input_type=input_type,
embedding_types=['float'],
)
return resp.embeddings.float[0]
def search():
query = 'links trip with not more than 2 people'
query_embedding = embed(query, 'search_query')
rows = conn.execute('SELECT * FROM embeddings ORDER BY embedding <=> %s::vector LIMIT 5', (query_embedding,)).fetchall()
for row in rows:
print(row['bucket'])
print(row['key'])
if __name__ == '__main__':
search()

View File

@@ -35,12 +35,18 @@ mcp = FastMCP("Cavepedia MCP")
def get_user_roles() -> list[str]: def get_user_roles() -> list[str]:
"""Extract user roles from the X-User-Roles header.""" """Extract user roles from the X-User-Roles header."""
headers = get_http_headers() headers = get_http_headers()
print(f"DEBUG: All headers received: {dict(headers)}")
roles_header = headers.get("x-user-roles", "") roles_header = headers.get("x-user-roles", "")
print(f"DEBUG: x-user-roles header value: '{roles_header}'")
if roles_header: if roles_header:
try: try:
return json.loads(roles_header) roles = json.loads(roles_header)
except json.JSONDecodeError: print(f"DEBUG: Parsed roles: {roles}")
return roles
except json.JSONDecodeError as e:
print(f"DEBUG: JSON decode error: {e}")
return [] return []
print("DEBUG: No roles header found, returning empty list")
return [] return []
def embed(text, input_type): def embed(text, input_type):

View File

@@ -25,4 +25,4 @@ EXPOSE 8000
ENV PORT=8000 ENV PORT=8000
ENV HOST="0.0.0.0" ENV HOST="0.0.0.0"
CMD ["uv", "run", "uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"] CMD ["uv", "run", "--frozen", "uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"]

View File

@@ -5,9 +5,10 @@ description = "Cavepedia AI Agent with MCP tools"
requires-python = ">=3.13" requires-python = ">=3.13"
dependencies = [ dependencies = [
"uvicorn", "uvicorn",
"starlette",
"pydantic-ai", "pydantic-ai",
"google-genai",
"mcp", "mcp",
"google-genai",
"ag-ui-protocol", "ag-ui-protocol",
"python-dotenv", "python-dotenv",
"httpx", "httpx",

View File

@@ -35,32 +35,52 @@ def check_mcp_available(url: str, timeout: float = 5.0) -> bool:
logger.warning(f"MCP server not reachable: {e}") logger.warning(f"MCP server not reachable: {e}")
return False return False
# Try to configure MCP if server is available # Check if MCP is available at startup
toolsets = [] MCP_AVAILABLE = check_mcp_available(CAVE_MCP_URL)
if check_mcp_available(CAVE_MCP_URL): logger.info(f"MCP server available: {MCP_AVAILABLE}")
try:
from pydantic_ai.mcp import MCPServerStreamableHTTP
mcp_server = MCPServerStreamableHTTP(
url=CAVE_MCP_URL,
timeout=30.0,
)
toolsets.append(mcp_server)
logger.info(f"MCP server configured: {CAVE_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_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.
agent = Agent(
model=GoogleModel("gemini-3-pro-preview"),
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: IMPORTANT RULES:
1. Always cite your sources at the end of each response when possible. 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. 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.""", 3. Provide accurate, helpful, and safety-conscious information."""
)
logger.info(f"Agent initialized successfully (MCP: {'enabled' if toolsets else 'disabled'})")
def create_agent(user_roles: list[str] | None = None):
"""Create an agent with MCP tools configured for the given user roles."""
toolsets = []
if MCP_AVAILABLE and user_roles:
try:
import json
from pydantic_ai.mcp import MCPServerStreamableHTTP
roles_header = json.dumps(user_roles)
logger.info(f"Creating MCP server with roles: {roles_header}")
mcp_server = MCPServerStreamableHTTP(
url=CAVE_MCP_URL,
headers={"x-user-roles": roles_header},
timeout=30.0,
)
toolsets.append(mcp_server)
logger.info(f"MCP server configured with roles: {user_roles}")
except Exception as e:
logger.warning(f"Could not configure MCP server: {e}")
elif not user_roles:
logger.info("No user roles provided - MCP tools disabled")
else:
logger.info("MCP server unavailable - running without MCP tools")
return Agent(
model=GoogleModel("gemini-2.5-flash"),
toolsets=toolsets if toolsets else None,
instructions=AGENT_INSTRUCTIONS,
)
# Create a default agent for health checks etc
agent = create_agent()
logger.info("Agent module initialized successfully")

View File

@@ -4,6 +4,7 @@ Self-hosted PydanticAI agent server using AG-UI protocol.
import os import os
import sys import sys
import json
import logging import logging
from dotenv import load_dotenv from dotenv import load_dotenv
@@ -24,13 +25,53 @@ if not os.getenv("GOOGLE_API_KEY"):
sys.exit(1) sys.exit(1)
import uvicorn import uvicorn
from src.agent import agent from starlette.applications import Starlette
from starlette.requests import Request
from starlette.responses import Response, JSONResponse
from starlette.routing import Route
from pydantic_ai.ui.ag_ui import AGUIAdapter
from src.agent import create_agent
logger.info("Creating AG-UI app...") 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") async def handle_agent_request(request: Request) -> Response:
app = agent.to_ag_ui(debug=debug_mode) """Handle incoming AG-UI requests with dynamic role-based MCP configuration."""
# Debug: log all incoming headers
logger.info(f"DEBUG: All request headers: {dict(request.headers)}")
# Extract user roles from request headers
roles_header = request.headers.get("x-user-roles", "")
logger.info(f"DEBUG: x-user-roles header value: '{roles_header}'")
user_roles = []
if roles_header:
try:
user_roles = json.loads(roles_header)
logger.info(f"Request received with roles: {user_roles}")
except json.JSONDecodeError as e:
logger.warning(f"Failed to parse x-user-roles header: {e}")
# Create agent with the user's roles
agent = create_agent(user_roles)
# Dispatch the request using AGUIAdapter
return await AGUIAdapter.dispatch_request(request, agent=agent)
async def health(request: Request) -> Response:
"""Health check endpoint."""
return JSONResponse({"status": "ok"})
app = Starlette(
routes=[
Route("/", handle_agent_request, methods=["POST"]),
Route("/health", health, methods=["GET"]),
],
)
logger.info("AG-UI app created successfully") logger.info("AG-UI app created successfully")

2
web/agent/uv.lock generated
View File

@@ -231,6 +231,7 @@ dependencies = [
{ name = "mcp" }, { name = "mcp" },
{ name = "pydantic-ai" }, { name = "pydantic-ai" },
{ name = "python-dotenv" }, { name = "python-dotenv" },
{ name = "starlette" },
{ name = "uvicorn" }, { name = "uvicorn" },
] ]
@@ -242,6 +243,7 @@ requires-dist = [
{ name = "mcp" }, { name = "mcp" },
{ name = "pydantic-ai" }, { name = "pydantic-ai" },
{ name = "python-dotenv" }, { name = "python-dotenv" },
{ name = "starlette" },
{ name = "uvicorn" }, { name = "uvicorn" },
] ]

View File

@@ -6,18 +6,31 @@ import {
import { HttpAgent } from "@ag-ui/client"; import { HttpAgent } from "@ag-ui/client";
import { NextRequest } from "next/server"; import { NextRequest } from "next/server";
import { auth0 } from "@/lib/auth0";
const serviceAdapter = new ExperimentalEmptyAdapter(); const serviceAdapter = new ExperimentalEmptyAdapter();
const runtime = new CopilotRuntime({
agents: {
vpi_1000: new HttpAgent({
url: process.env.AGENT_URL || "http://localhost:8000/",
}),
},
});
export const POST = async (req: NextRequest) => { export const POST = async (req: NextRequest) => {
// Get user session and roles
const session = await auth0.getSession();
const userRoles = (session?.user?.roles as string[]) || [];
console.log("DEBUG: User roles from session:", userRoles);
// Create HttpAgent with user roles header
const agent = new HttpAgent({
url: process.env.AGENT_URL || "http://localhost:8000/",
headers: {
"x-user-roles": JSON.stringify(userRoles),
},
});
const runtime = new CopilotRuntime({
agents: {
vpi_1000: agent,
},
});
const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({ const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
runtime, runtime,
serviceAdapter, serviceAdapter,