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.
Files changed (132) hide show
  1. minder/__init__.py +12 -0
  2. minder/api/routers/prompts.py +177 -0
  3. minder/application/__init__.py +1 -0
  4. minder/application/admin/__init__.py +11 -0
  5. minder/application/admin/dto.py +453 -0
  6. minder/application/admin/jobs.py +327 -0
  7. minder/application/admin/use_cases.py +1895 -0
  8. minder/auth/__init__.py +12 -0
  9. minder/auth/context.py +26 -0
  10. minder/auth/middleware.py +70 -0
  11. minder/auth/principal.py +59 -0
  12. minder/auth/rate_limiter.py +89 -0
  13. minder/auth/rbac.py +60 -0
  14. minder/auth/service.py +541 -0
  15. minder/bootstrap/__init__.py +9 -0
  16. minder/bootstrap/providers.py +109 -0
  17. minder/bootstrap/transport.py +807 -0
  18. minder/cache/__init__.py +10 -0
  19. minder/cache/providers.py +140 -0
  20. minder/chunking/__init__.py +4 -0
  21. minder/chunking/code_splitter.py +184 -0
  22. minder/chunking/splitter.py +136 -0
  23. minder/cli.py +1542 -0
  24. minder/config.py +179 -0
  25. minder/continuity.py +363 -0
  26. minder/dev.py +160 -0
  27. minder/embedding/__init__.py +9 -0
  28. minder/embedding/base.py +7 -0
  29. minder/embedding/local.py +65 -0
  30. minder/embedding/openai.py +7 -0
  31. minder/graph/__init__.py +11 -0
  32. minder/graph/edges.py +13 -0
  33. minder/graph/executor.py +127 -0
  34. minder/graph/graph.py +263 -0
  35. minder/graph/nodes/__init__.py +27 -0
  36. minder/graph/nodes/evaluator.py +21 -0
  37. minder/graph/nodes/guard.py +64 -0
  38. minder/graph/nodes/llm.py +59 -0
  39. minder/graph/nodes/planning.py +30 -0
  40. minder/graph/nodes/reasoning.py +87 -0
  41. minder/graph/nodes/reranker.py +141 -0
  42. minder/graph/nodes/retriever.py +86 -0
  43. minder/graph/nodes/verification.py +230 -0
  44. minder/graph/nodes/workflow_planner.py +250 -0
  45. minder/graph/runtime.py +15 -0
  46. minder/graph/state.py +26 -0
  47. minder/llm/__init__.py +5 -0
  48. minder/llm/base.py +14 -0
  49. minder/llm/local.py +381 -0
  50. minder/llm/openai.py +89 -0
  51. minder/models/__init__.py +109 -0
  52. minder/models/base.py +10 -0
  53. minder/models/client.py +137 -0
  54. minder/models/document.py +34 -0
  55. minder/models/error.py +32 -0
  56. minder/models/graph.py +114 -0
  57. minder/models/history.py +32 -0
  58. minder/models/job.py +62 -0
  59. minder/models/prompt.py +41 -0
  60. minder/models/repository.py +62 -0
  61. minder/models/rule.py +68 -0
  62. minder/models/session.py +51 -0
  63. minder/models/skill.py +52 -0
  64. minder/models/user.py +41 -0
  65. minder/models/workflow.py +35 -0
  66. minder/observability/__init__.py +57 -0
  67. minder/observability/audit.py +243 -0
  68. minder/observability/logging.py +253 -0
  69. minder/observability/metrics.py +448 -0
  70. minder/observability/tracing.py +215 -0
  71. minder/presentation/__init__.py +1 -0
  72. minder/presentation/http/__init__.py +1 -0
  73. minder/presentation/http/admin/__init__.py +3 -0
  74. minder/presentation/http/admin/api.py +1309 -0
  75. minder/presentation/http/admin/context.py +94 -0
  76. minder/presentation/http/admin/dashboard.py +111 -0
  77. minder/presentation/http/admin/jobs.py +208 -0
  78. minder/presentation/http/admin/memories.py +185 -0
  79. minder/presentation/http/admin/prompts.py +219 -0
  80. minder/presentation/http/admin/routes.py +127 -0
  81. minder/presentation/http/admin/runtime.py +650 -0
  82. minder/presentation/http/admin/search.py +368 -0
  83. minder/presentation/http/admin/skills.py +230 -0
  84. minder/prompts/__init__.py +646 -0
  85. minder/prompts/formatter.py +142 -0
  86. minder/resources/__init__.py +318 -0
  87. minder/retrieval/__init__.py +5 -0
  88. minder/retrieval/hybrid.py +178 -0
  89. minder/retrieval/mmr.py +116 -0
  90. minder/retrieval/multi_hop.py +115 -0
  91. minder/runtime.py +15 -0
  92. minder/server.py +145 -0
  93. minder/store/__init__.py +64 -0
  94. minder/store/document.py +115 -0
  95. minder/store/error.py +82 -0
  96. minder/store/feedback.py +114 -0
  97. minder/store/graph.py +588 -0
  98. minder/store/history.py +57 -0
  99. minder/store/interfaces.py +512 -0
  100. minder/store/milvus/__init__.py +11 -0
  101. minder/store/milvus/client.py +26 -0
  102. minder/store/milvus/collections.py +15 -0
  103. minder/store/milvus/vector_store.py +232 -0
  104. minder/store/mongodb/__init__.py +11 -0
  105. minder/store/mongodb/client.py +49 -0
  106. minder/store/mongodb/indexes.py +90 -0
  107. minder/store/mongodb/operational_store.py +993 -0
  108. minder/store/relational.py +1087 -0
  109. minder/store/repo_state.py +58 -0
  110. minder/store/rule.py +93 -0
  111. minder/store/vector.py +79 -0
  112. minder/tools/__init__.py +47 -0
  113. minder/tools/auth.py +94 -0
  114. minder/tools/graph.py +839 -0
  115. minder/tools/ingest.py +353 -0
  116. minder/tools/memory.py +381 -0
  117. minder/tools/query.py +307 -0
  118. minder/tools/registry.py +269 -0
  119. minder/tools/repo_scanner.py +1266 -0
  120. minder/tools/search.py +15 -0
  121. minder/tools/session.py +316 -0
  122. minder/tools/skills.py +899 -0
  123. minder/tools/workflow.py +215 -0
  124. minder/transport/__init__.py +4 -0
  125. minder/transport/base.py +286 -0
  126. minder/transport/sse.py +252 -0
  127. minder/transport/stdio.py +29 -0
  128. minder_cli-0.2.0.dist-info/METADATA +318 -0
  129. minder_cli-0.2.0.dist-info/RECORD +132 -0
  130. minder_cli-0.2.0.dist-info/WHEEL +4 -0
  131. minder_cli-0.2.0.dist-info/entry_points.txt +2 -0
  132. minder_cli-0.2.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,327 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import contextlib
