From c808f51eb7ebb22961becba1e9da323d834728ce Mon Sep 17 00:00:00 2001 From: Paul Walko Date: Sat, 13 Dec 2025 17:23:52 +0100 Subject: [PATCH] pass roles + debugging --- mcp/Dockerfile | 2 +- mcp/search.py | 48 --------------------- mcp/server.py | 10 ++++- web/agent/Dockerfile | 2 +- web/agent/pyproject.toml | 3 +- web/agent/src/agent.py | 66 +++++++++++++++++++---------- web/agent/src/main.py | 49 +++++++++++++++++++-- web/agent/uv.lock | 2 + web/src/app/api/copilotkit/route.ts | 29 +++++++++---- 9 files changed, 123 insertions(+), 88 deletions(-) delete mode 100644 mcp/search.py diff --git a/mcp/Dockerfile b/mcp/Dockerfile index 1b0e78f..ceaa8ff 100644 --- a/mcp/Dockerfile +++ b/mcp/Dockerfile @@ -25,4 +25,4 @@ EXPOSE 8021 ENV PORT=8021 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"] diff --git a/mcp/search.py b/mcp/search.py deleted file mode 100644 index b8399b7..0000000 --- a/mcp/search.py +++ /dev/null @@ -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() diff --git a/mcp/server.py b/mcp/server.py index faff71d..e1911f2 100644 --- a/mcp/server.py +++ b/mcp/server.py @@ -35,12 +35,18 @@ mcp = FastMCP("Cavepedia MCP") def get_user_roles() -> list[str]: """Extract user roles from the X-User-Roles header.""" headers = get_http_headers() + print(f"DEBUG: All headers received: {dict(headers)}") roles_header = headers.get("x-user-roles", "") + print(f"DEBUG: x-user-roles header value: '{roles_header}'") if roles_header: try: - return json.loads(roles_header) - except json.JSONDecodeError: + roles = json.loads(roles_header) + print(f"DEBUG: Parsed roles: {roles}") + return roles + except json.JSONDecodeError as e: + print(f"DEBUG: JSON decode error: {e}") return [] + print("DEBUG: No roles header found, returning empty list") return [] def embed(text, input_type): diff --git a/web/agent/Dockerfile b/web/agent/Dockerfile index 77df068..165d29f 100644 --- a/web/agent/Dockerfile +++ b/web/agent/Dockerfile @@ -25,4 +25,4 @@ EXPOSE 8000 ENV PORT=8000 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"] diff --git a/web/agent/pyproject.toml b/web/agent/pyproject.toml index 6bff04c..03cc559 100644 --- a/web/agent/pyproject.toml +++ b/web/agent/pyproject.toml @@ -5,9 +5,10 @@ description = "Cavepedia AI Agent with MCP tools" requires-python = ">=3.13" dependencies = [ "uvicorn", + "starlette", "pydantic-ai", - "google-genai", "mcp", + "google-genai", "ag-ui-protocol", "python-dotenv", "httpx", diff --git a/web/agent/src/agent.py b/web/agent/src/agent.py index cd69b2a..2a883f6 100644 --- a/web/agent/src/agent.py +++ b/web/agent/src/agent.py @@ -35,32 +35,52 @@ def check_mcp_available(url: str, timeout: float = 5.0) -> bool: logger.warning(f"MCP server not reachable: {e}") return False -# Try to configure MCP if server is available -toolsets = [] -if check_mcp_available(CAVE_MCP_URL): - 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") +# Check if MCP is available at startup +MCP_AVAILABLE = check_mcp_available(CAVE_MCP_URL) +logger.info(f"MCP server available: {MCP_AVAILABLE}") -# Create the agent with Google Gemini model -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. +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. 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.""", -) +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") diff --git a/web/agent/src/main.py b/web/agent/src/main.py index 5e1a1b9..833ed1f 100644 --- a/web/agent/src/main.py +++ b/web/agent/src/main.py @@ -4,6 +4,7 @@ Self-hosted PydanticAI agent server using AG-UI protocol. import os import sys +import json import logging from dotenv import load_dotenv @@ -24,13 +25,53 @@ if not os.getenv("GOOGLE_API_KEY"): sys.exit(1) 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...") -# 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) + +async def handle_agent_request(request: Request) -> Response: + """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") diff --git a/web/agent/uv.lock b/web/agent/uv.lock index c5394e9..aa61144 100644 --- a/web/agent/uv.lock +++ b/web/agent/uv.lock @@ -231,6 +231,7 @@ dependencies = [ { name = "mcp" }, { name = "pydantic-ai" }, { name = "python-dotenv" }, + { name = "starlette" }, { name = "uvicorn" }, ] @@ -242,6 +243,7 @@ requires-dist = [ { name = "mcp" }, { name = "pydantic-ai" }, { name = "python-dotenv" }, + { name = "starlette" }, { name = "uvicorn" }, ] diff --git a/web/src/app/api/copilotkit/route.ts b/web/src/app/api/copilotkit/route.ts index 5f0b040..8c17e82 100644 --- a/web/src/app/api/copilotkit/route.ts +++ b/web/src/app/api/copilotkit/route.ts @@ -6,18 +6,31 @@ import { import { HttpAgent } from "@ag-ui/client"; import { NextRequest } from "next/server"; +import { auth0 } from "@/lib/auth0"; 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) => { + // 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({ runtime, serviceAdapter,