pass roles + debugging
This commit is contained in:
@@ -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"]
|
||||||
|
|||||||
@@ -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()
|
|
||||||
@@ -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):
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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
2
web/agent/uv.lock
generated
@@ -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" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user