5
+ import uuid
6
+ from collections.abc import AsyncIterator, Awaitable, Callable
7
+ from datetime import UTC, datetime
8
+ from typing import Any
9
+
10
+ from minder.config import MinderConfig
11
+ from minder.store.interfaces import IOperationalStore
12
+ from minder.tools.skills import SkillTools
13
+
14
+
15
+ def _utcnow() -> datetime:
16
+ return datetime.now(UTC)
17
+
18
+
19
+ class AdminJobService:
20
+ _MAX_EVENT_HISTORY = 100
21
+
22
+ def __init__(self, store: IOperationalStore, config: MinderConfig) -> None:
23
+ self._store = store
24
+ self._config = config
25
+ self._tasks: dict[str, asyncio.Task[None]] = {}
26
+ self._subscribers: dict[
27
+ int, tuple[asyncio.Queue[dict[str, Any]], Callable[[dict[str, Any]], bool]]
28
+ ] = {}
29
+ self._subscriber_id = 0
30
+ self._lock = asyncio.Lock()
31
+
32
+ async def enqueue(
33
+ self,
34
+ *,
35
+ job_type: str,
36
+ title: str,
37
+ requested_by_user_id: uuid.UUID | None,
38
+ payload: dict[str, Any],
39
+ ) -> Any:
40
+ job = await self._store.create_admin_job(
41
+ id=uuid.uuid4(),
42
+ job_type=job_type,
43
+ title=title,
44
+ status="queued",
45
+ requested_by_user_id=requested_by_user_id,
46
+ payload=payload,
47
+ result_payload=None,
48
+ error_message=None,
49
+ progress_current=0,
50
+ progress_total=0,
51
+ message="Queued",
52
+ events=[
53
+ self._event_payload(
54
+ event_type="queued", status="queued", message="Job queued"
55
+ )
56
+ ],
57
+ created_at=_utcnow(),
58
+ updated_at=_utcnow(),
59
+ started_at=None,
60
+ finished_at=None,
61
+ )
62
+ await self._publish(self._snapshot(job))
63
+ self._ensure_running(job)
64
+ return job
65
+
66
+ def subscribe(
67
+ self,
68
+ *,
69
+ job_id: str | None = None,
70
+ job_type: str | None = None,
71
+ statuses: set[str] | None = None,
72
+ ) -> tuple[asyncio.Queue[dict[str, Any]], Callable[[], None]]:
73
+ queue: asyncio.Queue[dict[str, Any]] = asyncio.Queue()
74
+
75
+ def _matches(payload: dict[str, Any]) -> bool:
76
+ if job_id and str(payload.get("id")) != job_id:
77
+ return False
78
+ if job_type and str(payload.get("job_type")) != job_type:
79
+ return False
80
+ if statuses and str(payload.get("status")) not in statuses:
81
+ return False
82
+ return True
83
+
84
+ subscriber_key = self._subscriber_id
85
+ self._subscriber_id += 1
86
+ self._subscribers[subscriber_key] = (queue, _matches)
87
+
88
+ def unsubscribe() -> None:
89
+ self._subscribers.pop(subscriber_key, None)
90
+
91
+ return queue, unsubscribe
92
+
93
+ async def publish_existing(self, job: Any) -> None:
94
+ await self._publish(self._snapshot(job))
95
+
96
+ def _ensure_running(self, job: Any) -> None:
97
+ job_id = str(job.id)
98
+ existing = self._tasks.get(job_id)
99
+ if existing is not None and not existing.done():
100
+ return
101
+ self._tasks[job_id] = asyncio.create_task(self._run(job_id))
102
+
103
+ async def _run(self, job_id: str) -> None:
104
+ try:
105
+ job = await self._store.get_admin_job_by_id(uuid.UUID(job_id))
106
+ if job is None:
107
+ return
108
+ await self._update_job(
109
+ job_id,
110
+ status="running",
111
+ message="Starting job",
112
+ started_at=_utcnow(),
113
+ error_message=None,
114
+ events=self._append_event_list(
115
+ list(getattr(job, "events", []) or []),
116
+ self._event_payload(
117
+ event_type="started",
118
+ status="running",
119
+ message="Job started",
120
+ ),
121
+ ),
122
+ )
123
+ if str(getattr(job, "job_type", "")) == "skill_import_git":
124
+ await self._run_skill_import(job_id)
125
+ return
126
+ raise ValueError(
127
+ f"Unsupported admin job type: {getattr(job, 'job_type', None)}"
128
+ )
129
+ except Exception as exc:
130
+ await self._fail_job(job_id, str(exc))
131
+ finally:
132
+ self._tasks.pop(job_id, None)
133
+
134
+ async def _run_skill_import(self, job_id: str) -> None:
135
+ job = await self._store.get_admin_job_by_id(uuid.UUID(job_id))
136
+ if job is None:
137
+ return
138
+ payload = dict(getattr(job, "payload", {}) or {})
139
+ tools = SkillTools(self._store, self._config)
140
+
141
+ async def emit_progress(update: dict[str, Any]) -> None:
142
+ current_job = await self._store.get_admin_job_by_id(uuid.UUID(job_id))
143
+ if current_job is None:
144
+ return
145
+ message = str(
146
+ update.get("message") or getattr(current_job, "message", "Running")
147
+ )
148
+ progress_current = int(
149
+ update.get(
150
+ "progress_current", getattr(current_job, "progress_current", 0)
151
+ )
152
+ or 0
153
+ )
154
+ progress_total = int(
155
+ update.get("progress_total", getattr(current_job, "progress_total", 0))
156
+ or 0
157
+ )
158
+ details = (
159
+ update.get("details")
160
+ if isinstance(update.get("details"), dict)
161
+ else None
162
+ )
163
+ next_events = self._append_event_list(
164
+ list(getattr(current_job, "events", []) or []),
165
+ self._event_payload(
166
+ event_type=str(update.get("event_type") or "progress"),
167
+ status="running",
168
+ message=message,
169
+ progress_current=progress_current,
170
+ progress_total=progress_total,
171
+ details=details,
172
+ ),
173
+ )
174
+ await self._update_job(
175
+ job_id,
176
+ status="running",
177
+ message=message,
178
+ progress_current=progress_current,
179
+ progress_total=progress_total,
180
+ events=next_events,
181
+ )
182
+
183
+ result = await tools.minder_skill_import_git(
184
+ repo_url=str(payload.get("repo_url") or ""),
185
+ source_path=str(payload.get("path") or "skills"),
186
+ ref=str(payload.get("ref")) if payload.get("ref") else None,
187
+ provider=str(payload.get("provider")) if payload.get("provider") else None,
188
+ excerpt_kind=str(payload.get("excerpt_kind") or "none"),
189
+ progress_callback=emit_progress,
190
+ )
191
+ completed_job = await self._store.get_admin_job_by_id(uuid.UUID(job_id))
192
+ completed_events = self._append_event_list(
193
+ list(getattr(completed_job, "events", []) or []),
194
+ self._event_payload(
195
+ event_type="completed",
196
+ status="completed",
197
+ message="Job completed",
198
+ progress_current=int(result.get("imported_count", 0) or 0),
199
+ progress_total=int(result.get("imported_count", 0) or 0),
200
+ details={"imported_count": result.get("imported_count", 0)},
201
+ ),
202
+ )
203
+ await self._update_job(
204
+ job_id,
205
+ status="completed",
206
+ message="Skill import completed",
207
+ progress_current=int(result.get("imported_count", 0) or 0),
208
+ progress_total=int(result.get("imported_count", 0) or 0),
209
+ result_payload=result,
210
+ error_message=None,
211
+ finished_at=_utcnow(),
212
+ events=completed_events,
213
+ )
214
+
215
+ async def _fail_job(self, job_id: str, error_message: str) -> None:
216
+ job = await self._store.get_admin_job_by_id(uuid.UUID(job_id))
217
+ existing_events = (
218
+ list(getattr(job, "events", []) or []) if job is not None else []
219
+ )
220
+ await self._update_job(
221
+ job_id,
222
+ status="failed",
223
+ message=error_message,
224
+ error_message=error_message,
225
+ finished_at=_utcnow(),
226
+ events=self._append_event_list(
227
+ existing_events,
228
+ self._event_payload(
229
+ event_type="failed",
230
+ status="failed",
231
+ message=error_message,
232
+ ),
233
+ ),
234
+ )
235
+
236
+ async def _update_job(self, job_id: str, **kwargs: Any) -> Any:
237
+ kwargs["updated_at"] = _utcnow()
238
+ updated = await self._store.update_admin_job(uuid.UUID(job_id), **kwargs)
239
+ if updated is not None:
240
+ await self._publish(self._snapshot(updated))
241
+ return updated
242
+
243
+ async def _publish(self, payload: dict[str, Any]) -> None:
244
+ async with self._lock:
245
+ subscribers = list(self._subscribers.values())
246
+ for queue, matcher in subscribers:
247
+ if matcher(payload):
248
+ with contextlib.suppress(asyncio.QueueFull):
249
+ queue.put_nowait(payload)
250
+
251
+ @classmethod
252
+ def _event_payload(
253
+ cls,
254
+ *,
255
+ event_type: str,
256
+ status: str,
257
+ message: str,
258
+ progress_current: int | None = None,
259
+ progress_total: int | None = None,
260
+ details: dict[str, Any] | None = None,
261
+ ) -> dict[str, Any]:
262
+ return {
263
+ "id": str(uuid.uuid4()),
264
+ "event_type": event_type,
265
+ "status": status,
266
+ "message": message,
267
+ "progress_current": progress_current,
268
+ "progress_total": progress_total,
269
+ "details": details or {},
270
+ "created_at": _utcnow().isoformat(),
271
+ }
272
+
273
+ @classmethod
274
+ def _append_event_list(
275
+ cls,
276
+ existing: list[dict[str, Any]],
277
+ event: dict[str, Any],
278
+ ) -> list[dict[str, Any]]:
279
+ combined = [*existing, event]
280
+ if len(combined) <= cls._MAX_EVENT_HISTORY:
281
+ return combined
282
+ return combined[-cls._MAX_EVENT_HISTORY :]
283
+
284
+ @staticmethod
285
+ def _snapshot(job: Any) -> dict[str, Any]:
286
+ created_at = getattr(job, "created_at", None)
287
+ updated_at = getattr(job, "updated_at", None)
288
+ started_at = getattr(job, "started_at", None)
289
+ finished_at = getattr(job, "finished_at", None)
290
+ return {
291
+ "id": str(job.id),
292
+ "job_type": str(getattr(job, "job_type", "")),
293
+ "title": str(getattr(job, "title", "")),
294
+ "status": str(getattr(job, "status", "queued")),
295
+ "requested_by_user_id": (
296
+ str(getattr(job, "requested_by_user_id", ""))
297
+ if getattr(job, "requested_by_user_id", None)
298
+ else None
299
+ ),
300
+ "payload": dict(getattr(job, "payload", {}) or {}),
301
+ "result_payload": getattr(job, "result_payload", None),
302
+ "error_message": getattr(job, "error_message", None),
303
+ "progress_current": int(getattr(job, "progress_current", 0) or 0),
304
+ "progress_total": int(getattr(job, "progress_total", 0) or 0),
305
+ "message": getattr(job, "message", None),
306
+ "events": list(getattr(job, "events", []) or []),
307
+ "created_at": created_at.isoformat() if created_at else None,
308
+ "updated_at": updated_at.isoformat() if updated_at else None,
309
+ "started_at": started_at.isoformat() if started_at else None,
310
+ "finished_at": finished_at.isoformat() if finished_at else None,
311
+ }
312
+
313
+
314
+ async def iter_job_stream(
315
+ *,
316
+ queue: asyncio.Queue[dict[str, Any]],
317
+ request_is_disconnected: Callable[[], Awaitable[bool]],
318
+ ) -> AsyncIterator[dict[str, Any]]:
319
+ while True:
320
+ if await request_is_disconnected():
321
+ return
322
+ try:
323
+ payload = await asyncio.wait_for(queue.get(), timeout=15)
324
+ except TimeoutError:
325
+ yield {"type": "keepalive"}
326
+ continue
327
+ yield {"type": "job", "payload": payload}