remdb 0.3.181__py3-none-any.whl → 0.3.200__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.

@@ -20,6 +20,7 @@ Available Tools:
20
20
  - get_schema: Get detailed schema for a table (columns, types, indexes)
21
21
  """
22
22
 
23
+ import json
23
24
  from functools import wraps
24
25
  from typing import Any, Callable, Literal, cast
25
26
 
@@ -128,205 +129,228 @@ def mcp_tool_error_handler(func: Callable) -> Callable:
128
129
 
129
130
  @mcp_tool_error_handler
130
131
  async def search_rem(
131
- query_type: Literal["lookup", "fuzzy", "search", "sql", "traverse"],
132
- # LOOKUP parameters
133
- entity_key: str | None = None,
134
- # FUZZY parameters
135
- query_text: str | None = None,
136
- threshold: float = 0.7,
137
- # SEARCH parameters
138
- table: str | None = None,
132
+ query: str,
139
133
  limit: int = 20,
140
- # SQL parameters
141
- sql_query: str | None = None,
142
- # TRAVERSE parameters
143
- initial_query: str | None = None,
144
- edge_types: list[str] | None = None,
145
- depth: int = 1,
146
- # Optional context override (defaults to authenticated user)
147
- user_id: str | None = None,
148
134
  ) -> dict[str, Any]:
149
135
  """
150
- Execute REM queries for entity lookup, semantic search, and graph traversal.
151
-
152
- REM supports multiple query types for different retrieval patterns:
136
+ Execute a REM query using the REM query dialect.
153
137
 
