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:
@@ -0,0 +1,111 @@
|
||||
"""
|
||||
Request context using ContextVar for async-safe user/conversation tracking.
|
||||
|
||||
ContextVar provides task-local storage that automatically propagates through
|
||||
async calls, eliminating the need to thread user identity through every function.
|
||||
|
||||
Usage:
|
||||
# At request entry (router):
|
||||
token = current_user.set(request.user or "jpmschweitzer")
|
||||
try:
|
||||
await service.process(request)
|
||||
finally:
|
||||
current_user.reset(token)
|
||||
|
||||
# Anywhere in the codebase:
|
||||
from src.core.context import get_user
|
||||
user = get_user() # Returns current request's user
|
||||
"""
|
||||
from contextvars import ContextVar
|
||||
|
||||
# Default user for single-user homelab setup
|
||||
DEFAULT_USER = "jpmschweitzer"
|
||||
|
||||
# Request-scoped context variables (async-safe, isolated per request)
|
||||
current_user: ContextVar[str] = ContextVar("current_user", default=DEFAULT_USER)
|
||||
current_conversation: ContextVar[str | None] = ContextVar(
|
||||
"current_conversation", default=None
|
||||
)
|
||||
|
||||
|
||||
def get_user() -> str:
|
||||
"""
|
||||
Get current user from request context.
|
||||
|
||||
Returns:
|
||||
User identifier for the current request.
|
||||
Falls back to DEFAULT_USER if not set.
|
||||
|
||||
Example:
|
||||
user = get_user() # "jpmschweitzer" or whatever was set in router
|
||||
"""
|
||||
return current_user.get()
|
||||
|
||||
|
||||
def get_conversation_id() -> str | None:
|
||||
"""
|
||||
Get current conversation ID from request context.
|
||||
|
||||
Returns:
|
||||
Conversation ID if set, None otherwise.
|
||||
|
||||
Example:
|
||||
conv_id = get_conversation_id() # "conv_abc123" or None
|
||||
"""
|
||||
return current_conversation.get()
|
||||
|
||||
|
||||
class RequestContext:
|
||||
"""
|
||||
Context manager for setting request-scoped context.
|
||||
|
||||
Provides a cleaner alternative to manual token management.
|
||||
|
||||
Usage:
|
||||
async with RequestContext(user="alice", conversation_id="conv_123"):
|
||||
# All code here sees user="alice"
|
||||
result = await some_service.process()
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
user: str | None = None,
|
||||
conversation_id: str | None = None,
|
||||
):
|
||||
"""
|
||||
Initialize request context.
|
||||
|
||||
Args:
|
||||
user: User identifier (defaults to DEFAULT_USER if None)
|
||||
conversation_id: Conversation ID (optional)
|
||||
"""
|
||||
self.user = user or DEFAULT_USER
|
||||
self.conversation_id = conversation_id
|
||||
self._user_token = None
|
||||
self._conv_token = None
|
||||
|
||||
async def __aenter__(self) -> "RequestContext":
|
||||
"""Set context variables on entry."""
|
||||
self._user_token = current_user.set(self.user)
|
||||
self._conv_token = current_conversation.set(self.conversation_id)
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
|
||||
"""Reset context variables on exit."""
|
||||
if self._user_token is not None:
|
||||
current_user.reset(self._user_token)
|
||||
if self._conv_token is not None:
|
||||
current_conversation.reset(self._conv_token)
|
||||
|
||||
def __enter__(self) -> "RequestContext":
|
||||
"""Sync context manager entry (for non-async code)."""
|
||||
self._user_token = current_user.set(self.user)
|
||||
self._conv_token = current_conversation.set(self.conversation_id)
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
||||
"""Sync context manager exit."""
|
||||
if self._user_token is not None:
|
||||
current_user.reset(self._user_token)
|
||||
if self._conv_token is not None:
|
||||
current_conversation.reset(self._conv_token)
|
||||
Reference in New Issue
Block a user