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,232 @@
1
+ """
2
+ Milvus Vector Store — implements IVectorStore using PyMilvus.
3
+
4
+ All operations execute in a thread pool since PyMilvus is synchronous.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ import logging
11
+ import uuid
12
+ from typing import Any
13
+
14
+ from minder.store.interfaces import IDocumentRepository
15
+ from minder.store.milvus.client import MilvusClient
16
+ from minder.store.milvus.collections import get_document_schema
17
+
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ class MilvusVectorStore:
23
+ def __init__(
24
+ self,
25
+ client: MilvusClient,
26
+ document_store: IDocumentRepository,
27
+ prefix: str = "minder_",
28
+ dimensions: int = 768,
29
+ ) -> None:
30
+ self._client = client
31
+ self._document_store = document_store
32
+ self._prefix = prefix
33
+ self._dimensions = dimensions
34
+ self._doc_collection = f"{prefix}documents"
35
+ self._collection_ready = False
36
+ self._collection_lock = asyncio.Lock()
37
+
38
+ async def setup(self) -> None:
39
+ await self._ensure_collection_ready(force=True)
40
+
41
+ async def _ensure_collection_ready(self, *, force: bool = False) -> None:
42
+ if self._collection_ready and not force:
43
+ return
44
+
45
+ async with self._collection_lock:
46
+ if self._collection_ready and not force:
47
+ return
48
+
49
+ loop = asyncio.get_running_loop()
50
+
51
+ def _setup() -> None:
52
+ if self._client.client.has_collection(self._doc_collection):
53
+ metadata = self._client.client.describe_collection(self._doc_collection)
54
+ current_dim = self._extract_embedding_dimension(metadata)
55
+ if current_dim is not None and current_dim != self._dimensions:
56
+ logger.warning(
57
+ "Milvus collection %s dimension mismatch: existing=%s expected=%s; recreating collection for reindex",
58
+ self._doc_collection,
59
+ current_dim,
60
+ self._dimensions,
61
+ )
62
+ self._client.client.drop_collection(self._doc_collection)
63
+
64
+ if not self._client.client.has_collection(self._doc_collection):
65
+ self._create_collection()
66
+
67
+ await loop.run_in_executor(None, _setup)
68
+ self._collection_ready = True
69
+
70
+ def _create_collection(self) -> None:
71
+ schema = get_document_schema(self._dimensions)
72
+ index_params = self._client.client.prepare_index_params()
73
+ index_params.add_index(
74
+ field_name="embedding",
75
+ index_type="AUTOINDEX",
76
+ metric_type="COSINE",
77
+ )
78
+ self._client.client.create_collection(
79
+ collection_name=self._doc_collection,
80
+ schema=schema,
81
+ index_params=index_params,
82
+ )
83
+
84
+ @staticmethod
85
+ def _extract_embedding_dimension(metadata: Any) -> int | None:
86
+ if not isinstance(metadata, dict):
87
+ return None
88
+ fields = metadata.get("fields", [])
89
+ if not isinstance(fields, list):
90
+ return None
91
+
92
+ for field in fields:
93
+ if not isinstance(field, dict):
94
+ continue
95
+ field_name = field.get("name") or field.get("field_name")
96
+ if field_name != "embedding":
97
+ continue
98
+
99
+ for key in ("params", "type_params", "element_type_params"):
100
+ params = field.get(key)
101
+ if isinstance(params, dict) and params.get("dim") is not None:
102
+ return int(params["dim"])
103
+
104
+ if field.get("dim") is not None:
105
+ return int(field["dim"])
106
+
107
+ return None
108
+
109
+ def _validate_embedding_length(self, embedding: list[float]) -> None:
110
+ if len(embedding) != self._dimensions:
111
+ raise ValueError(
112
+ f"Embedding length {len(embedding)} does not match configured Milvus dimension {self._dimensions}"
113
+ )
114
+
115
+ @staticmethod
116
+ def _serialize_payload(payload: dict[str, Any]) -> dict[str, Any]:
117
+ return {
118
+ "title": str(payload.get("title", "")),
119
+ "source_path": str(payload.get("source_path", payload.get("path", ""))),
120
+ "doc_type": str(payload.get("doc_type", "")),
121
+ "project": str(payload.get("project", "")),
122
+ }
123
+
124
+ async def upsert_document(
125
+ self,
126
+ doc_id: uuid.UUID,
127
+ embedding: list[float],
128
+ payload: dict[str, Any],
129
+ ) -> None:
130
+ self._validate_embedding_length(embedding)
131
+ await self._ensure_collection_ready()
132
+ loop = asyncio.get_running_loop()
133
+
134
+ def _upsert() -> None:
135
+ self._client.client.upsert(
136
+ collection_name=self._doc_collection,
137
+ data=[
138
+ {
139
+ "id": str(doc_id),
140
+ "embedding": embedding,
141
+ "project": payload.get("project", ""),
142
+ "doc_type": payload.get("doc_type", ""),
143
+ "payload": self._serialize_payload(payload),
144
+ }
145
+ ],
146
+ )
147
+
148
+ await loop.run_in_executor(None, _upsert)
149
+
150
+ async def delete_documents(self, doc_ids: list[uuid.UUID]) -> None:
151
+ if not doc_ids:
152
+ return
153
+ loop = asyncio.get_running_loop()
154
+
155
+ def _delete() -> None:
156
+ id_list = [f"'{did}'" for did in doc_ids]
157
+ expr = f"id in [{', '.join(id_list)}]"
158
+ self._client.client.delete(
159
+ collection_name=self._doc_collection,
160
+ filter=expr,
161
+ )
162
+
163
+ await loop.run_in_executor(None, _delete)
164
+
165
+ async def search_documents(
166
+ self,
167
+ query_embedding: list[float],
168
+ *,
169
+ project: str | None = None,
170
+ doc_types: set[str] | None = None,
171
+ limit: int = 5,
172
+ score_threshold: float = 0.0,
173
+ ) -> list[dict[str, Any]]:
174
+ self._validate_embedding_length(query_embedding)
175
+ await self._ensure_collection_ready()
176
+ loop = asyncio.get_running_loop()
177
+
178
+ filter_expr = ""
179
+ filters = []
180
+ if project:
181
+ filters.append(f'project == "{project}"')
182
+ if doc_types:
183
+ types_str = ", ".join(f'"{t}"' for t in doc_types)
184
+ filters.append(f"doc_type in [{types_str}]")
185
+
186
+ if filters:
187
+ filter_expr = " and ".join(filters)
188
+
189
+ def _search() -> Any:
190
+ return self._client.client.search(
191
+ collection_name=self._doc_collection,
192
+ data=[query_embedding],
193
+ filter=filter_expr,
194
+ limit=limit,
195
+ output_fields=["payload"],
196
+ )
197
+
198
+ results = await loop.run_in_executor(None, _search)
199
+
200
+ hits_with_payload: list[tuple[Any, dict[str, Any]]] = []
201
+ doc_ids: list[uuid.UUID] = []
202
+ ranked: list[dict[str, Any]] = []
203
+ if results and len(results) > 0:
204
+ for hit in results[0]:
205
+ if hit.distance < score_threshold:
206
+ continue
207
+ payload = hit.entity.get("payload", {})
208
+ doc_id = uuid.UUID(hit.id) if isinstance(hit.id, str) else hit.id
209
+ if isinstance(doc_id, uuid.UUID):
210
+ doc_ids.append(doc_id)
211
+ hits_with_payload.append((hit, payload if isinstance(payload, dict) else {}))
212
+
213
+ docs_by_id = {
214
+ doc.id: doc for doc in await self._document_store.get_documents_by_ids(doc_ids)
215
+ }
216
+
217
+ for hit, payload in hits_with_payload:
218
+ doc_id = uuid.UUID(hit.id) if isinstance(hit.id, str) else hit.id
219
+ doc = docs_by_id.get(doc_id) if isinstance(doc_id, uuid.UUID) else None
220
+ ranked.append(
221
+ {
222
+ "id": doc_id,
223
+ "title": getattr(doc, "title", None) or payload.get("title", ""),
224
+ "path": getattr(doc, "source_path", None)
225
+ or payload.get("source_path", payload.get("path", "")),
226
+ "content": getattr(doc, "content", None) or "",
227
+ "score": round(hit.distance, 4),
228
+ "doc_type": getattr(doc, "doc_type", None) or payload.get("doc_type", ""),
229
+ }
230
+ )
231
+
232
+ return ranked
@@ -0,0 +1,11 @@
1
+ """
2
+ MongoDB Store Package — Motor-based async adapters implementing domain interfaces.
3
+ """
4
+
5
+ from minder.store.mongodb.client import MongoClient
6
+ from minder.store.mongodb.operational_store import MongoOperationalStore
7
+
8
+ __all__ = [
9
+ "MongoClient",
10
+ "MongoOperationalStore",
11
+ ]
@@ -0,0 +1,49 @@
1
+ """
2
+ MongoDB Async Client — Motor singleton with connection pooling.
3
+
4
+ Usage:
5
+ client = MongoClient(uri="mongodb://localhost:27017", database="minder")
6
+ db = client.db
7
+ await client.health_check()
8
+ await client.close()
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorDatabase
14
+
15
+
16
+ class MongoClient:
17
+ """Thin wrapper around Motor's async client with lifecycle helpers."""
18
+
19
+ def __init__(
20
+ self,
21
+ uri: str = "mongodb://localhost:27017",
22
+ database: str = "minder",
23
+ *,
24
+ min_pool_size: int = 5,
25
+ max_pool_size: int = 50,
26
+ ) -> None:
27
+ self._client: AsyncIOMotorClient = AsyncIOMotorClient( # type: ignore[type-arg]
28
+ uri,
29
+ minPoolSize=min_pool_size,
30
+ maxPoolSize=max_pool_size,
31
+ serverSelectionTimeoutMS=5000,
32
+ )
33
+ self._db: AsyncIOMotorDatabase = self._client[database] # type: ignore[type-arg]
34
+
35
+ @property
36
+ def db(self) -> AsyncIOMotorDatabase: # type: ignore[type-arg]
37
+ return self._db
38
+
39
+ async def health_check(self) -> bool:
40
+ """Ping the MongoDB server."""
41
+ try:
42
+ await self._client.admin.command("ping")
43
+ return True
44
+ except Exception:
45
+ return False
46
+
47
+ async def close(self) -> None:
48
+ """Close the Motor client."""
49
+ self._client.close()
@@ -0,0 +1,90 @@
1
+ """
2
+ MongoDB collection indexes — called once at application startup.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from motor.motor_asyncio import AsyncIOMotorDatabase
8
+ from pymongo import ASCENDING
9
+
10
+
11
+ async def ensure_indexes(db: AsyncIOMotorDatabase) -> None: # type: ignore[type-arg]
12
+ """Create all required MongoDB indexes (idempotent)."""
13
+
14
+ # Users
15
+ users = db["users"]
16
+ await users.create_index([("email", ASCENDING)], unique=True)
17
+ await users.create_index([("username", ASCENDING)], unique=True)
18
+ await users.create_index([("company_id", ASCENDING)])
19
+
20
+ # Clients
21
+ clients = db["clients"]
22
+ await clients.create_index([("slug", ASCENDING)], unique=True)
23
+ await clients.create_index([("created_by_user_id", ASCENDING)])
24
+
25
+ client_api_keys = db["client_api_keys"]
26
+ await client_api_keys.create_index([("client_id", ASCENDING)])
27
+ await client_api_keys.create_index([("status", ASCENDING)])
28
+
29
+ client_sessions = db["client_sessions"]
30
+ await client_sessions.create_index([("client_id", ASCENDING)])
31
+ await client_sessions.create_index([("access_token_id", ASCENDING)], unique=True)
32
+
33
+ audit_logs = db["audit_logs"]
34
+ await audit_logs.create_index([("actor_id", ASCENDING)])
35
+ await audit_logs.create_index([("event_type", ASCENDING)])
36
+
37
+ admin_jobs = db["admin_jobs"]
38
+ await admin_jobs.create_index([("job_type", ASCENDING)])
39
+ await admin_jobs.create_index([("status", ASCENDING)])
40
+ await admin_jobs.create_index([("requested_by_user_id", ASCENDING)])
41
+ await admin_jobs.create_index([("created_at", ASCENDING)])
42
+
43
+ # Skills
44
+ skills = db["skills"]
45
+ await skills.create_index([("company_id", ASCENDING)])
46
+ await skills.create_index([("title", ASCENDING)])
47
+ await skills.create_index([("language", ASCENDING)])
48
+
49
+ # Prompts
50
+ prompts = db["prompts"]
51
+ await prompts.create_index([("company_id", ASCENDING)])
52
+ await prompts.create_index([("name", ASCENDING)], unique=True)
53
+
54
+ # Sessions
55
+ sessions = db["sessions"]
56
+ await sessions.create_index([("company_id", ASCENDING)])
57
+ await sessions.create_index([("user_id", ASCENDING)])
58
+ await sessions.create_index([("repo_id", ASCENDING)])
59
+
60
+ # Workflows
61
+ workflows = db["workflows"]
62
+ await workflows.create_index([("company_id", ASCENDING)])
63
+ await workflows.create_index([("name", ASCENDING)])
64
+
65
+ # Repositories
66
+ repos = db["repositories"]
67
+ await repos.create_index([("company_id", ASCENDING)])
68
+ await repos.create_index([("repo_name", ASCENDING)])
69
+
70
+ # Repository Workflow States
71
+ workflow_states = db["repository_workflow_states"]
72
+ await workflow_states.create_index([("repo_id", ASCENDING)])
73
+ await workflow_states.create_index([("session_id", ASCENDING)])
74
+
75
+ # Documents
76
+ documents = db["documents"]
77
+ await documents.create_index([("doc_type", ASCENDING)])
78
+ await documents.create_index([("project", ASCENDING)])
79
+ await documents.create_index(
80
+ [("source_path", ASCENDING), ("project", ASCENDING)],
81
+ unique=True,
82
+ )
83
+
84
+ # History
85
+ history = db["history"]
86
+ await history.create_index([("session_id", ASCENDING)])
87
+
88
+ # Errors
89
+ errors = db["errors"]
90
+ await errors.create_index([("error_code", ASCENDING)])