remdb 0.3.202__py3-none-any.whl → 0.3.245__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 (44) hide show
  1. rem/agentic/README.md +36 -2
  2. rem/agentic/context.py +86 -3
  3. rem/agentic/context_builder.py +39 -33
  4. rem/agentic/mcp/tool_wrapper.py +2 -2
  5. rem/agentic/providers/pydantic_ai.py +68 -51
  6. rem/agentic/schema.py +2 -2
  7. rem/api/mcp_router/resources.py +223 -0
  8. rem/api/mcp_router/tools.py +170 -18
  9. rem/api/routers/admin.py +30 -4
  10. rem/api/routers/auth.py +175 -18
  11. rem/api/routers/chat/child_streaming.py +394 -0
  12. rem/api/routers/chat/completions.py +24 -29
  13. rem/api/routers/chat/sse_events.py +5 -1
  14. rem/api/routers/chat/streaming.py +242 -272
  15. rem/api/routers/chat/streaming_utils.py +327 -0
  16. rem/api/routers/common.py +18 -0
  17. rem/api/routers/dev.py +7 -1
  18. rem/api/routers/feedback.py +9 -1
  19. rem/api/routers/messages.py +80 -15
  20. rem/api/routers/models.py +9 -1
  21. rem/api/routers/query.py +17 -15
  22. rem/api/routers/shared_sessions.py +16 -0
  23. rem/cli/commands/ask.py +205 -114
  24. rem/cli/commands/process.py +12 -4
  25. rem/cli/commands/query.py +109 -0
  26. rem/cli/commands/session.py +117 -0
  27. rem/cli/main.py +2 -0
  28. rem/models/entities/session.py +1 -0
  29. rem/schemas/agents/rem.yaml +1 -1
  30. rem/services/postgres/repository.py +7 -7
  31. rem/services/rem/service.py +47 -0
  32. rem/services/session/__init__.py +2 -1
  33. rem/services/session/compression.py +14 -12
  34. rem/services/session/pydantic_messages.py +111 -11
  35. rem/services/session/reload.py +2 -1
  36. rem/settings.py +71 -0
  37. rem/sql/migrations/001_install.sql +4 -4
  38. rem/sql/migrations/004_cache_system.sql +3 -1
  39. rem/sql/migrations/migrate_session_id_to_uuid.sql +45 -0
  40. rem/utils/schema_loader.py +139 -111
  41. {remdb-0.3.202.dist-info → remdb-0.3.245.dist-info}/METADATA +2 -2
  42. {remdb-0.3.202.dist-info → remdb-0.3.245.dist-info}/RECORD +44 -39
  43. {remdb-0.3.202.dist-info → remdb-0.3.245.dist-info}/WHEEL +0 -0
  44. {remdb-0.3.202.dist-info → remdb-0.3.245.dist-info}/entry_points.txt +0 -0
rem/agentic/README.md CHANGED
@@ -716,11 +716,45 @@ curl -X POST http://localhost:8000/api/v1/chat/completions \
716
716
 
717
717
  See `rem/api/README.md` for full SSE event protocol documentation.
718
718
 
719
+ ## Multi-Agent Orchestration
720
+
721
+ Agents can delegate work to other agents via the `ask_agent` tool. This enables orchestrator patterns where a parent agent routes to specialists.
722
+
723
+ ### How It Works
724
+
725
+ 1. **Parent agent** calls `ask_agent(agent_name, input_text)`
726
+ 2. **Child agent** executes and streams its response
727
+ 3. **Child events** bubble up to parent via an event sink (asyncio.Queue in ContextVar)
728
+ 4. **All tool calls** are saved to the database for the session
729
+
730
+ ### Key Components
731
+
732
+ | Component | Location | Purpose |
733
+ |-----------|----------|---------|
734
+ | `ask_agent` tool | `mcp_router/tools.py` | Loads child agent, runs with streaming, pushes events to sink |
735
+ | Event sink | `context.py` | ContextVar holding asyncio.Queue for child→parent event flow |
736
+ | Streaming controller | `streaming.py` | Drains event sink, emits SSE events, saves to DB |
737
+
738
+ ### Event Types
739
+
740
+ Child agents emit events that the parent streams to the client:
741
+
742
+ - **`child_tool_start`**: Child is calling a tool (logged, streamed, saved to DB)
743
+ - **`child_content`**: Child's text response (streamed as SSE content delta)
744
+ - **`child_tool_result`**: Tool completed with result (metadata extraction)
745
+
746
+ ### Testing
747
+
748
+ Integration tests in `tests/integration/test_ask_agent_streaming.py` verify:
749
+ - Child content streams correctly to client
750
+ - Tool calls are persisted to database
751
+ - Multi-turn conversations save all messages
752
+
719
753
  ## Future Work
