remdb 0.3.14__py3-none-any.whl → 0.3.157__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 (112) hide show
  1. rem/agentic/README.md +76 -0
  2. rem/agentic/__init__.py +15 -0
  3. rem/agentic/agents/__init__.py +32 -2
  4. rem/agentic/agents/agent_manager.py +310 -0
  5. rem/agentic/agents/sse_simulator.py +502 -0
  6. rem/agentic/context.py +51 -27
  7. rem/agentic/context_builder.py +5 -3
  8. rem/agentic/llm_provider_models.py +301 -0
  9. rem/agentic/mcp/tool_wrapper.py +155 -18
  10. rem/agentic/otel/setup.py +93 -4
  11. rem/agentic/providers/phoenix.py +371 -108
  12. rem/agentic/providers/pydantic_ai.py +280 -57
  13. rem/agentic/schema.py +361 -21
  14. rem/agentic/tools/rem_tools.py +3 -3
  15. rem/api/README.md +215 -1
  16. rem/api/deps.py +255 -0
  17. rem/api/main.py +132 -40
  18. rem/api/mcp_router/resources.py +1 -1
  19. rem/api/mcp_router/server.py +28 -5
  20. rem/api/mcp_router/tools.py +555 -7
  21. rem/api/routers/admin.py +494 -0
  22. rem/api/routers/auth.py +278 -4
  23. rem/api/routers/chat/completions.py +402 -20
  24. rem/api/routers/chat/models.py +88 -10
  25. rem/api/routers/chat/otel_utils.py +33 -0
  26. rem/api/routers/chat/sse_events.py +542 -0
  27. rem/api/routers/chat/streaming.py +697 -45
  28. rem/api/routers/dev.py +81 -0
  29. rem/api/routers/feedback.py +268 -0
  30. rem/api/routers/messages.py +473 -0
  31. rem/api/routers/models.py +78 -0
  32. rem/api/routers/query.py +360 -0
  33. rem/api/routers/shared_sessions.py +406 -0
  34. rem/auth/__init__.py +13 -3
  35. rem/auth/middleware.py +186 -22
  36. rem/auth/providers/__init__.py +4 -1
  37. rem/auth/providers/email.py +215 -0
  38. rem/cli/commands/README.md +237 -64
  39. rem/cli/commands/cluster.py +1808 -0
  40. rem/cli/commands/configure.py +4 -7
  41. rem/cli/commands/db.py +386 -143
  42. rem/cli/commands/experiments.py +468 -76
  43. rem/cli/commands/process.py +14 -8
  44. rem/cli/commands/schema.py +97 -50
  45. rem/cli/commands/session.py +336 -0
  46. rem/cli/dreaming.py +2 -2
  47. rem/cli/main.py +29 -6
  48. rem/config.py +10 -3
  49. rem/models/core/core_model.py +7 -1
  50. rem/models/core/experiment.py +58 -14
  51. rem/models/core/rem_query.py +5 -2
  52. rem/models/entities/__init__.py +25 -0
  53. rem/models/entities/domain_resource.py +38 -0
  54. rem/models/entities/feedback.py +123 -0
  55. rem/models/entities/message.py +30 -1
  56. rem/models/entities/ontology.py +1 -1
  57. rem/models/entities/ontology_config.py +1 -1
  58. rem/models/entities/session.py +83 -0
  59. rem/models/entities/shared_session.py +180 -0
  60. rem/models/entities/subscriber.py +175 -0
  61. rem/models/entities/user.py +1 -0
  62. rem/registry.py +10 -4
  63. rem/schemas/agents/core/agent-builder.yaml +134 -0
  64. rem/schemas/agents/examples/contract-analyzer.yaml +1 -1
  65. rem/schemas/agents/examples/contract-extractor.yaml +1 -1
  66. rem/schemas/agents/examples/cv-parser.yaml +1 -1
  67. rem/schemas/agents/rem.yaml +7 -3
  68. rem/services/__init__.py +3 -1
  69. rem/services/content/service.py +92 -19
  70. rem/services/email/__init__.py +10 -0
  71. rem/services/email/service.py +459 -0
  72. rem/services/email/templates.py +360 -0
  73. rem/services/embeddings/api.py +4 -4
  74. rem/services/embeddings/worker.py +16 -16
  75. rem/services/phoenix/client.py +154 -14
  76. rem/services/postgres/README.md +197 -15
  77. rem/services/postgres/__init__.py +2 -1
  78. rem/services/postgres/diff_service.py +547 -0
  79. rem/services/postgres/pydantic_to_sqlalchemy.py +470 -140
  80. rem/services/postgres/repository.py +132 -0
  81. rem/services/postgres/schema_generator.py +205 -4
  82. rem/services/postgres/service.py +6 -6
  83. rem/services/rem/parser.py +44 -9
  84. rem/services/rem/service.py +36 -2
  85. rem/services/session/compression.py +137 -51
  86. rem/services/session/reload.py +15 -8
  87. rem/settings.py +515 -27
  88. rem/sql/background_indexes.sql +21 -16
  89. rem/sql/migrations/001_install.sql +387 -54
  90. rem/sql/migrations/002_install_models.sql +2304 -377
  91. rem/sql/migrations/003_optional_extensions.sql +326 -0
  92. rem/sql/migrations/004_cache_system.sql +548 -0
  93. rem/sql/migrations/005_schema_update.sql +145 -0
  94. rem/utils/README.md +45 -0
  95. rem/utils/__init__.py +18 -0
  96. rem/utils/date_utils.py +2 -2
  97. rem/utils/files.py +157 -1
  98. rem/utils/model_helpers.py +156 -1
  99. rem/utils/schema_loader.py +220 -22
  100. rem/utils/sql_paths.py +146 -0
  101. rem/utils/sql_types.py +3 -1
  102. rem/utils/vision.py +1 -1
  103. rem/workers/__init__.py +3 -1
  104. rem/workers/db_listener.py +579 -0
  105. rem/workers/unlogged_maintainer.py +463 -0
  106. {remdb-0.3.14.dist-info → remdb-0.3.157.dist-info}/METADATA +340 -229
  107. {remdb-0.3.14.dist-info → remdb-0.3.157.dist-info}/RECORD +109 -80
  108. {remdb-0.3.14.dist-info → remdb-0.3.157.dist-info}/WHEEL +1 -1
  109. rem/sql/002_install_models.sql +0 -1068
  110. rem/sql/install_models.sql +0 -1051
  111. rem/sql/migrations/003_seed_default_user.sql +0 -48
  112. {remdb-0.3.14.dist-info → remdb-0.3.157.dist-info}/entry_points.txt +0 -0
