Add multi-tenancy support and memory storage infrastructure:
- Add ContextVar-based request context (src/core/context.py)
- Async-safe user/conversation tracking via contextvars
- RequestContext manager for clean setup/teardown
- get_user(), get_conversation_id() helpers
- Add multi-tenancy utilities (src/core/multi_tenancy.py)
- User ID sanitization for collection/key names
- get_memory_collection_name(), get_session_key() helpers
- Add Ollama embedding client (src/core/embeddings.py)
- nomic-embed-text model (768 dimensions)
- embed(), embed_batch(), health_check() methods
- Add Qdrant client wrapper (src/core/qdrant.py)
- Per-user collection pattern: memories_{user}
- upsert_memory(), search_memories(), delete_memory()
- Type-based filtering support
- Add Redis memory cache (src/core/memory_cache.py)
- Session context with 24h TTL
- Recent entities tracking
- Separate from benchmarks (db=2)
- Update config with memory settings
- QDRANT_HOST, QDRANT_PORT, QDRANT_EMBEDDING_DIM
- OLLAMA_EMBEDDING_MODEL
- REDIS_MEMORY_DB, REDIS_MEMORY_TTL_HOURS
- Add user field to ResponseRequest (OpenAI standard)
- Set context in router, reset in finally block
- Update librarian client to use get_user() (12 methods)
All 333 unit tests pass.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
150 lines
4.8 KiB
Python
150 lines
4.8 KiB
Python
"""
|
|
Responses router.
|
|
|
|
OpenAI-compatible /v1/responses endpoint with streaming support.
|
|
"""
|
|
|
|
import logging
|
|
from fastapi import APIRouter, HTTPException
|
|
from sse_starlette.sse import EventSourceResponse
|
|
|
|
from src.responses import service
|
|
from src.responses.schemas import ResponseRequest, Response
|
|
from src.core.exceptions import ModelNotFoundError, AppException
|
|
from src.core.context import current_user, current_conversation
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(prefix="/responses", tags=["responses"])
|
|
|
|
|
|
@router.post("", response_model=Response)
|
|
async def create_response(
|
|
request: ResponseRequest,
|
|
) -> Response | EventSourceResponse:
|
|
"""
|
|
Create a response using Responses API format.
|
|
|
|
Supports:
|
|
- Reasoning summaries (thinking/reasoning display)
|
|
- Function calling (tool usage)
|
|
- Streaming responses
|
|
- Multi-turn conversations
|
|
- Error handling
|
|
|
|
Args:
|
|
request: Response request with model, input, optional reasoning/tools
|
|
|
|
Returns:
|
|
Response object or SSE stream
|
|
|
|
Example non-streaming request:
|
|
POST /v1/responses
|
|
{
|
|
"model": "lorem-tester",
|
|
"input": [{"role": "user", "content": "Hello"}],
|
|
"reasoning": {"effort": "medium", "summary": "auto"},
|
|
"stream": false
|
|
}
|
|
|
|
Example streaming request:
|
|
POST /v1/responses
|
|
{
|
|
"model": "lorem-tester",
|
|
"input": [{"role": "user", "content": "Hello"}],
|
|
"stream": true
|
|
}
|
|
|
|
Response format (non-streaming):
|
|
{
|
|
"id": "resp_...",
|
|
"object": "response",
|
|
"created_at": 1733529600,
|
|
"model": "lorem-tester",
|
|
"status": "completed",
|
|
"output": [
|
|
{
|
|
"type": "reasoning",
|
|
"id": "rs_...",
|
|
"summary": ["Analyzing...", "Considering..."]
|
|
},
|
|
{
|
|
"type": "message",
|
|
"id": "msg_...",
|
|
"role": "assistant",
|
|
"content": [{"type": "output_text", "text": "Lorem ipsum..."}]
|
|
}
|
|
],
|
|
"usage": {
|
|
"input_tokens": 10,
|
|
"output_tokens": 50,
|
|
"reasoning_tokens": 20,
|
|
"total_tokens": 80
|
|
}
|
|
}
|
|
|
|
Streaming format (SSE):
|
|
event: response.reasoning_summary_text.delta
|
|
data: {"delta": "Analyzing..."}
|
|
|
|
event: response.output_text.delta
|
|
data: {"delta": "Lorem"}
|
|
|
|
event: response.done
|
|
data: {"response": {...}}
|
|
"""
|
|
logger.info(f"Response request for model: {request.model}")
|
|
|
|
# Set request context (propagates through all async calls)
|
|
user_token = current_user.set(request.user or "jpmschweitzer")
|
|
conv_id = request.metadata.get("conversation_id") if request.metadata else None
|
|
conv_token = current_conversation.set(conv_id)
|
|
|
|
try:
|
|
# Check if this is a Tatlock request - use Steward preprocessing (Phase 2)
|
|
model_id = request.model
|
|
if "." in model_id:
|
|
model_id = model_id.split(".", 1)[1]
|
|
|
|
use_steward = model_id.lower() == "tatlock"
|
|
|
|
if request.stream:
|
|
logger.info("Streaming response requested")
|
|
if use_steward:
|
|
logger.info("Streaming with Steward preprocessing for Tatlock request")
|
|
# Use Steward + Tatlock streaming (Milestone 3.5)
|
|
from src.responses.streaming import StreamingCoordinator
|
|
coordinator = StreamingCoordinator()
|
|
return EventSourceResponse(
|
|
coordinator.stream_response_with_steward(request)
|
|
)
|
|
else:
|
|
# Regular streaming for non-Tatlock models
|
|
return EventSourceResponse(
|
|
service.create_response_stream(request)
|
|
)
|
|
|
|
# Use appropriate service method
|
|
if use_steward:
|
|
logger.info("Using Steward preprocessing for Tatlock request")
|
|
return await service.create_response_with_steward(request)
|
|
else:
|
|
return await service.create_response(request)
|
|
|
|
except ModelNotFoundError as e:
|
|
logger.error(f"Model not found: {e}")
|
|
raise HTTPException(status_code=404, detail=str(e))
|
|
|
|
except AppException as e:
|
|
logger.error(f"Application error: {e}")
|
|
raise HTTPException(status_code=e.status_code, detail=e.message)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Unexpected error: {e}", exc_info=True)
|
|
raise HTTPException(status_code=500, detail="Internal server error")
|
|
|
|
finally:
|
|
# Reset context (important for connection reuse)
|
|
current_user.reset(user_token)
|
|
current_conversation.reset(conv_token)
|