remdb 0.3.242__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of remdb might be problematic. Click here for more details.
- rem/__init__.py +129 -0
- rem/agentic/README.md +760 -0
- rem/agentic/__init__.py +54 -0
- rem/agentic/agents/README.md +155 -0
- rem/agentic/agents/__init__.py +38 -0
- rem/agentic/agents/agent_manager.py +311 -0
- rem/agentic/agents/sse_simulator.py +502 -0
- rem/agentic/context.py +425 -0
- rem/agentic/context_builder.py +360 -0
- rem/agentic/llm_provider_models.py +301 -0
- rem/agentic/mcp/__init__.py +0 -0
- rem/agentic/mcp/tool_wrapper.py +273 -0
- rem/agentic/otel/__init__.py +5 -0
- rem/agentic/otel/setup.py +240 -0
- rem/agentic/providers/phoenix.py +926 -0
- rem/agentic/providers/pydantic_ai.py +854 -0
- rem/agentic/query.py +117 -0
- rem/agentic/query_helper.py +89 -0
- rem/agentic/schema.py +737 -0
- rem/agentic/serialization.py +245 -0
- rem/agentic/tools/__init__.py +5 -0
- rem/agentic/tools/rem_tools.py +242 -0
- rem/api/README.md +657 -0
- rem/api/deps.py +253 -0
- rem/api/main.py +460 -0
- rem/api/mcp_router/prompts.py +182 -0
- rem/api/mcp_router/resources.py +820 -0
- rem/api/mcp_router/server.py +243 -0
- rem/api/mcp_router/tools.py +1605 -0
- rem/api/middleware/tracking.py +172 -0
- rem/api/routers/admin.py +520 -0
- rem/api/routers/auth.py +898 -0
- rem/api/routers/chat/__init__.py +5 -0
- rem/api/routers/chat/child_streaming.py +394 -0
- rem/api/routers/chat/completions.py +702 -0
- rem/api/routers/chat/json_utils.py +76 -0
- rem/api/routers/chat/models.py +202 -0
- rem/api/routers/chat/otel_utils.py +33 -0
- rem/api/routers/chat/sse_events.py +546 -0
- rem/api/routers/chat/streaming.py +950 -0
- rem/api/routers/chat/streaming_utils.py +327 -0
- rem/api/routers/common.py +18 -0
- rem/api/routers/dev.py +87 -0
- rem/api/routers/feedback.py +276 -0
- rem/api/routers/messages.py +620 -0
- rem/api/routers/models.py +86 -0
- rem/api/routers/query.py +362 -0
- rem/api/routers/shared_sessions.py +422 -0
- rem/auth/README.md +258 -0
- rem/auth/__init__.py +36 -0
- rem/auth/jwt.py +367 -0
- rem/auth/middleware.py +318 -0
- rem/auth/providers/__init__.py +16 -0
- rem/auth/providers/base.py +376 -0
- rem/auth/providers/email.py +215 -0
- rem/auth/providers/google.py +163 -0
- rem/auth/providers/microsoft.py +237 -0
- rem/cli/README.md +517 -0
- rem/cli/__init__.py +8 -0
- rem/cli/commands/README.md +299 -0
- rem/cli/commands/__init__.py +3 -0
- rem/cli/commands/ask.py +549 -0
- rem/cli/commands/cluster.py +1808 -0
- rem/cli/commands/configure.py +495 -0
- rem/cli/commands/db.py +828 -0
- rem/cli/commands/dreaming.py +324 -0
- rem/cli/commands/experiments.py +1698 -0
- rem/cli/commands/mcp.py +66 -0
- rem/cli/commands/process.py +388 -0
- rem/cli/commands/query.py +109 -0
- rem/cli/commands/scaffold.py +47 -0
- rem/cli/commands/schema.py +230 -0
- rem/cli/commands/serve.py +106 -0
- rem/cli/commands/session.py +453 -0
- rem/cli/dreaming.py +363 -0
- rem/cli/main.py +123 -0
- rem/config.py +244 -0
- rem/mcp_server.py +41 -0
- rem/models/core/__init__.py +49 -0
- rem/models/core/core_model.py +70 -0
- rem/models/core/engram.py +333 -0
- rem/models/core/experiment.py +672 -0
- rem/models/core/inline_edge.py +132 -0
- rem/models/core/rem_query.py +246 -0
- rem/models/entities/__init__.py +68 -0
- rem/models/entities/domain_resource.py +38 -0
- rem/models/entities/feedback.py +123 -0
- rem/models/entities/file.py +57 -0
- rem/models/entities/image_resource.py +88 -0
- rem/models/entities/message.py +64 -0
- rem/models/entities/moment.py +123 -0
- rem/models/entities/ontology.py +181 -0
- rem/models/entities/ontology_config.py +131 -0
- rem/models/entities/resource.py +95 -0
- rem/models/entities/schema.py +87 -0
- rem/models/entities/session.py +84 -0
- rem/models/entities/shared_session.py +180 -0
- rem/models/entities/subscriber.py +175 -0
- rem/models/entities/user.py +93 -0
- rem/py.typed +0 -0
- rem/registry.py +373 -0
- rem/schemas/README.md +507 -0
- rem/schemas/__init__.py +6 -0
- rem/schemas/agents/README.md +92 -0
- rem/schemas/agents/core/agent-builder.yaml +235 -0
- rem/schemas/agents/core/moment-builder.yaml +178 -0
- rem/schemas/agents/core/rem-query-agent.yaml +226 -0
- rem/schemas/agents/core/resource-affinity-assessor.yaml +99 -0
- rem/schemas/agents/core/simple-assistant.yaml +19 -0
- rem/schemas/agents/core/user-profile-builder.yaml +163 -0
- rem/schemas/agents/examples/contract-analyzer.yaml +317 -0
- rem/schemas/agents/examples/contract-extractor.yaml +134 -0
- rem/schemas/agents/examples/cv-parser.yaml +263 -0
- rem/schemas/agents/examples/hello-world.yaml +37 -0
- rem/schemas/agents/examples/query.yaml +54 -0
- rem/schemas/agents/examples/simple.yaml +21 -0
- rem/schemas/agents/examples/test.yaml +29 -0
- rem/schemas/agents/rem.yaml +132 -0
- rem/schemas/evaluators/hello-world/default.yaml +77 -0
- rem/schemas/evaluators/rem/faithfulness.yaml +219 -0
- rem/schemas/evaluators/rem/lookup-correctness.yaml +182 -0
- rem/schemas/evaluators/rem/retrieval-precision.yaml +199 -0
- rem/schemas/evaluators/rem/retrieval-recall.yaml +211 -0
- rem/schemas/evaluators/rem/search-correctness.yaml +192 -0
- rem/services/__init__.py +18 -0
- rem/services/audio/INTEGRATION.md +308 -0
- rem/services/audio/README.md +376 -0
- rem/services/audio/__init__.py +15 -0
- rem/services/audio/chunker.py +354 -0
- rem/services/audio/transcriber.py +259 -0
- rem/services/content/README.md +1269 -0
- rem/services/content/__init__.py +5 -0
- rem/services/content/providers.py +760 -0
- rem/services/content/service.py +762 -0
- rem/services/dreaming/README.md +230 -0
- rem/services/dreaming/__init__.py +53 -0
- rem/services/dreaming/affinity_service.py +322 -0
- rem/services/dreaming/moment_service.py +251 -0
- rem/services/dreaming/ontology_service.py +54 -0
- rem/services/dreaming/user_model_service.py +297 -0
- rem/services/dreaming/utils.py +39 -0
- rem/services/email/__init__.py +10 -0
- rem/services/email/service.py +522 -0
- rem/services/email/templates.py +360 -0
- rem/services/embeddings/__init__.py +11 -0
- rem/services/embeddings/api.py +127 -0
- rem/services/embeddings/worker.py +435 -0
- rem/services/fs/README.md +662 -0
- rem/services/fs/__init__.py +62 -0
- rem/services/fs/examples.py +206 -0
- rem/services/fs/examples_paths.py +204 -0
- rem/services/fs/git_provider.py +935 -0
- rem/services/fs/local_provider.py +760 -0
- rem/services/fs/parsing-hooks-examples.md +172 -0
- rem/services/fs/paths.py +276 -0
- rem/services/fs/provider.py +460 -0
- rem/services/fs/s3_provider.py +1042 -0
- rem/services/fs/service.py +186 -0
- rem/services/git/README.md +1075 -0
- rem/services/git/__init__.py +17 -0
- rem/services/git/service.py +469 -0
- rem/services/phoenix/EXPERIMENT_DESIGN.md +1146 -0
- rem/services/phoenix/README.md +453 -0
- rem/services/phoenix/__init__.py +46 -0
- rem/services/phoenix/client.py +960 -0
- rem/services/phoenix/config.py +88 -0
- rem/services/phoenix/prompt_labels.py +477 -0
- rem/services/postgres/README.md +757 -0
- rem/services/postgres/__init__.py +49 -0
- rem/services/postgres/diff_service.py +599 -0
- rem/services/postgres/migration_service.py +427 -0
- rem/services/postgres/programmable_diff_service.py +635 -0
- rem/services/postgres/pydantic_to_sqlalchemy.py +562 -0
- rem/services/postgres/register_type.py +353 -0
- rem/services/postgres/repository.py +481 -0
- rem/services/postgres/schema_generator.py +661 -0
- rem/services/postgres/service.py +802 -0
- rem/services/postgres/sql_builder.py +355 -0
- rem/services/rate_limit.py +113 -0
- rem/services/rem/README.md +318 -0
- rem/services/rem/__init__.py +23 -0
- rem/services/rem/exceptions.py +71 -0
- rem/services/rem/executor.py +293 -0
- rem/services/rem/parser.py +180 -0
- rem/services/rem/queries.py +196 -0
- rem/services/rem/query.py +371 -0
- rem/services/rem/service.py +608 -0
- rem/services/session/README.md +374 -0
- rem/services/session/__init__.py +13 -0
- rem/services/session/compression.py +488 -0
- rem/services/session/pydantic_messages.py +310 -0
- rem/services/session/reload.py +85 -0
- rem/services/user_service.py +130 -0
- rem/settings.py +1877 -0
- rem/sql/background_indexes.sql +52 -0
- rem/sql/migrations/001_install.sql +983 -0
- rem/sql/migrations/002_install_models.sql +3157 -0
- rem/sql/migrations/003_optional_extensions.sql +326 -0
- rem/sql/migrations/004_cache_system.sql +282 -0
- rem/sql/migrations/005_schema_update.sql +145 -0
- rem/sql/migrations/migrate_session_id_to_uuid.sql +45 -0
- rem/utils/AGENTIC_CHUNKING.md +597 -0
- rem/utils/README.md +628 -0
- rem/utils/__init__.py +61 -0
- rem/utils/agentic_chunking.py +622 -0
- rem/utils/batch_ops.py +343 -0
- rem/utils/chunking.py +108 -0
- rem/utils/clip_embeddings.py +276 -0
- rem/utils/constants.py +97 -0
- rem/utils/date_utils.py +228 -0
- rem/utils/dict_utils.py +98 -0
- rem/utils/embeddings.py +436 -0
- rem/utils/examples/embeddings_example.py +305 -0
- rem/utils/examples/sql_types_example.py +202 -0
- rem/utils/files.py +323 -0
- rem/utils/markdown.py +16 -0
- rem/utils/mime_types.py +158 -0
- rem/utils/model_helpers.py +492 -0
- rem/utils/schema_loader.py +649 -0
- rem/utils/sql_paths.py +146 -0
- rem/utils/sql_types.py +350 -0
- rem/utils/user_id.py +81 -0
- rem/utils/vision.py +325 -0
- rem/workers/README.md +506 -0
- rem/workers/__init__.py +7 -0
- rem/workers/db_listener.py +579 -0
- rem/workers/db_maintainer.py +74 -0
- rem/workers/dreaming.py +502 -0
- rem/workers/engram_processor.py +312 -0
- rem/workers/sqs_file_processor.py +193 -0
- rem/workers/unlogged_maintainer.py +463 -0
- remdb-0.3.242.dist-info/METADATA +1632 -0
- remdb-0.3.242.dist-info/RECORD +235 -0
- remdb-0.3.242.dist-info/WHEEL +4 -0
- remdb-0.3.242.dist-info/entry_points.txt +2 -0
rem/agentic/context.py
ADDED
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Agent execution context and configuration.
|
|
3
|
+
|
|
4
|
+
Design pattern for session context that can be constructed from:
|
|
5
|
+
- FastAPI Request object (preferred - extracts user from JWT via request.state)
|
|
6
|
+
- HTTP headers (X-User-Id, X-Session-Id, X-Model-Name, X-Is-Eval, etc.)
|
|
7
|
+
- Direct instantiation for testing/CLI
|
|
8
|
+
|
|
9
|
+
User ID Sources (in priority order):
|
|
10
|
+
1. request.state.user.id - From JWT token validated by auth middleware (SECURE)
|
|
11
|
+
2. X-User-Id header - Fallback for backwards compatibility (less secure)
|
|
12
|
+
|
|
13
|
+
Headers Mapping:
|
|
14
|
+
X-Tenant-Id → context.tenant_id (default: "default")
|
|
15
|
+
X-Session-Id → context.session_id
|
|
16
|
+
X-Agent-Schema → context.agent_schema_uri (default: "rem")
|
|
17
|
+
X-Model-Name → context.default_model
|
|
18
|
+
X-Is-Eval → context.is_eval (marks session as evaluation)
|
|
19
|
+
|
|
20
|
+
Key Design Pattern:
|
|
21
|
+
- AgentContext is passed to agent factory, not stored in agents
|
|
22
|
+
- Enables session tracking across API, CLI, and test execution
|
|
23
|
+
- Supports header-based configuration override (model, schema URI)
|
|
24
|
+
- Clean separation: context (who/what) vs agent (how)
|
|
25
|
+
|
|
26
|
+
Multi-Agent Context Propagation:
|
|
27
|
+
- ContextVar (_current_agent_context) threads context through nested agent calls
|
|
28
|
+
- Parent context is automatically available to child agents via get_current_context()
|
|
29
|
+
- Use agent_context_scope() context manager for scoped context setting
|
|
30
|
+
- Child agents inherit user_id, tenant_id, session_id, is_eval from parent
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
import asyncio
|
|
34
|
+
from contextlib import contextmanager
|
|
35
|
+
from contextvars import ContextVar
|
|
36
|
+
from typing import Any, Generator
|
|
37
|
+
|
|
38
|
+
from loguru import logger
|
|
39
|
+
from pydantic import BaseModel, Field
|
|
40
|
+
|
|
41
|
+
from ..settings import settings
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# Thread-local context for current agent execution
|
|
45
|
+
# This enables context propagation through nested agent calls (multi-agent)
|
|
46
|
+
_current_agent_context: ContextVar["AgentContext | None"] = ContextVar(
|
|
47
|
+
"current_agent_context", default=None
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# Event sink for streaming child agent events to parent
|
|
51
|
+
# When set, child agents (via ask_agent) should push their events here
|
|
52
|
+
# for the parent's streaming loop to proxy to the client
|
|
53
|
+
_parent_event_sink: ContextVar["asyncio.Queue | None"] = ContextVar(
|
|
54
|
+
"parent_event_sink", default=None
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def get_current_context() -> "AgentContext | None":
|
|
59
|
+
"""
|
|
60
|
+
Get the current agent context from context var.
|
|
61
|
+
|
|
62
|
+
Used by MCP tools (like ask_agent) to inherit context from parent agent.
|
|
63
|
+
Returns None if no context is set (e.g., direct CLI invocation without context).
|
|
64
|
+
|
|
65
|
+
Example:
|
|
66
|
+
# In an MCP tool
|
|
67
|
+
parent_context = get_current_context()
|
|
68
|
+
if parent_context:
|
|
69
|
+
# Inherit user_id, session_id, etc. from parent
|
|
70
|
+
child_context = parent_context.child_context(agent_schema_uri="child-agent")
|
|
71
|
+
"""
|
|
72
|
+
return _current_agent_context.get()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def set_current_context(ctx: "AgentContext | None") -> None:
|
|
76
|
+
"""
|
|
77
|
+
Set the current agent context.
|
|
78
|
+
|
|
79
|
+
Called by streaming layer before agent execution.
|
|
80
|
+
Should be cleared (set to None) after execution completes.
|
|
81
|
+
"""
|
|
82
|
+
_current_agent_context.set(ctx)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@contextmanager
|
|
86
|
+
def agent_context_scope(ctx: "AgentContext") -> Generator["AgentContext", None, None]:
|
|
87
|
+
"""
|
|
88
|
+
Context manager for scoped context setting.
|
|
89
|
+
|
|
90
|
+
Automatically restores previous context when exiting scope.
|
|
91
|
+
Safe for nested agent calls - each level preserves its parent's context.
|
|
92
|
+
|
|
93
|
+
Example:
|
|
94
|
+
context = AgentContext(user_id="user-123")
|
|
95
|
+
with agent_context_scope(context):
|
|
96
|
+
# Context is available via get_current_context()
|
|
97
|
+
result = await agent.run(...)
|
|
98
|
+
# Previous context (or None) is restored
|
|
99
|
+
"""
|
|
100
|
+
previous = _current_agent_context.get()
|
|
101
|
+
_current_agent_context.set(ctx)
|
|
102
|
+
try:
|
|
103
|
+
yield ctx
|
|
104
|
+
finally:
|
|
105
|
+
_current_agent_context.set(previous)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# =============================================================================
|
|
109
|
+
# Event Sink for Streaming Multi-Agent Delegation
|
|
110
|
+
# =============================================================================
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def get_event_sink() -> "asyncio.Queue | None":
|
|
114
|
+
"""
|
|
115
|
+
Get the parent's event sink for streaming child events.
|
|
116
|
+
|
|
117
|
+
Used by ask_agent to push child agent events to the parent's stream.
|
|
118
|
+
Returns None if not in a streaming context.
|
|
119
|
+
"""
|
|
120
|
+
return _parent_event_sink.get()
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def set_event_sink(sink: "asyncio.Queue | None") -> None:
|
|
124
|
+
"""Set the event sink for child agents to push events to."""
|
|
125
|
+
_parent_event_sink.set(sink)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@contextmanager
|
|
129
|
+
def event_sink_scope(sink: "asyncio.Queue") -> Generator["asyncio.Queue", None, None]:
|
|
130
|
+
"""
|
|
131
|
+
Context manager for scoped event sink setting.
|
|
132
|
+
|
|
133
|
+
Used by streaming layer to set up event proxying before tool execution.
|
|
134
|
+
Child agents (via ask_agent) will push their events to this sink.
|
|
135
|
+
|
|
136
|
+
Example:
|
|
137
|
+
event_queue = asyncio.Queue()
|
|
138
|
+
with event_sink_scope(event_queue):
|
|
139
|
+
# ask_agent will push child events to event_queue
|
|
140
|
+
async for event in tools_stream:
|
|
141
|
+
...
|
|
142
|
+
# Also consume from event_queue
|
|
143
|
+
"""
|
|
144
|
+
previous = _parent_event_sink.get()
|
|
145
|
+
_parent_event_sink.set(sink)
|
|
146
|
+
try:
|
|
147
|
+
yield sink
|
|
148
|
+
finally:
|
|
149
|
+
_parent_event_sink.set(previous)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
async def push_event(event: Any) -> bool:
|
|
153
|
+
"""
|
|
154
|
+
Push an event to the parent's event sink (if available).
|
|
155
|
+
|
|
156
|
+
Used by ask_agent to proxy child agent events to the parent's stream.
|
|
157
|
+
Returns True if event was pushed, False if no sink available.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
event: Any streaming event (ToolCallEvent, content chunk, etc.)
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
True if event was pushed to sink, False otherwise
|
|
164
|
+
"""
|
|
165
|
+
sink = _parent_event_sink.get()
|
|
166
|
+
if sink is not None:
|
|
167
|
+
await sink.put(event)
|
|
168
|
+
return True
|
|
169
|
+
return False
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class AgentContext(BaseModel):
|
|
173
|
+
"""
|
|
174
|
+
Session and configuration context for agent execution.
|
|
175
|
+
|
|
176
|
+
Provides session identifiers (user_id, tenant_id, session_id) and
|
|
177
|
+
configuration defaults (model) for agent factory and execution.
|
|
178
|
+
|
|
179
|
+
Design Pattern
|
|
180
|
+
- Construct from HTTP headers via from_headers()
|
|
181
|
+
- Pass to agent factory, not stored in agent
|
|
182
|
+
- Enables header-based model/schema override
|
|
183
|
+
- Supports observability (user tracking, session continuity)
|
|
184
|
+
|
|
185
|
+
Example:
|
|
186
|
+
# From HTTP request
|
|
187
|
+
context = AgentContext.from_headers(request.headers)
|
|
188
|
+
agent = await create_agent(context)
|
|
189
|
+
|
|
190
|
+
# Direct construction for testing
|
|
191
|
+
context = AgentContext(user_id="test-user", tenant_id="test-tenant")
|
|
192
|
+
agent = await create_agent(context)
|
|
193
|
+
"""
|
|
194
|
+
|
|
195
|
+
user_id: str | None = Field(
|
|
196
|
+
default=None,
|
|
197
|
+
description="User identifier for tracking and personalization",
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
tenant_id: str = Field(
|
|
201
|
+
default="default",
|
|
202
|
+
description="Tenant identifier for multi-tenancy isolation (REM requirement)",
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
session_id: str | None = Field(
|
|
206
|
+
default=None,
|
|
207
|
+
description="Session/conversation identifier for continuity",
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
default_model: str = Field(
|
|
211
|
+
default_factory=lambda: settings.llm.default_model,
|
|
212
|
+
description="Default LLM model (can be overridden via headers)",
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
agent_schema_uri: str | None = Field(
|
|
216
|
+
default=None,
|
|
217
|
+
description="Agent schema URI (e.g., 'rem-agents-query-agent')",
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
is_eval: bool = Field(
|
|
221
|
+
default=False,
|
|
222
|
+
description="Whether this is an evaluation session (set via X-Is-Eval header)",
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
model_config = {"populate_by_name": True}
|
|
226
|
+
|
|
227
|
+
def child_context(
|
|
228
|
+
self,
|
|
229
|
+
agent_schema_uri: str | None = None,
|
|
230
|
+
model_override: str | None = None,
|
|
231
|
+
) -> "AgentContext":
|
|
232
|
+
"""
|
|
233
|
+
Create a child context for nested agent calls.
|
|
234
|
+
|
|
235
|
+
Inherits user_id, tenant_id, session_id, is_eval from parent.
|
|
236
|
+
Allows overriding agent_schema_uri and default_model for the child.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
agent_schema_uri: Agent schema for the child agent (required for lineage)
|
|
240
|
+
model_override: Optional model override for child agent
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
New AgentContext for the child agent
|
|
244
|
+
|
|
245
|
+
Example:
|
|
246
|
+
parent_context = get_current_context()
|
|
247
|
+
child_context = parent_context.child_context(
|
|
248
|
+
agent_schema_uri="sentiment-analyzer"
|
|
249
|
+
)
|
|
250
|
+
agent = await create_agent(context=child_context)
|
|
251
|
+
"""
|
|
252
|
+
return AgentContext(
|
|
253
|
+
user_id=self.user_id,
|
|
254
|
+
tenant_id=self.tenant_id,
|
|
255
|
+
session_id=self.session_id,
|
|
256
|
+
default_model=model_override or self.default_model,
|
|
257
|
+
agent_schema_uri=agent_schema_uri or self.agent_schema_uri,
|
|
258
|
+
is_eval=self.is_eval,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
@staticmethod
|
|
262
|
+
def get_user_id_or_default(
|
|
263
|
+
user_id: str | None,
|
|
264
|
+
source: str = "context",
|
|
265
|
+
default: str | None = None,
|
|
266
|
+
) -> str | None:
|
|
267
|
+
"""
|
|
268
|
+
Get user_id or return None for anonymous access.
|
|
269
|
+
|
|
270
|
+
User ID convention:
|
|
271
|
+
- user_id is a deterministic UUID5 hash of the user's email address
|
|
272
|
+
- Use rem.utils.user_id.email_to_user_id(email) to generate
|
|
273
|
+
- The JWT's `sub` claim is NOT directly used as user_id
|
|
274
|
+
- Authentication middleware extracts email from JWT and hashes it
|
|
275
|
+
|
|
276
|
+
When user_id is None, queries return data with user_id IS NULL
|
|
277
|
+
(shared/public data). This is intentional - no fake user IDs.
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
user_id: User identifier (UUID5 hash of email, may be None for anonymous)
|
|
281
|
+
source: Source of the call (for logging clarity)
|
|
282
|
+
default: Explicit default (only for testing, not auto-generated)
|
|
283
|
+
|
|
284
|
+
Returns:
|
|
285
|
+
user_id if provided, explicit default if provided, otherwise None
|
|
286
|
+
|
|
287
|
+
Example:
|
|
288
|
+
# Generate user_id from email (done by auth middleware)
|
|
289
|
+
from rem.utils.user_id import email_to_user_id
|
|
290
|
+
user_id = email_to_user_id("alice@example.com")
|
|
291
|
+
# -> "2c5ea4c0-4067-5fef-942d-0a20124e06d8"
|
|
292
|
+
|
|
293
|
+
# In MCP tool - anonymous user sees shared data
|
|
294
|
+
user_id = AgentContext.get_user_id_or_default(
|
|
295
|
+
user_id, source="ask_rem_agent"
|
|
296
|
+
)
|
|
297
|
+
# Returns None if not authenticated -> queries WHERE user_id IS NULL
|
|
298
|
+
"""
|
|
299
|
+
if user_id is not None:
|
|
300
|
+
return user_id
|
|
301
|
+
if default is not None:
|
|
302
|
+
logger.debug(f"Using explicit default user_id '{default}' from {source}")
|
|
303
|
+
return default
|
|
304
|
+
# No fake user IDs - return None for anonymous/unauthenticated
|
|
305
|
+
logger.debug(f"No user_id from {source}, using None (anonymous/shared data)")
|
|
306
|
+
return None
|
|
307
|
+
|
|
308
|
+
@classmethod
|
|
309
|
+
def from_request(cls, request: "Request") -> "AgentContext":
|
|
310
|
+
"""
|
|
311
|
+
Construct AgentContext from a FastAPI Request object.
|
|
312
|
+
|
|
313
|
+
This is the PREFERRED method for API endpoints. It extracts user_id
|
|
314
|
+
from the authenticated user in request.state (set by auth middleware
|
|
315
|
+
from JWT token), which is more secure than trusting X-User-Id header.
|
|
316
|
+
|
|
317
|
+
Priority for user_id:
|
|
318
|
+
1. request.state.user.id - From validated JWT token (SECURE)
|
|
319
|
+
2. X-User-Id header - Fallback for backwards compatibility
|
|
320
|
+
|
|
321
|
+
Args:
|
|
322
|
+
request: FastAPI Request object
|
|
323
|
+
|
|
324
|
+
Returns:
|
|
325
|
+
AgentContext with user from JWT and other values from headers
|
|
326
|
+
|
|
327
|
+
Example:
|
|
328
|
+
@app.post("/api/v1/chat/completions")
|
|
329
|
+
async def chat(request: Request, body: ChatRequest):
|
|
330
|
+
context = AgentContext.from_request(request)
|
|
331
|
+
# context.user_id is from JWT, not header
|
|
332
|
+
"""
|
|
333
|
+
from typing import TYPE_CHECKING
|
|
334
|
+
if TYPE_CHECKING:
|
|
335
|
+
from starlette.requests import Request
|
|
336
|
+
|
|
337
|
+
# Get headers dict
|
|
338
|
+
headers = dict(request.headers)
|
|
339
|
+
normalized = {k.lower(): v for k, v in headers.items()}
|
|
340
|
+
|
|
341
|
+
# Extract user_id from authenticated user (JWT) - this is the source of truth
|
|
342
|
+
user_id = None
|
|
343
|
+
tenant_id = "default"
|
|
344
|
+
|
|
345
|
+
if hasattr(request, "state"):
|
|
346
|
+
user = getattr(request.state, "user", None)
|
|
347
|
+
if user and isinstance(user, dict):
|
|
348
|
+
user_id = user.get("id")
|
|
349
|
+
# Also get tenant_id from authenticated user if available
|
|
350
|
+
if user.get("tenant_id"):
|
|
351
|
+
tenant_id = user.get("tenant_id")
|
|
352
|
+
if user_id:
|
|
353
|
+
logger.debug(f"User ID from JWT: {user_id}")
|
|
354
|
+
|
|
355
|
+
# Fallback to X-User-Id header if no authenticated user
|
|
356
|
+
if not user_id:
|
|
357
|
+
user_id = normalized.get("x-user-id")
|
|
358
|
+
if user_id:
|
|
359
|
+
logger.debug(f"User ID from X-User-Id header (fallback): {user_id}")
|
|
360
|
+
|
|
361
|
+
# Override tenant_id from header if provided
|
|
362
|
+
header_tenant = normalized.get("x-tenant-id")
|
|
363
|
+
if header_tenant:
|
|
364
|
+
tenant_id = header_tenant
|
|
365
|
+
|
|
366
|
+
# Parse X-Is-Eval header
|
|
367
|
+
is_eval_str = normalized.get("x-is-eval", "").lower()
|
|
368
|
+
is_eval = is_eval_str in ("true", "1", "yes")
|
|
369
|
+
|
|
370
|
+
return cls(
|
|
371
|
+
user_id=user_id,
|
|
372
|
+
tenant_id=tenant_id,
|
|
373
|
+
session_id=normalized.get("x-session-id"),
|
|
374
|
+
default_model=normalized.get("x-model-name") or settings.llm.default_model,
|
|
375
|
+
agent_schema_uri=normalized.get("x-agent-schema"),
|
|
376
|
+
is_eval=is_eval,
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
@classmethod
|
|
380
|
+
def from_headers(cls, headers: dict[str, str]) -> "AgentContext":
|
|
381
|
+
"""
|
|
382
|
+
Construct AgentContext from HTTP headers dict.
|
|
383
|
+
|
|
384
|
+
NOTE: Prefer from_request() for API endpoints as it extracts user_id
|
|
385
|
+
from the validated JWT token in request.state, which is more secure.
|
|
386
|
+
|
|
387
|
+
Reads standard headers:
|
|
388
|
+
- X-User-Id: User identifier (fallback - prefer JWT)
|
|
389
|
+
- X-Tenant-Id: Tenant identifier
|
|
390
|
+
- X-Session-Id: Session identifier
|
|
391
|
+
- X-Model-Name: Model override
|
|
392
|
+
- X-Agent-Schema: Agent schema URI
|
|
393
|
+
- X-Is-Eval: Whether this is an evaluation session (true/false)
|
|
394
|
+
|
|
395
|
+
Args:
|
|
396
|
+
headers: Dictionary of HTTP headers (case-insensitive)
|
|
397
|
+
|
|
398
|
+
Returns:
|
|
399
|
+
AgentContext with values from headers
|
|
400
|
+
|
|
401
|
+
Example:
|
|
402
|
+
headers = {
|
|
403
|
+
"X-User-Id": "user123",
|
|
404
|
+
"X-Tenant-Id": "acme-corp",
|
|
405
|
+
"X-Session-Id": "sess-456",
|
|
406
|
+
"X-Model-Name": "anthropic:claude-opus-4-20250514",
|
|
407
|
+
"X-Is-Eval": "true"
|
|
408
|
+
}
|
|
409
|
+
context = AgentContext.from_headers(headers)
|
|
410
|
+
"""
|
|
411
|
+
# Normalize header keys to lowercase for case-insensitive lookup
|
|
412
|
+
normalized = {k.lower(): v for k, v in headers.items()}
|
|
413
|
+
|
|
414
|
+
# Parse X-Is-Eval header (accepts "true", "1", "yes" as truthy)
|
|
415
|
+
is_eval_str = normalized.get("x-is-eval", "").lower()
|
|
416
|
+
is_eval = is_eval_str in ("true", "1", "yes")
|
|
417
|
+
|
|
418
|
+
return cls(
|
|
419
|
+
user_id=normalized.get("x-user-id"),
|
|
420
|
+
tenant_id=normalized.get("x-tenant-id", "default"),
|
|
421
|
+
session_id=normalized.get("x-session-id"),
|
|
422
|
+
default_model=normalized.get("x-model-name") or settings.llm.default_model,
|
|
423
|
+
agent_schema_uri=normalized.get("x-agent-schema"),
|
|
424
|
+
is_eval=is_eval,
|
|
425
|
+
)
|