720
754
 
721
755
  - [ ] Phoenix evaluator integration
722
756
  - [ ] Agent schema registry (load schemas by URI)
723
757
  - [ ] Schema validation and versioning
724
- - [ ] Multi-turn conversation management
725
- - [ ] Agent composition (agents calling agents)
758
+ - [x] Multi-turn conversation management
759
+ - [x] Agent composition (agents calling agents)
726
760
  - [ ] Alternative provider implementations (if needed)
rem/agentic/context.py CHANGED
@@ -16,6 +16,7 @@ Headers Mapping:
16
16
  X-Agent-Schema → context.agent_schema_uri (default: "rem")
17
17
  X-Model-Name → context.default_model
18
18
  X-Is-Eval → context.is_eval (marks session as evaluation)
19
+ X-Client-Id → context.client_id (e.g., "web", "mobile", "cli")
19
20
 
20
21
  Key Design Pattern:
21
22
  - AgentContext is passed to agent factory, not stored in agents
@@ -30,9 +31,10 @@ Multi-Agent Context Propagation:
30
31
  - Child agents inherit user_id, tenant_id, session_id, is_eval from parent
31
32
  """
32
33
 
34
+ import asyncio
33
35
  from contextlib import contextmanager
34
36
  from contextvars import ContextVar
35
- from typing import Generator
37
+ from typing import Any, Generator
36
38
 
37
39
  from loguru import logger
38
40
  from pydantic import BaseModel, Field
@@ -46,6 +48,13 @@ _current_agent_context: ContextVar["AgentContext | None"] = ContextVar(
46
48
  "current_agent_context", default=None
47
49
  )
48
50
 
51
+ # Event sink for streaming child agent events to parent
52
+ # When set, child agents (via ask_agent) should push their events here
53
+ # for the parent's streaming loop to proxy to the client
54
+ _parent_event_sink: ContextVar["asyncio.Queue | None"] = ContextVar(
55
+ "parent_event_sink", default=None
56
+ )
57
+
49
58
 
50
59
  def get_current_context() -> "AgentContext | None":
51
60
  """
