remdb 0.3.171__py3-none-any.whl → 0.3.230__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.
Files changed (59) hide show
  1. rem/agentic/README.md +36 -2
  2. rem/agentic/context.py +173 -0
  3. rem/agentic/context_builder.py +12 -2
  4. rem/agentic/mcp/tool_wrapper.py +39 -16
  5. rem/agentic/providers/pydantic_ai.py +78 -45
  6. rem/agentic/schema.py +6 -5
  7. rem/agentic/tools/rem_tools.py +11 -0
  8. rem/api/main.py +1 -1
  9. rem/api/mcp_router/resources.py +75 -14
  10. rem/api/mcp_router/server.py +31 -24
  11. rem/api/mcp_router/tools.py +621 -166
  12. rem/api/routers/admin.py +30 -4
  13. rem/api/routers/auth.py +114 -15
  14. rem/api/routers/chat/child_streaming.py +379 -0
  15. rem/api/routers/chat/completions.py +74 -37
  16. rem/api/routers/chat/sse_events.py +7 -3
  17. rem/api/routers/chat/streaming.py +352 -257
  18. rem/api/routers/chat/streaming_utils.py +327 -0
  19. rem/api/routers/common.py +18 -0
  20. rem/api/routers/dev.py +7 -1
  21. rem/api/routers/feedback.py +9 -1
  22. rem/api/routers/messages.py +176 -38
  23. rem/api/routers/models.py +9 -1
  24. rem/api/routers/query.py +12 -1
  25. rem/api/routers/shared_sessions.py +16 -0
  26. rem/auth/jwt.py +19 -4
  27. rem/auth/middleware.py +42 -28
  28. rem/cli/README.md +62 -0
  29. rem/cli/commands/ask.py +61 -81
  30. rem/cli/commands/db.py +148 -70
  31. rem/cli/commands/process.py +171 -43
  32. rem/models/entities/ontology.py +91 -101
  33. rem/schemas/agents/rem.yaml +1 -1
  34. rem/services/content/service.py +18 -5
  35. rem/services/email/service.py +11 -2
  36. rem/services/embeddings/worker.py +26 -12
  37. rem/services/postgres/__init__.py +28 -3
  38. rem/services/postgres/diff_service.py +57 -5
  39. rem/services/postgres/programmable_diff_service.py +635 -0
  40. rem/services/postgres/pydantic_to_sqlalchemy.py +2 -2
  41. rem/services/postgres/register_type.py +12 -11
  42. rem/services/postgres/repository.py +39 -29
  43. rem/services/postgres/schema_generator.py +5 -5
  44. rem/services/postgres/sql_builder.py +6 -5
  45. rem/services/session/__init__.py +8 -1
  46. rem/services/session/compression.py +40 -2
  47. rem/services/session/pydantic_messages.py +292 -0
  48. rem/settings.py +34 -0
  49. rem/sql/background_indexes.sql +5 -0
  50. rem/sql/migrations/001_install.sql +157 -10
  51. rem/sql/migrations/002_install_models.sql +160 -132
  52. rem/sql/migrations/004_cache_system.sql +7 -275
  53. rem/sql/migrations/migrate_session_id_to_uuid.sql +45 -0
  54. rem/utils/model_helpers.py +101 -0
  55. rem/utils/schema_loader.py +79 -51
  56. {remdb-0.3.171.dist-info → remdb-0.3.230.dist-info}/METADATA +2 -2
  57. {remdb-0.3.171.dist-info → remdb-0.3.230.dist-info}/RECORD +59 -53
  58. {remdb-0.3.171.dist-info → remdb-0.3.230.dist-info}/WHEEL +0 -0
  59. {remdb-0.3.171.dist-info → remdb-0.3.230.dist-info}/entry_points.txt +0 -0
@@ -164,7 +164,7 @@ from .models import (
164
164
  ChatCompletionUsage,
165
165
  ChatMessage,
166
166
  )
167
- from .streaming import stream_openai_response, stream_openai_response_with_save, stream_simulator_response
167
+ from .streaming import stream_openai_response, stream_openai_response_with_save, stream_simulator_response, save_user_message
168
168
 
169
169
  router = APIRouter(prefix="/api/v1", tags=["chat"])
170
170
 
