minder-cli 0.2.0__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.
- minder/__init__.py +12 -0
- minder/api/routers/prompts.py +177 -0
- minder/application/__init__.py +1 -0
- minder/application/admin/__init__.py +11 -0
- minder/application/admin/dto.py +453 -0
- minder/application/admin/jobs.py +327 -0
- minder/application/admin/use_cases.py +1895 -0
- minder/auth/__init__.py +12 -0
- minder/auth/context.py +26 -0
- minder/auth/middleware.py +70 -0
- minder/auth/principal.py +59 -0
- minder/auth/rate_limiter.py +89 -0
- minder/auth/rbac.py +60 -0
- minder/auth/service.py +541 -0
- minder/bootstrap/__init__.py +9 -0
- minder/bootstrap/providers.py +109 -0
- minder/bootstrap/transport.py +807 -0
- minder/cache/__init__.py +10 -0
- minder/cache/providers.py +140 -0
- minder/chunking/__init__.py +4 -0
- minder/chunking/code_splitter.py +184 -0
- minder/chunking/splitter.py +136 -0
- minder/cli.py +1542 -0
- minder/config.py +179 -0
- minder/continuity.py +363 -0
- minder/dev.py +160 -0
- minder/embedding/__init__.py +9 -0
- minder/embedding/base.py +7 -0
- minder/embedding/local.py +65 -0
- minder/embedding/openai.py +7 -0
- minder/graph/__init__.py +11 -0
- minder/graph/edges.py +13 -0
- minder/graph/executor.py +127 -0
- minder/graph/graph.py +263 -0
- minder/graph/nodes/__init__.py +27 -0
- minder/graph/nodes/evaluator.py +21 -0
- minder/graph/nodes/guard.py +64 -0
- minder/graph/nodes/llm.py +59 -0
- minder/graph/nodes/planning.py +30 -0
- minder/graph/nodes/reasoning.py +87 -0
- minder/graph/nodes/reranker.py +141 -0
- minder/graph/nodes/retriever.py +86 -0
- minder/graph/nodes/verification.py +230 -0
- minder/graph/nodes/workflow_planner.py +250 -0
- minder/graph/runtime.py +15 -0
- minder/graph/state.py +26 -0
- minder/llm/__init__.py +5 -0
- minder/llm/base.py +14 -0
- minder/llm/local.py +381 -0
- minder/llm/openai.py +89 -0
- minder/models/__init__.py +109 -0
- minder/models/base.py +10 -0
- minder/models/client.py +137 -0
- minder/models/document.py +34 -0
- minder/models/error.py +32 -0
- minder/models/graph.py +114 -0
- minder/models/history.py +32 -0
- minder/models/job.py +62 -0
- minder/models/prompt.py +41 -0
- minder/models/repository.py +62 -0
- minder/models/rule.py +68 -0
- minder/models/session.py +51 -0
- minder/models/skill.py +52 -0
- minder/models/user.py +41 -0
- minder/models/workflow.py +35 -0
- minder/observability/__init__.py +57 -0
- minder/observability/audit.py +243 -0
- minder/observability/logging.py +253 -0
- minder/observability/metrics.py +448 -0
- minder/observability/tracing.py +215 -0
- minder/presentation/__init__.py +1 -0
- minder/presentation/http/__init__.py +1 -0
- minder/presentation/http/admin/__init__.py +3 -0
- minder/presentation/http/admin/api.py +1309 -0
- minder/presentation/http/admin/context.py +94 -0
- minder/presentation/http/admin/dashboard.py +111 -0
- minder/presentation/http/admin/jobs.py +208 -0
- minder/presentation/http/admin/memories.py +185 -0
- minder/presentation/http/admin/prompts.py +219 -0
- minder/presentation/http/admin/routes.py +127 -0
- minder/presentation/http/admin/runtime.py +650 -0
- minder/presentation/http/admin/search.py +368 -0
- minder/presentation/http/admin/skills.py +230 -0
- minder/prompts/__init__.py +646 -0
- minder/prompts/formatter.py +142 -0
- minder/resources/__init__.py +318 -0
- minder/retrieval/__init__.py +5 -0
- minder/retrieval/hybrid.py +178 -0
- minder/retrieval/mmr.py +116 -0
- minder/retrieval/multi_hop.py +115 -0
- minder/runtime.py +15 -0
- minder/server.py +145 -0
- minder/store/__init__.py +64 -0
- minder/store/document.py +115 -0
- minder/store/error.py +82 -0
- minder/store/feedback.py +114 -0
- minder/store/graph.py +588 -0
- minder/store/history.py +57 -0
- minder/store/interfaces.py +512 -0
- minder/store/milvus/__init__.py +11 -0
- minder/store/milvus/client.py +26 -0
- minder/store/milvus/collections.py +15 -0
- minder/store/milvus/vector_store.py +232 -0
- minder/store/mongodb/__init__.py +11 -0
- minder/store/mongodb/client.py +49 -0
- minder/store/mongodb/indexes.py +90 -0
- minder/store/mongodb/operational_store.py +993 -0
- minder/store/relational.py +1087 -0
- minder/store/repo_state.py +58 -0
- minder/store/rule.py +93 -0
- minder/store/vector.py +79 -0
- minder/tools/__init__.py +47 -0
- minder/tools/auth.py +94 -0
- minder/tools/graph.py +839 -0
- minder/tools/ingest.py +353 -0
- minder/tools/memory.py +381 -0
- minder/tools/query.py +307 -0
- minder/tools/registry.py +269 -0
- minder/tools/repo_scanner.py +1266 -0
- minder/tools/search.py +15 -0
- minder/tools/session.py +316 -0
- minder/tools/skills.py +899 -0
- minder/tools/workflow.py +215 -0
- minder/transport/__init__.py +4 -0
- minder/transport/base.py +286 -0
- minder/transport/sse.py +252 -0
- minder/transport/stdio.py +29 -0
- minder_cli-0.2.0.dist-info/METADATA +318 -0
- minder_cli-0.2.0.dist-info/RECORD +132 -0
- minder_cli-0.2.0.dist-info/WHEEL +4 -0
- minder_cli-0.2.0.dist-info/entry_points.txt +2 -0
- minder_cli-0.2.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,650 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import re
|
|
6
|
+
import uuid
|
|
7
|
+
from collections.abc import Mapping
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, Field
|
|
11
|
+
from starlette.responses import JSONResponse, StreamingResponse
|
|
12
|
+
from starlette.routing import BaseRoute, Route
|
|
13
|
+
|
|
14
|
+
from minder.tools.memory import MemoryTools
|
|
15
|
+
from minder.tools.query import QueryTools
|
|
16
|
+
from minder.tools.session import SessionTools
|
|
17
|
+
from minder.tools.skills import SkillTools
|
|
18
|
+
|
|
19
|
+
from .context import AdminRouteContext
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
_PROMPT_LEAK_MARKERS = (
|
|
24
|
+
"Workflow instruction:",
|
|
25
|
+
"Instruction envelope:",
|
|
26
|
+
"Continuity packet:",
|
|
27
|
+
"Tool capabilities:",
|
|
28
|
+
"Data access policy:",
|
|
29
|
+
"Repository context note:",
|
|
30
|
+
"User query:",
|
|
31
|
+
"Retrieved context:",
|
|
32
|
+
"Correction required:",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class RuntimeQueryRequest(BaseModel):
|
|
37
|
+
query: str
|
|
38
|
+
repo_id: str | None = None
|
|
39
|
+
workflow_name: str | None = None
|
|
40
|
+
max_attempts: int = Field(default=2, ge=1, le=4)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
_READ_LIST_VERBS = (
|
|
44
|
+
"list",
|
|
45
|
+
"show",
|
|
46
|
+
"view",
|
|
47
|
+
"what",
|
|
48
|
+
"which",
|
|
49
|
+
"liệt kê",
|
|
50
|
+
"liet ke",
|
|
51
|
+
"xem",
|
|
52
|
+
"danh sách",
|
|
53
|
+
"danh sach",
|
|
54
|
+
)
|
|
55
|
+
_CREATE_VERBS = (
|
|
56
|
+
"create",
|
|
57
|
+
"add",
|
|
58
|
+
"store",
|
|
59
|
+
"save",
|
|
60
|
+
"new",
|
|
61
|
+
"tạo",
|
|
62
|
+
"tao",
|
|
63
|
+
"thêm",
|
|
64
|
+
"them",
|
|
65
|
+
"lưu",
|
|
66
|
+
"luu",
|
|
67
|
+
)
|
|
68
|
+
_DELETE_VERBS = ("delete", "remove", "xoá", "xóa", "xoa")
|
|
69
|
+
_CLEANUP_VERBS = ("cleanup", "clean up", "purge", "dọn", "don")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _contains_any(text: str, needles: tuple[str, ...]) -> bool:
|
|
73
|
+
lowered = text.lower()
|
|
74
|
+
return any(needle in lowered for needle in needles)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _extract_quoted_field(query: str, aliases: tuple[str, ...]) -> str | None:
|
|
78
|
+
for alias in aliases:
|
|
79
|
+
pattern = rf"(?:{re.escape(alias)})\s*[:=]?\s*[\"']([^\"']+)[\"']"
|
|
80
|
+
match = re.search(pattern, query, flags=re.IGNORECASE)
|
|
81
|
+
if match:
|
|
82
|
+
value = match.group(1).strip()
|
|
83
|
+
if value:
|
|
84
|
+
return value
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _extract_uuid(query: str) -> str | None:
|
|
89
|
+
match = re.search(
|
|
90
|
+
r"\b[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}\b",
|
|
91
|
+
query,
|
|
92
|
+
)
|
|
93
|
+
if match:
|
|
94
|
+
return match.group(0)
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _extract_tags(query: str) -> list[str]:
|
|
99
|
+
raw = _extract_quoted_field(query, ("tags", "tag"))
|
|
100
|
+
if not raw:
|
|
101
|
+
return []
|
|
102
|
+
return [item.strip() for item in raw.split(",") if item.strip()]
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _agentic_payload(
|
|
106
|
+
*,
|
|
107
|
+
query: str,
|
|
108
|
+
answer: str,
|
|
109
|
+
repository: dict[str, Any],
|
|
110
|
+
agent_actions: list[dict[str, Any]],
|
|
111
|
+
) -> dict[str, Any]:
|
|
112
|
+
return {
|
|
113
|
+
"query": query,
|
|
114
|
+
"repository": repository,
|
|
115
|
+
"answer": answer,
|
|
116
|
+
"answer_sanitized": False,
|
|
117
|
+
"answer_warning": None,
|
|
118
|
+
"sources": [],
|
|
119
|
+
"workflow": {},
|
|
120
|
+
"guard_result": None,
|
|
121
|
+
"verification_result": None,
|
|
122
|
+
"evaluation": None,
|
|
123
|
+
"provider": "minder",
|
|
124
|
+
"model": "agentic-tool-router",
|
|
125
|
+
"runtime": "internal",
|
|
126
|
+
"orchestration_runtime": "agentic-tool-executor",
|
|
127
|
+
"transition_log": [
|
|
128
|
+
{"edge": "agent_tool_executed", "tool": action.get("tool")}
|
|
129
|
+
for action in agent_actions
|
|
130
|
+
],
|
|
131
|
+
"edge": "agent_tool_executed",
|
|
132
|
+
"cross_repo_graph": None,
|
|
133
|
+
"agent_actions": agent_actions,
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class RuntimeAgentExecutor:
|
|
138
|
+
def __init__(self, context: AdminRouteContext) -> None:
|
|
139
|
+
self._memory_tools = MemoryTools(context.store, context.config)
|
|
140
|
+
self._skill_tools = SkillTools(context.store, context.config)
|
|
141
|
+
self._session_tools = SessionTools(context.store)
|
|
142
|
+
|
|
143
|
+
async def execute(
|
|
144
|
+
self,
|
|
145
|
+
*,
|
|
146
|
+
query: str,
|
|
147
|
+
repository: dict[str, Any],
|
|
148
|
+
admin_user_id: uuid.UUID,
|
|
149
|
+
) -> dict[str, Any] | None:
|
|
150
|
+
normalized = query.lower().strip()
|
|
151
|
+
if not normalized:
|
|
152
|
+
return None
|
|
153
|
+
|
|
154
|
+
if "memory" in normalized or "memories" in normalized:
|
|
155
|
+
return await self._execute_memory(query=query, repository=repository)
|
|
156
|
+
if "skill" in normalized or "skills" in normalized:
|
|
157
|
+
return await self._execute_skill(query=query, repository=repository)
|
|
158
|
+
if "session" in normalized or "sessions" in normalized:
|
|
159
|
+
return await self._execute_session(
|
|
160
|
+
query=query,
|
|
161
|
+
repository=repository,
|
|
162
|
+
admin_user_id=admin_user_id,
|
|
163
|
+
)
|
|
164
|
+
return None
|
|
165
|
+
|
|
166
|
+
async def _execute_memory(
|
|
167
|
+
self,
|
|
168
|
+
*,
|
|
169
|
+
query: str,
|
|
170
|
+
repository: dict[str, Any],
|
|
171
|
+
) -> dict[str, Any] | None:
|
|
172
|
+
normalized = query.lower()
|
|
173
|
+
if _contains_any(normalized, _READ_LIST_VERBS):
|
|
174
|
+
memories = await self._memory_tools.minder_memory_list()
|
|
175
|
+
preview = (
|
|
176
|
+
"\n".join(f"- {item['id']}: {item['title']}" for item in memories[:10])
|
|
177
|
+
if memories
|
|
178
|
+
else "- No memories found."
|
|
179
|
+
)
|
|
180
|
+
return _agentic_payload(
|
|
181
|
+
query=query,
|
|
182
|
+
repository=repository,
|
|
183
|
+
answer=f"Listed {len(memories)} memories.\n{preview}",
|
|
184
|
+
agent_actions=[
|
|
185
|
+
{
|
|
186
|
+
"tool": "minder_memory_list",
|
|
187
|
+
"mode": "read",
|
|
188
|
+
"status": "success",
|
|
189
|
+
"count": len(memories),
|
|
190
|
+
}
|
|
191
|
+
],
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
if _contains_any(normalized, _CREATE_VERBS):
|
|
195
|
+
title = _extract_quoted_field(query, ("title", "memory", "memory title"))
|
|
196
|
+
content = _extract_quoted_field(
|
|
197
|
+
query,
|
|
198
|
+
("content", "body", "note", "memory content"),
|
|
199
|
+
)
|
|
200
|
+
if not title or not content:
|
|
201
|
+
return None
|
|
202
|
+
created = await self._memory_tools.minder_memory_store(
|
|
203
|
+
title=title,
|
|
204
|
+
content=content,
|
|
205
|
+
tags=_extract_tags(query),
|
|
206
|
+
language=_extract_quoted_field(query, ("language",)) or "markdown",
|
|
207
|
+
)
|
|
208
|
+
return _agentic_payload(
|
|
209
|
+
query=query,
|
|
210
|
+
repository=repository,
|
|
211
|
+
answer=f"Created memory '{created['title']}' with id {created['id']}.",
|
|
212
|
+
agent_actions=[
|
|
213
|
+
{
|
|
214
|
+
"tool": "minder_memory_store",
|
|
215
|
+
"mode": "write",
|
|
216
|
+
"status": "success",
|
|
217
|
+
"result": created,
|
|
218
|
+
}
|
|
219
|
+
],
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
if _contains_any(normalized, _DELETE_VERBS):
|
|
223
|
+
memory_id = _extract_uuid(query)
|
|
224
|
+
if not memory_id:
|
|
225
|
+
return None
|
|
226
|
+
deleted = await self._memory_tools.minder_memory_delete(memory_id)
|
|
227
|
+
return _agentic_payload(
|
|
228
|
+
query=query,
|
|
229
|
+
repository=repository,
|
|
230
|
+
answer=(
|
|
231
|
+
f"Deleted memory {memory_id}."
|
|
232
|
+
if deleted.get("deleted")
|
|
233
|
+
else f"Memory {memory_id} was not deleted."
|
|
234
|
+
),
|
|
235
|
+
agent_actions=[
|
|
236
|
+
{
|
|
237
|
+
"tool": "minder_memory_delete",
|
|
238
|
+
"mode": "write",
|
|
239
|
+
"status": "success",
|
|
240
|
+
"result": deleted,
|
|
241
|
+
}
|
|
242
|
+
],
|
|
243
|
+
)
|
|
244
|
+
return None
|
|
245
|
+
|
|
246
|
+
async def _execute_skill(
|
|
247
|
+
self,
|
|
248
|
+
*,
|
|
249
|
+
query: str,
|
|
250
|
+
repository: dict[str, Any],
|
|
251
|
+
) -> dict[str, Any] | None:
|
|
252
|
+
normalized = query.lower()
|
|
253
|
+
if _contains_any(normalized, _READ_LIST_VERBS):
|
|
254
|
+
skills = await self._skill_tools.minder_skill_list()
|
|
255
|
+
preview = (
|
|
256
|
+
"\n".join(f"- {item['id']}: {item['title']}" for item in skills[:10])
|
|
257
|
+
if skills
|
|
258
|
+
else "- No skills found."
|
|
259
|
+
)
|
|
260
|
+
return _agentic_payload(
|
|
261
|
+
query=query,
|
|
262
|
+
repository=repository,
|
|
263
|
+
answer=f"Listed {len(skills)} skills.\n{preview}",
|
|
264
|
+
agent_actions=[
|
|
265
|
+
{
|
|
266
|
+
"tool": "minder_skill_list",
|
|
267
|
+
"mode": "read",
|
|
268
|
+
"status": "success",
|
|
269
|
+
"count": len(skills),
|
|
270
|
+
}
|
|
271
|
+
],
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
if _contains_any(normalized, _CREATE_VERBS):
|
|
275
|
+
title = _extract_quoted_field(query, ("title", "skill", "skill title"))
|
|
276
|
+
content = _extract_quoted_field(
|
|
277
|
+
query,
|
|
278
|
+
("content", "body", "skill content"),
|
|
279
|
+
)
|
|
280
|
+
if not title or not content:
|
|
281
|
+
return None
|
|
282
|
+
created = await self._skill_tools.minder_skill_store(
|
|
283
|
+
title=title,
|
|
284
|
+
content=content,
|
|
285
|
+
language=_extract_quoted_field(query, ("language",)) or "markdown",
|
|
286
|
+
tags=_extract_tags(query),
|
|
287
|
+
)
|
|
288
|
+
return _agentic_payload(
|
|
289
|
+
query=query,
|
|
290
|
+
repository=repository,
|
|
291
|
+
answer=f"Created skill '{created['title']}' with id {created['id']}.",
|
|
292
|
+
agent_actions=[
|
|
293
|
+
{
|
|
294
|
+
"tool": "minder_skill_store",
|
|
295
|
+
"mode": "write",
|
|
296
|
+
"status": "success",
|
|
297
|
+
"result": created,
|
|
298
|
+
}
|
|
299
|
+
],
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
if _contains_any(normalized, _DELETE_VERBS):
|
|
303
|
+
skill_id = _extract_uuid(query)
|
|
304
|
+
if not skill_id:
|
|
305
|
+
return None
|
|
306
|
+
deleted = await self._skill_tools.minder_skill_delete(skill_id)
|
|
307
|
+
return _agentic_payload(
|
|
308
|
+
query=query,
|
|
309
|
+
repository=repository,
|
|
310
|
+
answer=(
|
|
311
|
+
f"Deleted skill {skill_id}."
|
|
312
|
+
if deleted.get("deleted")
|
|
313
|
+
else f"Skill {skill_id} was not deleted."
|
|
314
|
+
),
|
|
315
|
+
agent_actions=[
|
|
316
|
+
{
|
|
317
|
+
"tool": "minder_skill_delete",
|
|
318
|
+
"mode": "write",
|
|
319
|
+
"status": "success",
|
|
320
|
+
"result": deleted,
|
|
321
|
+
}
|
|
322
|
+
],
|
|
323
|
+
)
|
|
324
|
+
return None
|
|
325
|
+
|
|
326
|
+
async def _execute_session(
|
|
327
|
+
self,
|
|
328
|
+
*,
|
|
329
|
+
query: str,
|
|
330
|
+
repository: dict[str, Any],
|
|
331
|
+
admin_user_id: uuid.UUID,
|
|
332
|
+
) -> dict[str, Any] | None:
|
|
333
|
+
normalized = query.lower()
|
|
334
|
+
if _contains_any(normalized, _READ_LIST_VERBS):
|
|
335
|
+
sessions = await self._session_tools.minder_session_list(
|
|
336
|
+
user_id=admin_user_id
|
|
337
|
+
)
|
|
338
|
+
items = list(sessions.get("sessions", []) or [])
|
|
339
|
+
preview = (
|
|
340
|
+
"\n".join(
|
|
341
|
+
f"- {item['session_id']}: {item.get('name') or 'unnamed'}"
|
|
342
|
+
for item in items[:10]
|
|
343
|
+
)
|
|
344
|
+
if items
|
|
345
|
+
else "- No sessions found."
|
|
346
|
+
)
|
|
347
|
+
return _agentic_payload(
|
|
348
|
+
query=query,
|
|
349
|
+
repository=repository,
|
|
350
|
+
answer=f"Listed {len(items)} sessions.\n{preview}",
|
|
351
|
+
agent_actions=[
|
|
352
|
+
{
|
|
353
|
+
"tool": "minder_session_list",
|
|
354
|
+
"mode": "read",
|
|
355
|
+
"status": "success",
|
|
356
|
+
"count": len(items),
|
|
357
|
+
}
|
|
358
|
+
],
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
if _contains_any(normalized, _CLEANUP_VERBS):
|
|
362
|
+
cleaned = await self._session_tools.minder_session_cleanup(
|
|
363
|
+
user_id=admin_user_id
|
|
364
|
+
)
|
|
365
|
+
return _agentic_payload(
|
|
366
|
+
query=query,
|
|
367
|
+
repository=repository,
|
|
368
|
+
answer=(
|
|
369
|
+
f"Cleaned up {cleaned.get('deleted_sessions', 0)} expired sessions and "
|
|
370
|
+
f"{cleaned.get('deleted_history', 0)} history records."
|
|
371
|
+
),
|
|
372
|
+
agent_actions=[
|
|
373
|
+
{
|
|
374
|
+
"tool": "minder_session_cleanup",
|
|
375
|
+
"mode": "write",
|
|
376
|
+
"status": "success",
|
|
377
|
+
"result": cleaned,
|
|
378
|
+
}
|
|
379
|
+
],
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
if _contains_any(normalized, _CREATE_VERBS):
|
|
383
|
+
name = _extract_quoted_field(query, ("name", "session", "session name"))
|
|
384
|
+
if not name:
|
|
385
|
+
return None
|
|
386
|
+
repo_id_value = repository.get("id")
|
|
387
|
+
created = await self._session_tools.minder_session_create(
|
|
388
|
+
user_id=admin_user_id,
|
|
389
|
+
name=name,
|
|
390
|
+
repo_id=uuid.UUID(str(repo_id_value)) if repo_id_value else None,
|
|
391
|
+
project_context=(
|
|
392
|
+
{"repository_name": repository.get("name")}
|
|
393
|
+
if repository.get("name")
|
|
394
|
+
else None
|
|
395
|
+
),
|
|
396
|
+
)
|
|
397
|
+
return _agentic_payload(
|
|
398
|
+
query=query,
|
|
399
|
+
repository=repository,
|
|
400
|
+
answer=f"Created session '{created.get('name') or name}' with id {created['session_id']}.",
|
|
401
|
+
agent_actions=[
|
|
402
|
+
{
|
|
403
|
+
"tool": "minder_session_create",
|
|
404
|
+
"mode": "write",
|
|
405
|
+
"status": "success",
|
|
406
|
+
"result": created,
|
|
407
|
+
}
|
|
408
|
+
],
|
|
409
|
+
)
|
|
410
|
+
return None
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def _fallback_answer(query: str, sources: list[dict[str, object]]) -> str:
|
|
414
|
+
source_paths = [
|
|
415
|
+
str(source.get("path", "")).strip()
|
|
416
|
+
for source in sources[:3]
|
|
417
|
+
if isinstance(source, dict) and str(source.get("path", "")).strip()
|
|
418
|
+
]
|
|
419
|
+
if source_paths:
|
|
420
|
+
return (
|
|
421
|
+
f'The local runtime did not return a clean natural-language answer for "{query}". '
|
|
422
|
+
f"Start by inspecting: {', '.join(source_paths)}."
|
|
423
|
+
)
|
|
424
|
+
return (
|
|
425
|
+
f'The local runtime did not return a clean natural-language answer for "{query}". '
|
|
426
|
+
"Try a narrower question or inspect the transition log for the current reasoning path."
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def _sanitize_answer(
|
|
431
|
+
answer: object,
|
|
432
|
+
*,
|
|
433
|
+
query: str,
|
|
434
|
+
sources: list[dict[str, object]],
|
|
435
|
+
) -> tuple[str, bool, str | None]:
|
|
436
|
+
text = str(answer or "").strip()
|
|
437
|
+
if not text:
|
|
438
|
+
return (
|
|
439
|
+
_fallback_answer(query, sources),
|
|
440
|
+
True,
|
|
441
|
+
"Empty model response replaced with a runtime summary.",
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
marker_hits = sum(text.count(marker) for marker in _PROMPT_LEAK_MARKERS)
|
|
445
|
+
looks_like_prompt_echo = marker_hits >= 2 or text.startswith(
|
|
446
|
+
"Workflow instruction:"
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
if not looks_like_prompt_echo:
|
|
450
|
+
return text, False, None
|
|
451
|
+
|
|
452
|
+
for line in text.splitlines():
|
|
453
|
+
stripped = line.strip()
|
|
454
|
+
if stripped.lower().startswith("answer:"):
|
|
455
|
+
candidate = stripped.split(":", 1)[1].strip()
|
|
456
|
+
if candidate:
|
|
457
|
+
return (
|
|
458
|
+
candidate,
|
|
459
|
+
True,
|
|
460
|
+
"Prompt envelope was removed from the visible answer.",
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
return (
|
|
464
|
+
_fallback_answer(query, sources),
|
|
465
|
+
True,
|
|
466
|
+
"Prompt envelope leaked into the model output, so the dashboard replaced it with a cleaner summary.",
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def build_runtime_routes(context: AdminRouteContext) -> list[BaseRoute]:
|
|
471
|
+
async def _resolve_request(
|
|
472
|
+
request,
|
|
473
|
+
) -> tuple[RuntimeQueryRequest, Mapping[str, object], str | None] | JSONResponse:
|
|
474
|
+
try:
|
|
475
|
+
await context.admin_user_from_request(request)
|
|
476
|
+
except PermissionError:
|
|
477
|
+
return JSONResponse({"error": "Admin role required"}, status_code=403)
|
|
478
|
+
except Exception as exc:
|
|
479
|
+
return JSONResponse({"error": str(exc)}, status_code=401)
|
|
480
|
+
|
|
481
|
+
try:
|
|
482
|
+
payload = RuntimeQueryRequest(**(await request.json()))
|
|
483
|
+
except Exception as exc:
|
|
484
|
+
return JSONResponse({"error": str(exc)}, status_code=400)
|
|
485
|
+
|
|
486
|
+
query = str(payload.query).strip()
|
|
487
|
+
if not query:
|
|
488
|
+
return JSONResponse({"error": "query is required"}, status_code=400)
|
|
489
|
+
|
|
490
|
+
if not payload.repo_id:
|
|
491
|
+
return payload, {}, None
|
|
492
|
+
|
|
493
|
+
try:
|
|
494
|
+
repo_id = uuid.UUID(str(payload.repo_id))
|
|
495
|
+
except ValueError:
|
|
496
|
+
return JSONResponse({"error": "Invalid repo_id"}, status_code=400)
|
|
497
|
+
|
|
498
|
+
try:
|
|
499
|
+
repository_payload = await context.use_cases.get_repository_detail(repo_id)
|
|
500
|
+
except LookupError:
|
|
501
|
+
return JSONResponse({"error": "Repository not found"}, status_code=404)
|
|
502
|
+
|
|
503
|
+
repository = (
|
|
504
|
+
repository_payload.get("repository", {})
|
|
505
|
+
if isinstance(repository_payload, dict)
|
|
506
|
+
else {}
|
|
507
|
+
)
|
|
508
|
+
repo_path = str(repository.get("path", "") or "").strip()
|
|
509
|
+
if not repo_path:
|
|
510
|
+
return JSONResponse(
|
|
511
|
+
{"error": "Repository path is required for runtime query"},
|
|
512
|
+
status_code=400,
|
|
513
|
+
)
|
|
514
|
+
return payload, repository, repo_path
|
|
515
|
+
|
|
516
|
+
async def runtime_query(request) -> JSONResponse:
|
|
517
|
+
resolved = await _resolve_request(request)
|
|
518
|
+
if isinstance(resolved, JSONResponse):
|
|
519
|
+
return resolved
|
|
520
|
+
payload, repository, repo_path = resolved
|
|
521
|
+
admin_user = await context.admin_user_from_request(request)
|
|
522
|
+
query = str(payload.query).strip()
|
|
523
|
+
repo_id = uuid.UUID(str(payload.repo_id)) if payload.repo_id else None
|
|
524
|
+
repository_payload = {
|
|
525
|
+
"id": (
|
|
526
|
+
str(repository.get("id") or payload.repo_id)
|
|
527
|
+
if (repository.get("id") or payload.repo_id)
|
|
528
|
+
else None
|
|
529
|
+
),
|
|
530
|
+
"name": repository.get("name") if repository else None,
|
|
531
|
+
"path": repo_path,
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
agentic_result = await RuntimeAgentExecutor(context).execute(
|
|
535
|
+
query=query,
|
|
536
|
+
repository=repository_payload,
|
|
537
|
+
admin_user_id=admin_user.id,
|
|
538
|
+
)
|
|
539
|
+
if agentic_result is not None:
|
|
540
|
+
return JSONResponse(agentic_result)
|
|
541
|
+
|
|
542
|
+
try:
|
|
543
|
+
result = await QueryTools(context.store, context.config).minder_query(
|
|
544
|
+
query=query,
|
|
545
|
+
repo_path=repo_path,
|
|
546
|
+
repo_id=repo_id,
|
|
547
|
+
workflow_name=payload.workflow_name,
|
|
548
|
+
max_attempts=payload.max_attempts,
|
|
549
|
+
)
|
|
550
|
+
except Exception as exc:
|
|
551
|
+
logger.exception("Runtime query failed", exc_info=exc)
|
|
552
|
+
return JSONResponse({"error": str(exc)}, status_code=400)
|
|
553
|
+
|
|
554
|
+
sources = list(result.get("sources", []) or [])
|
|
555
|
+
cleaned_answer, answer_sanitized, answer_warning = _sanitize_answer(
|
|
556
|
+
result.get("answer", ""),
|
|
557
|
+
query=query,
|
|
558
|
+
sources=sources,
|
|
559
|
+
)
|
|
560
|
+
|
|
561
|
+
return JSONResponse(
|
|
562
|
+
{
|
|
563
|
+
**result,
|
|
564
|
+
"query": query,
|
|
565
|
+
"repository": repository_payload,
|
|
566
|
+
"answer": cleaned_answer,
|
|
567
|
+
"answer_sanitized": answer_sanitized,
|
|
568
|
+
"answer_warning": answer_warning,
|
|
569
|
+
"agent_actions": [],
|
|
570
|
+
}
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
async def runtime_query_stream(request) -> StreamingResponse | JSONResponse:
|
|
574
|
+
resolved = await _resolve_request(request)
|
|
575
|
+
if isinstance(resolved, JSONResponse):
|
|
576
|
+
return resolved
|
|
577
|
+
payload, repository, repo_path = resolved
|
|
578
|
+
admin_user = await context.admin_user_from_request(request)
|
|
579
|
+
query = str(payload.query).strip()
|
|
580
|
+
repo_id = uuid.UUID(str(payload.repo_id)) if payload.repo_id else None
|
|
581
|
+
|
|
582
|
+
async def event_stream():
|
|
583
|
+
repository_payload = {
|
|
584
|
+
"id": (
|
|
585
|
+
str(repository.get("id") or payload.repo_id)
|
|
586
|
+
if (repository.get("id") or payload.repo_id)
|
|
587
|
+
else None
|
|
588
|
+
),
|
|
589
|
+
"name": repository.get("name") if repository else None,
|
|
590
|
+
"path": repo_path,
|
|
591
|
+
}
|
|
592
|
+
yield json.dumps({"type": "meta", "repository": repository_payload}) + "\n"
|
|
593
|
+
agentic_result = await RuntimeAgentExecutor(context).execute(
|
|
594
|
+
query=query,
|
|
595
|
+
repository=repository_payload,
|
|
596
|
+
admin_user_id=admin_user.id,
|
|
597
|
+
)
|
|
598
|
+
if agentic_result is not None:
|
|
599
|
+
yield json.dumps({"type": "final", "payload": agentic_result}) + "\n"
|
|
600
|
+
return
|
|
601
|
+
|
|
602
|
+
query_tools = QueryTools(context.store, context.config)
|
|
603
|
+
try:
|
|
604
|
+
async for event in query_tools.minder_query_stream(
|
|
605
|
+
query=query,
|
|
606
|
+
repo_path=repo_path,
|
|
607
|
+
repo_id=repo_id,
|
|
608
|
+
workflow_name=payload.workflow_name,
|
|
609
|
+
max_attempts=payload.max_attempts,
|
|
610
|
+
):
|
|
611
|
+
event_type = str(event.get("type"))
|
|
612
|
+
if event_type == "final":
|
|
613
|
+
payload_result = dict(event.get("payload", {}) or {})
|
|
614
|
+
sources = list(payload_result.get("sources", []) or [])
|
|
615
|
+
cleaned_answer, answer_sanitized, answer_warning = (
|
|
616
|
+
_sanitize_answer(
|
|
617
|
+
payload_result.get("answer", ""),
|
|
618
|
+
query=query,
|
|
619
|
+
sources=sources,
|
|
620
|
+
)
|
|
621
|
+
)
|
|
622
|
+
yield json.dumps(
|
|
623
|
+
{
|
|
624
|
+
"type": "final",
|
|
625
|
+
"payload": {
|
|
626
|
+
**payload_result,
|
|
627
|
+
"query": query,
|
|
628
|
+
"repository": repository_payload,
|
|
629
|
+
"answer": cleaned_answer,
|
|
630
|
+
"answer_sanitized": answer_sanitized,
|
|
631
|
+
"answer_warning": answer_warning,
|
|
632
|
+
"agent_actions": [],
|
|
633
|
+
},
|
|
634
|
+
}
|
|
635
|
+
) + "\n"
|
|
636
|
+
continue
|
|
637
|
+
yield json.dumps(event) + "\n"
|
|
638
|
+
except Exception as exc:
|
|
639
|
+
logger.exception("Runtime query stream failed", exc_info=exc)
|
|
640
|
+
yield json.dumps({"type": "error", "error": str(exc)}) + "\n"
|
|
641
|
+
|
|
642
|
+
return StreamingResponse(
|
|
643
|
+
event_stream(),
|
|
644
|
+
media_type="application/x-ndjson",
|
|
645
|
+
)
|
|
646
|
+
|
|
647
|
+
return [
|
|
648
|
+
Route("/api/v1/runtime/query", runtime_query, methods=["POST"]),
|
|
649
|
+
Route("/api/v1/runtime/query/stream", runtime_query_stream, methods=["POST"]),
|
|
650
|
+
]
|