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.

Files changed (235) hide show
  1. rem/__init__.py +129 -0
  2. rem/agentic/README.md +760 -0
  3. rem/agentic/__init__.py +54 -0
  4. rem/agentic/agents/README.md +155 -0
  5. rem/agentic/agents/__init__.py +38 -0
  6. rem/agentic/agents/agent_manager.py +311 -0
  7. rem/agentic/agents/sse_simulator.py +502 -0
  8. rem/agentic/context.py +425 -0
  9. rem/agentic/context_builder.py +360 -0
  10. rem/agentic/llm_provider_models.py +301 -0
  11. rem/agentic/mcp/__init__.py +0 -0
  12. rem/agentic/mcp/tool_wrapper.py +273 -0
  13. rem/agentic/otel/__init__.py +5 -0
  14. rem/agentic/otel/setup.py +240 -0
  15. rem/agentic/providers/phoenix.py +926 -0
  16. rem/agentic/providers/pydantic_ai.py +854 -0
  17. rem/agentic/query.py +117 -0
  18. rem/agentic/query_helper.py +89 -0
  19. rem/agentic/schema.py +737 -0
  20. rem/agentic/serialization.py +245 -0
  21. rem/agentic/tools/__init__.py +5 -0
  22. rem/agentic/tools/rem_tools.py +242 -0
  23. rem/api/README.md +657 -0
  24. rem/api/deps.py +253 -0
  25. rem/api/main.py +460 -0
  26. rem/api/mcp_router/prompts.py +182 -0
  27. rem/api/mcp_router/resources.py +820 -0
  28. rem/api/mcp_router/server.py +243 -0
  29. rem/api/mcp_router/tools.py +1605 -0
  30. rem/api/middleware/tracking.py +172 -0
  31. rem/api/routers/admin.py +520 -0
  32. rem/api/routers/auth.py +898 -0
  33. rem/api/routers/chat/__init__.py +5 -0
  34. rem/api/routers/chat/child_streaming.py +394 -0
  35. rem/api/routers/chat/completions.py +702 -0
  36. rem/api/routers/chat/json_utils.py +76 -0
  37. rem/api/routers/chat/models.py +202 -0
  38. rem/api/routers/chat/otel_utils.py +33 -0
  39. rem/api/routers/chat/sse_events.py +546 -0
  40. rem/api/routers/chat/streaming.py +950 -0
  41. rem/api/routers/chat/streaming_utils.py +327 -0
  42. rem/api/routers/common.py +18 -0
  43. rem/api/routers/dev.py +87 -0
  44. rem/api/routers/feedback.py +276 -0
  45. rem/api/routers/messages.py +620 -0
  46. rem/api/routers/models.py +86 -0
  47. rem/api/routers/query.py +362 -0
  48. rem/api/routers/shared_sessions.py +422 -0
  49. rem/auth/README.md +258 -0
  50. rem/auth/__init__.py +36 -0
  51. rem/auth/jwt.py +367 -0
  52. rem/auth/middleware.py +318 -0
  53. rem/auth/providers/__init__.py +16 -0
  54. rem/auth/providers/base.py +376 -0
  55. rem/auth/providers/email.py +215 -0
  56. rem/auth/providers/google.py +163 -0
  57. rem/auth/providers/microsoft.py +237 -0
  58. rem/cli/README.md +517 -0
  59. rem/cli/__init__.py +8 -0
  60. rem/cli/commands/README.md +299 -0
  61. rem/cli/commands/__init__.py +3 -0
  62. rem/cli/commands/ask.py +549 -0
  63. rem/cli/commands/cluster.py +1808 -0
  64. rem/cli/commands/configure.py +495 -0
  65. rem/cli/commands/db.py +828 -0
  66. rem/cli/commands/dreaming.py +324 -0
  67. rem/cli/commands/experiments.py +1698 -0
  68. rem/cli/commands/mcp.py +66 -0
  69. rem/cli/commands/process.py +388 -0
  70. rem/cli/commands/query.py +109 -0
  71. rem/cli/commands/scaffold.py +47 -0
  72. rem/cli/commands/schema.py +230 -0
  73. rem/cli/commands/serve.py +106 -0
  74. rem/cli/commands/session.py +453 -0
  75. rem/cli/dreaming.py +363 -0
  76. rem/cli/main.py +123 -0
  77. rem/config.py +244 -0
  78. rem/mcp_server.py +41 -0
  79. rem/models/core/__init__.py +49 -0
  80. rem/models/core/core_model.py +70 -0
  81. rem/models/core/engram.py +333 -0
  82. rem/models/core/experiment.py +672 -0
  83. rem/models/core/inline_edge.py +132 -0
  84. rem/models/core/rem_query.py +246 -0
  85. rem/models/entities/__init__.py +68 -0
  86. rem/models/entities/domain_resource.py +38 -0
  87. rem/models/entities/feedback.py +123 -0
  88. rem/models/entities/file.py +57 -0
  89. rem/models/entities/image_resource.py +88 -0
  90. rem/models/entities/message.py +64 -0
  91. rem/models/entities/moment.py +123 -0
  92. rem/models/entities/ontology.py +181 -0
  93. rem/models/entities/ontology_config.py +131 -0
  94. rem/models/entities/resource.py +95 -0
  95. rem/models/entities/schema.py +87 -0
  96. rem/models/entities/session.py +84 -0
  97. rem/models/entities/shared_session.py +180 -0
  98. rem/models/entities/subscriber.py +175 -0
  99. rem/models/entities/user.py +93 -0
  100. rem/py.typed +0 -0
  101. rem/registry.py +373 -0
  102. rem/schemas/README.md +507 -0
  103. rem/schemas/__init__.py +6 -0
  104. rem/schemas/agents/README.md +92 -0
  105. rem/schemas/agents/core/agent-builder.yaml +235 -0
  106. rem/schemas/agents/core/moment-builder.yaml +178 -0
  107. rem/schemas/agents/core/rem-query-agent.yaml +226 -0
  108. rem/schemas/agents/core/resource-affinity-assessor.yaml +99 -0
  109. rem/schemas/agents/core/simple-assistant.yaml +19 -0
  110. rem/schemas/agents/core/user-profile-builder.yaml +163 -0
  111. rem/schemas/agents/examples/contract-analyzer.yaml +317 -0
  112. rem/schemas/agents/examples/contract-extractor.yaml +134 -0
  113. rem/schemas/agents/examples/cv-parser.yaml +263 -0
  114. rem/schemas/agents/examples/hello-world.yaml +37 -0
  115. rem/schemas/agents/examples/query.yaml +54 -0
  116. rem/schemas/agents/examples/simple.yaml +21 -0
  117. rem/schemas/agents/examples/test.yaml +29 -0
  118. rem/schemas/agents/rem.yaml +132 -0
  119. rem/schemas/evaluators/hello-world/default.yaml +77 -0
  120. rem/schemas/evaluators/rem/faithfulness.yaml +219 -0
  121. rem/schemas/evaluators/rem/lookup-correctness.yaml +182 -0
  122. rem/schemas/evaluators/rem/retrieval-precision.yaml +199 -0
  123. rem/schemas/evaluators/rem/retrieval-recall.yaml +211 -0
  124. rem/schemas/evaluators/rem/search-correctness.yaml +192 -0
  125. rem/services/__init__.py +18 -0
  126. rem/services/audio/INTEGRATION.md +308 -0
  127. rem/services/audio/README.md +376 -0
  128. rem/services/audio/__init__.py +15 -0
  129. rem/services/audio/chunker.py +354 -0
  130. rem/services/audio/transcriber.py +259 -0
  131. rem/services/content/README.md +1269 -0
  132. rem/services/content/__init__.py +5 -0
  133. rem/services/content/providers.py +760 -0
  134. rem/services/content/service.py +762 -0
  135. rem/services/dreaming/README.md +230 -0
  136. rem/services/dreaming/__init__.py +53 -0
  137. rem/services/dreaming/affinity_service.py +322 -0
  138. rem/services/dreaming/moment_service.py +251 -0
  139. rem/services/dreaming/ontology_service.py +54 -0
  140. rem/services/dreaming/user_model_service.py +297 -0
  141. rem/services/dreaming/utils.py +39 -0
  142. rem/services/email/__init__.py +10 -0
  143. rem/services/email/service.py +522 -0
  144. rem/services/email/templates.py +360 -0
  145. rem/services/embeddings/__init__.py +11 -0
  146. rem/services/embeddings/api.py +127 -0
  147. rem/services/embeddings/worker.py +435 -0
  148. rem/services/fs/README.md +662 -0
  149. rem/services/fs/__init__.py +62 -0
  150. rem/services/fs/examples.py +206 -0
  151. rem/services/fs/examples_paths.py +204 -0
  152. rem/services/fs/git_provider.py +935 -0
  153. rem/services/fs/local_provider.py +760 -0
  154. rem/services/fs/parsing-hooks-examples.md +172 -0
  155. rem/services/fs/paths.py +276 -0
  156. rem/services/fs/provider.py +460 -0
  157. rem/services/fs/s3_provider.py +1042 -0
  158. rem/services/fs/service.py +186 -0
  159. rem/services/git/README.md +1075 -0
  160. rem/services/git/__init__.py +17 -0
  161. rem/services/git/service.py +469 -0
  162. rem/services/phoenix/EXPERIMENT_DESIGN.md +1146 -0
  163. rem/services/phoenix/README.md +453 -0
  164. rem/services/phoenix/__init__.py +46 -0
  165. rem/services/phoenix/client.py +960 -0
  166. rem/services/phoenix/config.py +88 -0
  167. rem/services/phoenix/prompt_labels.py +477 -0
  168. rem/services/postgres/README.md +757 -0
  169. rem/services/postgres/__init__.py +49 -0
  170. rem/services/postgres/diff_service.py +599 -0
  171. rem/services/postgres/migration_service.py +427 -0
  172. rem/services/postgres/programmable_diff_service.py +635 -0
  173. rem/services/postgres/pydantic_to_sqlalchemy.py +562 -0
  174. rem/services/postgres/register_type.py +353 -0
  175. rem/services/postgres/repository.py +481 -0
  176. rem/services/postgres/schema_generator.py +661 -0
  177. rem/services/postgres/service.py +802 -0
  178. rem/services/postgres/sql_builder.py +355 -0
  179. rem/services/rate_limit.py +113 -0
  180. rem/services/rem/README.md +318 -0
  181. rem/services/rem/__init__.py +23 -0
  182. rem/services/rem/exceptions.py +71 -0
  183. rem/services/rem/executor.py +293 -0
  184. rem/services/rem/parser.py +180 -0
  185. rem/services/rem/queries.py +196 -0
  186. rem/services/rem/query.py +371 -0
  187. rem/services/rem/service.py +608 -0
  188. rem/services/session/README.md +374 -0
  189. rem/services/session/__init__.py +13 -0
  190. rem/services/session/compression.py +488 -0
  191. rem/services/session/pydantic_messages.py +310 -0
  192. rem/services/session/reload.py +85 -0
  193. rem/services/user_service.py +130 -0
  194. rem/settings.py +1877 -0
  195. rem/sql/background_indexes.sql +52 -0
  196. rem/sql/migrations/001_install.sql +983 -0
  197. rem/sql/migrations/002_install_models.sql +3157 -0
  198. rem/sql/migrations/003_optional_extensions.sql +326 -0
  199. rem/sql/migrations/004_cache_system.sql +282 -0
  200. rem/sql/migrations/005_schema_update.sql +145 -0
  201. rem/sql/migrations/migrate_session_id_to_uuid.sql +45 -0
  202. rem/utils/AGENTIC_CHUNKING.md +597 -0
  203. rem/utils/README.md +628 -0
  204. rem/utils/__init__.py +61 -0
  205. rem/utils/agentic_chunking.py +622 -0
  206. rem/utils/batch_ops.py +343 -0
  207. rem/utils/chunking.py +108 -0
  208. rem/utils/clip_embeddings.py +276 -0
  209. rem/utils/constants.py +97 -0
  210. rem/utils/date_utils.py +228 -0
  211. rem/utils/dict_utils.py +98 -0
  212. rem/utils/embeddings.py +436 -0
  213. rem/utils/examples/embeddings_example.py +305 -0
  214. rem/utils/examples/sql_types_example.py +202 -0
  215. rem/utils/files.py +323 -0
  216. rem/utils/markdown.py +16 -0
  217. rem/utils/mime_types.py +158 -0
  218. rem/utils/model_helpers.py +492 -0
  219. rem/utils/schema_loader.py +649 -0
  220. rem/utils/sql_paths.py +146 -0
  221. rem/utils/sql_types.py +350 -0
  222. rem/utils/user_id.py +81 -0
  223. rem/utils/vision.py +325 -0
  224. rem/workers/README.md +506 -0
  225. rem/workers/__init__.py +7 -0
  226. rem/workers/db_listener.py +579 -0
  227. rem/workers/db_maintainer.py +74 -0
  228. rem/workers/dreaming.py +502 -0
  229. rem/workers/engram_processor.py +312 -0
  230. rem/workers/sqs_file_processor.py +193 -0
  231. rem/workers/unlogged_maintainer.py +463 -0
  232. remdb-0.3.242.dist-info/METADATA +1632 -0
  233. remdb-0.3.242.dist-info/RECORD +235 -0
  234. remdb-0.3.242.dist-info/WHEEL +4 -0
  235. 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
+ )