154
- **LOOKUP** - O(1) entity resolution by natural language key:
155
- - Fast exact match across all tables
156
- - Uses indexed label_vector for instant retrieval
157
- - Example: LOOKUP "Sarah Chen" returns all entities named "Sarah Chen"
158
- - **Ontology Note**: Ontology content may contain markdown links like
159
- `[sertraline](../../drugs/antidepressants/sertraline.md)`. The link name
160
- (e.g., "sertraline") can be used as a LOOKUP subject, while the relative
161
- path provides semantic context (e.g., it's a drug, specifically an antidepressant).
138
+ **REM Query Syntax:**
162
139
 
163
- **FUZZY** - Fuzzy text matching with similarity threshold:
164
- - Finds partial matches and typos
165
- - Example: FUZZY "sara" threshold=0.7 finds "Sarah Chen", "Sara Martinez"
140
+ LOOKUP <entity_key>
141
+ Find entity by exact name/key. Searches across all tables.
142
+ Example: LOOKUP phq-9-procedure
143
+ Example: LOOKUP sertraline
166
144
 
167
- **SEARCH** - Semantic vector search (table-specific):
168
- - Finds conceptually similar entities
169
- - Example: SEARCH "database migration" table=resources returns related documents
145
+ SEARCH <text> IN <table>
146
+ Semantic vector search within a specific table.
147
+ Tables: 'ontologies' (clinical knowledge, procedures, drugs, DSM criteria)
148
+ 'resources' (documents, files, user content)
149
+ Example: SEARCH depression IN ontologies
150
+ Example: SEARCH Module F IN ontologies
170
151
 
171
- **SQL** - Direct SQL queries for structured data:
172
- - Full PostgreSQL query power (scoped to table)
173
- - Example: SQL "role = 'engineer'" (WHERE clause only)
152
+ FUZZY <text>
153
+ Fuzzy text matching for partial matches and typos.
154
+ Example: FUZZY setraline
174
155
 
175
- **TRAVERSE** - Graph traversal following relationships:
176
- - Explores entity neighborhood via graph edges
177
- - Supports depth control and edge type filtering
178
- - Example: TRAVERSE "Sarah Chen" edge_types=["manages", "reports_to"] depth=2
156
+ TRAVERSE <start_entity>
157
+ Graph traversal from a starting entity.
158
+ Example: TRAVERSE sarah-chen
179
159
 
180
160
  Args:
181
- query_type: Type of query (lookup, fuzzy, search, sql, traverse)
182
- entity_key: Entity key for LOOKUP (e.g., "Sarah Chen")
183
- query_text: Search text for FUZZY or SEARCH
184
- threshold: Similarity threshold for FUZZY (0.0-1.0)
185
- table: Target table for SEARCH (resources, moments, users, etc.)
186
- limit: Max results for SEARCH
187
- sql_query: SQL WHERE clause for SQL type (e.g. "id = '123'")
188
- initial_query: Starting entity for TRAVERSE
189
- edge_types: Edge types to follow for TRAVERSE (e.g., ["manages", "reports_to"])
190
- depth: Traversal depth for TRAVERSE (0=plan only, 1-5=actual traversal)
191
- user_id: Optional user identifier (defaults to authenticated user or "default")
161
+ query: REM query string (e.g., "LOOKUP phq-9-procedure", "SEARCH depression IN ontologies")
162
+ limit: Maximum results to return (default: 20)
192
163
 
193
164
  Returns:
194
- Dict with query results, metadata, and execution info
165
+ Dict with query results and metadata. If no results found, includes
166
+ 'suggestions' with alternative search strategies.
195
167
 
196
168
  Examples:
197
- # Lookup entity (uses authenticated user context)
198
- search_rem(
199
- query_type="lookup",
200
- entity_key="Sarah Chen"
201
- )
202
-
203
- # Semantic search
204
- search_rem(
205
- query_type="search",
206
- query_text="database migration",
207
- table="resources",
208
- limit=10
209
- )
210
-
211
- # SQL query (WHERE clause only)
212
- search_rem(
213
- query_type="sql",
214
- table="resources",
215
- sql_query="category = 'document'"
216
- )
217
-
218
- # Graph traversal
219
- search_rem(
220
- query_type="traverse",
221
- initial_query="Sarah Chen",
222
- edge_types=["manages", "reports_to"],
223
- depth=2
224
- )
169
+ search_rem("LOOKUP phq-9-procedure")
170
+ search_rem("SEARCH depression IN ontologies")
171
+ search_rem("SEARCH anxiety treatment IN ontologies", limit=10)
172
+ search_rem("FUZZY setraline")
225
173
  """
226
174
  # Get RemService instance (lazy initialization)
227
175
  rem_service = await get_rem_service()
228
176
 
229
- # Get user_id from context if not provided
230
- # TODO: Extract from authenticated session context when auth is enabled
231
- user_id = AgentContext.get_user_id_or_default(user_id, source="search_rem")
177
+ # Get user_id from context
178
+ user_id = AgentContext.get_user_id_or_default(None, source="search_rem")
179
+
180
+ # Parse the REM query string
181
+ if not query or not query.strip():
182
+ return {
183
+ "status": "error",
184
+ "error": "Empty query. Use REM syntax: LOOKUP <key>, SEARCH <text> IN <table>, FUZZY <text>, or TRAVERSE <entity>",
185
+ }
232
186
 
233
- # Normalize query_type to lowercase for case-insensitive REM dialect
234
- query_type = cast(Literal["lookup", "fuzzy", "search", "sql", "traverse"], query_type.lower())
187
+ query = query.strip()
188
+ parts = query.split(None, 1) # Split on first whitespace
189
+
190
+ if len(parts) < 2:
191
+ return {
192
+ "status": "error",
193
+ "error": f"Invalid query format: '{query}'. Expected: LOOKUP <key>, SEARCH <text> IN <table>, FUZZY <text>, or TRAVERSE <entity>",
194
+ }
195
+
196
+ query_type = parts[0].upper()
197
+ remainder = parts[1].strip()
235
198
 
236
199
  # Build RemQuery based on query_type
237
- if query_type == "lookup":
238
- if not entity_key:
239
- return {"status": "error", "error": "entity_key required for LOOKUP"}
200
+ if query_type == "LOOKUP":
201
+ if not remainder:
202
+ return {
203
+ "status": "error",
204
+ "error": "LOOKUP requires an entity key. Example: LOOKUP phq-9-procedure",
205
+ }
240
206
 
241
- query = RemQuery(
207
+ rem_query = RemQuery(
242
208
  query_type=QueryType.LOOKUP,
243
209
  parameters=LookupParameters(
244
- key=entity_key,
210
+ key=remainder,
245
211
  user_id=user_id,
246
212
  ),
247
213
  user_id=user_id,
248
214
  )
215
+ table = None # LOOKUP searches all tables
216
+
217
+ elif query_type == "SEARCH":
218
+ # Parse "text IN table" format
219
+ if " IN " in remainder.upper():
220
+ # Find the last " IN " to handle cases like "SEARCH pain IN back IN ontologies"
221
+ in_pos = remainder.upper().rfind(" IN ")
222
+ search_text = remainder[:in_pos].strip()
223
+ table = remainder[in_pos + 4:].strip().lower()
224
+ else:
225
+ return {
226
+ "status": "error",
227
+ "error": f"SEARCH requires table: SEARCH <text> IN <table>. "
228
+ "Use 'ontologies' for clinical knowledge or 'resources' for documents. "
229
+ f"Example: SEARCH {remainder} IN ontologies",
230
+ }
249
231
 
250
- elif query_type == "fuzzy":
251
- if not query_text:
252
- return {"status": "error", "error": "query_text required for FUZZY"}
253
-
254
- query = RemQuery(
255
- query_type=QueryType.FUZZY,
256
- parameters=FuzzyParameters(
257
- query_text=query_text,
258
- threshold=threshold,
259
- limit=limit, # Limit was missing in original logic but likely intended
260
- ),
261
- user_id=user_id,
262
- )
263
-
264
- elif query_type == "search":
265
- if not query_text:
266
- return {"status": "error", "error": "query_text required for SEARCH"}
267
- if not table:
268
- return {"status": "error", "error": "table required for SEARCH"}
232
+ if not search_text:
233
+ return {
234
+ "status": "error",
235
+ "error": "SEARCH requires search text. Example: SEARCH depression IN ontologies",
236
+ }
269
237
 
270
- query = RemQuery(
238
+ rem_query = RemQuery(
271
239
  query_type=QueryType.SEARCH,
272
240
  parameters=SearchParameters(
273
- query_text=query_text,
241
+ query_text=search_text,
274
242
  table_name=table,
275
243
  limit=limit,
276
244
  ),
277
245
  user_id=user_id,
278
246
  )
279
247
 
280
- elif query_type == "sql":
281
- if not sql_query:
282
- return {"status": "error", "error": "sql_query required for SQL"}
283
-
284
- # SQLParameters requires table_name. If not provided, we cannot execute.
285
- # Assuming sql_query is just the WHERE clause based on RemService implementation,
286
- # OR if table is provided we use it.
287
- if not table:
288
- return {"status": "error", "error": "table required for SQL queries (parameter: table)"}
289
-
290
- query = RemQuery(
291
- query_type=QueryType.SQL,
292
- parameters=SQLParameters(
293
- table_name=table,
294
- where_clause=sql_query,
248
+ elif query_type == "FUZZY":
249
+ if not remainder:
250
+ return {
251
+ "status": "error",
252
+ "error": "FUZZY requires search text. Example: FUZZY setraline",
253
+ }
254
+
255
+ rem_query = RemQuery(
256
+ query_type=QueryType.FUZZY,
257
+ parameters=FuzzyParameters(
258
+ query_text=remainder,
259
+ threshold=0.3, # pg_trgm similarity - 0.3 is reasonable for typo correction
295
260
  limit=limit,
296
261
  ),
297
262
  user_id=user_id,
298
263
  )
264
+ table = None
299
265
 
300
- elif query_type == "traverse":
301
- if not initial_query:
266
+ elif query_type == "TRAVERSE":
267
+ if not remainder:
302
268
  return {
303
269
  "status": "error",
304
- "error": "initial_query required for TRAVERSE",
270
+ "error": "TRAVERSE requires a starting entity. Example: TRAVERSE sarah-chen",
305
271
  }
306
272
 
307
- query = RemQuery(
273
+ rem_query = RemQuery(
308
274
  query_type=QueryType.TRAVERSE,
309
275
  parameters=TraverseParameters(
310
- initial_query=initial_query,
311
- edge_types=edge_types or [],
312
- max_depth=depth,
276
+ initial_query=remainder,
277
+ edge_types=[],
278
+ max_depth=1,
313
279
  ),
314
280
  user_id=user_id,
315
281
  )
282
+ table = None
316
283
 
317
284
  else:
318
- return {"status": "error", "error": f"Unknown query_type: {query_type}"}
285
+ return {
286
+ "status": "error",
287
+ "error": f"Unknown query type: '{query_type}'. Valid types: LOOKUP, SEARCH, FUZZY, TRAVERSE. "
288
+ "Examples: LOOKUP phq-9-procedure, SEARCH depression IN ontologies",
289
+ }
319
290
 
320
291
  # Execute query (errors handled by decorator)
321
292
  logger.info(f"Executing REM query: {query_type} for user {user_id}")
322
- result = await rem_service.execute_query(query)
293
+ result = await rem_service.execute_query(rem_query)
323
294
 
324
295
  logger.info(f"Query completed successfully: {query_type}")
325
- return {
296
+
297
+ # Provide helpful guidance when no results found
298
+ response: dict[str, Any] = {
326
299
  "query_type": query_type,
327
300
  "results": result,
328
301
  }
329
302
 
303
+ # Check if results are empty - handle both list and dict result formats
304
+ is_empty = False
305
+ if not result:
306
+ is_empty = True
307
+ elif isinstance(result, list) and len(result) == 0:
308
+ is_empty = True
309
+ elif isinstance(result, dict):
310
+ # RemService returns dict with 'results' key containing actual matches
311
+ inner_results = result.get("results", [])
312
+ count = result.get("count", len(inner_results) if isinstance(inner_results, list) else 0)
313
+ is_empty = count == 0 or (isinstance(inner_results, list) and len(inner_results) == 0)
314
+
315
+ if is_empty:
316
+ # Build helpful suggestions based on query type
317
+ suggestions = []
318
+
319
+ if query_type in ("LOOKUP", "FUZZY"):
320
+ suggestions.append(
321
+ "LOOKUP/FUZZY searches across ALL tables. If you expected results, "
322
+ "verify the entity name is spelled correctly."
323
+ )
324
+
325
+ if query_type == "SEARCH":
326
+ if table == "resources":
327
+ suggestions.append(
328
+ "No results in 'resources' table. Try: SEARCH <text> IN ontologies - "
329
+ "clinical procedures, drug info, and diagnostic criteria are stored there."
330
+ )
331
+ elif table == "ontologies":
332
+ suggestions.append(
333
+ "No results in 'ontologies' table. Try: SEARCH <text> IN resources - "
334
+ "for user-uploaded documents and general content."
335
+ )
336
+ else:
337
+ suggestions.append(
338
+ "Try: SEARCH <text> IN ontologies (clinical knowledge, procedures, drugs) "
339
+ "or SEARCH <text> IN resources (documents, files)."
340
+ )
341
+
342
+ # Always suggest both tables if no specific table guidance given
343
+ if not suggestions:
344
+ suggestions.append(
345
+ "No results found. Try: SEARCH <text> IN ontologies (clinical procedures, drugs) "
346
+ "or SEARCH <text> IN resources (documents, files)."
347
+ )
348
+
349
+ response["suggestions"] = suggestions
350
+ response["hint"] = "0 results returned. See 'suggestions' for alternative search strategies."
351
+
352
+ return response
353
+
330
354
 
331
355
  @mcp_tool_error_handler
332
356
  async def ask_rem_agent(
@@ -377,20 +401,45 @@ async def ask_rem_agent(
377
401
  query="Show me Sarah's reporting chain and their recent projects"
378
402
  )
379
403
  """
380
- # Get user_id from context if not provided
381
- # TODO: Extract from authenticated session context when auth is enabled
382
- user_id = AgentContext.get_user_id_or_default(user_id, source="ask_rem_agent")
383
-
384
404
  from ...agentic import create_agent
405
+ from ...agentic.context import get_current_context
385
406
  from ...utils.schema_loader import load_agent_schema
386
407
 
387
- # Create agent context
388
- # Note: tenant_id defaults to "default" if user_id is None
389
- context = AgentContext(
390
- user_id=user_id,
391
- tenant_id=user_id or "default", # Use default tenant for anonymous users
392
- default_model=settings.llm.default_model,
393
- )
408
+ # Get parent context for multi-agent support
409
+ # This enables context propagation from parent agent to child agent
410
+ parent_context = get_current_context()
411
+
412
+ # Build child context: inherit from parent if available, otherwise use defaults
413
+ if parent_context is not None:
414
+ # Inherit user_id, tenant_id, session_id, is_eval from parent
415
+ # Allow explicit user_id override if provided
416
+ effective_user_id = user_id or parent_context.user_id
417
+ context = parent_context.child_context(agent_schema_uri=agent_schema)
418
+ if user_id is not None:
419
+ # Override user_id if explicitly provided
420
+ context = AgentContext(
421
+ user_id=user_id,
422
+ tenant_id=parent_context.tenant_id,
423
+ session_id=parent_context.session_id,
424
+ default_model=parent_context.default_model,
425
+ agent_schema_uri=agent_schema,
426
+ is_eval=parent_context.is_eval,
427
+ )
428
+ logger.debug(
429
+ f"ask_rem_agent inheriting context from parent: "
430
+ f"user_id={context.user_id}, session_id={context.session_id}"
431
+ )
432
+ else:
433
+ # No parent context - create fresh context (backwards compatible)
434
+ effective_user_id = AgentContext.get_user_id_or_default(
435
+ user_id, source="ask_rem_agent"
436
+ )
437
+ context = AgentContext(
438
+ user_id=effective_user_id,
439
+ tenant_id=effective_user_id or "default",
440
+ default_model=settings.llm.default_model,
441
+ agent_schema_uri=agent_schema,
442
+ )
394
443
 
395
444
  # Load agent schema
396
445
  try:
@@ -430,15 +479,18 @@ async def ingest_into_rem(
430
479
  category: str | None = None,
431
480
  tags: list[str] | None = None,
432
481
  is_local_server: bool = False,
433
- user_id: str | None = None,
434
482
  resource_type: str | None = None,
435
483
  ) -> dict[str, Any]:
436
484
  """
437
- Ingest file into REM, creating searchable resources and embeddings.
485
+ Ingest file into REM, creating searchable PUBLIC resources and embeddings.
486
+
487
+ **IMPORTANT: All ingested data is PUBLIC by default.** This is correct for
488
+ shared knowledge bases (ontologies, procedures, reference data). Private
489
+ user-scoped data requires different handling via the CLI with --make-private.
438
490
 
439
491
  This tool provides the complete file ingestion pipeline:
440
492
  1. **Read**: File from local/S3/HTTP
441
- 2. **Store**: To user-scoped internal storage
493
+ 2. **Store**: To internal storage (public namespace)
442
494
  3. **Parse**: Extract content, metadata, tables, images
443
495
  4. **Chunk**: Semantic chunking for embeddings
444
496
  5. **Embed**: Create Resource chunks with vector embeddings
@@ -457,7 +509,6 @@ async def ingest_into_rem(
457
509
  category: Optional category (document, code, audio, etc.)
458
510
  tags: Optional tags for file
459
511
  is_local_server: True if running as local/stdio MCP server
460
- user_id: Optional user identifier (defaults to authenticated user or "default")
461
512
  resource_type: Optional resource type for storing chunks (case-insensitive).
462
513
  Supports flexible naming:
463
514
  - "resource", "resources", "Resource" → Resource (default)
@@ -476,10 +527,10 @@ async def ingest_into_rem(
476
527
  - message: Human-readable status message
477
528
 
478
529
  Examples:
479
- # Ingest local file (local server only, uses authenticated user context)
530
+ # Ingest local file (local server only)
480
531
  ingest_into_rem(
481
- file_uri="/Users/me/contract.pdf",
482
- category="legal",
532
+ file_uri="/Users/me/procedure.pdf",
533
+ category="medical",
483
534
  is_local_server=True
484
535
  )
485
536
 
@@ -503,15 +554,14 @@ async def ingest_into_rem(
503
554
  """
504
555
  from ...services.content import ContentService
505
556
 
506
- # Get user_id from context if not provided
507
- # TODO: Extract from authenticated session context when auth is enabled
508
- user_id = AgentContext.get_user_id_or_default(user_id, source="ingest_into_rem")
557
+ # Data is PUBLIC by default (user_id=None)
558
+ # Private user-scoped data requires CLI with --make-private flag
509
559
 
510
560
  # Delegate to ContentService for centralized ingestion (errors handled by decorator)
511
561
  content_service = ContentService()
512
562
  result = await content_service.ingest_file(
513
563
  file_uri=file_uri,
514
- user_id=user_id,
564
+ user_id=None, # PUBLIC - all ingested data is shared/public
515
565
  category=category,
516
566
  tags=tags,
517
567
  is_local_server=is_local_server,
@@ -621,6 +671,8 @@ async def register_metadata(
621
671
  recommended_action: str | None = None,
622
672
  # Generic extension - any additional key-value pairs
623
673
  extra: dict[str, Any] | None = None,
674
+ # Agent schema (auto-populated from context if not provided)
675
+ agent_schema: str | None = None,
624
676
  ) -> dict[str, Any]:
625
677
  """
626
678
  Register response metadata to be emitted as an SSE MetadataEvent.
@@ -661,6 +713,8 @@ async def register_metadata(
661
713
  extra: Dict of arbitrary additional metadata. Use this for any
662
714
  domain-specific fields not covered by the standard parameters.
663
715
  Example: {"topics_detected": ["anxiety", "sleep"], "session_count": 5}
716
+ agent_schema: Optional agent schema name. If not provided, automatically
717
+ populated from the current agent context (for multi-agent tracing).
664
718
 
665
719
  Returns:
666
720
  Dict with:
@@ -704,10 +758,17 @@ async def register_metadata(
704
758
  }
705
759
  )
706
760
  """
761
+ # Auto-populate agent_schema from context if not provided
762
+ if agent_schema is None:
763
+ from ...agentic.context import get_current_context
764
+ current_context = get_current_context()
765
+ if current_context and current_context.agent_schema_uri:
766
+ agent_schema = current_context.agent_schema_uri
767
+
707
768
  logger.debug(
708
769
  f"Registering metadata: confidence={confidence}, "
709
770
  f"risk_level={risk_level}, refs={len(references or [])}, "
710
- f"sources={len(sources or [])}"
771
+ f"sources={len(sources or [])}, agent_schema={agent_schema}"
711
772
  )
712
773
 
713
774
  result = {
@@ -717,6 +778,7 @@ async def register_metadata(
717
778
  "references": references,
718
779
  "sources": sources,
719
780
  "flags": flags,
781
+ "agent_schema": agent_schema, # Include agent schema for tracing
720
782
  }
721
783
 
722
784
  # Add session name if provided
@@ -1138,6 +1200,180 @@ async def save_agent(
1138
1200
  return result
1139
1201
 
1140
1202
 
1203
+ # =============================================================================
1204
+ # Multi-Agent Tools
1205
+ # =============================================================================
1206
+
1207
+
1208
+ @mcp_tool_error_handler
1209
+ async def ask_agent(
1210
+ agent_name: str,
1211
+ input_text: str,
1212
+ input_data: dict[str, Any] | None = None,
1213
+ user_id: str | None = None,
1214
+ timeout_seconds: int = 300,
1215
+ ) -> dict[str, Any]:
1216
+ """
1217
+ Invoke another agent by name and return its response.
1218
+
1219
+ This tool enables multi-agent orchestration by allowing one agent to call
1220
+ another. The child agent inherits the parent's context (user_id, session_id,
1221
+ tenant_id, is_eval) for proper scoping and continuity.
1222
+
1223
+ Use Cases:
1224
+ - Orchestrator agents that delegate to specialized sub-agents
1225
+ - Workflow agents that chain multiple processing steps
1226
+ - Ensemble agents that aggregate responses from multiple specialists
1227
+
1228
+ Args:
1229
+ agent_name: Name of the agent to invoke. Can be:
1230
+ - A user-created agent (saved via save_agent)
1231
+ - A system agent (e.g., "ask_rem", "knowledge-query")
1232
+ input_text: The user message/query to send to the agent
1233
+ input_data: Optional structured input data for the agent
1234
+ user_id: Optional user override (defaults to parent's user_id)
1235
+ timeout_seconds: Maximum execution time (default: 300s)
1236
+
1237
+ Returns:
1238
+ Dict with:
1239
+ - status: "success" or "error"
1240
+ - output: Agent's structured output (if using output schema)
1241
+ - text_response: Agent's text response
1242
+ - agent_schema: Name of the invoked agent
1243
+ - metadata: Any metadata registered by the agent (confidence, etc.)
1244
+
1245
+ Examples:
1246
+ # Simple delegation
1247
+ ask_agent(
1248
+ agent_name="sentiment-analyzer",
1249
+ input_text="I love this product! Best purchase ever."
1250
+ )
1251
+ # Returns: {"status": "success", "output": {"sentiment": "positive"}, ...}
1252
+
1253
+ # Orchestrator pattern
1254
+ ask_agent(
1255
+ agent_name="knowledge-query",
1256
+ input_text="What are the latest Q3 results?"
1257
+ )
1258
+
1259
+ # Chain with structured input
1260
+ ask_agent(
1261
+ agent_name="summarizer",
1262
+ input_text="Summarize this document",
1263
+ input_data={"document_id": "doc-123", "max_length": 500}
1264
+ )
1265
+ """
1266
+ import asyncio
1267
+ from ...agentic import create_agent
1268
+ from ...agentic.context import get_current_context, agent_context_scope
1269
+ from ...agentic.agents.agent_manager import get_agent
1270
+ from ...utils.schema_loader import load_agent_schema
1271
+
1272
+ # Get parent context for inheritance
1273
+ parent_context = get_current_context()
1274
+
1275
+ # Determine effective user_id
1276
+ if parent_context is not None:
1277
+ effective_user_id = user_id or parent_context.user_id
1278
+ else:
1279
+ effective_user_id = AgentContext.get_user_id_or_default(
1280
+ user_id, source="ask_agent"
1281
+ )
1282
+
1283
+ # Build child context
1284
+ if parent_context is not None:
1285
+ child_context = parent_context.child_context(agent_schema_uri=agent_name)
1286
+ if user_id is not None:
1287
+ # Explicit user_id override
1288
+ child_context = AgentContext(
1289
+ user_id=user_id,
1290
+ tenant_id=parent_context.tenant_id,
1291
+ session_id=parent_context.session_id,
1292
+ default_model=parent_context.default_model,
1293
+ agent_schema_uri=agent_name,
1294
+ is_eval=parent_context.is_eval,
1295
+ )
1296
+ logger.debug(
1297
+ f"ask_agent '{agent_name}' inheriting context: "
1298
+ f"user_id={child_context.user_id}, session_id={child_context.session_id}"
1299
+ )
1300
+ else:
1301
+ child_context = AgentContext(
1302
+ user_id=effective_user_id,
1303
+ tenant_id=effective_user_id or "default",
1304
+ default_model=settings.llm.default_model,
1305
+ agent_schema_uri=agent_name,
1306
+ )
1307
+
1308
+ # Try to load agent schema from:
1309
+ # 1. Database (user-created or system agents)
1310
+ # 2. File system (packaged agents)
1311
+ schema = None
1312
+
1313
+ # Try database first
1314
+ if effective_user_id:
1315
+ schema = await get_agent(agent_name, user_id=effective_user_id)
1316
+ if schema:
1317
+ logger.debug(f"Loaded agent '{agent_name}' from database")
1318
+
1319
+ # Fall back to file system
1320
+ if schema is None:
1321
+ try:
1322
+ schema = load_agent_schema(agent_name)
1323
+ logger.debug(f"Loaded agent '{agent_name}' from file system")
1324
+ except FileNotFoundError:
1325
+ pass
1326
+
1327
+ if schema is None:
1328
+ return {
1329
+ "status": "error",
1330
+ "error": f"Agent not found: {agent_name}",
1331
+ "hint": "Use list_agents to see available agents, or save_agent to create one",
1332
+ }
1333
+
1334
+ # Create agent runtime
1335
+ agent_runtime = await create_agent(
1336
+ context=child_context,
1337
+ agent_schema_override=schema,
1338
+ )
1339
+
1340
+ # Build prompt with optional input_data
1341
+ prompt = input_text
1342
+ if input_data:
1343
+ prompt = f"{input_text}\n\nInput data: {json.dumps(input_data)}"
1344
+
1345
+ # Run agent with timeout and context propagation
1346
+ logger.info(f"Invoking agent '{agent_name}' with prompt: {prompt[:100]}...")
1347
+
1348
+ try:
1349
+ # Set child context for nested tool calls
1350
+ with agent_context_scope(child_context):
1351
+ result = await asyncio.wait_for(
1352
+ agent_runtime.run(prompt),
1353
+ timeout=timeout_seconds
1354
+ )
1355
+ except asyncio.TimeoutError:
1356
+ return {
1357
+ "status": "error",
1358
+ "error": f"Agent '{agent_name}' timed out after {timeout_seconds}s",
1359
+ "agent_schema": agent_name,
1360
+ }
1361
+
1362
+ # Serialize output
1363
+ from rem.agentic.serialization import serialize_agent_result
1364
+ output = serialize_agent_result(result.output)
1365
+
1366
+ logger.info(f"Agent '{agent_name}' completed successfully")
1367
+
1368
+ return {
1369
+ "status": "success",
1370
+ "output": output,
1371
+ "text_response": str(result.output),
1372
+ "agent_schema": agent_name,
1373
+ "input_text": input_text,
1374
+ }
1375
+
1376
+
1141
1377
  # =============================================================================
1142
1378
  # Test/Debug Tools (for development only)
1143
1379
  # =============================================================================