Build and Push / build (release) Successful in 52s
### Added - Environment-aware configuration: - Auto-selected logging (DEBUG for dev, WARNING for prod) - Auto-selected default user (llm_tester for dev isolation) - User context logging at request entry - Direct delegation bypass: - Pure memory/librarian requests skip Tatlock LLM - Reduces latency for memory-only requests - Text-based delegation fallback: - Parse [DELEGATE:agent] patterns from LLM output - Sequential and parallel execution support - Comprehensive E2E test suite: - 22 orchestration tests with QdrantVerifier - assert_llm_behavior() for flexible pattern matching - Tests for memory, delegation, isolation, scenarios ### Fixed - Unit test mocks for streaming (async generator) - Temporal context handling in tests - LLM non-determinism with pytest.xfail() - Streaming test timeouts increased 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
128 lines
3.9 KiB
Python
128 lines
3.9 KiB
Python
"""
|
|
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 get_default_user())
|
|
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
|
|
|
|
|
|
def get_default_user() -> str:
|
|
"""
|
|
Get default user from config (environment-aware).
|
|
|
|
- development/testing: llm_tester (isolated test scope)
|
|
- production: jpmschweitzer (real user)
|
|
"""
|
|
# Import here to avoid circular dependency
|
|
from src.core.config import config
|
|
return config.effective_default_user
|
|
|
|
|
|
# Request-scoped context variables (async-safe, isolated per request)
|
|
# Note: ContextVar default is evaluated at definition, so we use a sentinel
|
|
# and resolve the real default in get_user()
|
|
_USER_NOT_SET = "__user_not_set__"
|
|
current_user: ContextVar[str] = ContextVar("current_user", default=_USER_NOT_SET)
|
|
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 environment-aware default if not set.
|
|
|
|
Example:
|
|
user = get_user() # "llm_tester" (dev) or "jpmschweitzer" (prod)
|
|
"""
|
|
user = current_user.get()
|
|
if user == _USER_NOT_SET:
|
|
return get_default_user()
|
|
return user
|
|
|
|
|
|
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 environment-aware user if None)
|
|
conversation_id: Conversation ID (optional)
|
|
"""
|
|
self.user = user or get_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)
|