@@ -97,6 +106,70 @@ def agent_context_scope(ctx: "AgentContext") -> Generator["AgentContext", None,
97
106
  _current_agent_context.set(previous)
98
107
 
99
108
 
109
+ # =============================================================================
110
+ # Event Sink for Streaming Multi-Agent Delegation
111
+ # =============================================================================
112
+
113
+
114
+ def get_event_sink() -> "asyncio.Queue | None":
115
+ """
116
+ Get the parent's event sink for streaming child events.
117
+
118
+ Used by ask_agent to push child agent events to the parent's stream.
119
+ Returns None if not in a streaming context.
120
+ """
121
+ return _parent_event_sink.get()
122
+
123
+
124
+ def set_event_sink(sink: "asyncio.Queue | None") -> None:
125
+ """Set the event sink for child agents to push events to."""
126
+ _parent_event_sink.set(sink)
127
+
128
+
129
+ @contextmanager
130
+ def event_sink_scope(sink: "asyncio.Queue") -> Generator["asyncio.Queue", None, None]:
131
+ """
132
+ Context manager for scoped event sink setting.
133
+
134
+ Used by streaming layer to set up event proxying before tool execution.
135
+ Child agents (via ask_agent) will push their events to this sink.
136
+
137
+ Example:
138
+ event_queue = asyncio.Queue()
139
+ with event_sink_scope(event_queue):
140
+ # ask_agent will push child events to event_queue
141
+ async for event in tools_stream:
142
+ ...
143
+ # Also consume from event_queue
144
+ """
145
+ previous = _parent_event_sink.get()
146
+ _parent_event_sink.set(sink)
147
+ try:
148
+ yield sink
149
+ finally:
150
+ _parent_event_sink.set(previous)
151
+
152
+
153
+ async def push_event(event: Any) -> bool:
154
+ """
155
+ Push an event to the parent's event sink (if available).
156
+
157
+ Used by ask_agent to proxy child agent events to the parent's stream.
158
+ Returns True if event was pushed, False if no sink available.
159
+
160
+ Args:
161
+ event: Any streaming event (ToolCallEvent, content chunk, etc.)
162
+
163
+ Returns:
164
+ True if event was pushed to sink, False otherwise
165
+ """
166
+ sink = _parent_event_sink.get()
167
+ if sink is not None:
168
+ await sink.put(event)
169
+ return True
170
+ return False
171
+
172
+
100
173
  class AgentContext(BaseModel):
101
174
  """
102
175
  Session and configuration context for agent execution.
@@ -150,6 +223,11 @@ class AgentContext(BaseModel):
150
223
  description="Whether this is an evaluation session (set via X-Is-Eval header)",
151
224
  )
152
225
 
226
+ client_id: str | None = Field(
227
+ default=None,
228
+ description="Client identifier (e.g., 'web', 'mobile', 'cli') set via X-Client-Id header",
229
+ )
230
+
153
231
  model_config = {"populate_by_name": True}
154
232
 
155
233
  def child_context(
@@ -160,7 +238,7 @@ class AgentContext(BaseModel):
160
238
  """
161
239
  Create a child context for nested agent calls.
162
240
 
163
- Inherits user_id, tenant_id, session_id, is_eval from parent.
241
+ Inherits user_id, tenant_id, session_id, is_eval, client_id from parent.
164
242
  Allows overriding agent_schema_uri and default_model for the child.
165
243
 
166
244
  Args:
@@ -184,6 +262,7 @@ class AgentContext(BaseModel):
184
262
  default_model=model_override or self.default_model,
185
263
  agent_schema_uri=agent_schema_uri or self.agent_schema_uri,
186
264
  is_eval=self.is_eval,
265
+ client_id=self.client_id,
187
266
  )
188
267
 
189
268
  @staticmethod
@@ -302,6 +381,7 @@ class AgentContext(BaseModel):
302
381
  default_model=normalized.get("x-model-name") or settings.llm.default_model,
303
382
  agent_schema_uri=normalized.get("x-agent-schema"),
304
383
  is_eval=is_eval,
384
+ client_id=normalized.get("x-client-id"),
305
385
  )
306
386
 
307
387
  @classmethod
@@ -319,6 +399,7 @@ class AgentContext(BaseModel):
319
399
  - X-Model-Name: Model override
320
400
  - X-Agent-Schema: Agent schema URI
321
401
  - X-Is-Eval: Whether this is an evaluation session (true/false)
402
+ - X-Client-Id: Client identifier (e.g., "web", "mobile", "cli")
322
403
 
323
404
  Args:
324
405
  headers: Dictionary of HTTP headers (case-insensitive)
@@ -332,7 +413,8 @@ class AgentContext(BaseModel):
332
413
  "X-Tenant-Id": "acme-corp",
333
414
  "X-Session-Id": "sess-456",
334
415
  "X-Model-Name": "anthropic:claude-opus-4-20250514",
335
- "X-Is-Eval": "true"
416
+ "X-Is-Eval": "true",
417
+ "X-Client-Id": "web"
336
418
  }
337
419
  context = AgentContext.from_headers(headers)