@@ -12,12 +12,12 @@ from rem.services.content import ContentService
12
12
 
13
13
  @click.command(name="ingest")
14
14
  @click.argument("file_path", type=click.Path(exists=True))
15
- @click.option("--user-id", required=True, help="User ID to own the file")
15
+ @click.option("--user-id", default=None, help="User ID to scope file privately (default: public/shared)")
16
16
  @click.option("--category", help="Optional file category")
17
17
  @click.option("--tags", help="Optional comma-separated tags")
18
18
  def process_ingest(
19
19
  file_path: str,
20
- user_id: str,
20
+ user_id: str | None,
21
21
  category: str | None,
22
22
  tags: str | None,
23
23
  ):
@@ -32,8 +32,9 @@ def process_ingest(
32
32
  5. Creates a File entity record.
33
33
 
34
34
  Examples:
35
- rem process ingest sample.pdf --user-id user-123
36
- rem process ingest contract.docx --user-id user-123 --category legal --tags contract,2023
35
+ rem process ingest sample.pdf
36
+ rem process ingest contract.docx --category legal --tags contract,2023
37
+ rem process ingest agent.yaml # Auto-detects kind=agent, saves to schemas table
37
38
  """
38
39
  import asyncio
39
40
  from ...services.content import ContentService
@@ -56,7 +57,8 @@ def process_ingest(
56
57
 
57
58
  tag_list = tags.split(",") if tags else None
58
59
 
59
- logger.info(f"Ingesting file: {file_path} for user: {user_id}")
60
+ scope_msg = f"user: {user_id}" if user_id else "public"
61
+ logger.info(f"Ingesting file: {file_path} ({scope_msg})")
60
62
  result = await service.ingest_file(
61
63
  file_uri=file_path,
62
64
  user_id=user_id,
@@ -65,11 +67,15 @@ def process_ingest(
65
67
  is_local_server=True, # CLI is local
66
68
  )
67
69
 
68
- if result.get("processing_status") == "completed":
69
- logger.success(f"File ingested successfully: {result['file_name']}")
70
+ # Handle schema ingestion (agents/evaluators)
71
+ if result.get("schema_name"):
72
+ logger.success(f"Schema ingested: {result['schema_name']} (kind={result.get('kind', 'agent')})")
73
+ logger.info(f"Version: {result.get('version', '1.0.0')}")
74
+ # Handle file ingestion
75
+ elif result.get("processing_status") == "completed":
76
+ logger.success(f"File ingested: {result['file_name']}")
70
77
  logger.info(f"File ID: {result['file_id']}")
71
78
  logger.info(f"Resources created: {result['resources_created']}")
72
- logger.info(f"Status: {result['processing_status']}")
73
79
  else:
74
80
  logger.error(f"Ingestion failed: {result.get('message', 'Unknown error')}")
75
81
  sys.exit(1)
@@ -8,6 +8,7 @@ Usage:
8
8
  """
9
9
 
10
10
  import asyncio
11
+ import importlib
11
12
  from pathlib import Path
12
13
 
13
14
  import click
@@ -15,68 +16,116 @@ from loguru import logger
15
16
 
16
17
  from ...settings import settings
17
18
  from ...services.postgres.schema_generator import SchemaGenerator
19
+ from ...utils.sql_paths import get_package_sql_dir, get_package_migrations_dir
20
+
21
+
22
+ def _import_model_modules() -> list[str]:
23
+ """
24
+ Import modules specified in MODELS__IMPORT_MODULES setting.
25
+
26
+ This ensures downstream models decorated with @rem.register_model
27
+ are registered before schema generation.
28
+
29
+ Returns:
30
+ List of successfully imported module names
31
+ """
32
+ imported = []
33
+ for module_name in settings.models.module_list:
34
+ try:
35
+ importlib.import_module(module_name)
36
+ imported.append(module_name)
37
+ logger.debug(f"Imported model module: {module_name}")
38
+ except ImportError as e:
39
+ logger.warning(f"Failed to import model module '{module_name}': {e}")
40
+ click.echo(
41
+ click.style(f" ⚠ Could not import '{module_name}': {e}", fg="yellow"),
42
+ err=True,
43
+ )
44
+ return imported
18
45
 
19
46
 
20
47
  @click.command()
21
- @click.option(
22
- "--models",
23
- "-m",
24
- required=True,
25
- type=click.Path(exists=True, path_type=Path),
26
- help="Directory containing Pydantic models",
27
- )
28
48
  @click.option(
29
49
  "--output",
30
50
  "-o",
31
51
  type=click.Path(path_type=Path),
32
- default="install_models.sql",
33
- help="Output SQL file (default: install_models.sql)",
52
+ default="002_install_models.sql",
53
+ help="Output SQL file (default: 002_install_models.sql)",
34
54
  )
35
55
  @click.option(
36
56
  "--output-dir",
37
57
  type=click.Path(path_type=Path),
38
58
  default=None,
39
- help=f"Base output directory (default: {settings.sql_dir})",
59
+ help="Base output directory (default: package sql/migrations)",
40
60
  )
41
- def generate(models: Path, output: Path, output_dir: Path | None):
61
+ def generate(output: Path, output_dir: Path | None):
42
62
  """
43
- Generate database schema from Pydantic models.
63
+ Generate database schema from registered Pydantic models.
44
64
 
45
- Scans the specified directory for Pydantic models and generates:
65
+ Uses the model registry (core models + user-registered models) to generate:
46
66
  - CREATE TABLE statements
47
67
  - Embeddings tables (embeddings_<table>)
48
68
  - KV_STORE triggers for cache maintenance
49
69
  - Indexes (foreground only)
50
70
 
51
- Output is written to src/rem/sql/install_models.sql by default.
71
+ Output is written to src/rem/sql/migrations/002_install_models.sql by default.
52
72
 
53
73
  Example:
54
- rem db schema generate --models src/rem/models/entities
74
+ rem db schema generate
75
+
76
+ To register custom models in downstream apps:
77
+
78
+ 1. Create models with @rem.register_model decorator:
79
+
80
+ # models/__init__.py
81
+ import rem
82
+ from rem.models.core import CoreModel
83
+
84
+ @rem.register_model
85
+ class MyEntity(CoreModel):
86
+ name: str
87
+
88
+ 2. Set MODELS__IMPORT_MODULES in your .env:
89
+
90
+ MODELS__IMPORT_MODULES=models
91
+
92
+ 3. Run schema generation:
93
+
94
+ rem db schema generate
55
95
 
56
96
  This creates:
57
- - src/rem/sql/install_models.sql - Entity tables and triggers
97
+ - src/rem/sql/migrations/002_install_models.sql - Entity tables and triggers
58
98
  - src/rem/sql/background_indexes.sql - HNSW indexes (apply after data load)
59
99
 
60
- After generation, apply with:
61
- rem db migrate
100
+ After generation, verify with:
101
+ rem db diff
62
102
  """
63
- click.echo(f"Discovering models in {models}")
103
+ from ...registry import get_model_registry
104
+
105
+ # Import downstream model modules to trigger @rem.register_model decorators
106
+ imported_modules = _import_model_modules()
107
+ if imported_modules:
108
+ click.echo(f"Imported model modules: {', '.join(imported_modules)}")
109
+
110
+ registry = get_model_registry()
111
+ models = registry.get_models(include_core=True)
112
+ click.echo(f"Generating schema from {len(models)} registered models")
64
113
 
65
- # Use settings.sql_dir if not provided
66
- actual_output_dir = output_dir or Path(settings.sql_dir)
114
+ # Default to package migrations directory
115
+ actual_output_dir = output_dir or get_package_migrations_dir()
67
116
  generator = SchemaGenerator(output_dir=actual_output_dir)
68
117
 
69
- # Generate schema
118
+ # Generate schema from registry
70
119
  try:
71
- schema_sql = asyncio.run(generator.generate_from_directory(models, output_file=output.name))
120
+ schema_sql = asyncio.run(generator.generate_from_registry(output_file=output.name))
72
121
 
73
122
  click.echo(f"✓ Schema generated: {len(generator.schemas)} tables")
74
123
  click.echo(f"✓ Written to: {actual_output_dir / output.name}")
75
124
 
76
- # Generate background indexes
125
+ # Generate background indexes in parent sql dir
77
126
  background_indexes = generator.generate_background_indexes()
78
127
  if background_indexes:
79
- bg_file = actual_output_dir / "background_indexes.sql"
128
+ bg_file = get_package_sql_dir() / "background_indexes.sql"
80
129
  bg_file.write_text(background_indexes)
81
130
  click.echo(f"✓ Background indexes: {bg_file}")
82
131
 
@@ -94,48 +143,46 @@ def generate(models: Path, output: Path, output_dir: Path | None):
94
143
 
95
144
 
96
145
  @click.command()
97
- @click.option(
98
- "--models",
99
- "-m",
100
- required=True,
101
- type=click.Path(exists=True, path_type=Path),
102
- help="Directory containing Pydantic models",
103
- )
104
- def validate(models: Path):
146
+ def validate():
105
147
  """
106
- Validate Pydantic models for schema generation.
148
+ Validate registered Pydantic models for schema generation.
107
149
 
108
150
  Checks:
109
- - Models can be loaded
151
+ - Models can be loaded from registry
110
152
  - Models have suitable entity_key fields
111
153
  - Fields with embeddings are properly configured
154
+
155
+ Set MODELS__IMPORT_MODULES to include custom models from downstream apps.
112
156
  """
113
- click.echo(f"Validating models in {models}")
157
+ from ...registry import get_model_registry
114
158
 
115
- generator = SchemaGenerator()
116
- discovered = generator.discover_models(models)
159
+ # Import downstream model modules to trigger @rem.register_model decorators
160
+ imported_modules = _import_model_modules()
161
+ if imported_modules:
162
+ click.echo(f"Imported model modules: {', '.join(imported_modules)}")
117
163
 
118
- if not discovered:
119
- click.echo("✗ No models found", err=True)
120
- raise click.Abort()
164
+ registry = get_model_registry()
165
+ models = registry.get_models(include_core=True)
166
+
167
+ click.echo(f"Validating {len(models)} registered models")
121
168
 
122
- click.echo(f"✓ Discovered {len(discovered)} models")
169
+ if not models:
170
+ click.echo("✗ No models found in registry", err=True)
171
+ raise click.Abort()
123
172
 
173
+ generator = SchemaGenerator()
124
174
  errors: list[str] = []
125
175
  warnings: list[str] = []
126
176
 
127
- for model_name, model in discovered.items():
128
- table_name = generator.infer_table_name(model)
129
- entity_key = generator.infer_entity_key_field(model)
177
+ for model_name, ext in models.items():
178
+ model = ext.model
179
+ table_name = ext.table_name or generator.infer_table_name(model)
180
+ entity_key = ext.entity_key_field or generator.infer_entity_key_field(model)
130
181
 
131
182
  # Check for entity_key
132
183
  if entity_key == "id":
133
184
  warnings.append(f"{model_name}: No natural key field, using 'id'")
134
185
 
135
- # Check for embeddable fields
136
- # TODO: Implement should_embed_field check
137
- embeddable: list[str] = [] # Placeholder - needs implementation
138
-
139
186
  click.echo(f" {model_name} -> {table_name} (key: {entity_key})")
140
187
 
141
188
  if warnings:
@@ -158,7 +205,7 @@ def validate(models: Path):
158
205
  "-o",
159
206
  type=click.Path(path_type=Path),
160
207
  default=None,
161
- help=f"Output file for background indexes (default: {settings.sql_dir}/background_indexes.sql)",
208
+ help="Output file for background indexes (default: package sql/background_indexes.sql)",
162
209
  )
