Files
patchy631--ai-engineering-hub/ai-avatar-demo/backend.py
T
2026-07-13 12:37:47 +08:00

280 lines
9.0 KiB
Python

import json
import time
from typing import List, Optional, Dict, Any
from fastapi import FastAPI, HTTPException, Query
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
from services.zep_service import zep_service
from services.llm_service import llm_service
from config.settings import settings
# -------------------------------
# Pydantic Models
# -------------------------------
class Message(BaseModel):
role: str
content: str
class ChatRequest(BaseModel):
messages: List[Message]
class SessionCreateRequest(BaseModel):
user_id: str
first_name: Optional[str] = "Demo"
last_name: Optional[str] = None
email: Optional[str] = None
# -------------------------------
# FastAPI App
# -------------------------------
app = FastAPI(
title="Zep + LLM Backend for Anam",
description="FastAPI backend that integrates Zep Cloud KG + your LLM for Anam avatar.",
version="1.0.0",
)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # Allow all origins for local development
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
expose_headers=["*"],
)
# -------------------------------
# Health & Debug Endpoints
# -------------------------------
@app.get("/health")
async def health_check():
return {"status": "ok"}
@app.get("/zep/test-graph")
async def test_graph(q: str = Query("what is Zep?")):
"""
Simple debug endpoint to verify Zep graph connectivity.
"""
try:
nodes = await zep_service.search_graph(
query=q,
user_id=settings.zep_docs_user_id,
limit=3,
scope="nodes",
)
return {"query": q, "results": nodes}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
# -------------------------------
# Zep Session Helper Endpoint
# -------------------------------
@app.post("/zep/session")
async def create_zep_session(body: SessionCreateRequest):
"""
Helper endpoint to create a Zep user + thread (session) for a given user_id.
You can call this from Streamlit or any client instead of doing it inline.
"""
try:
user = await zep_service.create_user(
user_id=body.user_id,
first_name=body.first_name,
last_name=body.last_name,
email=body.email,
)
# 2) Create thread for this user
session_id = f"session-{body.user_id}"
thread = await zep_service.create_thread(
thread_id=session_id,
user_id=body.user_id,
)
return {
"user": user,
"thread": thread,
"session_id": session_id,
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
# -------------------------------
# Core: LLM Stream Endpoint for Anam
# -------------------------------
@app.post("/llm/stream")
async def llm_stream(
payload: ChatRequest,
session_id: str = Query(..., description="Zep thread/session ID"),
):
"""
Streaming LLM endpoint with Zep KG integration.
This is what Anam's customLLMHandler() should call.
- Expects: JSON body { "messages": [ { "role": "...", "content": "..." }, ... ] }
- Requires: ?session_id=... query param (thread_id in Zep)
- Returns: SSE stream with chunks as { "text": "<token or chunk>" }
"""
messages: List[Message] = payload.messages
if not messages:
raise HTTPException(status_code=400, detail="No messages provided")
# Extract latest user message
user_message: Optional[str] = None
for m in reversed(messages):
if m.role == "user":
user_message = m.content
break
if not user_message:
raise HTTPException(status_code=400, detail="No user message found")
async def event_generator():
"""
Async generator that:
1) Saves the user message to Zep
2) Queries the Zep Knowledge Graph for docs context
3) Gets conversation context from Zep
4) Calls your LLM in streaming mode
5) Yields SSE 'data: { "text": ... }' lines to the client
6) Saves the final assistant message back into Zep
"""
full_response: str = ""
try:
print(f"\n{'='*60}")
print(f"NEW USER MESSAGE for session: {session_id}")
print(f"Message: {user_message}")
print(f"{'='*60}")
# 1) Save user message into Zep thread
await zep_service.add_message(
thread_id=session_id,
role="user",
content=user_message,
)
print("User message saved to Zep thread")
# 2) Build base system prompt
system_prompt = """
You are a helpful AI assistant. When provided with relevant information or context below,
use it to answer the user's question accurately and completely. Always use the information
provided to give thorough, detailed answers. Be conversational and friendly while being informative.
If the context contains the answer, state it clearly and add relevant details from the context.
""".strip()
# 3) Zep Knowledge Graph lookup
print(f"\nQUERYING KNOWLEDGE GRAPH...")
print(f" Query: '{user_message}'")
print(f" User ID: {settings.zep_docs_user_id}")
graph_start_time = time.time()
graph_results = await zep_service.search_graph(
query=user_message,
user_id=settings.zep_docs_user_id,
limit=3,
scope="nodes",
)
graph_end_time = time.time()
# Convert to milliseconds
graph_duration = (graph_end_time - graph_start_time) * 1000
print(
f"\nKNOWLEDGE GRAPH RESULTS: {len(graph_results)} nodes found")
print(f"Fetch time: {graph_duration:.2f}ms")
if graph_results:
print(f"\nRetrieved nodes:")
for i, node in enumerate(graph_results, 1):
print(f" {i}. {node.get('name', 'N/A')}")
summary_preview = node.get('summary', '')[:100]
print(f" Summary: {summary_preview}...")
docs_context = "\n\n".join(
[f"- {node['name']}: {node['summary']}" for node in graph_results]
)
system_prompt += f"\n\n=== RELEVANT INFORMATION ===\n{docs_context}\n\nUse the above information to answer the user's question."
print(
f"\nGRAPH CONTEXT ADDED TO PROMPT: {len(docs_context)} characters")
else:
print(
f"\nNO GRAPH RESULTS FOUND - LLM will respond without context")
# 4) Conversation context from this thread
print(f"\nFETCHING USER CONTEXT for thread: {session_id}")
user_context = await zep_service.get_user_context(thread_id=session_id)
if user_context:
system_prompt += f"\n\n=== CONVERSATION CONTEXT ===\n{user_context}"
print(f"USER CONTEXT ADDED: {len(user_context)} characters")
else:
print(f"No user context available yet (new conversation)")
# 5) Reformat messages for LLM
formatted_messages: List[Dict[str, Any]] = [
{"role": m.role, "content": m.content} for m in messages
]
# 6) Stream from LLM and send SSE chunks
print(f"\nSTREAMING LLM RESPONSE...")
print(f" Total prompt size: {len(system_prompt)} characters")
chunk_count = 0
async for chunk in llm_service.stream_chat_completion(
messages=formatted_messages,
system_prompt=system_prompt,
):
if not chunk:
continue
chunk_count += 1
full_response += chunk
payload = json.dumps({"content": chunk})
yield f"data: {payload}\n\n"
if chunk_count % 10 == 0:
print(f" Streamed {chunk_count} chunks so far...")
print(f" Total chunks streamed: {chunk_count}")
# 7) Save assistant message in Zep
if full_response.strip():
await zep_service.add_message(
thread_id=session_id,
role="assistant",
content=full_response,
)
print(
f"\nASSISTANT RESPONSE SAVED: {len(full_response)} characters")
print(f"{'='*60}\n")
except Exception as e:
# On error, stream an error message as SSE
err_payload = json.dumps({"content": "Error: " + str(e)})
yield f"data: {err_payload}\n\n"
# Return a streaming SSE response
return StreamingResponse(
event_generator(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"X-Accel-Buffering": "no",
},
)