338
420
  """
@@ -350,4 +432,5 @@ class AgentContext(BaseModel):
350
432
  default_model=normalized.get("x-model-name") or settings.llm.default_model,
351
433
  agent_schema_uri=normalized.get("x-agent-schema"),
352
434
  is_eval=is_eval,
435
+ client_id=normalized.get("x-client-id"),
353
436
  )
@@ -4,15 +4,12 @@ Centralized context builder for agent execution.
4
4
  Session History (ALWAYS loaded with compression):
5
5
  - Each chat request is a single message, so session history MUST be recovered
6
6
  - Uses SessionMessageStore with compression to keep context efficient
7
- - Long assistant responses include REM LOOKUP hints: "... [REM LOOKUP session-{id}-msg-{index}] ..."
8
- - Agent can retrieve full content on-demand using REM LOOKUP
9
7
  - Prevents context window bloat while maintaining conversation continuity
10
8
 
11
9
  User Context (on-demand by default):
12
- - System message includes REM LOOKUP hint for user profile
13
- - Agent decides whether to load profile based on query
14
- - More efficient for queries that don't need personalization
15
- - Example: "User: sarah@example.com. To load user profile: Use REM LOOKUP \"sarah@example.com\""
10
+ - System message includes user email for context awareness
11
+ - Fails silently if user not found - agent proceeds without user context
12
+ - Example: "User: sarah@example.com"
16
13
 
17
14
  User Context (auto-inject when enabled):
18
15
  - Set CHAT__AUTO_INJECT_USER_CONTEXT=true
@@ -22,8 +19,8 @@ User Context (auto-inject when enabled):
22
19
  Design Pattern:
23
20
  1. Extract AgentContext from headers (user_id, tenant_id, session_id)
24
21
  2. If auto-inject enabled: Load User/Session from database
25
- 3. If auto-inject disabled: Provide REM LOOKUP hints in system message
26
- 4. Construct system message with date + context (injected or hints)
22
+ 3. If auto-inject disabled: Show user email for context (fail silently if not found)
23
+ 4. Construct system message with date + context
27
24
  5. Return complete context ready for agent execution
28
25
 
29
26
  Integration Points:
@@ -40,11 +37,10 @@ Usage (on-demand, default):
40
37
 
41
38
  # Messages list structure (on-demand):
42
39
  # [
43
- # {"role": "system", "content": "Today's date: 2025-11-22\nUser: sarah@example.com\nTo load user profile: Use REM LOOKUP \"sarah@example.com\"\nSession ID: sess-123\nTo load session history: Use REM LOOKUP messages?session_id=sess-123"},
40
+ # {"role": "system", "content": "Today's date: 2025-11-22\n\nUser: sarah@example.com"},
44
41
  # {"role": "user", "content": "What's next for the API migration?"}
45
42
  # ]
46
43
 
47
- # Agent receives hints and can decide to load context if needed
48
44
  agent = await create_agent(context=context, ...)
49
45
  prompt = "\n".join(msg.content for msg in messages)
50
46
  result = await agent.run(prompt)
@@ -52,7 +48,7 @@ Usage (on-demand, default):
52
48
  Usage (auto-inject, CHAT__AUTO_INJECT_USER_CONTEXT=true):
53
49
  # Messages list structure (auto-inject):
54
50
  # [
55
- # {"role": "system", "content": "Today's date: 2025-11-22\n\nUser Context (auto-injected):\nSummary: ...\nInterests: ...\n\nSession History (auto-injected, 5 messages):"},
51
+ # {"role": "system", "content": "Today's date: 2025-11-22\n\nUser Context (auto-injected):\nSummary: ...\nInterests: ..."},
56
52
  # {"role": "user", "content": "Previous message"},
57
53
  # {"role": "assistant", "content": "Previous response"},
58
54
  # {"role": "user", "content": "What's next for the API migration?"}
@@ -110,13 +106,11 @@ class ContextBuilder:
110
106
 
111
107
  Session History (ALWAYS loaded with compression):
112
108
  - If session_id provided, session history is ALWAYS loaded using SessionMessageStore
113
- - Compression keeps it efficient with REM LOOKUP hints for long messages
114
- - Example: "... [Message truncated - REM LOOKUP session-{id}-msg-{index}] ..."
115
- - Agent can retrieve full content on-demand using REM LOOKUP
109
+ - Compression keeps context efficient
116
110
 
117
111
  User Context (on-demand by default):
118
- - System message includes REM LOOKUP hint: "User: {email}. To load user profile: Use REM LOOKUP \"{email}\""
119
- - Agent decides whether to load profile based on query
112
+ - System message includes user email: "User: {email}"
113
+ - Fails silently if user not found - agent proceeds without user context
120
114
 
121
115
  User Context (auto-inject when enabled):
122
116
  - Set CHAT__AUTO_INJECT_USER_CONTEXT=true
@@ -137,9 +131,9 @@ class ContextBuilder:
137
131
 
138
132
  # messages structure:
139
133
  # [
140
- # {"role": "system", "content": "Today's date: 2025-11-22\nUser: sarah@example.com\nTo load user profile: Use REM LOOKUP \"sarah@example.com\""},
134
+ # {"role": "system", "content": "Today's date: 2025-11-22\n\nUser: sarah@example.com"},
141
135
  # {"role": "user", "content": "Previous message"},
142
- # {"role": "assistant", "content": "Start of long response... [REM LOOKUP session-123-msg-1] ...end"},
136
+ # {"role": "assistant", "content": "Previous response"},
143
137
  # {"role": "user", "content": "New message"}
144
138
  # ]
145
139
  """