@@ -215,7 +215,7 @@ async def ensure_session_with_metadata(
215
215
  Merges request metadata with existing session metadata.
216
216
 
217
217
  Args:
218
- session_id: Session identifier (maps to Session.name)
218
+ session_id: Session UUID from X-Session-Id header
219
219
  user_id: User identifier
220
220
  tenant_id: Tenant identifier
221
221
  is_eval: Whether this is an evaluation session
@@ -228,12 +228,8 @@ async def ensure_session_with_metadata(
228
228
  try:
229
229
  repo = Repository(Session, table_name="sessions")
230
230
 
231
- # Try to load existing session by name (session_id is the name field)
232
- existing_list = await repo.find(
233
- filters={"name": session_id, "tenant_id": tenant_id},
234
- limit=1,
235
- )
236
- existing = existing_list[0] if existing_list else None
231
+ # Look up session by UUID (id field)
232
+ existing = await repo.get_by_id(session_id)
237
233
 
238
234
  if existing:
239
235
  # Merge metadata if provided
@@ -254,9 +250,10 @@ async def ensure_session_with_metadata(
254
250
  await repo.upsert(existing)
255
251
  logger.debug(f"Updated session {session_id} (eval={is_eval}, metadata keys={list(merged_metadata.keys())})")
256
252
  else:
257
- # Create new session
253
+ # Create new session with the provided UUID as the id
258
254
  session = Session(
259
- name=session_id,
255
+ id=session_id, # Use the provided UUID as session id
256
+ name=session_id, # Default name to UUID, can be updated later with LLM-generated name
260
257
  mode=SessionMode.EVALUATION if is_eval else SessionMode.NORMAL,
261
258
  user_id=user_id,
262
259
  tenant_id=tenant_id,
@@ -503,16 +500,51 @@ async def chat_completions(body: ChatCompletionRequest, request: Request):
503
500
  logger.error(f"Failed to transcribe audio: {e}")
504
501
  # Fall through with original content (will likely fail at agent)
505
502
 
506
- # Use ContextBuilder to construct complete message list with:
507
- # 1. System context hint (date + user profile)
508
- # 2. Session history (if session_id provided)
509
- # 3. New messages from request body (transcribed if audio)
503
+ # Use ContextBuilder to construct context and basic messages
504
+ # Note: We load session history separately for proper pydantic-ai message_history
510
505
  context, messages = await ContextBuilder.build_from_headers(
511
506
  headers=dict(request.headers),
512
507
  new_messages=new_messages,
513
508
  user_id=temp_context.user_id, # From JWT token (source of truth)
514
509
  )
515
510
 
511
+ # Load raw session history for proper pydantic-ai message_history format
512
+ # This enables proper tool call/return pairing for LLM API compatibility
513
+ from ....services.session import SessionMessageStore, session_to_pydantic_messages, audit_session_history
514
+ from ....agentic.schema import get_system_prompt
515
+
516
+ pydantic_message_history = None
517
+ if context.session_id and settings.postgres.enabled:
518
+ try:
519
+ store = SessionMessageStore(user_id=context.user_id or settings.test.effective_user_id)
520
+ raw_session_history = await store.load_session_messages(
521
+ session_id=context.session_id,
522
+ user_id=context.user_id,
523
+ compress_on_load=False, # Don't compress - we need full data for reconstruction
524
+ )
525
+ if raw_session_history:
526
+ # CRITICAL: Extract and pass the agent's system prompt
527
+ # pydantic-ai only auto-adds system prompts when message_history is empty
528
+ # When we pass message_history, we must include the system prompt ourselves
529
+ agent_system_prompt = get_system_prompt(agent_schema) if agent_schema else None
530
+ pydantic_message_history = session_to_pydantic_messages(
531
+ raw_session_history,
532
+ system_prompt=agent_system_prompt,
533
+ )
534
+ logger.debug(f"Converted {len(raw_session_history)} session messages to {len(pydantic_message_history)} pydantic-ai messages (with system prompt)")
535
+
536
+ # Audit session history if enabled (for debugging)
537
+ audit_session_history(
538
+ session_id=context.session_id,
539
+ agent_name=schema_name or "default",
540
+ prompt=body.messages[-1].content if body.messages else "",
541
+ raw_session_history=raw_session_history,
542
+ pydantic_messages_count=len(pydantic_message_history),
543
+ )
544
+ except Exception as e:
545
+ logger.warning(f"Failed to load session history for message_history: {e}")
546
+ # Fall back to old behavior (concatenated prompt)
547
+
516
548
  logger.info(f"Built context with {len(messages)} total messages (includes history + user context)")
517
549
 
518
550
  # Ensure session exists with metadata and eval mode if applicable
@@ -533,33 +565,30 @@ async def chat_completions(body: ChatCompletionRequest, request: Request):
533
565
  model_override=body.model, # type: ignore[arg-type]
534
566
  )
535
567
 
536
- # Combine all messages into single prompt for agent
537
- # ContextBuilder already assembled: system context + history + new messages
538
- prompt = "\n".join(msg.content for msg in messages)
568
+ # Build the prompt for the agent
569
+ # If we have proper message_history, use just the latest user message as prompt
570
+ # Otherwise, fall back to concatenating all messages (legacy behavior)
571
+ if pydantic_message_history:
572
+ # Use the latest user message as the prompt, with history passed separately
573
+ user_prompt = body.messages[-1].content if body.messages else ""
574
+ prompt = user_prompt
575
+ logger.debug(f"Using message_history with {len(pydantic_message_history)} messages")
576
+ else:
577
+ # Legacy: Combine all messages into single prompt for agent
578
+ prompt = "\n".join(msg.content for msg in messages)
539
579
 
540
580
  # Generate OpenAI-compatible request ID
541
581
  request_id = f"chatcmpl-{uuid.uuid4().hex[:24]}"
542
582
 
543
583
  # Streaming mode
544
584
  if body.stream:
545
- # Save user message before streaming starts
546
- if settings.postgres.enabled and context.session_id:
547
- user_message = {
548
- "role": "user",
549
- "content": body.messages[-1].content if body.messages else "",
550
- "timestamp": datetime.utcnow().isoformat(),
551
- }
552
- try:
553
- store = SessionMessageStore(user_id=context.user_id or settings.test.effective_user_id)
554
- await store.store_session_messages(
555
- session_id=context.session_id,
556
- messages=[user_message],
557
- user_id=context.user_id,
558
- compress=False, # User messages are typically short
559
- )
560
- logger.debug(f"Saved user message to session {context.session_id}")
561
- except Exception as e:
562
- logger.error(f"Failed to save user message: {e}", exc_info=True)
585
+ # Save user message before streaming starts (using shared utility)
586
+ if context.session_id:
587
+ await save_user_message(
588
+ session_id=context.session_id,
589
+ user_id=context.user_id,
590
+ content=body.messages[-1].content if body.messages else "",
591
+ )
563
592
 
564
593
  return StreamingResponse(
565
594
  stream_openai_response_with_save(
@@ -570,6 +599,8 @@ async def chat_completions(body: ChatCompletionRequest, request: Request):
570
599
  agent_schema=schema_name,
571
600
  session_id=context.session_id,
572
601
  user_id=context.user_id,
602
+ agent_context=context, # Pass context for multi-agent support
603
+ message_history=pydantic_message_history, # Native pydantic-ai message history
573
604
  ),
574
605
  media_type="text/event-stream",
575
606
  headers={"Cache-Control": "no-cache", "Connection": "keep-alive"},
@@ -592,10 +623,16 @@ async def chat_completions(body: ChatCompletionRequest, request: Request):
592
623
  ) as span:
593
624
  # Capture trace context from the span we just created
594
625
  trace_id, span_id = get_current_trace_context()
595
- result = await agent.run(prompt)
626
+ if pydantic_message_history:
627
+ result = await agent.run(prompt, message_history=pydantic_message_history)
628
+ else:
629
+ result = await agent.run(prompt)
596
630
  else:
597
631
  # No tracer available, run without tracing
598
- result = await agent.run(prompt)
632
+ if pydantic_message_history:
633
+ result = await agent.run(prompt, message_history=pydantic_message_history)
634
+ else:
635
+ result = await agent.run(prompt)
599
636
 
600
637
  # Determine content format based on response_format request
601
638
  if body.response_format and body.response_format.type == "json_object":
@@ -321,7 +321,11 @@ class MetadataEvent(BaseModel):
321
321
  # Agent info
322
322
  agent_schema: str | None = Field(
323
323
  default=None,
324
- description="Name of the agent schema used for this response (e.g., 'rem', 'query-assistant')"
324
+ description="Name of the top-level agent schema (e.g., 'siggy', 'rem')"
325
+ )
326
+ responding_agent: str | None = Field(
327
+ default=None,
328
+ description="Name of the agent that produced this response (may differ from agent_schema if delegated via ask_agent)"
325
329
  )
326
330
 
327
331
  # Session info
@@ -409,9 +413,9 @@ class ToolCallEvent(BaseModel):
409
413
  default=None,
410
414
  description="Tool arguments (for 'started' status)"
411
415
  )
412
- result: str | None = Field(
416
+ result: str | dict[str, Any] | None = Field(
413
417
  default=None,
414
- description="Tool result summary (for 'completed' status)"
418
+ description="Tool result - full dict for finalize_intake, summary string for others"
415
419
  )
416
420
  error: str | None = Field(
417
421
  default=None,