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,646 @@
|
|
|
1
|
+
"""MCP prompt registration and runtime sync for Minder."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from types import SimpleNamespace
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Iterable
|
|
7
|
+
|
|
8
|
+
from mcp.server.fastmcp import FastMCP
|
|
9
|
+
from mcp.server.fastmcp.prompts.base import Prompt, PromptArgument
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from minder.store.interfaces import IOperationalStore
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class PromptRegistry:
|
|
16
|
+
"""Registers all Minder MCP prompts onto a :class:`FastMCP` app."""
|
|
17
|
+
|
|
18
|
+
_BUILTIN_NAMES = {"debug", "review", "explain", "tdd_step", "query_reasoning"}
|
|
19
|
+
_BUILTIN_DEFINITIONS: dict[str, dict[str, Any]] = {
|
|
20
|
+
"debug": {
|
|
21
|
+
"title": "Debug Assistant",
|
|
22
|
+
"description": (
|
|
23
|
+
"Structured prompt to diagnose errors with root cause analysis, "
|
|
24
|
+
"ranked hypotheses, and a minimal fix proposal."
|
|
25
|
+
),
|
|
26
|
+
"arguments": ["error", "context"],
|
|
27
|
+
"defaults": {
|
|
28
|
+
"error": "Paste the error message or stack trace here.",
|
|
29
|
+
"context": "",
|
|
30
|
+
},
|
|
31
|
+
"content_template": "\n\n".join(
|
|
32
|
+
[
|
|
33
|
+
"## Error\n```\n{error}\n```",
|
|
34
|
+
"## Context\n{context}",
|
|
35
|
+
"## Task",
|
|
36
|
+
"1. Identify the root cause of the error above.",
|
|
37
|
+
"2. List 2-3 hypotheses ranked by likelihood.",
|
|
38
|
+
"3. Propose the minimal fix with a code snippet.",
|
|
39
|
+
"4. Describe how to verify the fix.",
|
|
40
|
+
]
|
|
41
|
+
),
|
|
42
|
+
},
|
|
43
|
+
"review": {
|
|
44
|
+
"title": "Code Reviewer",
|
|
45
|
+
"description": (
|
|
46
|
+
"Code review checklist prompt for structured diff analysis covering "
|
|
47
|
+
"correctness, edge cases, tests, performance, security, and readability."
|
|
48
|
+
),
|
|
49
|
+
"arguments": ["diff", "context"],
|
|
50
|
+
"defaults": {
|
|
51
|
+
"diff": "Paste the diff or changed code here.",
|
|
52
|
+
"context": "",
|
|
53
|
+
},
|
|
54
|
+
"content_template": "\n\n".join(
|
|
55
|
+
[
|
|
56
|
+
"## Diff\n```diff\n{diff}\n```",
|
|
57
|
+
"## Context\n{context}",
|
|
58
|
+
"## Review Checklist",
|
|
59
|
+
"- [ ] **Correctness** — Does the logic match the stated intent?",
|
|
60
|
+
"- [ ] **Edge cases** — Are failure modes and null/empty states handled?",
|
|
61
|
+
"- [ ] **Tests** — Are new behaviours covered by automated tests?",
|
|
62
|
+
"- [ ] **Performance** — Any N+1 queries, unbounded loops, or hot-path regressions?",
|
|
63
|
+
"- [ ] **Security** — Any injection vectors, auth bypass, or data leakage?",
|
|
64
|
+
"- [ ] **Readability** — Is naming clear and are comments useful?",
|
|
65
|
+
"",
|
|
66
|
+
"Provide **BLOCKING** issues first, then **RECOMMENDED** improvements, then **SUGGESTIONS**.",
|
|
67
|
+
]
|
|
68
|
+
),
|
|
69
|
+
},
|
|
70
|
+
"explain": {
|
|
71
|
+
"title": "Code Explainer",
|
|
72
|
+
"description": (
|
|
73
|
+
"Explain a code snippet in plain language with a summary, "
|
|
74
|
+
"step-by-step walkthrough, gotchas, and a usage example."
|
|
75
|
+
),
|
|
76
|
+
"arguments": ["code", "language"],
|
|
77
|
+
"defaults": {
|
|
78
|
+
"code": "Paste the code snippet here.",
|
|
79
|
+
"language": "python",
|
|
80
|
+
},
|
|
81
|
+
"content_template": "\n\n".join(
|
|
82
|
+
[
|
|
83
|
+
"## Code ({language})\n```{language}\n{code}\n```",
|
|
84
|
+
"## Task",
|
|
85
|
+
"Explain the code above clearly:",
|
|
86
|
+
"1. **What it does** — one-sentence summary.",
|
|
87
|
+
"2. **How it works** — step-by-step walkthrough of the key logic.",
|
|
88
|
+
"3. **Gotchas** — any non-obvious behaviour, edge cases, or side effects.",
|
|
89
|
+
"4. **Example** — show a concrete usage example if helpful.",
|
|
90
|
+
]
|
|
91
|
+
),
|
|
92
|
+
},
|
|
93
|
+
"tdd_step": {
|
|
94
|
+
"title": "TDD Step Guide",
|
|
95
|
+
"description": (
|
|
96
|
+
"Workflow-aware TDD guidance tailored to the current step "
|
|
97
|
+
"(Test Writing, Implementation, or Review)."
|
|
98
|
+
),
|
|
99
|
+
"arguments": ["current_step", "failing_tests"],
|
|
100
|
+
"defaults": {
|
|
101
|
+
"current_step": "Test Writing",
|
|
102
|
+
"failing_tests": "",
|
|
103
|
+
},
|
|
104
|
+
"content_template": "\n\n".join(
|
|
105
|
+
[
|
|
106
|
+
"## Current Workflow Step: {current_step}",
|
|
107
|
+
"## Failing Tests\n```\n{failing_tests}\n```",
|
|
108
|
+
"## Guidance",
|
|
109
|
+
"Tailor the response to the workflow step above and keep the next action concrete.",
|
|
110
|
+
]
|
|
111
|
+
),
|
|
112
|
+
},
|
|
113
|
+
"query_reasoning": {
|
|
114
|
+
"title": "Query Reasoning",
|
|
115
|
+
"description": (
|
|
116
|
+
"Primary query reasoning prompt that injects workflow instruction, "
|
|
117
|
+
"continuity packet, retrieved context, and corrective retry guidance."
|
|
118
|
+
),
|
|
119
|
+
"arguments": [
|
|
120
|
+
"workflow_instruction",
|
|
121
|
+
"instruction_envelope",
|
|
122
|
+
"continuity_brief",
|
|
123
|
+
"continuity_packet",
|
|
124
|
+
"tool_capabilities",
|
|
125
|
+
"data_access_policy",
|
|
126
|
+
"repository_context_note",
|
|
127
|
+
"user_query",
|
|
128
|
+
"retrieved_context",
|
|
129
|
+
"correction_required",
|
|
130
|
+
],
|
|
131
|
+
"defaults": {
|
|
132
|
+
"workflow_instruction": "",
|
|
133
|
+
"instruction_envelope": "{}",
|
|
134
|
+
"continuity_brief": "{}",
|
|
135
|
+
"continuity_packet": "{}",
|
|
136
|
+
"tool_capabilities": "No capability manifest was provided.",
|
|
137
|
+
"data_access_policy": "No data-access policy was provided.",
|
|
138
|
+
"repository_context_note": "No repository context policy was provided.",
|
|
139
|
+
"user_query": "Summarize the current repository state.",
|
|
140
|
+
"retrieved_context": "No repository context found.",
|
|
141
|
+
"correction_required": "",
|
|
142
|
+
},
|
|
143
|
+
"content_template": "\n\n".join(
|
|
144
|
+
[
|
|
145
|
+
"Workflow instruction:\n{workflow_instruction}",
|
|
146
|
+
"Instruction envelope:\n{instruction_envelope}",
|
|
147
|
+
"Continuity packet:\n{continuity_packet}",
|
|
148
|
+
"Tool capabilities:\n{tool_capabilities}",
|
|
149
|
+
"Data access policy:\n{data_access_policy}",
|
|
150
|
+
"Repository context note:\n{repository_context_note}",
|
|
151
|
+
"User query:\n{user_query}",
|
|
152
|
+
"Retrieved context:\n{retrieved_context}",
|
|
153
|
+
"Correction required:\n{correction_required}",
|
|
154
|
+
"Respond with grounded reasoning and cite source paths.",
|
|
155
|
+
]
|
|
156
|
+
),
|
|
157
|
+
},
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
@staticmethod
|
|
161
|
+
def get_builtin_definition(name: str) -> dict[str, Any] | None:
|
|
162
|
+
definition = PromptRegistry._BUILTIN_DEFINITIONS.get(name)
|
|
163
|
+
if definition is None:
|
|
164
|
+
return None
|
|
165
|
+
return {
|
|
166
|
+
"title": definition["title"],
|
|
167
|
+
"description": definition["description"],
|
|
168
|
+
"arguments": list(definition["arguments"]),
|
|
169
|
+
"defaults": dict(definition.get("defaults", {})),
|
|
170
|
+
"content_template": str(definition["content_template"]),
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
@staticmethod
|
|
174
|
+
def get_builtin_prompt_model(name: str) -> SimpleNamespace | None:
|
|
175
|
+
definition = PromptRegistry.get_builtin_definition(name)
|
|
176
|
+
if definition is None:
|
|
177
|
+
return None
|
|
178
|
+
return SimpleNamespace(
|
|
179
|
+
id=f"builtin:{name}",
|
|
180
|
+
name=name,
|
|
181
|
+
title=definition["title"],
|
|
182
|
+
description=definition["description"],
|
|
183
|
+
content_template=definition["content_template"],
|
|
184
|
+
arguments=list(definition["arguments"]),
|
|
185
|
+
defaults=dict(definition.get("defaults", {})),
|
|
186
|
+
created_at=None,
|
|
187
|
+
updated_at=None,
|
|
188
|
+
is_builtin=True,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
@staticmethod
|
|
192
|
+
async def resolve_prompt_model(
|
|
193
|
+
name: str,
|
|
194
|
+
store: IOperationalStore | None = None,
|
|
195
|
+
) -> Any | None:
|
|
196
|
+
if store is not None:
|
|
197
|
+
prompt = await store.get_prompt_by_name(name)
|
|
198
|
+
if prompt is not None:
|
|
199
|
+
return prompt
|
|
200
|
+
return PromptRegistry.get_builtin_prompt_model(name)
|
|
201
|
+
|
|
202
|
+
@staticmethod
|
|
203
|
+
def render_content_template(
|
|
204
|
+
content_template: str,
|
|
205
|
+
arguments: dict[str, Any],
|
|
206
|
+
*,
|
|
207
|
+
defaults: dict[str, Any] | None = None,
|
|
208
|
+
) -> str:
|
|
209
|
+
rendered = str(content_template)
|
|
210
|
+
merged_arguments = dict(defaults or {})
|
|
211
|
+
merged_arguments.update(arguments)
|
|
212
|
+
for arg_name, arg_val in merged_arguments.items():
|
|
213
|
+
rendered = rendered.replace("{" + str(arg_name) + "}", str(arg_val or ""))
|
|
214
|
+
return rendered
|
|
215
|
+
|
|
216
|
+
@staticmethod
|
|
217
|
+
def _prompt_manager(app: FastMCP) -> Any:
|
|
218
|
+
return getattr(app, "_prompt_manager", None)
|
|
219
|
+
|
|
220
|
+
@staticmethod
|
|
221
|
+
def _prompt_mapping(app: FastMCP) -> dict[str, Any]:
|
|
222
|
+
manager = PromptRegistry._prompt_manager(app)
|
|
223
|
+
if manager is not None and hasattr(manager, "_prompts"):
|
|
224
|
+
return manager._prompts
|
|
225
|
+
|
|
226
|
+
prompts = getattr(app, "_prompts", None)
|
|
227
|
+
if isinstance(prompts, dict):
|
|
228
|
+
return prompts
|
|
229
|
+
|
|
230
|
+
raise AttributeError("Prompt registry storage is not available on app")
|
|
231
|
+
|
|
232
|
+
@staticmethod
|
|
233
|
+
def _upsert_prompt(app: FastMCP, prompt: Prompt) -> None:
|
|
234
|
+
manager = PromptRegistry._prompt_manager(app)
|
|
235
|
+
prompt_mapping = PromptRegistry._prompt_mapping(app)
|
|
236
|
+
existing = prompt_mapping.get(prompt.name)
|
|
237
|
+
if existing is None:
|
|
238
|
+
if hasattr(app, "add_prompt"):
|
|
239
|
+
app.add_prompt(prompt)
|
|
240
|
+
else:
|
|
241
|
+
prompt_mapping[prompt.name] = prompt.fn
|
|
242
|
+
return
|
|
243
|
+
if manager is not None and hasattr(existing, "title"):
|
|
244
|
+
existing.title = prompt.title
|
|
245
|
+
existing.description = prompt.description
|
|
246
|
+
existing.arguments = prompt.arguments
|
|
247
|
+
existing.fn = prompt.fn
|
|
248
|
+
existing.context_kwarg = prompt.context_kwarg
|
|
249
|
+
return
|
|
250
|
+
|
|
251
|
+
prompt_mapping[prompt.name] = prompt.fn
|
|
252
|
+
|
|
253
|
+
@staticmethod
|
|
254
|
+
def _remove_prompt(app: FastMCP, name: str) -> None:
|
|
255
|
+
prompt_mapping = PromptRegistry._prompt_mapping(app)
|
|
256
|
+
prompt_mapping.pop(name, None)
|
|
257
|
+
|
|
258
|
+
@staticmethod
|
|
259
|
+
def _optional_arguments(name: str) -> list[PromptArgument]:
|
|
260
|
+
definition = PromptRegistry._BUILTIN_DEFINITIONS[name]
|
|
261
|
+
return [
|
|
262
|
+
PromptArgument(
|
|
263
|
+
name=argument_name,
|
|
264
|
+
required=False,
|
|
265
|
+
description=f"{argument_name} argument",
|
|
266
|
+
)
|
|
267
|
+
for argument_name in definition["arguments"]
|
|
268
|
+
]
|
|
269
|
+
|
|
270
|
+
@staticmethod
|
|
271
|
+
def _configure_builtin_prompt(prompt: Prompt) -> Prompt:
|
|
272
|
+
prompt.arguments = PromptRegistry._optional_arguments(prompt.name)
|
|
273
|
+
return prompt
|
|
274
|
+
|
|
275
|
+
@staticmethod
|
|
276
|
+
def builtin_prompt_models() -> list[SimpleNamespace]:
|
|
277
|
+
models: list[SimpleNamespace] = []
|
|
278
|
+
for name in PromptRegistry._BUILTIN_DEFINITIONS:
|
|
279
|
+
model = PromptRegistry.get_builtin_prompt_model(name)
|
|
280
|
+
if model is not None:
|
|
281
|
+
models.append(model)
|
|
282
|
+
return models
|
|
283
|
+
|
|
284
|
+
@staticmethod
|
|
285
|
+
def _normalize_argument_names(raw_arguments: Any) -> list[str]:
|
|
286
|
+
if raw_arguments is None:
|
|
287
|
+
return []
|
|
288
|
+
if isinstance(raw_arguments, dict):
|
|
289
|
+
candidates: Iterable[Any] = raw_arguments.keys()
|
|
290
|
+
elif isinstance(raw_arguments, (list, tuple, set)):
|
|
291
|
+
candidates = raw_arguments
|
|
292
|
+
else:
|
|
293
|
+
candidates = [raw_arguments]
|
|
294
|
+
|
|
295
|
+
normalized: list[str] = []
|
|
296
|
+
seen: set[str] = set()
|
|
297
|
+
for item in candidates:
|
|
298
|
+
value = item.get("name") if isinstance(item, dict) else item
|
|
299
|
+
argument = str(value or "").strip()
|
|
300
|
+
if not argument or argument in seen:
|
|
301
|
+
continue
|
|
302
|
+
seen.add(argument)
|
|
303
|
+
normalized.append(argument)
|
|
304
|
+
return normalized
|
|
305
|
+
|
|
306
|
+
@staticmethod
|
|
307
|
+
def _build_dynamic_handler(prompt_model: Any, store: IOperationalStore):
|
|
308
|
+
async def dynamic_handler(**kwargs):
|
|
309
|
+
from minder.auth.context import get_current_principal
|
|
310
|
+
|
|
311
|
+
principal = get_current_principal()
|
|
312
|
+
actor_id = str(principal.principal_id) if principal else "unknown"
|
|
313
|
+
actor_type = principal.principal_type if principal else "unknown"
|
|
314
|
+
client_id = (
|
|
315
|
+
getattr(principal, "client_slug", "unknown") if principal else "unknown"
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
try:
|
|
319
|
+
await store.create_audit_log(
|
|
320
|
+
actor_type=actor_type,
|
|
321
|
+
actor_id=actor_id,
|
|
322
|
+
event_type="prompt_request",
|
|
323
|
+
resource_type="prompt",
|
|
324
|
+
resource_id=prompt_model.name,
|
|
325
|
+
outcome="success",
|
|
326
|
+
audit_metadata={"client_id": client_id},
|
|
327
|
+
)
|
|
328
|
+
except Exception:
|
|
329
|
+
pass
|
|
330
|
+
|
|
331
|
+
content = PromptRegistry.render_content_template(
|
|
332
|
+
str(prompt_model.content_template),
|
|
333
|
+
kwargs,
|
|
334
|
+
defaults=dict(getattr(prompt_model, "defaults", {}) or {}),
|
|
335
|
+
)
|
|
336
|
+
return [{"role": "user", "content": content}]
|
|
337
|
+
|
|
338
|
+
dynamic_handler.__name__ = f"prompt_{prompt_model.name}"
|
|
339
|
+
return dynamic_handler
|
|
340
|
+
|
|
341
|
+
@staticmethod
|
|
342
|
+
def register(app: FastMCP, store: IOperationalStore | None = None) -> None:
|
|
343
|
+
"""Register builtin prompts using a mutable runtime registry."""
|
|
344
|
+
|
|
345
|
+
debug_defaults = PromptRegistry._BUILTIN_DEFINITIONS["debug"]["defaults"]
|
|
346
|
+
review_defaults = PromptRegistry._BUILTIN_DEFINITIONS["review"]["defaults"]
|
|
347
|
+
explain_defaults = PromptRegistry._BUILTIN_DEFINITIONS["explain"]["defaults"]
|
|
348
|
+
tdd_step_defaults = PromptRegistry._BUILTIN_DEFINITIONS["tdd_step"]["defaults"]
|
|
349
|
+
query_reasoning_defaults = PromptRegistry._BUILTIN_DEFINITIONS[
|
|
350
|
+
"query_reasoning"
|
|
351
|
+
]["defaults"]
|
|
352
|
+
|
|
353
|
+
async def _log_prompt(name: str):
|
|
354
|
+
if store is not None:
|
|
355
|
+
from minder.auth.context import get_current_principal
|
|
356
|
+
|
|
357
|
+
p = get_current_principal()
|
|
358
|
+
actor_id = "unknown"
|
|
359
|
+
actor_type = "unknown"
|
|
360
|
+
client_id = "unknown"
|
|
361
|
+
if p:
|
|
362
|
+
actor_id = str(p.principal_id)
|
|
363
|
+
actor_type = p.principal_type
|
|
364
|
+
client_id = getattr(p, "client_slug", "unknown")
|
|
365
|
+
|
|
366
|
+
try:
|
|
367
|
+
await store.create_audit_log(
|
|
368
|
+
actor_type=actor_type,
|
|
369
|
+
actor_id=actor_id,
|
|
370
|
+
event_type="prompt_request",
|
|
371
|
+
resource_type="prompt",
|
|
372
|
+
resource_id=name,
|
|
373
|
+
outcome="success",
|
|
374
|
+
audit_metadata={"client_id": client_id},
|
|
375
|
+
)
|
|
376
|
+
except Exception:
|
|
377
|
+
pass
|
|
378
|
+
|
|
379
|
+
async def debug_prompt(
|
|
380
|
+
error: str = str(debug_defaults["error"]),
|
|
381
|
+
context: str = str(debug_defaults["context"]),
|
|
382
|
+
) -> list[dict[str, str]]:
|
|
383
|
+
await _log_prompt("debug")
|
|
384
|
+
"""Generate a debug analysis prompt.
|
|
385
|
+
|
|
386
|
+
Args:
|
|
387
|
+
error: The error message or stack trace to diagnose.
|
|
388
|
+
context: Optional surrounding context (file, function, recent changes).
|
|
389
|
+
"""
|
|
390
|
+
parts = [f"## Error\n```\n{error}\n```"]
|
|
391
|
+
if context:
|
|
392
|
+
parts.append(f"## Context\n{context}")
|
|
393
|
+
parts += [
|
|
394
|
+
"## Task",
|
|
395
|
+
"1. Identify the root cause of the error above.",
|
|
396
|
+
"2. List 2-3 hypotheses ranked by likelihood.",
|
|
397
|
+
"3. Propose the minimal fix with a code snippet.",
|
|
398
|
+
"4. Describe how to verify the fix.",
|
|
399
|
+
]
|
|
400
|
+
return [{"role": "user", "content": "\n\n".join(parts)}]
|
|
401
|
+
|
|
402
|
+
async def review_prompt(
|
|
403
|
+
diff: str = str(review_defaults["diff"]),
|
|
404
|
+
context: str = str(review_defaults["context"]),
|
|
405
|
+
) -> list[dict[str, str]]:
|
|
406
|
+
await _log_prompt("review")
|
|
407
|
+
"""Generate a code review prompt.
|
|
408
|
+
|
|
409
|
+
Args:
|
|
410
|
+
diff: The unified diff or changed code to review.
|
|
411
|
+
context: Optional ticket description, requirements, or intent.
|
|
412
|
+
"""
|
|
413
|
+
parts = [f"## Diff\n```diff\n{diff}\n```"]
|
|
414
|
+
if context:
|
|
415
|
+
parts.append(f"## Context\n{context}")
|
|
416
|
+
parts += [
|
|
417
|
+
"## Review Checklist",
|
|
418
|
+
"- [ ] **Correctness** — Does the logic match the stated intent?",
|
|
419
|
+
"- [ ] **Edge cases** — Are failure modes and null/empty states handled?",
|
|
420
|
+
"- [ ] **Tests** — Are new behaviours covered by automated tests?",
|
|
421
|
+
"- [ ] **Performance** — Any N+1 queries, unbounded loops, or hot-path regressions?",
|
|
422
|
+
"- [ ] **Security** — Any injection vectors, auth bypass, or data leakage?",
|
|
423
|
+
"- [ ] **Readability** — Is naming clear and are comments useful?",
|
|
424
|
+
"",
|
|
425
|
+
"Provide **BLOCKING** issues first, then **RECOMMENDED** improvements, "
|
|
426
|
+
"then **SUGGESTIONS**.",
|
|
427
|
+
]
|
|
428
|
+
return [{"role": "user", "content": "\n\n".join(parts)}]
|
|
429
|
+
|
|
430
|
+
async def explain_prompt(
|
|
431
|
+
code: str = str(explain_defaults["code"]),
|
|
432
|
+
language: str = str(explain_defaults["language"]),
|
|
433
|
+
) -> list[dict[str, str]]:
|
|
434
|
+
await _log_prompt("explain")
|
|
435
|
+
"""Generate a code explanation prompt.
|
|
436
|
+
|
|
437
|
+
Args:
|
|
438
|
+
code: The source code snippet to explain.
|
|
439
|
+
language: The programming language (default: python).
|
|
440
|
+
"""
|
|
441
|
+
content = "\n\n".join(
|
|
442
|
+
[
|
|
443
|
+
f"## Code ({language})\n```{language}\n{code}\n```",
|
|
444
|
+
"## Task",
|
|
445
|
+
"Explain the code above clearly:",
|
|
446
|
+
"1. **What it does** — one-sentence summary.",
|
|
447
|
+
"2. **How it works** — step-by-step walkthrough of the key logic.",
|
|
448
|
+
"3. **Gotchas** — any non-obvious behaviour, edge cases, or side effects.",
|
|
449
|
+
"4. **Example** — show a concrete usage example if helpful.",
|
|
450
|
+
]
|
|
451
|
+
)
|
|
452
|
+
return [{"role": "user", "content": content}]
|
|
453
|
+
|
|
454
|
+
async def tdd_step_prompt(
|
|
455
|
+
current_step: str = str(tdd_step_defaults["current_step"]),
|
|
456
|
+
failing_tests: str = str(tdd_step_defaults["failing_tests"]),
|
|
457
|
+
) -> list[dict[str, str]]:
|
|
458
|
+
await _log_prompt("tdd_step")
|
|
459
|
+
"""Generate a TDD step guidance prompt.
|
|
460
|
+
|
|
461
|
+
Args:
|
|
462
|
+
current_step: The name of the current workflow step.
|
|
463
|
+
failing_tests: Optional failing test output to include as context.
|
|
464
|
+
"""
|
|
465
|
+
parts = [f"## Current Workflow Step: {current_step}"]
|
|
466
|
+
if failing_tests:
|
|
467
|
+
parts.append(f"## Failing Tests\n```\n{failing_tests}\n```")
|
|
468
|
+
|
|
469
|
+
lowered = current_step.lower()
|
|
470
|
+
if "test" in lowered:
|
|
471
|
+
parts += [
|
|
472
|
+
"## Guidance",
|
|
473
|
+
"You are in the **Test Writing** phase.",
|
|
474
|
+
"1. Write failing tests that specify the exact behaviour required.",
|
|
475
|
+
"2. Do NOT write any implementation code yet.",
|
|
476
|
+
"3. Each test should express a single, clear assertion.",
|
|
477
|
+
"4. Name tests descriptively: `test_<behaviour>_when_<condition>`.",
|
|
478
|
+
]
|
|
479
|
+
elif "implement" in lowered:
|
|
480
|
+
parts += [
|
|
481
|
+
"## Guidance",
|
|
482
|
+
"You are in the **Implementation** phase.",
|
|
483
|
+
"1. Write the minimal code to make all failing tests pass.",
|
|
484
|
+
"2. Do not add features beyond what the tests require.",
|
|
485
|
+
"3. Run tests after every small change.",
|
|
486
|
+
"4. Refactor only after all tests are green.",
|
|
487
|
+
]
|
|
488
|
+
elif "review" in lowered:
|
|
489
|
+
parts += [
|
|
490
|
+
"## Guidance",
|
|
491
|
+
"You are in the **Review** phase.",
|
|
492
|
+
"1. Verify that all acceptance criteria are met.",
|
|
493
|
+
"2. Confirm no regressions exist in the existing test suite.",
|
|
494
|
+
"3. Ensure code quality meets project standards.",
|
|
495
|
+
]
|
|
496
|
+
else:
|
|
497
|
+
parts += [
|
|
498
|
+
"## Guidance",
|
|
499
|
+
f"Complete **{current_step}** fully before advancing to the next step.",
|
|
500
|
+
"Do not skip or partially satisfy prerequisites.",
|
|
501
|
+
]
|
|
502
|
+
|
|
503
|
+
return [{"role": "user", "content": "\n\n".join(parts)}]
|
|
504
|
+
|
|
505
|
+
async def query_reasoning_prompt(
|
|
506
|
+
workflow_instruction: str = str(
|
|
507
|
+
query_reasoning_defaults["workflow_instruction"]
|
|
508
|
+
),
|
|
509
|
+
instruction_envelope: str = str(
|
|
510
|
+
query_reasoning_defaults["instruction_envelope"]
|
|
511
|
+
),
|
|
512
|
+
continuity_brief: str = str(query_reasoning_defaults["continuity_brief"]),
|
|
513
|
+
continuity_packet: str = str(query_reasoning_defaults["continuity_packet"]),
|
|
514
|
+
user_query: str = str(query_reasoning_defaults["user_query"]),
|
|
515
|
+
retrieved_context: str = str(query_reasoning_defaults["retrieved_context"]),
|
|
516
|
+
correction_required: str = str(
|
|
517
|
+
query_reasoning_defaults["correction_required"]
|
|
518
|
+
),
|
|
519
|
+
) -> list[dict[str, str]]:
|
|
520
|
+
await _log_prompt("query_reasoning")
|
|
521
|
+
content = PromptRegistry.render_content_template(
|
|
522
|
+
str(
|
|
523
|
+
PromptRegistry._BUILTIN_DEFINITIONS["query_reasoning"][
|
|
524
|
+
"content_template"
|
|
525
|
+
]
|
|
526
|
+
),
|
|
527
|
+
{
|
|
528
|
+
"workflow_instruction": workflow_instruction,
|
|
529
|
+
"instruction_envelope": instruction_envelope,
|
|
530
|
+
"continuity_brief": continuity_brief,
|
|
531
|
+
"continuity_packet": continuity_packet,
|
|
532
|
+
"user_query": user_query,
|
|
533
|
+
"retrieved_context": retrieved_context,
|
|
534
|
+
"correction_required": correction_required,
|
|
535
|
+
},
|
|
536
|
+
defaults=query_reasoning_defaults,
|
|
537
|
+
)
|
|
538
|
+
return [{"role": "user", "content": content}]
|
|
539
|
+
|
|
540
|
+
PromptRegistry._upsert_prompt(
|
|
541
|
+
app,
|
|
542
|
+
PromptRegistry._configure_builtin_prompt(
|
|
543
|
+
Prompt.from_function(
|
|
544
|
+
debug_prompt,
|
|
545
|
+
name="debug",
|
|
546
|
+
title=PromptRegistry._BUILTIN_DEFINITIONS["debug"]["title"],
|
|
547
|
+
description=PromptRegistry._BUILTIN_DEFINITIONS["debug"][
|
|
548
|
+
"description"
|
|
549
|
+
],
|
|
550
|
+
)
|
|
551
|
+
),
|
|
552
|
+
)
|
|
553
|
+
PromptRegistry._upsert_prompt(
|
|
554
|
+
app,
|
|
555
|
+
PromptRegistry._configure_builtin_prompt(
|
|
556
|
+
Prompt.from_function(
|
|
557
|
+
review_prompt,
|
|
558
|
+
name="review",
|
|
559
|
+
title=PromptRegistry._BUILTIN_DEFINITIONS["review"]["title"],
|
|
560
|
+
description=PromptRegistry._BUILTIN_DEFINITIONS["review"][
|
|
561
|
+
"description"
|
|
562
|
+
],
|
|
563
|
+
)
|
|
564
|
+
),
|
|
565
|
+
)
|
|
566
|
+
PromptRegistry._upsert_prompt(
|
|
567
|
+
app,
|
|
568
|
+
PromptRegistry._configure_builtin_prompt(
|
|
569
|
+
Prompt.from_function(
|
|
570
|
+
explain_prompt,
|
|
571
|
+
name="explain",
|
|
572
|
+
title=PromptRegistry._BUILTIN_DEFINITIONS["explain"]["title"],
|
|
573
|
+
description=PromptRegistry._BUILTIN_DEFINITIONS["explain"][
|
|
574
|
+
"description"
|
|
575
|
+
],
|
|
576
|
+
)
|
|
577
|
+
),
|
|
578
|
+
)
|
|
579
|
+
PromptRegistry._upsert_prompt(
|
|
580
|
+
app,
|
|
581
|
+
PromptRegistry._configure_builtin_prompt(
|
|
582
|
+
Prompt.from_function(
|
|
583
|
+
tdd_step_prompt,
|
|
584
|
+
name="tdd_step",
|
|
585
|
+
title=PromptRegistry._BUILTIN_DEFINITIONS["tdd_step"]["title"],
|
|
586
|
+
description=PromptRegistry._BUILTIN_DEFINITIONS["tdd_step"][
|
|
587
|
+
"description"
|
|
588
|
+
],
|
|
589
|
+
)
|
|
590
|
+
),
|
|
591
|
+
)
|
|
592
|
+
PromptRegistry._upsert_prompt(
|
|
593
|
+
app,
|
|
594
|
+
PromptRegistry._configure_builtin_prompt(
|
|
595
|
+
Prompt.from_function(
|
|
596
|
+
query_reasoning_prompt,
|
|
597
|
+
name="query_reasoning",
|
|
598
|
+
title=PromptRegistry._BUILTIN_DEFINITIONS["query_reasoning"][
|
|
599
|
+
"title"
|
|
600
|
+
],
|
|
601
|
+
description=PromptRegistry._BUILTIN_DEFINITIONS["query_reasoning"][
|
|
602
|
+
"description"
|
|
603
|
+
],
|
|
604
|
+
)
|
|
605
|
+
),
|
|
606
|
+
)
|
|
607
|
+
|
|
608
|
+
@staticmethod
|
|
609
|
+
async def sync(app: FastMCP, store: IOperationalStore) -> None:
|
|
610
|
+
"""Synchronize FastMCP prompts with builtin and database-backed prompt data."""
|
|
611
|
+
PromptRegistry.register(app, store=store)
|
|
612
|
+
try:
|
|
613
|
+
prompts = await store.list_prompts()
|
|
614
|
+
except Exception:
|
|
615
|
+
return
|
|
616
|
+
|
|
617
|
+
dynamic_names: set[str] = set()
|
|
618
|
+
for prompt_model in prompts:
|
|
619
|
+
argument_names = PromptRegistry._normalize_argument_names(
|
|
620
|
+
getattr(prompt_model, "arguments", None)
|
|
621
|
+
)
|
|
622
|
+
dynamic_handler = PromptRegistry._build_dynamic_handler(prompt_model, store)
|
|
623
|
+
|
|
624
|
+
dynamic_prompt = Prompt.from_function(
|
|
625
|
+
dynamic_handler,
|
|
626
|
+
name=str(prompt_model.name),
|
|
627
|
+
title=str(prompt_model.title),
|
|
628
|
+
description=str(prompt_model.description),
|
|
629
|
+
)
|
|
630
|
+
dynamic_prompt.arguments = [
|
|
631
|
+
PromptArgument(
|
|
632
|
+
name=name, required=False, description=f"{name} argument"
|
|
633
|
+
)
|
|
634
|
+
for name in argument_names
|
|
635
|
+
]
|
|
636
|
+
PromptRegistry._upsert_prompt(app, dynamic_prompt)
|
|
637
|
+
dynamic_names.add(str(prompt_model.name))
|
|
638
|
+
|
|
639
|
+
previous_dynamic_names = set(
|
|
640
|
+
getattr(app, "_minder_dynamic_prompt_names", set())
|
|
641
|
+
)
|
|
642
|
+
for stale_name in previous_dynamic_names - dynamic_names:
|
|
643
|
+
if stale_name not in PromptRegistry._BUILTIN_NAMES:
|
|
644
|
+
PromptRegistry._remove_prompt(app, stale_name)
|
|
645
|
+
|
|
646
|
+
setattr(app, "_minder_dynamic_prompt_names", dynamic_names)
|