@@ -158,6 +152,7 @@ class ContextBuilder:
158
152
  default_model=context.default_model,
159
153
  agent_schema_uri=context.agent_schema_uri,
160
154
  is_eval=context.is_eval,
155
+ client_id=context.client_id,
161
156
  )
162
157
 
163
158
  # Initialize DB if not provided and needed (for user context or session history)
@@ -177,6 +172,10 @@ class ContextBuilder:
177
172
  today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
178
173
  context_hint = f"Today's date: {today}."
179
174
 
175
+ # Add client identifier if present
176
+ if context.client_id:
177
+ context_hint += f"\nClient: {context.client_id}"
178
+
180
179
  # Add user context (auto-inject or on-demand hint)
181
180
  if settings.chat.auto_inject_user_context and context.user_id and db:
182
181
  # Auto-inject: Load and include user profile
@@ -189,18 +188,18 @@ class ContextBuilder:
189
188
  context_hint += f"\n\nUser Context (auto-injected):\n{user_context_content}"
190
189
  else:
191
190
  context_hint += "\n\nNo user context available (anonymous or new user)."
192
- elif context.user_id:
193
- # On-demand: Provide hint to use REM LOOKUP
194
- # user_id is UUID5 hash of email - load user to get email for display and LOOKUP
195
- user_repo = Repository(User, "users", db=db)
196
- user = await user_repo.get_by_id(context.user_id, context.tenant_id)
197
- if user and user.email:
198
- # Show email (more useful than UUID) and LOOKUP hint
199
- context_hint += f"\n\nUser: {user.email}"
200
- context_hint += f"\nTo load user profile: Use REM LOOKUP \"{user.email}\""
201
- else:
202
- context_hint += f"\n\nUser ID: {context.user_id}"
203
- context_hint += "\nUser profile not available."
191
+ elif context.user_id and db:
192
+ # On-demand: Show user email for context (no REM LOOKUP - it requires exact user_id match)
193
+ # Fail silently if user lookup fails - just proceed without user context
194
+ try:
195
+ user_repo = Repository(User, "users", db=db)
196
+ user = await user_repo.get_by_id(context.user_id, context.tenant_id)
197
+ if user and user.email:
198
+ context_hint += f"\n\nUser: {user.email}"
199
+ # If user not found, just proceed without adding user context
200
+ except Exception as e:
201
+ # Fail silently - don't block agent execution if user lookup fails
202
+ logger.debug(f"Could not load user context: {e}")
204
203
 
205
204
  # Add system context hint
206
205
  messages.append(ContextMessage(role="system", content=context_hint))
@@ -318,6 +317,7 @@ class ContextBuilder:
318
317
  session_id: str | None = None,
319
318
  message: str = "Hello",
320
319
  model: str | None = None,
