feat: add Phase F.1 memory infrastructure
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>
This commit is contained in:
@@ -11,6 +11,7 @@ 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__)
|
||||
|
||||
@@ -94,6 +95,11 @@ async def create_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
|
||||
@@ -136,3 +142,8 @@ async def create_response(
|
||||
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)
|
||||
|
||||
@@ -138,6 +138,10 @@ class ResponseRequest(CustomBaseModel):
|
||||
default=None,
|
||||
description="Stop sequences"
|
||||
)
|
||||
user: str | None = Field(
|
||||
default=None,
|
||||
description="Unique identifier for end-user (OpenAI standard)"
|
||||
)
|
||||
|
||||
@field_validator('reasoning')
|
||||
@classmethod
|
||||
|
||||
Reference in New Issue
Block a user