remdb 0.3.242__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.
- rem/__init__.py +129 -0
- rem/agentic/README.md +760 -0
- rem/agentic/__init__.py +54 -0
- rem/agentic/agents/README.md +155 -0
- rem/agentic/agents/__init__.py +38 -0
- rem/agentic/agents/agent_manager.py +311 -0
- rem/agentic/agents/sse_simulator.py +502 -0
- rem/agentic/context.py +425 -0
- rem/agentic/context_builder.py +360 -0
- rem/agentic/llm_provider_models.py +301 -0
- rem/agentic/mcp/__init__.py +0 -0
- rem/agentic/mcp/tool_wrapper.py +273 -0
- rem/agentic/otel/__init__.py +5 -0
- rem/agentic/otel/setup.py +240 -0
- rem/agentic/providers/phoenix.py +926 -0
- rem/agentic/providers/pydantic_ai.py +854 -0
- rem/agentic/query.py +117 -0
- rem/agentic/query_helper.py +89 -0
- rem/agentic/schema.py +737 -0
- rem/agentic/serialization.py +245 -0
- rem/agentic/tools/__init__.py +5 -0
- rem/agentic/tools/rem_tools.py +242 -0
- rem/api/README.md +657 -0
- rem/api/deps.py +253 -0
- rem/api/main.py +460 -0
- rem/api/mcp_router/prompts.py +182 -0
- rem/api/mcp_router/resources.py +820 -0
- rem/api/mcp_router/server.py +243 -0
- rem/api/mcp_router/tools.py +1605 -0
- rem/api/middleware/tracking.py +172 -0
- rem/api/routers/admin.py +520 -0
- rem/api/routers/auth.py +898 -0
- rem/api/routers/chat/__init__.py +5 -0
- rem/api/routers/chat/child_streaming.py +394 -0
- rem/api/routers/chat/completions.py +702 -0
- rem/api/routers/chat/json_utils.py +76 -0
- rem/api/routers/chat/models.py +202 -0
- rem/api/routers/chat/otel_utils.py +33 -0
- rem/api/routers/chat/sse_events.py +546 -0
- rem/api/routers/chat/streaming.py +950 -0
- rem/api/routers/chat/streaming_utils.py +327 -0
- rem/api/routers/common.py +18 -0
- rem/api/routers/dev.py +87 -0
- rem/api/routers/feedback.py +276 -0
- rem/api/routers/messages.py +620 -0
- rem/api/routers/models.py +86 -0
- rem/api/routers/query.py +362 -0
- rem/api/routers/shared_sessions.py +422 -0
- rem/auth/README.md +258 -0
- rem/auth/__init__.py +36 -0
- rem/auth/jwt.py +367 -0
- rem/auth/middleware.py +318 -0
- rem/auth/providers/__init__.py +16 -0
- rem/auth/providers/base.py +376 -0
- rem/auth/providers/email.py +215 -0
- rem/auth/providers/google.py +163 -0
- rem/auth/providers/microsoft.py +237 -0
- rem/cli/README.md +517 -0
- rem/cli/__init__.py +8 -0
- rem/cli/commands/README.md +299 -0
- rem/cli/commands/__init__.py +3 -0
- rem/cli/commands/ask.py +549 -0
- rem/cli/commands/cluster.py +1808 -0
- rem/cli/commands/configure.py +495 -0
- rem/cli/commands/db.py +828 -0
- rem/cli/commands/dreaming.py +324 -0
- rem/cli/commands/experiments.py +1698 -0
- rem/cli/commands/mcp.py +66 -0
- rem/cli/commands/process.py +388 -0
- rem/cli/commands/query.py +109 -0
- rem/cli/commands/scaffold.py +47 -0
- rem/cli/commands/schema.py +230 -0
- rem/cli/commands/serve.py +106 -0
- rem/cli/commands/session.py +453 -0
- rem/cli/dreaming.py +363 -0
- rem/cli/main.py +123 -0
- rem/config.py +244 -0
- rem/mcp_server.py +41 -0
- rem/models/core/__init__.py +49 -0
- rem/models/core/core_model.py +70 -0
- rem/models/core/engram.py +333 -0
- rem/models/core/experiment.py +672 -0
- rem/models/core/inline_edge.py +132 -0
- rem/models/core/rem_query.py +246 -0
- rem/models/entities/__init__.py +68 -0
- rem/models/entities/domain_resource.py +38 -0
- rem/models/entities/feedback.py +123 -0
- rem/models/entities/file.py +57 -0
- rem/models/entities/image_resource.py +88 -0
- rem/models/entities/message.py +64 -0
- rem/models/entities/moment.py +123 -0
- rem/models/entities/ontology.py +181 -0
- rem/models/entities/ontology_config.py +131 -0
- rem/models/entities/resource.py +95 -0
- rem/models/entities/schema.py +87 -0
- rem/models/entities/session.py +84 -0
- rem/models/entities/shared_session.py +180 -0
- rem/models/entities/subscriber.py +175 -0
- rem/models/entities/user.py +93 -0
- rem/py.typed +0 -0
- rem/registry.py +373 -0
- rem/schemas/README.md +507 -0
- rem/schemas/__init__.py +6 -0
- rem/schemas/agents/README.md +92 -0
- rem/schemas/agents/core/agent-builder.yaml +235 -0
- rem/schemas/agents/core/moment-builder.yaml +178 -0
- rem/schemas/agents/core/rem-query-agent.yaml +226 -0
- rem/schemas/agents/core/resource-affinity-assessor.yaml +99 -0
- rem/schemas/agents/core/simple-assistant.yaml +19 -0
- rem/schemas/agents/core/user-profile-builder.yaml +163 -0
- rem/schemas/agents/examples/contract-analyzer.yaml +317 -0
- rem/schemas/agents/examples/contract-extractor.yaml +134 -0
- rem/schemas/agents/examples/cv-parser.yaml +263 -0
- rem/schemas/agents/examples/hello-world.yaml +37 -0
- rem/schemas/agents/examples/query.yaml +54 -0
- rem/schemas/agents/examples/simple.yaml +21 -0
- rem/schemas/agents/examples/test.yaml +29 -0
- rem/schemas/agents/rem.yaml +132 -0
- rem/schemas/evaluators/hello-world/default.yaml +77 -0
- rem/schemas/evaluators/rem/faithfulness.yaml +219 -0
- rem/schemas/evaluators/rem/lookup-correctness.yaml +182 -0
- rem/schemas/evaluators/rem/retrieval-precision.yaml +199 -0
- rem/schemas/evaluators/rem/retrieval-recall.yaml +211 -0
- rem/schemas/evaluators/rem/search-correctness.yaml +192 -0
- rem/services/__init__.py +18 -0
- rem/services/audio/INTEGRATION.md +308 -0
- rem/services/audio/README.md +376 -0
- rem/services/audio/__init__.py +15 -0
- rem/services/audio/chunker.py +354 -0
- rem/services/audio/transcriber.py +259 -0
- rem/services/content/README.md +1269 -0
- rem/services/content/__init__.py +5 -0
- rem/services/content/providers.py +760 -0
- rem/services/content/service.py +762 -0
- rem/services/dreaming/README.md +230 -0
- rem/services/dreaming/__init__.py +53 -0
- rem/services/dreaming/affinity_service.py +322 -0
- rem/services/dreaming/moment_service.py +251 -0
- rem/services/dreaming/ontology_service.py +54 -0
- rem/services/dreaming/user_model_service.py +297 -0
- rem/services/dreaming/utils.py +39 -0
- rem/services/email/__init__.py +10 -0
- rem/services/email/service.py +522 -0
- rem/services/email/templates.py +360 -0
- rem/services/embeddings/__init__.py +11 -0
- rem/services/embeddings/api.py +127 -0
- rem/services/embeddings/worker.py +435 -0
- rem/services/fs/README.md +662 -0
- rem/services/fs/__init__.py +62 -0
- rem/services/fs/examples.py +206 -0
- rem/services/fs/examples_paths.py +204 -0
- rem/services/fs/git_provider.py +935 -0
- rem/services/fs/local_provider.py +760 -0
- rem/services/fs/parsing-hooks-examples.md +172 -0
- rem/services/fs/paths.py +276 -0
- rem/services/fs/provider.py +460 -0
- rem/services/fs/s3_provider.py +1042 -0
- rem/services/fs/service.py +186 -0
- rem/services/git/README.md +1075 -0
- rem/services/git/__init__.py +17 -0
- rem/services/git/service.py +469 -0
- rem/services/phoenix/EXPERIMENT_DESIGN.md +1146 -0
- rem/services/phoenix/README.md +453 -0
- rem/services/phoenix/__init__.py +46 -0
- rem/services/phoenix/client.py +960 -0
- rem/services/phoenix/config.py +88 -0
- rem/services/phoenix/prompt_labels.py +477 -0
- rem/services/postgres/README.md +757 -0
- rem/services/postgres/__init__.py +49 -0
- rem/services/postgres/diff_service.py +599 -0
- rem/services/postgres/migration_service.py +427 -0
- rem/services/postgres/programmable_diff_service.py +635 -0
- rem/services/postgres/pydantic_to_sqlalchemy.py +562 -0
- rem/services/postgres/register_type.py +353 -0
- rem/services/postgres/repository.py +481 -0
- rem/services/postgres/schema_generator.py +661 -0
- rem/services/postgres/service.py +802 -0
- rem/services/postgres/sql_builder.py +355 -0
- rem/services/rate_limit.py +113 -0
- rem/services/rem/README.md +318 -0
- rem/services/rem/__init__.py +23 -0
- rem/services/rem/exceptions.py +71 -0
- rem/services/rem/executor.py +293 -0
- rem/services/rem/parser.py +180 -0
- rem/services/rem/queries.py +196 -0
- rem/services/rem/query.py +371 -0
- rem/services/rem/service.py +608 -0
- rem/services/session/README.md +374 -0
- rem/services/session/__init__.py +13 -0
- rem/services/session/compression.py +488 -0
- rem/services/session/pydantic_messages.py +310 -0
- rem/services/session/reload.py +85 -0
- rem/services/user_service.py +130 -0
- rem/settings.py +1877 -0
- rem/sql/background_indexes.sql +52 -0
- rem/sql/migrations/001_install.sql +983 -0
- rem/sql/migrations/002_install_models.sql +3157 -0
- rem/sql/migrations/003_optional_extensions.sql +326 -0
- rem/sql/migrations/004_cache_system.sql +282 -0
- rem/sql/migrations/005_schema_update.sql +145 -0
- rem/sql/migrations/migrate_session_id_to_uuid.sql +45 -0
- rem/utils/AGENTIC_CHUNKING.md +597 -0
- rem/utils/README.md +628 -0
- rem/utils/__init__.py +61 -0
- rem/utils/agentic_chunking.py +622 -0
- rem/utils/batch_ops.py +343 -0
- rem/utils/chunking.py +108 -0
- rem/utils/clip_embeddings.py +276 -0
- rem/utils/constants.py +97 -0
- rem/utils/date_utils.py +228 -0
- rem/utils/dict_utils.py +98 -0
- rem/utils/embeddings.py +436 -0
- rem/utils/examples/embeddings_example.py +305 -0
- rem/utils/examples/sql_types_example.py +202 -0
- rem/utils/files.py +323 -0
- rem/utils/markdown.py +16 -0
- rem/utils/mime_types.py +158 -0
- rem/utils/model_helpers.py +492 -0
- rem/utils/schema_loader.py +649 -0
- rem/utils/sql_paths.py +146 -0
- rem/utils/sql_types.py +350 -0
- rem/utils/user_id.py +81 -0
- rem/utils/vision.py +325 -0
- rem/workers/README.md +506 -0
- rem/workers/__init__.py +7 -0
- rem/workers/db_listener.py +579 -0
- rem/workers/db_maintainer.py +74 -0
- rem/workers/dreaming.py +502 -0
- rem/workers/engram_processor.py +312 -0
- rem/workers/sqs_file_processor.py +193 -0
- rem/workers/unlogged_maintainer.py +463 -0
- remdb-0.3.242.dist-info/METADATA +1632 -0
- remdb-0.3.242.dist-info/RECORD +235 -0
- remdb-0.3.242.dist-info/WHEEL +4 -0
- remdb-0.3.242.dist-info/entry_points.txt +2 -0
rem/cli/commands/ask.py
ADDED
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI command for testing Pydantic AI agents.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
rem ask query-agent "Find all documents by Sarah" --model anthropic:claude-sonnet-4-5-20250929
|
|
6
|
+
rem ask schemas/query-agent.yaml "What is the weather?" --temperature 0.7 --max-turns 5
|
|
7
|
+
rem ask my-agent "Hello" --stream --version 1.2.0
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import json
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
import click
|
|
17
|
+
from loguru import logger
|
|
18
|
+
|
|
19
|
+
from ...agentic.context import AgentContext
|
|
20
|
+
from ...agentic.providers.pydantic_ai import create_agent
|
|
21
|
+
from ...agentic.query import AgentQuery
|
|
22
|
+
from ...settings import settings
|
|
23
|
+
from ...utils.schema_loader import load_agent_schema
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
async def load_schema_from_registry(
|
|
27
|
+
name: str, version: str | None = None
|
|
28
|
+
) -> dict[str, Any]:
|
|
29
|
+
"""
|
|
30
|
+
Load agent schema from registry (database or cache).
|
|
31
|
+
|
|
32
|
+
TODO: Implement schema registry with:
|
|
33
|
+
- Database table: agent_schemas (name, version, schema_json, created_at)
|
|
34
|
+
- Cache layer: Redis/in-memory for fast lookups
|
|
35
|
+
- Versioning: semantic versioning with latest fallback
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
name: Schema name (e.g., "query-agent", "rem-agents-query-agent")
|
|
39
|
+
version: Optional version (e.g., "1.2.0", defaults to latest)
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Agent schema as dictionary
|
|
43
|
+
|
|
44
|
+
Example:
|
|
45
|
+
schema = await load_schema_from_registry("query-agent", version="1.0.0")
|
|
46
|
+
"""
|
|
47
|
+
# TODO: Implement database/cache lookup
|
|
48
|
+
# from ...db import get_db_pool
|
|
49
|
+
# async with get_db_pool() as pool:
|
|
50
|
+
# if version:
|
|
51
|
+
# query = "SELECT schema_json FROM agent_schemas WHERE name = $1 AND version = $2"
|
|
52
|
+
# row = await pool.fetchrow(query, name, version)
|
|
53
|
+
# else:
|
|
54
|
+
# query = "SELECT schema_json FROM agent_schemas WHERE name = $1 ORDER BY created_at DESC LIMIT 1"
|
|
55
|
+
# row = await pool.fetchrow(query, name)
|
|
56
|
+
#
|
|
57
|
+
# if not row:
|
|
58
|
+
# raise ValueError(f"Schema not found: {name} (version: {version or 'latest'})")
|
|
59
|
+
#
|
|
60
|
+
# return json.loads(row["schema_json"])
|
|
61
|
+
|
|
62
|
+
raise NotImplementedError(
|
|
63
|
+
f"Schema registry not implemented yet. Please use a file path instead.\n"
|
|
64
|
+
f"Attempted to load: {name} (version: {version or 'latest'})"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
async def run_agent_streaming(
|
|
69
|
+
agent,
|
|
70
|
+
prompt: str,
|
|
71
|
+
max_turns: int = 10,
|
|
72
|
+
context: AgentContext | None = None,
|
|
73
|
+
max_iterations: int | None = None,
|
|
74
|
+
user_message: str | None = None,
|
|
75
|
+
) -> None:
|
|
76
|
+
"""
|
|
77
|
+
Run agent in streaming mode using the SAME code path as the API.
|
|
78
|
+
|
|
79
|
+
This uses stream_openai_response_with_save from the API to ensure:
|
|
80
|
+
1. Tool calls are saved as separate "tool" messages (not embedded in content)
|
|
81
|
+
2. Assistant response is clean text only (no [Calling: ...] markers)
|
|
82
|
+
3. CLI testing is equivalent to API testing
|
|
83
|
+
|
|
84
|
+
The CLI displays tool calls as [Calling: tool_name] for visibility,
|
|
85
|
+
but these are NOT saved to the database.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
agent: Pydantic AI agent
|
|
89
|
+
prompt: Complete prompt (includes system context + history + query)
|
|
90
|
+
max_turns: Maximum turns for agent execution (not used in current API)
|
|
91
|
+
context: Optional AgentContext for session persistence
|
|
92
|
+
max_iterations: Maximum iterations/requests (from agent schema or settings)
|
|
93
|
+
user_message: The user's original message (for database storage)
|
|
94
|
+
"""
|
|
95
|
+
import json
|
|
96
|
+
from rem.api.routers.chat.streaming import stream_openai_response_with_save, save_user_message
|
|
97
|
+
|
|
98
|
+
logger.info("Running agent in streaming mode...")
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
# Save user message BEFORE streaming (same as API, using shared utility)
|
|
102
|
+
if context and context.session_id and user_message:
|
|
103
|
+
await save_user_message(
|
|
104
|
+
session_id=context.session_id,
|
|
105
|
+
user_id=context.user_id,
|
|
106
|
+
content=user_message,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# Use the API streaming code path for consistency
|
|
110
|
+
# This properly handles tool calls and message persistence
|
|
111
|
+
model_name = getattr(agent, 'model', 'unknown')
|
|
112
|
+
if hasattr(model_name, 'model_name'):
|
|
113
|
+
model_name = model_name.model_name
|
|
114
|
+
elif hasattr(model_name, 'name'):
|
|
115
|
+
model_name = model_name.name
|
|
116
|
+
else:
|
|
117
|
+
model_name = str(model_name)
|
|
118
|
+
|
|
119
|
+
async for chunk in stream_openai_response_with_save(
|
|
120
|
+
agent=agent.agent if hasattr(agent, 'agent') else agent,
|
|
121
|
+
prompt=prompt,
|
|
122
|
+
model=model_name,
|
|
123
|
+
session_id=context.session_id if context else None,
|
|
124
|
+
user_id=context.user_id if context else None,
|
|
125
|
+
agent_context=context,
|
|
126
|
+
):
|
|
127
|
+
# Parse SSE chunks for CLI display
|
|
128
|
+
if chunk.startswith("event: tool_call"):
|
|
129
|
+
# Extract tool call info from next data line
|
|
130
|
+
continue
|
|
131
|
+
elif chunk.startswith("data: ") and not chunk.startswith("data: [DONE]"):
|
|
132
|
+
try:
|
|
133
|
+
data_str = chunk[6:].strip()
|
|
134
|
+
if data_str:
|
|
135
|
+
data = json.loads(data_str)
|
|
136
|
+
# Check for tool_call event
|
|
137
|
+
if data.get("type") == "tool_call":
|
|
138
|
+
tool_name = data.get("tool_name", "tool")
|
|
139
|
+
status = data.get("status", "")
|
|
140
|
+
if status == "started":
|
|
141
|
+
print(f"\n[Calling: {tool_name}]", flush=True)
|
|
142
|
+
# Check for text content (OpenAI format)
|
|
143
|
+
elif "choices" in data and data["choices"]:
|
|
144
|
+
delta = data["choices"][0].get("delta", {})
|
|
145
|
+
content = delta.get("content")
|
|
146
|
+
if content:
|
|
147
|
+
print(content, end="", flush=True)
|
|
148
|
+
except (json.JSONDecodeError, KeyError, IndexError):
|
|
149
|
+
pass
|
|
150
|
+
|
|
151
|
+
print("\n") # Final newline after streaming
|
|
152
|
+
logger.info("Final structured result:")
|
|
153
|
+
|
|
154
|
+
except Exception as e:
|
|
155
|
+
logger.error(f"Agent execution failed: {e}")
|
|
156
|
+
raise
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
async def run_agent_non_streaming(
|
|
160
|
+
agent,
|
|
161
|
+
prompt: str,
|
|
162
|
+
max_turns: int = 10,
|
|
163
|
+
output_file: Path | None = None,
|
|
164
|
+
context: AgentContext | None = None,
|
|
165
|
+
plan: bool = False,
|
|
166
|
+
max_iterations: int | None = None,
|
|
167
|
+
) -> dict[str, Any] | None:
|
|
168
|
+
"""
|
|
169
|
+
Run agent in non-streaming mode using agent.run() with usage limits.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
agent: Pydantic AI agent
|
|
173
|
+
prompt: Complete prompt (includes system context + history + query)
|
|
174
|
+
max_turns: Maximum turns for agent execution (not used in current API)
|
|
175
|
+
output_file: Optional path to save output
|
|
176
|
+
context: Optional AgentContext for session persistence
|
|
177
|
+
plan: If True, output only the generated query (for query-agent)
|
|
178
|
+
max_iterations: Maximum iterations/requests (from agent schema or settings)
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
Output data if successful, None otherwise
|
|
182
|
+
"""
|
|
183
|
+
from pydantic_ai import UsageLimits
|
|
184
|
+
from rem.utils.date_utils import to_iso_with_z, utc_now
|
|
185
|
+
|
|
186
|
+
logger.info("Running agent in non-streaming mode...")
|
|
187
|
+
|
|
188
|
+
try:
|
|
189
|
+
# Run agent and get complete result with usage limits
|
|
190
|
+
usage_limits = UsageLimits(request_limit=max_iterations) if max_iterations else None
|
|
191
|
+
result = await agent.run(prompt, usage_limits=usage_limits)
|
|
192
|
+
|
|
193
|
+
# Extract output data
|
|
194
|
+
output_data = None
|
|
195
|
+
assistant_content = None
|
|
196
|
+
if hasattr(result, "output"):
|
|
197
|
+
output = result.output
|
|
198
|
+
from rem.agentic.serialization import serialize_agent_result
|
|
199
|
+
output_data = serialize_agent_result(output)
|
|
200
|
+
|
|
201
|
+
if plan and isinstance(output_data, dict) and "query" in output_data:
|
|
202
|
+
# Plan mode: Output only the query
|
|
203
|
+
# Use sql formatting if possible or just raw string
|
|
204
|
+
assistant_content = output_data["query"]
|
|
205
|
+
print(assistant_content)
|
|
206
|
+
else:
|
|
207
|
+
# Normal mode
|
|
208
|
+
assistant_content = json.dumps(output_data, indent=2)
|
|
209
|
+
print(assistant_content)
|
|
210
|
+
else:
|
|
211
|
+
# Fallback for text-only results
|
|
212
|
+
assistant_content = str(result)
|
|
213
|
+
print(assistant_content)
|
|
214
|
+
|
|
215
|
+
# Save to file if requested
|
|
216
|
+
if output_file and output_data:
|
|
217
|
+
await _save_output_file(output_file, output_data)
|
|
218
|
+
|
|
219
|
+
# Save session messages (if session_id provided and postgres enabled)
|
|
220
|
+
if context and context.session_id and settings.postgres.enabled:
|
|
221
|
+
from ...services.session.compression import SessionMessageStore
|
|
222
|
+
|
|
223
|
+
# Extract just the user query from prompt
|
|
224
|
+
# Prompt format from ContextBuilder: system + history + user message
|
|
225
|
+
# We need to extract the last user message
|
|
226
|
+
user_message_content = prompt.split("\n\n")[-1] if "\n\n" in prompt else prompt
|
|
227
|
+
|
|
228
|
+
user_message = {
|
|
229
|
+
"role": "user",
|
|
230
|
+
"content": user_message_content,
|
|
231
|
+
"timestamp": to_iso_with_z(utc_now()),
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
assistant_message = {
|
|
235
|
+
"role": "assistant",
|
|
236
|
+
"content": assistant_content,
|
|
237
|
+
"timestamp": to_iso_with_z(utc_now()),
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
# Store messages with compression
|
|
241
|
+
store = SessionMessageStore(user_id=context.user_id or settings.test.effective_user_id)
|
|
242
|
+
await store.store_session_messages(
|
|
243
|
+
session_id=context.session_id,
|
|
244
|
+
messages=[user_message, assistant_message],
|
|
245
|
+
user_id=context.user_id,
|
|
246
|
+
compress=True,
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
logger.debug(f"Saved conversation to session {context.session_id}")
|
|
250
|
+
|
|
251
|
+
return output_data
|
|
252
|
+
|
|
253
|
+
except Exception as e:
|
|
254
|
+
logger.error(f"Agent execution failed: {e}")
|
|
255
|
+
raise
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
async def _load_input_file(
|
|
259
|
+
file_path: Path, user_id: str | None = None
|
|
260
|
+
) -> str:
|
|
261
|
+
"""
|
|
262
|
+
Load content from input file using ContentService.
|
|
263
|
+
|
|
264
|
+
Simple parse operation - just extracts content without creating Resources.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
file_path: Path to input file
|
|
268
|
+
user_id: Optional user ID (not used for simple parse)
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
Parsed file content as string (markdown format)
|
|
272
|
+
"""
|
|
273
|
+
from ...services.content import ContentService
|
|
274
|
+
|
|
275
|
+
# Create ContentService instance
|
|
276
|
+
content_service = ContentService()
|
|
277
|
+
|
|
278
|
+
# Parse file (read-only, no database writes)
|
|
279
|
+
logger.info(f"Parsing file: {file_path}")
|
|
280
|
+
result = content_service.process_uri(str(file_path))
|
|
281
|
+
content = result["content"]
|
|
282
|
+
|
|
283
|
+
logger.info(
|
|
284
|
+
f"Loaded {len(content)} characters from {file_path.suffix} file using {result['provider']}"
|
|
285
|
+
)
|
|
286
|
+
return content
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
async def _save_output_file(file_path: Path, data: dict[str, Any]) -> None:
|
|
290
|
+
"""
|
|
291
|
+
Save output data to file in YAML format.
|
|
292
|
+
|
|
293
|
+
Args:
|
|
294
|
+
file_path: Path to output file
|
|
295
|
+
data: Data to save
|
|
296
|
+
"""
|
|
297
|
+
import yaml
|
|
298
|
+
|
|
299
|
+
with open(file_path, "w", encoding="utf-8") as f:
|
|
300
|
+
yaml.safe_dump(data, f, default_flow_style=False, allow_unicode=True, sort_keys=False)
|
|
301
|
+
|
|
302
|
+
logger.success(f"Output saved to: {file_path}")
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
@click.command()
|
|
306
|
+
@click.argument("name_or_query")
|
|
307
|
+
@click.argument("query", required=False)
|
|
308
|
+
@click.option(
|
|
309
|
+
"--model",
|
|
310
|
+
"-m",
|
|
311
|
+
default=None,
|
|
312
|
+
help=f"LLM model (default: {settings.llm.default_model})",
|
|
313
|
+
)
|
|
314
|
+
@click.option(
|
|
315
|
+
"--temperature",
|
|
316
|
+
"-t",
|
|
317
|
+
type=float,
|
|
318
|
+
default=None,
|
|
319
|
+
help=f"Temperature for generation (default: {settings.llm.default_temperature})",
|
|
320
|
+
)
|
|
321
|
+
@click.option(
|
|
322
|
+
"--max-turns",
|
|
323
|
+
type=int,
|
|
324
|
+
default=10,
|
|
325
|
+
help="Maximum turns for agent execution (default: 10)",
|
|
326
|
+
)
|
|
327
|
+
@click.option(
|
|
328
|
+
"--version",
|
|
329
|
+
"-v",
|
|
330
|
+
default=None,
|
|
331
|
+
help="Schema version (for registry lookup, defaults to latest)",
|
|
332
|
+
)
|
|
333
|
+
@click.option(
|
|
334
|
+
"--stream/--no-stream",
|
|
335
|
+
default=False,
|
|
336
|
+
help="Enable streaming mode (default: disabled)",
|
|
337
|
+
)
|
|
338
|
+
@click.option(
|
|
339
|
+
"--user-id",
|
|
340
|
+
default=None,
|
|
341
|
+
help="User ID for context (default: from settings.test.effective_user_id)",
|
|
342
|
+
)
|
|
343
|
+
@click.option(
|
|
344
|
+
"--session-id",
|
|
345
|
+
default=None,
|
|
346
|
+
help="Session ID for context (default: auto-generated)",
|
|
347
|
+
)
|
|
348
|
+
@click.option(
|
|
349
|
+
"--input-file",
|
|
350
|
+
"-i",
|
|
351
|
+
type=click.Path(exists=True, path_type=Path),
|
|
352
|
+
default=None,
|
|
353
|
+
help="Read input from file instead of QUERY argument (supports PDF, TXT, Markdown)",
|
|
354
|
+
)
|
|
355
|
+
@click.option(
|
|
356
|
+
"--output-file",
|
|
357
|
+
"-o",
|
|
358
|
+
type=click.Path(path_type=Path),
|
|
359
|
+
default=None,
|
|
360
|
+
help="Write output to file (YAML format)",
|
|
361
|
+
)
|
|
362
|
+
@click.option(
|
|
363
|
+
"--plan",
|
|
364
|
+
is_flag=True,
|
|
365
|
+
default=False,
|
|
366
|
+
help="Output only the generated plan/query (useful for query-agent)",
|
|
367
|
+
)
|
|
368
|
+
def ask(
|
|
369
|
+
name_or_query: str,
|
|
370
|
+
query: str | None,
|
|
371
|
+
model: str | None,
|
|
372
|
+
temperature: float | None,
|
|
373
|
+
max_turns: int,
|
|
374
|
+
version: str | None,
|
|
375
|
+
stream: bool,
|
|
376
|
+
user_id: str | None,
|
|
377
|
+
session_id: str | None,
|
|
378
|
+
input_file: Path | None,
|
|
379
|
+
output_file: Path | None,
|
|
380
|
+
plan: bool,
|
|
381
|
+
):
|
|
382
|
+
"""
|
|
383
|
+
Run an agent with a query or file input.
|
|
384
|
+
|
|
385
|
+
Arguments:
|
|
386
|
+
NAME_OR_QUERY: Agent schema name OR query string.
|
|
387
|
+
QUERY: Query string (if first arg is agent name).
|
|
388
|
+
|
|
389
|
+
Examples:
|
|
390
|
+
# Simple query (uses default 'rem' agent)
|
|
391
|
+
rem ask "What documents did I upload?"
|
|
392
|
+
|
|
393
|
+
# Explicit agent
|
|
394
|
+
rem ask contract-analyzer "Analyze this contract"
|
|
395
|
+
|
|
396
|
+
# Process file
|
|
397
|
+
rem ask contract-analyzer -i contract.pdf -o output.yaml
|
|
398
|
+
"""
|
|
399
|
+
# Smart argument handling
|
|
400
|
+
name = "rem" # Default agent
|
|
401
|
+
|
|
402
|
+
if query is None and not input_file:
|
|
403
|
+
# Single argument provided
|
|
404
|
+
# Heuristic: If it looks like a schema file or known agent, treat as name
|
|
405
|
+
# Otherwise treat as query
|
|
406
|
+
if name_or_query.endswith((".yaml", ".yml", ".json")) or name_or_query in ["rem", "query-agent", "rem-query-agent"]:
|
|
407
|
+
# It's an agent name, query is missing (unless input_file)
|
|
408
|
+
name = name_or_query
|
|
409
|
+
# Query remains None, _ask_async will check input_file
|
|
410
|
+
else:
|
|
411
|
+
# It's a query, use default agent
|
|
412
|
+
query = name_or_query
|
|
413
|
+
elif query is not None:
|
|
414
|
+
# Two arguments provided
|
|
415
|
+
name = name_or_query
|
|
416
|
+
|
|
417
|
+
# Resolve user_id from settings if not provided
|
|
418
|
+
effective_user_id = user_id or settings.test.effective_user_id
|
|
419
|
+
|
|
420
|
+
asyncio.run(
|
|
421
|
+
_ask_async(
|
|
422
|
+
name=name,
|
|
423
|
+
query=query,
|
|
424
|
+
model=model,
|
|
425
|
+
temperature=temperature,
|
|
426
|
+
max_turns=max_turns,
|
|
427
|
+
version=version,
|
|
428
|
+
stream=stream,
|
|
429
|
+
user_id=effective_user_id,
|
|
430
|
+
session_id=session_id,
|
|
431
|
+
input_file=input_file,
|
|
432
|
+
output_file=output_file,
|
|
433
|
+
plan=plan,
|
|
434
|
+
)
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
async def _ask_async(
|
|
439
|
+
name: str,
|
|
440
|
+
query: str | None,
|
|
441
|
+
model: str | None,
|
|
442
|
+
temperature: float | None,
|
|
443
|
+
max_turns: int,
|
|
444
|
+
version: str | None,
|
|
445
|
+
stream: bool,
|
|
446
|
+
user_id: str,
|
|
447
|
+
session_id: str | None,
|
|
448
|
+
input_file: Path | None,
|
|
449
|
+
output_file: Path | None,
|
|
450
|
+
plan: bool,
|
|
451
|
+
):
|
|
452
|
+
"""Async implementation of ask command."""
|
|
453
|
+
import uuid
|
|
454
|
+
from ...agentic.context_builder import ContextBuilder
|
|
455
|
+
|
|
456
|
+
# Validate input arguments
|
|
457
|
+
if not query and not input_file:
|
|
458
|
+
logger.error("Either QUERY argument or --input-file must be provided")
|
|
459
|
+
sys.exit(1)
|
|
460
|
+
|
|
461
|
+
if query and input_file:
|
|
462
|
+
logger.error("Cannot use both QUERY argument and --input-file")
|
|
463
|
+
sys.exit(1)
|
|
464
|
+
|
|
465
|
+
# Load input from file if specified
|
|
466
|
+
if input_file:
|
|
467
|
+
logger.info(f"Loading input from file: {input_file}")
|
|
468
|
+
query = await _load_input_file(input_file, user_id=user_id)
|
|
469
|
+
|
|
470
|
+
# Load schema using centralized utility
|
|
471
|
+
# Handles both file paths and schema names automatically
|
|
472
|
+
# Falls back to database LOOKUP if not found in filesystem
|
|
473
|
+
logger.info(f"Loading schema: {name} (version: {version or 'latest'})")
|
|
474
|
+
try:
|
|
475
|
+
schema = load_agent_schema(name, user_id=user_id)
|
|
476
|
+
except FileNotFoundError as e:
|
|
477
|
+
logger.error(str(e))
|
|
478
|
+
sys.exit(1)
|
|
479
|
+
|
|
480
|
+
# Generate session ID if not provided
|
|
481
|
+
if not session_id:
|
|
482
|
+
session_id = str(uuid.uuid4())
|
|
483
|
+
logger.info(f"Generated session ID: {session_id}")
|
|
484
|
+
|
|
485
|
+
# Build context with session history using ContextBuilder
|
|
486
|
+
# This provides:
|
|
487
|
+
# - System context message with date and user profile hints
|
|
488
|
+
# - Compressed session history (if session exists)
|
|
489
|
+
# - Proper message structure for agent
|
|
490
|
+
logger.info(f"Building context for user {user_id}, session {session_id}")
|
|
491
|
+
|
|
492
|
+
# Prepare new message for ContextBuilder
|
|
493
|
+
new_messages = [{"role": "user", "content": query}]
|
|
494
|
+
|
|
495
|
+
# Build context with session history
|
|
496
|
+
context, messages = await ContextBuilder.build_from_headers(
|
|
497
|
+
headers={
|
|
498
|
+
"X-User-Id": user_id,
|
|
499
|
+
"X-Session-Id": session_id,
|
|
500
|
+
},
|
|
501
|
+
new_messages=new_messages,
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
# Override model if specified via CLI flag
|
|
505
|
+
if model:
|
|
506
|
+
context.default_model = model
|
|
507
|
+
|
|
508
|
+
logger.info(
|
|
509
|
+
f"Creating agent: model={context.default_model}, stream={stream}, max_turns={max_turns}, messages={len(messages)}"
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
# Create agent
|
|
513
|
+
agent = await create_agent(
|
|
514
|
+
context=context,
|
|
515
|
+
agent_schema_override=schema,
|
|
516
|
+
model_override=model,
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
# Temperature is now handled in agent factory (schema override or settings default)
|
|
520
|
+
if temperature is not None:
|
|
521
|
+
logger.warning(
|
|
522
|
+
f"CLI temperature override ({temperature}) not yet supported. "
|
|
523
|
+
"Use agent schema 'override_temperature' field or LLM__DEFAULT_TEMPERATURE setting."
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
# Combine messages into single prompt
|
|
527
|
+
# ContextBuilder already assembled: system context + history + new message
|
|
528
|
+
prompt = "\n\n".join(msg.content for msg in messages)
|
|
529
|
+
|
|
530
|
+
# Run agent with session persistence
|
|
531
|
+
if stream:
|
|
532
|
+
await run_agent_streaming(agent, prompt, max_turns=max_turns, context=context, user_message=query)
|
|
533
|
+
else:
|
|
534
|
+
await run_agent_non_streaming(
|
|
535
|
+
agent,
|
|
536
|
+
prompt,
|
|
537
|
+
max_turns=max_turns,
|
|
538
|
+
output_file=output_file,
|
|
539
|
+
context=context,
|
|
540
|
+
plan=plan,
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
# Log session ID for reuse
|
|
544
|
+
logger.success(f"Session ID: {session_id} (use --session-id to continue this conversation)")
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
def register_command(parent_group):
|
|
548
|
+
"""Register ask command with parent CLI group."""
|
|
549
|
+
parent_group.add_command(ask)
|