320
+ client_id: str | None = None,
321
321
  ) -> tuple[AgentContext, list[ContextMessage]]:
322
322
  """
323
323
  Build context for testing (no database lookup).
@@ -325,7 +325,7 @@ class ContextBuilder:
325
325
  Creates minimal context with:
326
326
  - Test user (test@rem.ai)
327
327
  - Test tenant
328
- - Context hint with date
328
+ - Context hint with date and client
329
329
  - Single user message
330
330
 
331
331
  Args:
@@ -334,6 +334,7 @@ class ContextBuilder:
334
334
  session_id: Optional session ID
335
335
  message: User message content
336
336
  model: Optional model override
337
+ client_id: Optional client identifier (e.g., "cli", "test")
337
338
 
338
339
  Returns:
339
340
  Tuple of (AgentContext, messages list)
@@ -341,7 +342,8 @@ class ContextBuilder:
341
342
  Example:
342
343
  context, messages = await ContextBuilder.build_from_test(
343
344
  user_id="test@rem.ai",
344
- message="What's the weather like?"
345
+ message="What's the weather like?",
346
+ client_id="cli"
345
347
  )
346
348
  """
347
349
  from ..settings import settings
@@ -352,11 +354,15 @@ class ContextBuilder:
352
354
  tenant_id=tenant_id,
353
355
  session_id=session_id,
354
356
  default_model=model or settings.llm.default_model,
357
+ client_id=client_id,
355
358
  )
356
359
 
357
360
  # Build minimal messages
358
361
  today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
359
- context_hint = f"Today's date: {today}.\n\nTest user context: {user_id} (test mode, no profile loaded)."
362
+ context_hint = f"Today's date: {today}."
363
+ if client_id:
364
+ context_hint += f"\nClient: {client_id}"
365
+ context_hint += f"\n\nTest user context: {user_id} (test mode, no profile loaded)."
360
366
 
361
367
  messages = [
362
368
  ContextMessage(role="system", content=context_hint),
@@ -116,7 +116,7 @@ def create_resource_tool(uri: str, usage: str = "", mcp_server: Any = None) -> T
116
116
  the artificial MCP distinction between tools and resources.
117
117
 
118
118
  Supports both:
119
- - Concrete URIs: "rem://schemas" -> tool with no parameters
119
+ - Concrete URIs: "rem://agents" -> tool with no parameters
120
120
  - Template URIs: "patient-profile://field/{field_key}" -> tool with field_key parameter
121
121
 
122
122
  Args:
@@ -131,7 +131,7 @@ def create_resource_tool(uri: str, usage: str = "", mcp_server: Any = None) -> T
131
131
 
132
132
  Example:
133
133
  # Concrete URI -> no-param tool
134
- tool = create_resource_tool("rem://schemas", "List all agent schemas")
134
+ tool = create_resource_tool("rem://agents", "List all agent schemas")
135
135
 
136
136
  # Template URI -> parameterized tool
137
137
  tool = create_resource_tool("patient-profile://field/{field_key}", "Get field definition", mcp_server=mcp)
@@ -357,6 +357,9 @@ def _convert_properties_to_prompt(properties: dict[str, Any]) -> str:
357
357
  definition into natural language guidance that informs the agent
358
358
  about the expected response structure without forcing JSON output.
359
359
 
360
+ IMPORTANT: The 'answer' field is the OUTPUT to the user. All other
361
+ fields are INTERNAL tracking that should NOT appear in the output.
362
+
360
363
  Args:
361
364
  properties: JSON Schema properties dict
362
365
 
@@ -368,45 +371,59 @@ def _convert_properties_to_prompt(properties: dict[str, Any]) -> str:
368
371
  "answer": {"type": "string", "description": "The answer"},
369
372
  "confidence": {"type": "number", "description": "Confidence 0-1"}
370
373
  }