163
210
  def indexes(output: Path):
164
211
  """
@@ -0,0 +1,336 @@
1
+ """
2
+ CLI command for viewing and simulating session conversations.
3
+
4
+ Usage:
5
+ rem session show <user_id> [--session-id] [--role user|assistant|system]
6
+ rem session show <user_id> --simulate-next [--save] [--custom-sim-prompt "..."]
7
+
8
+ Examples:
9
+ # Show all messages for a user
10
+ rem session show 11111111-1111-1111-1111-111111111001
11
+
12
+ # Show only user messages
13
+ rem session show 11111111-1111-1111-1111-111111111001 --role user
14
+
15
+ # Simulate next user message
16
+ rem session show 11111111-1111-1111-1111-111111111001 --simulate-next
17
+
18
+ # Simulate with custom prompt and save
19
+ rem session show 11111111-1111-1111-1111-111111111001 --simulate-next --save \
20
+ --custom-sim-prompt "Respond as an anxious patient"
21
+ """
22
+
23
+ import asyncio
24
+ from pathlib import Path
25
+ from typing import Literal
26
+
27
+ import click
28
+ import yaml
29
+ from loguru import logger
30
+
31
+ from ...models.entities.user import User
32
+ from ...models.entities.message import Message
33
+ from ...services.postgres import get_postgres_service
34
+ from ...services.postgres.repository import Repository
35
+ from ...settings import settings
36
+
37
+
38
+ SIMULATOR_PROMPT = """You are simulating a patient in a mental health conversation.
39
+
40
+ ## Context
41
+ You are continuing a conversation with a clinical evaluation agent. Based on the
42
+ user profile and conversation history below, generate the next realistic patient message.
43
+
44
+ ## User Profile
45
+ {user_profile}
46
+
47
+ ## Conversation History
48
+ {conversation_history}
49
+
50
+ ## Instructions
51
+ - Stay in character as the patient described in the profile
52
+ - Your response should be natural, conversational, and consistent with the patient's presentation
53
+ - Consider the patient's risk level, symptoms, and communication style
54
+ - Do NOT include any metadata or role labels - just the raw message content
55
+ - Keep responses concise (1-3 sentences typical for conversation)
56
+
57
+ Generate the next patient message:"""
58
+
59
+
60
+ async def _load_user_and_messages(
61
+ user_id: str,
62
+ session_id: str | None = None,
63
+ role_filter: str | None = None,
64
+ limit: int = 100,
65
+ ) -> tuple[User | None, list[Message]]:
66
+ """Load user profile and messages from database."""
67
+ pg = get_postgres_service()
68
+ if not pg:
69
+ logger.error("PostgreSQL not available")
70
+ return None, []
71
+
72
+ await pg.connect()
73
+
74
+ try:
75
+ # Load user
76
+ user_repo = Repository(User, "users", db=pg)
77
+ user = await user_repo.get_by_id(user_id, tenant_id="default")
78
+
79
+ # Load messages
80
+ message_repo = Repository(Message, "messages", db=pg)
81
+ filters = {"user_id": user_id}
82
+ if session_id:
83
+ filters["session_id"] = session_id
84
+
85
+ messages = await message_repo.find(
86
+ filters=filters,
87
+ order_by="created_at ASC",
88
+ limit=limit,
89
+ )
90
+
91
+ # Filter by role if specified
92
+ if role_filter:
93
+ messages = [m for m in messages if m.message_type == role_filter]
94
+
95
+ return user, messages
96
+
97
+ finally:
98
+ await pg.disconnect()
99
+
100
+
101
+ def _format_user_yaml(user: User | None) -> str:
102
+ """Format user profile as YAML."""
103
+ if not user:
104
+ return "# No user found"
105
+
106
+ data = {
107
+ "id": str(user.id),
108
+ "name": user.name,
109
+ "summary": user.summary,
110
+ "interests": user.interests,
111
+ "preferred_topics": user.preferred_topics,
112
+ "metadata": user.metadata,
113
+ }
114
+ return yaml.dump(data, default_flow_style=False, allow_unicode=True)
115
+
116
+
117
+ def _format_messages_yaml(messages: list[Message]) -> str:
118
+ """Format messages as YAML."""
119
+ if not messages:
120
+ return "# No messages found"
121
+
122
+ data = []
123
+ for msg in messages:
124
+ data.append({
125
+ "role": msg.message_type or "unknown",
126
+ "content": msg.content,
127
+ "session_id": msg.session_id,
128
+ "created_at": msg.created_at.isoformat() if msg.created_at else None,
129
+ })
130
+ return yaml.dump(data, default_flow_style=False, allow_unicode=True)
131
+
132
+
133
+ def _format_conversation_for_llm(messages: list[Message]) -> str:
134
+ """Format conversation history for LLM context."""
135
+ lines = []
136
+ for msg in messages:
137
+ role = msg.message_type or "unknown"
138
+ lines.append(f"[{role.upper()}]: {msg.content}")
139
+ return "\n\n".join(lines) if lines else "(No previous messages)"
140
+
141
+
142
+ async def _simulate_next_message(
143
+ user: User | None,
144
+ messages: list[Message],
145
+ custom_prompt: str | None = None,
146
+ ) -> str:
147
+ """Use LLM to simulate the next patient message."""
148
+ from pydantic_ai import Agent
149
+
150
+ # Build context
151
+ user_profile = _format_user_yaml(user) if user else "Unknown patient"
152
+ conversation_history = _format_conversation_for_llm(messages)
153
+
154
+ # Use custom prompt or default
155
+ if custom_prompt:
156
+ # Check if it's a file path
157
+ if Path(custom_prompt).exists():
158
+ prompt_template = Path(custom_prompt).read_text()
159
+ else:
160
+ prompt_template = custom_prompt
161
+ # Simple variable substitution
162
+ prompt = prompt_template.replace("{user_profile}", user_profile)
163
+ prompt = prompt.replace("{conversation_history}", conversation_history)
164
+ else:
165
+ prompt = SIMULATOR_PROMPT.format(
166
+ user_profile=user_profile,
167
+ conversation_history=conversation_history,
168
+ )
169
+
170
+ # Create simple agent for simulation
171
+ agent = Agent(
172
+ model=settings.llm.default_model,
173
+ system_prompt="You are a patient simulator. Generate realistic patient responses.",
174
+ )
175
+
176
+ result = await agent.run(prompt)
177
+ return result.output
178
+
179
+
180
+ async def _save_message(
181
+ user_id: str,
182
+ session_id: str | None,
183
+ content: str,
184
+ role: str = "user",
185
+ ) -> Message:
186
+ """Save a simulated message to the database."""
187
+ from uuid import uuid4
188
+
189
+ pg = get_postgres_service()
190
+ if not pg:
191
+ raise RuntimeError("PostgreSQL not available")
192
+
193
+ await pg.connect()
194
+
195
+ try:
196
+ message_repo = Repository(Message, "messages", db=pg)
197
+
198
+ message = Message(
199
+ id=uuid4(),
200
+ user_id=user_id,
201
+ tenant_id="default",
202
+ session_id=session_id or str(uuid4()),
203
+ content=content,
204
+ message_type=role,
205
+ )
206
+
207
+ await message_repo.upsert(message)
208
+ return message
209
+
210
+ finally:
211
+ await pg.disconnect()
212
+
213
+
214
+ @click.group()
215
+ def session():
216
+ """Session viewing and simulation commands."""
217
+ pass
218
+
219
+
220
+ @session.command("show")
221
+ @click.argument("user_id")
222
+ @click.option("--session-id", "-s", help="Filter by session ID")
223
+ @click.option(
224
+ "--role", "-r",
225
+ type=click.Choice(["user", "assistant", "system", "tool"]),
226
+ help="Filter messages by role",
227
+ )
228
+ @click.option("--limit", "-l", default=100, help="Max messages to load")
229
+ @click.option("--simulate-next", is_flag=True, help="Simulate the next patient message")
230
+ @click.option("--save", is_flag=True, help="Save simulated message to database")
231
+ @click.option(
232
+ "--custom-sim-prompt", "-p",
233
+ help="Custom simulation prompt (text or file path)",
234
+ )
235
+ def show(
236
+ user_id: str,
237
+ session_id: str | None,
238
+ role: str | None,
239
+ limit: int,
240
+ simulate_next: bool,
241
+ save: bool,
242
+ custom_sim_prompt: str | None,
243
+ ):
244
+ """
245
+ Show user profile and session messages.
246
+
247
+ USER_ID: The user identifier to load.
248
+
249
+ Examples:
250
+
251
+ # Show user and all messages
252
+ rem session show 11111111-1111-1111-1111-111111111001
253
+
254
+ # Show only assistant responses
255
+ rem session show 11111111-1111-1111-1111-111111111001 --role assistant
256
+
257
+ # Simulate next patient message
258
+ rem session show 11111111-1111-1111-1111-111111111001 --simulate-next
259
+
260
+ # Simulate and save to database
261
+ rem session show 11111111-1111-1111-1111-111111111001 --simulate-next --save
262
+ """
263
+ asyncio.run(_show_async(
264
+ user_id=user_id,
265
+ session_id=session_id,
266
+ role_filter=role,
267
+ limit=limit,
268
+ simulate_next=simulate_next,
269
+ save=save,
270
+ custom_sim_prompt=custom_sim_prompt,
271
+ ))
272
+
273
+
274
+ async def _show_async(
275
+ user_id: str,
276
+ session_id: str | None,
277
+ role_filter: str | None,
278
+ limit: int,
279
+ simulate_next: bool,
280
+ save: bool,
281
+ custom_sim_prompt: str | None,
282
+ ):
283
+ """Async implementation of show command."""
284
+ # Load data
285
+ user, messages = await _load_user_and_messages(
286
+ user_id=user_id,
287
+ session_id=session_id,
288
+ role_filter=role_filter if not simulate_next else None, # Need all messages for simulation
289
+ limit=limit,
290
+ )
291
+
292
+ # Display user profile
293
+ click.echo("\n# User Profile")
294
+ click.echo("---")
295
+ click.echo(_format_user_yaml(user))
296
+
297
+ # Display messages (apply filter for display if simulating)
298
+ display_messages = messages
299
+ if simulate_next and role_filter:
300
+ display_messages = [m for m in messages if m.message_type == role_filter]
301
+
302
+ click.echo("\n# Messages")
303
+ click.echo("---")
304
+ click.echo(_format_messages_yaml(display_messages))
305
+
306
+ # Simulate next message if requested
307
+ if simulate_next:
308
+ click.echo("\n# Simulated Next Message")
309
+ click.echo("---")
310
+
311
+ try:
312
+ simulated = await _simulate_next_message(
313
+ user=user,
314
+ messages=messages,
315
+ custom_prompt=custom_sim_prompt,
316
+ )
317
+ click.echo(f"role: user")
318
+ click.echo(f"content: |\n {simulated}")
319
+
320
+ if save:
321
+ saved_msg = await _save_message(
322
+ user_id=user_id,
323
+ session_id=session_id,
324
+ content=simulated,
325
+ role="user",
326
+ )
327
+ logger.success(f"Saved message: {saved_msg.id}")
328
+
329
+ except Exception as e:
330
+ logger.error(f"Simulation failed: {e}")
331
+ raise
332
+
333
+
334
+ def register_command(cli_group):
335
+ """Register the session command group."""
336
+ cli_group.add_command(session)
rem/cli/dreaming.py CHANGED
@@ -43,7 +43,7 @@ rem-dreaming full --user-id=user-123 --rem-api-url=http://localhost:8000
43
43
  Environment Variables:
44
44
  - REM_API_URL: REM API endpoint (default: http://rem-api:8000)
45
45
  - REM_EMBEDDING_PROVIDER: Embedding provider (default: text-embedding-3-small)
46
- - REM_DEFAULT_MODEL: LLM model (default: gpt-4o)
46
+ - REM_DEFAULT_MODEL: LLM model (default: gpt-4.1)
47
47
  - REM_LOOKBACK_HOURS: Default lookback window (default: 24)
48
48
  - OPENAI_API_KEY: OpenAI API key
49
49
 
@@ -83,7 +83,7 @@ def get_worker() -> DreamingWorker:
83
83
  embedding_provider=os.getenv(
84
84
  "REM_EMBEDDING_PROVIDER", "text-embedding-3-small"
85
85
  ),
86
- default_model=os.getenv("REM_DEFAULT_MODEL", "gpt-4o"),
86
+ default_model=os.getenv("REM_DEFAULT_MODEL", "gpt-4.1"),
87
87
  lookback_hours=int(os.getenv("REM_LOOKBACK_HOURS", "24")),
88
88
  )
89
89