remdb 0.3.180__py3-none-any.whl → 0.3.258__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 (70) hide show
  1. rem/agentic/README.md +36 -2
  2. rem/agentic/__init__.py +10 -1
  3. rem/agentic/context.py +185 -1
  4. rem/agentic/context_builder.py +56 -35
  5. rem/agentic/mcp/tool_wrapper.py +2 -2
  6. rem/agentic/providers/pydantic_ai.py +303 -111
  7. rem/agentic/schema.py +2 -2
  8. rem/api/main.py +1 -1
  9. rem/api/mcp_router/resources.py +223 -0
  10. rem/api/mcp_router/server.py +4 -0
  11. rem/api/mcp_router/tools.py +608 -166
  12. rem/api/routers/admin.py +30 -4
  13. rem/api/routers/auth.py +219 -20
  14. rem/api/routers/chat/child_streaming.py +393 -0
  15. rem/api/routers/chat/completions.py +77 -40
  16. rem/api/routers/chat/sse_events.py +7 -3
  17. rem/api/routers/chat/streaming.py +381 -291
  18. rem/api/routers/chat/streaming_utils.py +325 -0
  19. rem/api/routers/common.py +18 -0
  20. rem/api/routers/dev.py +7 -1
  21. rem/api/routers/feedback.py +11 -3
  22. rem/api/routers/messages.py +176 -38
  23. rem/api/routers/models.py +9 -1
  24. rem/api/routers/query.py +17 -15
  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 +205 -114
  30. rem/cli/commands/db.py +55 -31
  31. rem/cli/commands/experiments.py +1 -1
  32. rem/cli/commands/process.py +179 -43
  33. rem/cli/commands/query.py +109 -0
  34. rem/cli/commands/session.py +117 -0
  35. rem/cli/main.py +2 -0
  36. rem/models/core/experiment.py +1 -1
  37. rem/models/entities/ontology.py +18 -20
  38. rem/models/entities/session.py +1 -0
  39. rem/schemas/agents/core/agent-builder.yaml +1 -1
  40. rem/schemas/agents/rem.yaml +1 -1
  41. rem/schemas/agents/test_orchestrator.yaml +42 -0
  42. rem/schemas/agents/test_structured_output.yaml +52 -0
  43. rem/services/content/providers.py +151 -49
  44. rem/services/content/service.py +18 -5
  45. rem/services/embeddings/worker.py +26 -12
  46. rem/services/postgres/__init__.py +28 -3
  47. rem/services/postgres/diff_service.py +57 -5
  48. rem/services/postgres/programmable_diff_service.py +635 -0
  49. rem/services/postgres/pydantic_to_sqlalchemy.py +2 -2
  50. rem/services/postgres/register_type.py +11 -10
  51. rem/services/postgres/repository.py +39 -28
  52. rem/services/postgres/schema_generator.py +5 -5
  53. rem/services/postgres/sql_builder.py +6 -5
  54. rem/services/rem/README.md +4 -3
  55. rem/services/rem/parser.py +7 -10
  56. rem/services/rem/service.py +47 -0
  57. rem/services/session/__init__.py +8 -1
  58. rem/services/session/compression.py +47 -5
  59. rem/services/session/pydantic_messages.py +310 -0
  60. rem/services/session/reload.py +2 -1
  61. rem/settings.py +92 -7
  62. rem/sql/migrations/001_install.sql +125 -7
  63. rem/sql/migrations/002_install_models.sql +159 -149
  64. rem/sql/migrations/004_cache_system.sql +10 -276
  65. rem/sql/migrations/migrate_session_id_to_uuid.sql +45 -0
  66. rem/utils/schema_loader.py +180 -120
  67. {remdb-0.3.180.dist-info → remdb-0.3.258.dist-info}/METADATA +7 -6
  68. {remdb-0.3.180.dist-info → remdb-0.3.258.dist-info}/RECORD +70 -61
  69. {remdb-0.3.180.dist-info → remdb-0.3.258.dist-info}/WHEEL +0 -0
  70. {remdb-0.3.180.dist-info → remdb-0.3.258.dist-info}/entry_points.txt +0 -0
