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.

Files changed (235) hide show
  1. rem/__init__.py +129 -0
  2. rem/agentic/README.md +760 -0
  3. rem/agentic/__init__.py +54 -0
  4. rem/agentic/agents/README.md +155 -0
  5. rem/agentic/agents/__init__.py +38 -0
  6. rem/agentic/agents/agent_manager.py +311 -0
  7. rem/agentic/agents/sse_simulator.py +502 -0
  8. rem/agentic/context.py +425 -0
  9. rem/agentic/context_builder.py +360 -0
  10. rem/agentic/llm_provider_models.py +301 -0
  11. rem/agentic/mcp/__init__.py +0 -0
  12. rem/agentic/mcp/tool_wrapper.py +273 -0
  13. rem/agentic/otel/__init__.py +5 -0
  14. rem/agentic/otel/setup.py +240 -0
  15. rem/agentic/providers/phoenix.py +926 -0
  16. rem/agentic/providers/pydantic_ai.py +854 -0
  17. rem/agentic/query.py +117 -0
  18. rem/agentic/query_helper.py +89 -0
  19. rem/agentic/schema.py +737 -0
  20. rem/agentic/serialization.py +245 -0
  21. rem/agentic/tools/__init__.py +5 -0
  22. rem/agentic/tools/rem_tools.py +242 -0
  23. rem/api/README.md +657 -0
  24. rem/api/deps.py +253 -0
  25. rem/api/main.py +460 -0
  26. rem/api/mcp_router/prompts.py +182 -0
  27. rem/api/mcp_router/resources.py +820 -0
  28. rem/api/mcp_router/server.py +243 -0
  29. rem/api/mcp_router/tools.py +1605 -0
  30. rem/api/middleware/tracking.py +172 -0
  31. rem/api/routers/admin.py +520 -0
  32. rem/api/routers/auth.py +898 -0
  33. rem/api/routers/chat/__init__.py +5 -0
  34. rem/api/routers/chat/child_streaming.py +394 -0
  35. rem/api/routers/chat/completions.py +702 -0
  36. rem/api/routers/chat/json_utils.py +76 -0
  37. rem/api/routers/chat/models.py +202 -0
  38. rem/api/routers/chat/otel_utils.py +33 -0
  39. rem/api/routers/chat/sse_events.py +546 -0
  40. rem/api/routers/chat/streaming.py +950 -0
  41. rem/api/routers/chat/streaming_utils.py +327 -0
  42. rem/api/routers/common.py +18 -0
  43. rem/api/routers/dev.py +87 -0
  44. rem/api/routers/feedback.py +276 -0
  45. rem/api/routers/messages.py +620 -0
  46. rem/api/routers/models.py +86 -0
  47. rem/api/routers/query.py +362 -0
  48. rem/api/routers/shared_sessions.py +422 -0
  49. rem/auth/README.md +258 -0
  50. rem/auth/__init__.py +36 -0
  51. rem/auth/jwt.py +367 -0
  52. rem/auth/middleware.py +318 -0
  53. rem/auth/providers/__init__.py +16 -0
  54. rem/auth/providers/base.py +376 -0
  55. rem/auth/providers/email.py +215 -0
  56. rem/auth/providers/google.py +163 -0
  57. rem/auth/providers/microsoft.py +237 -0
  58. rem/cli/README.md +517 -0
  59. rem/cli/__init__.py +8 -0
  60. rem/cli/commands/README.md +299 -0
  61. rem/cli/commands/__init__.py +3 -0
  62. rem/cli/commands/ask.py +549 -0
  63. rem/cli/commands/cluster.py +1808 -0
  64. rem/cli/commands/configure.py +495 -0
  65. rem/cli/commands/db.py +828 -0
  66. rem/cli/commands/dreaming.py +324 -0
  67. rem/cli/commands/experiments.py +1698 -0
  68. rem/cli/commands/mcp.py +66 -0
  69. rem/cli/commands/process.py +388 -0
  70. rem/cli/commands/query.py +109 -0
  71. rem/cli/commands/scaffold.py +47 -0
  72. rem/cli/commands/schema.py +230 -0
  73. rem/cli/commands/serve.py +106 -0
  74. rem/cli/commands/session.py +453 -0
  75. rem/cli/dreaming.py +363 -0
  76. rem/cli/main.py +123 -0
  77. rem/config.py +244 -0
  78. rem/mcp_server.py +41 -0
  79. rem/models/core/__init__.py +49 -0
  80. rem/models/core/core_model.py +70 -0
  81. rem/models/core/engram.py +333 -0
  82. rem/models/core/experiment.py +672 -0
  83. rem/models/core/inline_edge.py +132 -0
  84. rem/models/core/rem_query.py +246 -0
  85. rem/models/entities/__init__.py +68 -0
  86. rem/models/entities/domain_resource.py +38 -0
  87. rem/models/entities/feedback.py +123 -0
  88. rem/models/entities/file.py +57 -0
  89. rem/models/entities/image_resource.py +88 -0
  90. rem/models/entities/message.py +64 -0
  91. rem/models/entities/moment.py +123 -0
  92. rem/models/entities/ontology.py +181 -0
  93. rem/models/entities/ontology_config.py +131 -0
  94. rem/models/entities/resource.py +95 -0
  95. rem/models/entities/schema.py +87 -0
  96. rem/models/entities/session.py +84 -0
  97. rem/models/entities/shared_session.py +180 -0
  98. rem/models/entities/subscriber.py +175 -0
  99. rem/models/entities/user.py +93 -0
  100. rem/py.typed +0 -0
  101. rem/registry.py +373 -0
  102. rem/schemas/README.md +507 -0
  103. rem/schemas/__init__.py +6 -0
  104. rem/schemas/agents/README.md +92 -0
  105. rem/schemas/agents/core/agent-builder.yaml +235 -0
  106. rem/schemas/agents/core/moment-builder.yaml +178 -0
  107. rem/schemas/agents/core/rem-query-agent.yaml +226 -0
  108. rem/schemas/agents/core/resource-affinity-assessor.yaml +99 -0
  109. rem/schemas/agents/core/simple-assistant.yaml +19 -0
  110. rem/schemas/agents/core/user-profile-builder.yaml +163 -0
  111. rem/schemas/agents/examples/contract-analyzer.yaml +317 -0
  112. rem/schemas/agents/examples/contract-extractor.yaml +134 -0
  113. rem/schemas/agents/examples/cv-parser.yaml +263 -0
  114. rem/schemas/agents/examples/hello-world.yaml +37 -0
  115. rem/schemas/agents/examples/query.yaml +54 -0
  116. rem/schemas/agents/examples/simple.yaml +21 -0
  117. rem/schemas/agents/examples/test.yaml +29 -0
  118. rem/schemas/agents/rem.yaml +132 -0
  119. rem/schemas/evaluators/hello-world/default.yaml +77 -0
  120. rem/schemas/evaluators/rem/faithfulness.yaml +219 -0
  121. rem/schemas/evaluators/rem/lookup-correctness.yaml +182 -0
  122. rem/schemas/evaluators/rem/retrieval-precision.yaml +199 -0
  123. rem/schemas/evaluators/rem/retrieval-recall.yaml +211 -0
  124. rem/schemas/evaluators/rem/search-correctness.yaml +192 -0
  125. rem/services/__init__.py +18 -0
  126. rem/services/audio/INTEGRATION.md +308 -0
  127. rem/services/audio/README.md +376 -0
  128. rem/services/audio/__init__.py +15 -0
  129. rem/services/audio/chunker.py +354 -0
  130. rem/services/audio/transcriber.py +259 -0
  131. rem/services/content/README.md +1269 -0
  132. rem/services/content/__init__.py +5 -0
  133. rem/services/content/providers.py +760 -0
  134. rem/services/content/service.py +762 -0
  135. rem/services/dreaming/README.md +230 -0
  136. rem/services/dreaming/__init__.py +53 -0
  137. rem/services/dreaming/affinity_service.py +322 -0
  138. rem/services/dreaming/moment_service.py +251 -0
  139. rem/services/dreaming/ontology_service.py +54 -0
  140. rem/services/dreaming/user_model_service.py +297 -0
  141. rem/services/dreaming/utils.py +39 -0
  142. rem/services/email/__init__.py +10 -0
  143. rem/services/email/service.py +522 -0
  144. rem/services/email/templates.py +360 -0
  145. rem/services/embeddings/__init__.py +11 -0
  146. rem/services/embeddings/api.py +127 -0
  147. rem/services/embeddings/worker.py +435 -0
  148. rem/services/fs/README.md +662 -0
  149. rem/services/fs/__init__.py +62 -0
  150. rem/services/fs/examples.py +206 -0
  151. rem/services/fs/examples_paths.py +204 -0
  152. rem/services/fs/git_provider.py +935 -0
  153. rem/services/fs/local_provider.py +760 -0
  154. rem/services/fs/parsing-hooks-examples.md +172 -0
  155. rem/services/fs/paths.py +276 -0
  156. rem/services/fs/provider.py +460 -0
  157. rem/services/fs/s3_provider.py +1042 -0
  158. rem/services/fs/service.py +186 -0
  159. rem/services/git/README.md +1075 -0
  160. rem/services/git/__init__.py +17 -0
  161. rem/services/git/service.py +469 -0
  162. rem/services/phoenix/EXPERIMENT_DESIGN.md +1146 -0
  163. rem/services/phoenix/README.md +453 -0
  164. rem/services/phoenix/__init__.py +46 -0
  165. rem/services/phoenix/client.py +960 -0
  166. rem/services/phoenix/config.py +88 -0
  167. rem/services/phoenix/prompt_labels.py +477 -0
  168. rem/services/postgres/README.md +757 -0
  169. rem/services/postgres/__init__.py +49 -0
  170. rem/services/postgres/diff_service.py +599 -0
  171. rem/services/postgres/migration_service.py +427 -0
  172. rem/services/postgres/programmable_diff_service.py +635 -0
  173. rem/services/postgres/pydantic_to_sqlalchemy.py +562 -0
  174. rem/services/postgres/register_type.py +353 -0
  175. rem/services/postgres/repository.py +481 -0
  176. rem/services/postgres/schema_generator.py +661 -0
  177. rem/services/postgres/service.py +802 -0
  178. rem/services/postgres/sql_builder.py +355 -0
  179. rem/services/rate_limit.py +113 -0
  180. rem/services/rem/README.md +318 -0
  181. rem/services/rem/__init__.py +23 -0
  182. rem/services/rem/exceptions.py +71 -0
  183. rem/services/rem/executor.py +293 -0
  184. rem/services/rem/parser.py +180 -0
  185. rem/services/rem/queries.py +196 -0
  186. rem/services/rem/query.py +371 -0
  187. rem/services/rem/service.py +608 -0
  188. rem/services/session/README.md +374 -0
  189. rem/services/session/__init__.py +13 -0
  190. rem/services/session/compression.py +488 -0
  191. rem/services/session/pydantic_messages.py +310 -0
  192. rem/services/session/reload.py +85 -0
  193. rem/services/user_service.py +130 -0
  194. rem/settings.py +1877 -0
  195. rem/sql/background_indexes.sql +52 -0
  196. rem/sql/migrations/001_install.sql +983 -0
  197. rem/sql/migrations/002_install_models.sql +3157 -0
  198. rem/sql/migrations/003_optional_extensions.sql +326 -0
  199. rem/sql/migrations/004_cache_system.sql +282 -0
  200. rem/sql/migrations/005_schema_update.sql +145 -0
  201. rem/sql/migrations/migrate_session_id_to_uuid.sql +45 -0
  202. rem/utils/AGENTIC_CHUNKING.md +597 -0
  203. rem/utils/README.md +628 -0
  204. rem/utils/__init__.py +61 -0
  205. rem/utils/agentic_chunking.py +622 -0
  206. rem/utils/batch_ops.py +343 -0
  207. rem/utils/chunking.py +108 -0
  208. rem/utils/clip_embeddings.py +276 -0
  209. rem/utils/constants.py +97 -0
  210. rem/utils/date_utils.py +228 -0
  211. rem/utils/dict_utils.py +98 -0
  212. rem/utils/embeddings.py +436 -0
  213. rem/utils/examples/embeddings_example.py +305 -0
  214. rem/utils/examples/sql_types_example.py +202 -0
  215. rem/utils/files.py +323 -0
  216. rem/utils/markdown.py +16 -0
  217. rem/utils/mime_types.py +158 -0
  218. rem/utils/model_helpers.py +492 -0
  219. rem/utils/schema_loader.py +649 -0
  220. rem/utils/sql_paths.py +146 -0
  221. rem/utils/sql_types.py +350 -0
  222. rem/utils/user_id.py +81 -0
  223. rem/utils/vision.py +325 -0
  224. rem/workers/README.md +506 -0
  225. rem/workers/__init__.py +7 -0
  226. rem/workers/db_listener.py +579 -0
  227. rem/workers/db_maintainer.py +74 -0
  228. rem/workers/dreaming.py +502 -0
  229. rem/workers/engram_processor.py +312 -0
  230. rem/workers/sqs_file_processor.py +193 -0
  231. rem/workers/unlogged_maintainer.py +463 -0
  232. remdb-0.3.242.dist-info/METADATA +1632 -0
  233. remdb-0.3.242.dist-info/RECORD +235 -0
  234. remdb-0.3.242.dist-info/WHEEL +4 -0
  235. remdb-0.3.242.dist-info/entry_points.txt +2 -0
@@ -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)