371
- # Returns:
372
- # "## Response Structure\n\nYour response should include:\n- **answer**: The answer\n..."
374
+ # Returns guidance that only answer should be output
373
375
  """
374
376
  if not properties:
375
377
  return ""
376
378
 
377
- lines = ["## Response Guidelines", "", "Your response should address the following elements:"]
378
-
379
- for field_name, field_def in properties.items():
380
- field_type = field_def.get("type", "any")
381
- description = field_def.get("description", "")
382
-
383
- # Format based on type
384
- if field_type == "array":
385
- type_hint = "list"
386
- elif field_type == "number":
387
- type_hint = "number"
388
- # Include min/max if specified
389
- if "minimum" in field_def or "maximum" in field_def:
390
- min_val = field_def.get("minimum", "")
391
- max_val = field_def.get("maximum", "")
392
- if min_val != "" and max_val != "":
393
- type_hint = f"number ({min_val}-{max_val})"
394
- elif field_type == "boolean":
395
- type_hint = "yes/no"
396
- else:
397
- type_hint = field_type
379
+ # Separate answer (output) from other fields (internal tracking)
380
+ answer_field = properties.get("answer")
381
+ internal_fields = {k: v for k, v in properties.items() if k != "answer"}
382
+
383
+ lines = ["## Internal Thinking Structure (DO NOT output these labels)"]
384
+ lines.append("")
385
+ lines.append("Use this structure to organize your thinking, but ONLY output the answer content:")
386
+ lines.append("")
387
+
388
+ # If there's an answer field, emphasize it's the ONLY output
389
+ if answer_field:
390
+ answer_desc = answer_field.get("description", "Your response")
391
+ lines.append(f"**OUTPUT (what the user sees):** {answer_desc}")
392
+ lines.append("")
393
+
394
+ # Document internal fields for tracking/thinking
395
+ if internal_fields:
396
+ lines.append("**INTERNAL (for your tracking only - do NOT include in output):**")
397
+ for field_name, field_def in internal_fields.items():
398
+ field_type = field_def.get("type", "any")
399
+ description = field_def.get("description", "")
400
+
401
+ # Format based on type
402
+ if field_type == "array":
403
+ type_hint = "list"
404
+ elif field_type == "number":
405
+ type_hint = "number"
406
+ if "minimum" in field_def or "maximum" in field_def:
407
+ min_val = field_def.get("minimum", "")
408
+ max_val = field_def.get("maximum", "")
409
+ if min_val != "" and max_val != "":
410
+ type_hint = f"number ({min_val}-{max_val})"
411
+ elif field_type == "boolean":
412
+ type_hint = "yes/no"
413
+ else:
414
+ type_hint = field_type
398
415
 
399
- # Build field description
400
- field_line = f"- **{field_name}**"
401
- if type_hint and type_hint != "string":
402
- field_line += f" ({type_hint})"
403
- if description:
404
- field_line += f": {description}"
416
+ field_line = f"- {field_name}"
417
+ if type_hint and type_hint != "string":
418
+ field_line += f" ({type_hint})"
419
+ if description:
420
+ field_line += f": {description}"
405
421
 
406
- lines.append(field_line)
422
+ lines.append(field_line)
407
423
 
408
424
  lines.append("")
409
- lines.append("Respond naturally in prose, addressing these elements where relevant.")
425
+ lines.append("⚠️ CRITICAL: Your response must be ONLY the conversational answer text.")
426
+ lines.append("Do NOT output field names like 'answer:' or 'diverge_output:' - just the response itself.")
410
427
 
411
428
  return "\n".join(lines)
412
429
 
@@ -664,26 +681,26 @@ async def create_agent(
664
681
 
665
682
  set_agent_resource_attributes(agent_schema=agent_schema)
666
683
 
667
- # Extract schema metadata for search_rem tool description suffix
668
- # This allows entity schemas to add context-specific notes to the search_rem tool
669
- search_rem_suffix = None
670
- if metadata:
671
- # Check for default_search_table in metadata (set by entity schemas)
672
- extra = agent_schema.get("json_schema_extra", {}) if agent_schema else {}
673
- default_table = extra.get("default_search_table")
674
- has_embeddings = extra.get("has_embeddings", False)
675
-
676
- if default_table:
677
- # Build description suffix for search_rem
678
- search_rem_suffix = f"\n\nFor this schema, use `search_rem` to query `{default_table}`. "
679
- if has_embeddings:
680
- search_rem_suffix += f"SEARCH works well on {default_table} (has embeddings). "
681
- search_rem_suffix += f"Example: `SEARCH \"your query\" FROM {default_table} LIMIT 10`"
682
-
683
684
  # Add tools from MCP server (in-process, no subprocess)
684
685
  # Track loaded MCP servers for resource resolution
685
686
  loaded_mcp_server = None
686
687
 
688
+ # Build map of tool_name → schema description from agent schema tools section
689
+ # This allows agent-specific tool guidance to override/augment MCP tool descriptions
690
+ schema_tool_descriptions: dict[str, str] = {}
691
+ tool_configs = metadata.tools if metadata and hasattr(metadata, 'tools') else []
692
+ for tool_config in tool_configs:
693
+ if hasattr(tool_config, 'name'):
694
+ t_name = tool_config.name
695
+ t_desc = tool_config.description or ""
696
+ else:
697
+ t_name = tool_config.get("name", "")
698
+ t_desc = tool_config.get("description", "")
699
+ # Skip resource URIs (handled separately below)
700
+ if t_name and "://" not in t_name and t_desc:
701
+ schema_tool_descriptions[t_name] = t_desc
702
+ logger.debug(f"Schema tool description for '{t_name}': {len(t_desc)} chars")
703
+
687
704
  for server_config in mcp_server_configs:
688
705
  server_type = server_config.get("type")
689
706
  server_id = server_config.get("id", "mcp-server")
@@ -708,8 +725,8 @@ async def create_agent(
708
725
  mcp_tools_dict = await mcp_server.get_tools()
709
726
 
710
727
  for tool_name, tool_func in mcp_tools_dict.items():
711
- # Add description suffix to search_rem tool if schema specifies a default table
712
- tool_suffix = search_rem_suffix if tool_name == "search_rem" else None
728
+ # Get schema description suffix if agent schema defines one for this tool
729
+ tool_suffix = schema_tool_descriptions.get(tool_name)
713
730
 
714
731
  wrapped_tool = create_mcp_tool_wrapper(
715
732
  tool_name,
@@ -718,7 +735,7 @@ async def create_agent(
718
735
  description_suffix=tool_suffix,
719
736
  )
720
737
  tools.append(wrapped_tool)
721
- logger.debug(f"Loaded MCP tool: {tool_name}" + (" (with schema suffix)" if tool_suffix else ""))
738
+ logger.debug(f"Loaded MCP tool: {tool_name}" + (" (with schema desc)" if tool_suffix else ""))
722
739
 
723
740
  logger.info(f"Loaded {len(mcp_tools_dict)} tools from MCP server: {server_id} (in-process)")
724
741
 
@@ -732,7 +749,7 @@ async def create_agent(
732
749
  # the artificial MCP distinction between tools and resources
733
750
  #
734
751
  # Supports both concrete and template URIs:
735
- # - Concrete: "rem://schemas" -> no-param tool
752
+ # - Concrete: "rem://agents" -> no-param tool
736
753
  # - Template: "patient-profile://field/{field_key}" -> tool with field_key param
737
754
  from ..mcp.tool_wrapper import create_resource_tool
738
755
 
rem/agentic/schema.py CHANGED
@@ -79,7 +79,7 @@ class MCPResourceReference(BaseModel):
79
79
 
80
80
  Example (exact URI):
81
81
  {
82
- "uri": "rem://schemas",
82
+ "uri": "rem://agents",
83
83
  "name": "Agent Schemas",
84
84
  "description": "List all available agent schemas"
85
85
  }
@@ -96,7 +96,7 @@ class MCPResourceReference(BaseModel):
96
96
  default=None,
97
97
  description=(
98
98
  "Exact resource URI or URI with query parameters. "
99
- "Examples: 'rem://schemas', 'rem://resources?category=drug.*'"
99
+ "Examples: 'rem://agents', 'rem://resources?category=drug.*'"
100
100
  )
101
101
  )
102
102