@@ -542,6 +542,227 @@ def register_status_resources(mcp: FastMCP):
542
542
  """
543
543
 
544
544
 
545
+ def register_session_resources(mcp: FastMCP):
546
+ """
547
+ Register session resources for loading conversation history.
548
+
549
+ Args:
550
+ mcp: FastMCP server instance
551
+ """
552
+
553
+ @mcp.resource("rem://sessions/{session_id}")
554
+ async def get_session_messages(session_id: str) -> str:
555
+ """
556
+ Load a conversation session by ID.
557
+
558
+ Returns the full message history including user messages, assistant responses,
559
+ and tool calls. Useful for evaluators and analysis agents.
560
+
561
+ Args:
562
+ session_id: Session UUID or identifier
563
+
564
+ Returns:
565
+ Formatted conversation history as markdown string with:
566
+ - Message type (user/assistant/tool)
567
+ - Content
568
+ - Timestamps
569
+ - Tool call details (if any)
570
+ """
571
+ from ...services.postgres import get_postgres_service
572
+
573
+ pg = get_postgres_service()
574
+ await pg.connect()
575
+
576
+ try:
577
+ # Query messages for session
578
+ query = """
579
+ SELECT id, message_type, content, metadata, created_at
580
+ FROM messages
581
+ WHERE session_id = $1
582
+ ORDER BY created_at ASC
583
+ """
584
+ messages = await pg.fetch(query, session_id)
585
+
586
+ if not messages:
587
+ return f"# Session Not Found\n\nNo messages found for session_id: {session_id}"
588
+
589
+ # Format output
590
+ output = [f"# Session: {session_id}\n"]
591
+ output.append(f"**Total messages:** {len(messages)}\n")
592
+
593
+ for i, msg in enumerate(messages, 1):
594
+ msg_type = msg['message_type']
595
+ content = msg['content'] or "(empty)"
596
+ created = msg['created_at']
597
+ metadata = msg.get('metadata') or {}
598
+
599
+ # Format based on message type
600
+ if msg_type == 'user':
601
+ output.append(f"\n## [{i}] USER ({created})")
602
+ output.append(f"```\n{content[:1000]}{'...' if len(content) > 1000 else ''}\n```")
603
+ elif msg_type == 'assistant':
604
+ output.append(f"\n## [{i}] ASSISTANT ({created})")
605
+ output.append(f"```\n{content[:1000]}{'...' if len(content) > 1000 else ''}\n```")
606
+ elif msg_type == 'tool':
607
+ tool_name = metadata.get('tool_name', 'unknown')
608
+ output.append(f"\n## [{i}] TOOL: {tool_name} ({created})")
609
+ # Truncate tool results more aggressively
610
+ output.append(f"```json\n{content[:500]}{'...' if len(content) > 500 else ''}\n```")
611
+ else:
612
+ output.append(f"\n## [{i}] {msg_type.upper()} ({created})")
613
+ output.append(f"```\n{content[:500]}{'...' if len(content) > 500 else ''}\n```")
614
+
615
+ return "\n".join(output)
616
+
617
+ finally:
618
+ await pg.disconnect()
619
+
620
+ @mcp.resource("rem://sessions")
621
+ async def list_recent_sessions() -> str:
622
+ """
623
+ List recent sessions with basic info.
624
+
625
+ Returns the most recent 20 sessions with:
626
+ - Session ID
627
+ - First user message (preview)
628
+ - Message count
629
+ - Timestamp
630
+ """
631
+ from ...services.postgres import get_postgres_service
632
+
633
+ pg = get_postgres_service()
634
+ await pg.connect()
635
+
636
+ try:
637
+ # Query recent sessions
638
+ query = """
639
+ SELECT
640
+ session_id,
641
+ MIN(created_at) as started_at,
642
+ COUNT(*) as message_count,
643
+ MIN(CASE WHEN message_type = 'user' THEN content END) as first_message
644
+ FROM messages
645
+ WHERE session_id IS NOT NULL
646
+ GROUP BY session_id
647
+ ORDER BY MIN(created_at) DESC
648
+ LIMIT 20
649
+ """
650
+ sessions = await pg.fetch(query)
651
+
652
+ if not sessions:
653
+ return "# Recent Sessions\n\nNo sessions found."
654
+
655
+ output = ["# Recent Sessions\n"]
656
+ output.append(f"Showing {len(sessions)} most recent sessions:\n")
657
+
658
+ for session in sessions:
659
+ session_id = session['session_id']
660
+ started = session['started_at']
661
+ count = session['message_count']
662
+ first_msg = session['first_message'] or "(no user message)"
663
+ preview = first_msg[:80] + "..." if len(first_msg) > 80 else first_msg
664
+
665
+ output.append(f"\n## {session_id}")
666
+ output.append(f"- **Started:** {started}")
667
+ output.append(f"- **Messages:** {count}")
668
+ output.append(f"- **First message:** {preview}")
669
+ output.append(f"- **Load:** `rem://sessions/{session_id}`")
670
+
671
+ return "\n".join(output)
672
+
673
+ finally:
674
+ await pg.disconnect()
675
+
676
+
677
+ def register_user_resources(mcp: FastMCP):
678
+ """
679
+ Register user profile resources for on-demand profile loading.
680
+
681
+ Args:
682
+ mcp: FastMCP server instance
683
+ """
684
+
685
+ @mcp.resource("user://profile/{user_id}")
686
+ async def get_user_profile(user_id: str) -> str:
687
+ """
688
+ Load a user's profile by ID.
689
+
690
+ Returns the user's profile information including:
691
+ - Email and name
692
+ - Summary (AI-generated profile summary)
693
+ - Interests and preferred topics
694
+ - Activity level
695
+
696
+ This resource is protected - each user can only access their own profile.
697
+ The user_id should match the authenticated user's ID from the JWT token.
698
+
699
+ Args:
700
+ user_id: User UUID from authentication
701
+
702
+ Returns:
703
+ Formatted user profile as markdown string, or error if not found
704
+ """
705
+ from ...services.postgres import get_postgres_service
706
+ from ...services.postgres.repository import Repository
707
+ from ...models.entities.user import User
708
+
709
+ pg = get_postgres_service()
710
+ await pg.connect()
711
+
712
+ try:
713
+ user_repo = Repository(User, "users", db=pg)
714
+ # Look up user by ID (user_id from JWT is the primary key)
715
+ user = await user_repo.get_by_id(user_id, tenant_id=None)
716
+
717
+ if not user:
718
+ return f"# User Profile Not Found\n\nNo user found with ID: {user_id}"
719
+
720
+ # Build profile output
721
+ output = [f"# User Profile: {user.name or user.email or 'Unknown'}"]
722
+ output.append("")
723
+
724
+ if user.email:
725
+ output.append(f"**Email:** {user.email}")
726
+
727
+ if user.role:
728
+ output.append(f"**Role:** {user.role}")
729
+
730
+ if user.tier:
731
+ output.append(f"**Tier:** {user.tier.value if hasattr(user.tier, 'value') else user.tier}")
732
+
733
+ if user.summary:
734
+ output.append(f"\n## Summary\n{user.summary}")
735
+
736
+ if user.interests:
737
+ output.append(f"\n## Interests\n- " + "\n- ".join(user.interests[:10]))
738
+
739
+ if user.preferred_topics:
740
+ output.append(f"\n## Preferred Topics\n- " + "\n- ".join(user.preferred_topics[:10]))
741
+
742
+ if user.activity_level:
743
+ output.append(f"\n**Activity Level:** {user.activity_level}")
744
+
745
+ if user.last_active_at:
746
+ output.append(f"**Last Active:** {user.last_active_at}")
747
+
748
+ # Add metadata if present (but redact sensitive fields)
749
+ if user.metadata:
750
+ safe_metadata = {k: v for k, v in user.metadata.items()
751
+ if k not in ('login_code', 'password', 'token', 'secret')}
752
+ if safe_metadata:
753
+ output.append(f"\n## Additional Info")
754
+ for key, value in list(safe_metadata.items())[:5]:
755
+ output.append(f"- **{key}:** {value}")
756
+
757
+ return "\n".join(output)
758
+
759
+ except Exception as e:
760
+ return f"# Error Loading Profile\n\nFailed to load user profile: {e}"
761
+
762
+ finally:
763
+ await pg.disconnect()
764
+
765
+
545
766
  # Resource dispatcher for read_resource tool
546
767
  async def load_resource(uri: str) -> dict | str:
547
768
  """
@@ -571,6 +792,8 @@ async def load_resource(uri: str) -> dict | str:
571
792
  register_agent_resources(mcp)
572
793
  register_file_resources(mcp)
573
794
  register_status_resources(mcp)
795
+ register_session_resources(mcp)
796
+ register_user_resources(mcp)
574
797
 
575
798
  # 1. Try exact match in regular resources
576
799
  resources = await mcp.get_resources()
@@ -34,6 +34,7 @@ from .resources import (
34
34
  register_status_resources,
35
35
  )
36
36
  from .tools import (
37
+ ask_agent,
37
38
  ask_rem_agent,
38
39
  get_schema,
39
40
  ingest_into_rem,
@@ -203,6 +204,9 @@ def create_mcp_server(is_local: bool = False) -> FastMCP:
203
204
  mcp.tool()(get_schema)
204
205
  mcp.tool()(save_agent)
205
206
 
207
+ # Register multi-agent tools
208
+ mcp.tool()(ask_agent)
209
+
206
210
  # Register test tool only in development environment (not staging/production)
207
211
  if settings.environment not in ("staging", "production"):
208
212
  mcp.tool()(test_error_handling)