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.
- rem/agentic/README.md +76 -0
- rem/agentic/__init__.py +15 -0
- rem/agentic/agents/__init__.py +32 -2
- rem/agentic/agents/agent_manager.py +310 -0
- rem/agentic/agents/sse_simulator.py +502 -0
- rem/agentic/context.py +51 -27
- rem/agentic/context_builder.py +5 -3
- rem/agentic/llm_provider_models.py +301 -0
- rem/agentic/mcp/tool_wrapper.py +155 -18
- rem/agentic/otel/setup.py +93 -4
- rem/agentic/providers/phoenix.py +371 -108
- rem/agentic/providers/pydantic_ai.py +280 -57
- rem/agentic/schema.py +361 -21
- rem/agentic/tools/rem_tools.py +3 -3
- rem/api/README.md +215 -1
- rem/api/deps.py +255 -0
- rem/api/main.py +132 -40
- rem/api/mcp_router/resources.py +1 -1
- rem/api/mcp_router/server.py +28 -5
- rem/api/mcp_router/tools.py +555 -7
- rem/api/routers/admin.py +494 -0
- rem/api/routers/auth.py +278 -4
- rem/api/routers/chat/completions.py +402 -20
- rem/api/routers/chat/models.py +88 -10
- rem/api/routers/chat/otel_utils.py +33 -0
- rem/api/routers/chat/sse_events.py +542 -0
- rem/api/routers/chat/streaming.py +697 -45
- rem/api/routers/dev.py +81 -0
- rem/api/routers/feedback.py +268 -0
- rem/api/routers/messages.py +473 -0
- rem/api/routers/models.py +78 -0
- rem/api/routers/query.py +360 -0
- rem/api/routers/shared_sessions.py +406 -0
- rem/auth/__init__.py +13 -3
- rem/auth/middleware.py +186 -22
- rem/auth/providers/__init__.py +4 -1
- rem/auth/providers/email.py +215 -0
- rem/cli/commands/README.md +237 -64
- rem/cli/commands/cluster.py +1808 -0
- rem/cli/commands/configure.py +4 -7
- rem/cli/commands/db.py +386 -143
- rem/cli/commands/experiments.py +468 -76
- rem/cli/commands/process.py +14 -8
- rem/cli/commands/schema.py +97 -50
- rem/cli/commands/session.py +336 -0
- rem/cli/dreaming.py +2 -2
- rem/cli/main.py +29 -6
- rem/config.py +10 -3
- rem/models/core/core_model.py +7 -1
- rem/models/core/experiment.py +58 -14
- rem/models/core/rem_query.py +5 -2
- rem/models/entities/__init__.py +25 -0
- rem/models/entities/domain_resource.py +38 -0
- rem/models/entities/feedback.py +123 -0
- rem/models/entities/message.py +30 -1
- rem/models/entities/ontology.py +1 -1
- rem/models/entities/ontology_config.py +1 -1
- rem/models/entities/session.py +83 -0
- rem/models/entities/shared_session.py +180 -0
- rem/models/entities/subscriber.py +175 -0
- rem/models/entities/user.py +1 -0
- rem/registry.py +10 -4
- rem/schemas/agents/core/agent-builder.yaml +134 -0
- rem/schemas/agents/examples/contract-analyzer.yaml +1 -1
- rem/schemas/agents/examples/contract-extractor.yaml +1 -1
- rem/schemas/agents/examples/cv-parser.yaml +1 -1
- rem/schemas/agents/rem.yaml +7 -3
- rem/services/__init__.py +3 -1
- rem/services/content/service.py +92 -19
- rem/services/email/__init__.py +10 -0
- rem/services/email/service.py +459 -0
- rem/services/email/templates.py +360 -0
- rem/services/embeddings/api.py +4 -4
- rem/services/embeddings/worker.py +16 -16
- rem/services/phoenix/client.py +154 -14
- rem/services/postgres/README.md +197 -15
- rem/services/postgres/__init__.py +2 -1
- rem/services/postgres/diff_service.py +547 -0
- rem/services/postgres/pydantic_to_sqlalchemy.py +470 -140
- rem/services/postgres/repository.py +132 -0
- rem/services/postgres/schema_generator.py +205 -4
- rem/services/postgres/service.py +6 -6
- rem/services/rem/parser.py +44 -9
- rem/services/rem/service.py +36 -2
- rem/services/session/compression.py +137 -51
- rem/services/session/reload.py +15 -8
- rem/settings.py +515 -27
- rem/sql/background_indexes.sql +21 -16
- rem/sql/migrations/001_install.sql +387 -54
- rem/sql/migrations/002_install_models.sql +2304 -377
- rem/sql/migrations/003_optional_extensions.sql +326 -0
- rem/sql/migrations/004_cache_system.sql +548 -0
- rem/sql/migrations/005_schema_update.sql +145 -0
- rem/utils/README.md +45 -0
- rem/utils/__init__.py +18 -0
- rem/utils/date_utils.py +2 -2
- rem/utils/files.py +157 -1
- rem/utils/model_helpers.py +156 -1
- rem/utils/schema_loader.py +220 -22
- rem/utils/sql_paths.py +146 -0
- rem/utils/sql_types.py +3 -1
- rem/utils/vision.py +1 -1
- rem/workers/__init__.py +3 -1
- rem/workers/db_listener.py +579 -0
- rem/workers/unlogged_maintainer.py +463 -0
- {remdb-0.3.14.dist-info → remdb-0.3.157.dist-info}/METADATA +340 -229
- {remdb-0.3.14.dist-info → remdb-0.3.157.dist-info}/RECORD +109 -80
- {remdb-0.3.14.dist-info → remdb-0.3.157.dist-info}/WHEEL +1 -1
- rem/sql/002_install_models.sql +0 -1068
- rem/sql/install_models.sql +0 -1051
- rem/sql/migrations/003_seed_default_user.sql +0 -48
- {remdb-0.3.14.dist-info → remdb-0.3.157.dist-info}/entry_points.txt +0 -0
rem/cli/commands/process.py
CHANGED
|
@@ -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",
|
|
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
|
|
36
|
-
rem process ingest contract.docx --
|
|
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
|
-
|
|
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
|
-
|
|
69
|
-
|
|
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)
|
rem/cli/commands/schema.py
CHANGED
|
@@ -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="
|
|
33
|
-
help="Output SQL file (default:
|
|
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=
|
|
59
|
+
help="Base output directory (default: package sql/migrations)",
|
|
40
60
|
)
|
|
41
|
-
def generate(
|
|
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
|
-
|
|
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/
|
|
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
|
|
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/
|
|
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,
|
|
61
|
-
rem db
|
|
100
|
+
After generation, verify with:
|
|
101
|
+
rem db diff
|
|
62
102
|
"""
|
|
63
|
-
|
|
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
|
-
#
|
|
66
|
-
actual_output_dir = output_dir or
|
|
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.
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
157
|
+
from ...registry import get_model_registry
|
|
114
158
|
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
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,
|
|
128
|
-
|
|
129
|
-
|
|
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=
|
|
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-
|
|
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-
|
|
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
|
|