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,993 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MongoDB Operational Store — Motor-based adapter implementing IOperationalStore.
|
|
3
|
+
|
|
4
|
+
This is the MongoDB-backed replacement for the SQLite RelationalStore.
|
|
5
|
+
It implements all domain repository interfaces through a single composite class,
|
|
6
|
+
matching the current RelationalStore API surface for backwards compatibility.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import math
|
|
12
|
+
import uuid
|
|
13
|
+
from collections import Counter
|
|
14
|
+
from datetime import UTC, datetime, timedelta
|
|
15
|
+
from typing import Any, cast
|
|
16
|
+
|
|
17
|
+
from motor.motor_asyncio import AsyncIOMotorDatabase
|
|
18
|
+
|
|
19
|
+
from minder.store.mongodb.client import MongoClient
|
|
20
|
+
from minder.store.mongodb.indexes import ensure_indexes
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _uuid_to_str(value: uuid.UUID) -> str:
|
|
24
|
+
"""Serialize UUID to string for MongoDB storage."""
|
|
25
|
+
return str(value)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _str_to_uuid(value: str) -> uuid.UUID:
|
|
29
|
+
"""Deserialize string to UUID from MongoDB storage."""
|
|
30
|
+
return uuid.UUID(value)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _now() -> datetime:
|
|
34
|
+
return datetime.now(UTC)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _normalize_datetime(value: datetime | None) -> datetime | None:
|
|
38
|
+
if value is None:
|
|
39
|
+
return None
|
|
40
|
+
if value.tzinfo is None:
|
|
41
|
+
return value.replace(tzinfo=UTC)
|
|
42
|
+
return value.astimezone(UTC)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class _MongoDoc:
|
|
46
|
+
"""
|
|
47
|
+
Lightweight wrapper around a MongoDB document dict to provide
|
|
48
|
+
attribute-style access, matching SQLAlchemy model access patterns.
|
|
49
|
+
|
|
50
|
+
This lets existing application code like `user.email` work without
|
|
51
|
+
changes, whether the object came from SQLAlchemy or MongoDB.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(self, data: dict[str, Any]) -> None:
|
|
55
|
+
self._data = data
|
|
56
|
+
# Convert _id to id if present
|
|
57
|
+
if "_id" in self._data and "id" not in self._data:
|
|
58
|
+
self._data["id"] = self._data.pop("_id")
|
|
59
|
+
# Convert string UUIDs back to uuid.UUID for id fields
|
|
60
|
+
for field in ("id", "user_id", "repo_id", "session_id", "workflow_id"):
|
|
61
|
+
val = self._data.get(field)
|
|
62
|
+
if isinstance(val, str):
|
|
63
|
+
try:
|
|
64
|
+
self._data[field] = uuid.UUID(val)
|
|
65
|
+
except ValueError:
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
def __getattr__(self, name: str) -> Any:
|
|
69
|
+
try:
|
|
70
|
+
return self._data[name]
|
|
71
|
+
except KeyError:
|
|
72
|
+
raise AttributeError(f"MongoDoc has no attribute '{name}'")
|
|
73
|
+
|
|
74
|
+
def __repr__(self) -> str:
|
|
75
|
+
return f"MongoDoc({self._data!r})"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _to_doc(data: dict[str, Any]) -> _MongoDoc:
|
|
79
|
+
"""Convert a raw MongoDB document to an attribute-accessible object."""
|
|
80
|
+
return _MongoDoc(data)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class MongoOperationalStore:
|
|
84
|
+
"""
|
|
85
|
+
MongoDB-backed operational store implementing the full
|
|
86
|
+
IOperationalStore interface (user, skill, session, workflow,
|
|
87
|
+
repository, workflow_state, document, history, error repos).
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
def __init__(self, client: MongoClient) -> None:
|
|
91
|
+
self._client = client
|
|
92
|
+
self._db: AsyncIOMotorDatabase = client.db # type: ignore[type-arg]
|
|
93
|
+
|
|
94
|
+
# ------------------------------------------------------------------
|
|
95
|
+
# Lifecycle
|
|
96
|
+
# ------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
async def init_db(self) -> None:
|
|
99
|
+
"""Create all indexes (idempotent)."""
|
|
100
|
+
await ensure_indexes(self._db)
|
|
101
|
+
|
|
102
|
+
async def dispose(self) -> None:
|
|
103
|
+
"""Close the MongoDB client."""
|
|
104
|
+
await self._client.close()
|
|
105
|
+
|
|
106
|
+
# ------------------------------------------------------------------
|
|
107
|
+
# Prompts
|
|
108
|
+
# ------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
async def create_prompt(self, **kwargs: Any) -> _MongoDoc:
|
|
111
|
+
if "id" not in kwargs:
|
|
112
|
+
kwargs["id"] = _uuid_to_str(uuid.uuid4())
|
|
113
|
+
else:
|
|
114
|
+
kwargs["id"] = _uuid_to_str(kwargs["id"])
|
|
115
|
+
kwargs.setdefault("company_id", "default")
|
|
116
|
+
kwargs.setdefault("arguments", [])
|
|
117
|
+
kwargs.setdefault("created_at", _now())
|
|
118
|
+
kwargs.setdefault("updated_at", _now())
|
|
119
|
+
kwargs["_id"] = kwargs.pop("id")
|
|
120
|
+
await self._db.prompts.insert_one(kwargs)
|
|
121
|
+
return _to_doc(kwargs)
|
|
122
|
+
|
|
123
|
+
async def get_prompt_by_id(self, prompt_id: uuid.UUID) -> _MongoDoc | None:
|
|
124
|
+
doc = await self._db.prompts.find_one({"_id": _uuid_to_str(prompt_id)})
|
|
125
|
+
return _to_doc(doc) if doc else None
|
|
126
|
+
|
|
127
|
+
async def get_prompt_by_name(self, name: str) -> _MongoDoc | None:
|
|
128
|
+
doc = await self._db.prompts.find_one({"name": name})
|
|
129
|
+
return _to_doc(doc) if doc else None
|
|
130
|
+
|
|
131
|
+
async def list_prompts(self) -> list[_MongoDoc]:
|
|
132
|
+
cursor = self._db.prompts.find().sort("name", 1)
|
|
133
|
+
return [_to_doc(doc) async for doc in cursor]
|
|
134
|
+
|
|
135
|
+
async def update_prompt(
|
|
136
|
+
self, prompt_id: uuid.UUID, **kwargs: Any
|
|
137
|
+
) -> _MongoDoc | None:
|
|
138
|
+
if not kwargs:
|
|
139
|
+
return await self.get_prompt_by_id(prompt_id)
|
|
140
|
+
kwargs["updated_at"] = _now()
|
|
141
|
+
await self._db.prompts.update_one(
|
|
142
|
+
{"_id": _uuid_to_str(prompt_id)},
|
|
143
|
+
{"$set": kwargs},
|
|
144
|
+
)
|
|
145
|
+
return await self.get_prompt_by_id(prompt_id)
|
|
146
|
+
|
|
147
|
+
async def delete_prompt(self, prompt_id: uuid.UUID) -> None:
|
|
148
|
+
await self._db.prompts.delete_one({"_id": _uuid_to_str(prompt_id)})
|
|
149
|
+
|
|
150
|
+
# ------------------------------------------------------------------
|
|
151
|
+
# User
|
|
152
|
+
# ------------------------------------------------------------------
|
|
153
|
+
|
|
154
|
+
async def create_user(self, **kwargs: Any) -> _MongoDoc:
|
|
155
|
+
if "id" not in kwargs:
|
|
156
|
+
kwargs["id"] = _uuid_to_str(uuid.uuid4())
|
|
157
|
+
else:
|
|
158
|
+
kwargs["id"] = _uuid_to_str(kwargs["id"])
|
|
159
|
+
for field in ("company_id",):
|
|
160
|
+
kwargs.setdefault(field, "default")
|
|
161
|
+
kwargs.setdefault("is_active", True)
|
|
162
|
+
kwargs.setdefault("settings", {})
|
|
163
|
+
kwargs.setdefault("created_at", _now())
|
|
164
|
+
kwargs.setdefault("last_login", None)
|
|
165
|
+
kwargs["_id"] = kwargs.pop("id")
|
|
166
|
+
await self._db.users.insert_one(kwargs)
|
|
167
|
+
return _to_doc(kwargs)
|
|
168
|
+
|
|
169
|
+
async def get_user_by_id(self, user_id: uuid.UUID) -> _MongoDoc | None:
|
|
170
|
+
doc = await self._db.users.find_one({"_id": _uuid_to_str(user_id)})
|
|
171
|
+
return _to_doc(doc) if doc else None
|
|
172
|
+
|
|
173
|
+
async def get_user_by_email(self, email: str) -> _MongoDoc | None:
|
|
174
|
+
doc = await self._db.users.find_one({"email": email})
|
|
175
|
+
return _to_doc(doc) if doc else None
|
|
176
|
+
|
|
177
|
+
async def get_user_by_username(self, username: str) -> _MongoDoc | None:
|
|
178
|
+
doc = await self._db.users.find_one({"username": username})
|
|
179
|
+
return _to_doc(doc) if doc else None
|
|
180
|
+
|
|
181
|
+
async def list_users(self, active_only: bool = True) -> list[_MongoDoc]:
|
|
182
|
+
query: dict[str, Any] = {}
|
|
183
|
+
if active_only:
|
|
184
|
+
query["is_active"] = True
|
|
185
|
+
cursor = self._db.users.find(query)
|
|
186
|
+
return [_to_doc(doc) async for doc in cursor]
|
|
187
|
+
|
|
188
|
+
async def update_user(self, user_id: uuid.UUID, **kwargs: Any) -> _MongoDoc | None:
|
|
189
|
+
if not kwargs:
|
|
190
|
+
return await self.get_user_by_id(user_id)
|
|
191
|
+
await self._db.users.update_one(
|
|
192
|
+
{"_id": _uuid_to_str(user_id)}, {"$set": kwargs}
|
|
193
|
+
)
|
|
194
|
+
return await self.get_user_by_id(user_id)
|
|
195
|
+
|
|
196
|
+
async def delete_user(self, user_id: uuid.UUID) -> None:
|
|
197
|
+
await self._db.users.delete_one({"_id": _uuid_to_str(user_id)})
|
|
198
|
+
|
|
199
|
+
async def has_admin_users(self) -> bool:
|
|
200
|
+
doc = await self._db.users.find_one({"role": "admin"})
|
|
201
|
+
return doc is not None
|
|
202
|
+
|
|
203
|
+
# ------------------------------------------------------------------
|
|
204
|
+
# Client Gateway
|
|
205
|
+
# ------------------------------------------------------------------
|
|
206
|
+
|
|
207
|
+
async def create_client(self, **kwargs: Any) -> _MongoDoc:
|
|
208
|
+
if "id" not in kwargs:
|
|
209
|
+
kwargs["id"] = _uuid_to_str(uuid.uuid4())
|
|
210
|
+
else:
|
|
211
|
+
kwargs["id"] = _uuid_to_str(kwargs["id"])
|
|
212
|
+
if "created_by_user_id" in kwargs and isinstance(
|
|
213
|
+
kwargs["created_by_user_id"], uuid.UUID
|
|
214
|
+
):
|
|
215
|
+
kwargs["created_by_user_id"] = _uuid_to_str(kwargs["created_by_user_id"])
|
|
216
|
+
kwargs.setdefault("description", "")
|
|
217
|
+
kwargs.setdefault("status", "active")
|
|
218
|
+
kwargs.setdefault("transport_modes", ["sse", "stdio"])
|
|
219
|
+
kwargs.setdefault("tool_scopes", [])
|
|
220
|
+
kwargs.setdefault("repo_scopes", [])
|
|
221
|
+
kwargs.setdefault("workflow_scopes", [])
|
|
222
|
+
kwargs.setdefault("rate_limit_policy", {})
|
|
223
|
+
kwargs.setdefault("created_at", _now())
|
|
224
|
+
kwargs.setdefault("updated_at", _now())
|
|
225
|
+
kwargs["_id"] = kwargs.pop("id")
|
|
226
|
+
await self._db.clients.insert_one(kwargs)
|
|
227
|
+
return _to_doc(kwargs)
|
|
228
|
+
|
|
229
|
+
async def get_client_by_id(self, client_id: uuid.UUID) -> _MongoDoc | None:
|
|
230
|
+
doc = await self._db.clients.find_one({"_id": _uuid_to_str(client_id)})
|
|
231
|
+
return _to_doc(doc) if doc else None
|
|
232
|
+
|
|
233
|
+
async def get_client_by_slug(self, slug: str) -> _MongoDoc | None:
|
|
234
|
+
doc = await self._db.clients.find_one({"slug": slug})
|
|
235
|
+
return _to_doc(doc) if doc else None
|
|
236
|
+
|
|
237
|
+
async def list_clients(self) -> list[_MongoDoc]:
|
|
238
|
+
cursor = self._db.clients.find()
|
|
239
|
+
return [_to_doc(doc) async for doc in cursor]
|
|
240
|
+
|
|
241
|
+
async def update_client(
|
|
242
|
+
self, client_id: uuid.UUID, **kwargs: Any
|
|
243
|
+
) -> _MongoDoc | None:
|
|
244
|
+
if not kwargs:
|
|
245
|
+
return await self.get_client_by_id(client_id)
|
|
246
|
+
kwargs["updated_at"] = _now()
|
|
247
|
+
await self._db.clients.update_one(
|
|
248
|
+
{"_id": _uuid_to_str(client_id)},
|
|
249
|
+
{"$set": kwargs},
|
|
250
|
+
)
|
|
251
|
+
return await self.get_client_by_id(client_id)
|
|
252
|
+
|
|
253
|
+
async def create_client_api_key(self, **kwargs: Any) -> _MongoDoc:
|
|
254
|
+
if "id" not in kwargs:
|
|
255
|
+
kwargs["id"] = _uuid_to_str(uuid.uuid4())
|
|
256
|
+
else:
|
|
257
|
+
kwargs["id"] = _uuid_to_str(kwargs["id"])
|
|
258
|
+
for uuid_field in ("client_id", "created_by_user_id"):
|
|
259
|
+
if uuid_field in kwargs and isinstance(kwargs[uuid_field], uuid.UUID):
|
|
260
|
+
kwargs[uuid_field] = _uuid_to_str(kwargs[uuid_field])
|
|
261
|
+
kwargs.setdefault("status", "active")
|
|
262
|
+
kwargs.setdefault("last_used_at", None)
|
|
263
|
+
kwargs.setdefault("created_at", _now())
|
|
264
|
+
kwargs.setdefault("expires_at", None)
|
|
265
|
+
kwargs.setdefault("revoked_at", None)
|
|
266
|
+
kwargs["_id"] = kwargs.pop("id")
|
|
267
|
+
await self._db.client_api_keys.insert_one(kwargs)
|
|
268
|
+
return _to_doc(kwargs)
|
|
269
|
+
|
|
270
|
+
async def list_client_api_keys(self, client_id: uuid.UUID) -> list[_MongoDoc]:
|
|
271
|
+
cursor = self._db.client_api_keys.find({"client_id": _uuid_to_str(client_id)})
|
|
272
|
+
return [_to_doc(doc) async for doc in cursor]
|
|
273
|
+
|
|
274
|
+
async def update_client_api_key(
|
|
275
|
+
self, key_id: uuid.UUID, **kwargs: Any
|
|
276
|
+
) -> _MongoDoc | None:
|
|
277
|
+
if not kwargs:
|
|
278
|
+
doc = await self._db.client_api_keys.find_one({"_id": _uuid_to_str(key_id)})
|
|
279
|
+
return _to_doc(doc) if doc else None
|
|
280
|
+
await self._db.client_api_keys.update_one(
|
|
281
|
+
{"_id": _uuid_to_str(key_id)},
|
|
282
|
+
{"$set": kwargs},
|
|
283
|
+
)
|
|
284
|
+
doc = await self._db.client_api_keys.find_one({"_id": _uuid_to_str(key_id)})
|
|
285
|
+
return _to_doc(doc) if doc else None
|
|
286
|
+
|
|
287
|
+
async def create_client_session(self, **kwargs: Any) -> _MongoDoc:
|
|
288
|
+
if "id" not in kwargs:
|
|
289
|
+
kwargs["id"] = _uuid_to_str(uuid.uuid4())
|
|
290
|
+
else:
|
|
291
|
+
kwargs["id"] = _uuid_to_str(kwargs["id"])
|
|
292
|
+
if "client_id" in kwargs and isinstance(kwargs["client_id"], uuid.UUID):
|
|
293
|
+
kwargs["client_id"] = _uuid_to_str(kwargs["client_id"])
|
|
294
|
+
kwargs.setdefault("status", "active")
|
|
295
|
+
kwargs.setdefault("scopes", [])
|
|
296
|
+
kwargs.setdefault("issued_at", _now())
|
|
297
|
+
kwargs.setdefault("last_seen_at", None)
|
|
298
|
+
kwargs.setdefault("session_metadata", {})
|
|
299
|
+
kwargs["_id"] = kwargs.pop("id")
|
|
300
|
+
await self._db.client_sessions.insert_one(kwargs)
|
|
301
|
+
return _to_doc(kwargs)
|
|
302
|
+
|
|
303
|
+
async def get_client_session_by_token_id(self, token_id: str) -> _MongoDoc | None:
|
|
304
|
+
doc = await self._db.client_sessions.find_one({"access_token_id": token_id})
|
|
305
|
+
return _to_doc(doc) if doc else None
|
|
306
|
+
|
|
307
|
+
async def update_client_session(
|
|
308
|
+
self, session_id: uuid.UUID, **kwargs: Any
|
|
309
|
+
) -> _MongoDoc | None:
|
|
310
|
+
if not kwargs:
|
|
311
|
+
doc = await self._db.client_sessions.find_one(
|
|
312
|
+
{"_id": _uuid_to_str(session_id)}
|
|
313
|
+
)
|
|
314
|
+
return _to_doc(doc) if doc else None
|
|
315
|
+
await self._db.client_sessions.update_one(
|
|
316
|
+
{"_id": _uuid_to_str(session_id)},
|
|
317
|
+
{"$set": kwargs},
|
|
318
|
+
)
|
|
319
|
+
doc = await self._db.client_sessions.find_one({"_id": _uuid_to_str(session_id)})
|
|
320
|
+
return _to_doc(doc) if doc else None
|
|
321
|
+
|
|
322
|
+
async def count_active_client_sessions(self) -> int:
|
|
323
|
+
now = _now()
|
|
324
|
+
query = {"status": "active", "expires_at": {"$gt": now}}
|
|
325
|
+
return int(await self._db.client_sessions.count_documents(query))
|
|
326
|
+
|
|
327
|
+
async def create_audit_log(self, **kwargs: Any) -> _MongoDoc:
|
|
328
|
+
if "id" not in kwargs:
|
|
329
|
+
kwargs["id"] = _uuid_to_str(uuid.uuid4())
|
|
330
|
+
else:
|
|
331
|
+
kwargs["id"] = _uuid_to_str(kwargs["id"])
|
|
332
|
+
kwargs.setdefault("audit_metadata", {})
|
|
333
|
+
kwargs.setdefault("created_at", _now())
|
|
334
|
+
kwargs["_id"] = kwargs.pop("id")
|
|
335
|
+
await self._db.audit_logs.insert_one(kwargs)
|
|
336
|
+
return _to_doc(kwargs)
|
|
337
|
+
|
|
338
|
+
async def list_audit_logs(
|
|
339
|
+
self,
|
|
340
|
+
*,
|
|
341
|
+
actor_id: str | None = None,
|
|
342
|
+
event_type: str | None = None,
|
|
343
|
+
outcome: str | None = None,
|
|
344
|
+
limit: int | None = None,
|
|
345
|
+
offset: int = 0,
|
|
346
|
+
) -> list[_MongoDoc]:
|
|
347
|
+
query: dict[str, Any] = {}
|
|
348
|
+
if actor_id is not None:
|
|
349
|
+
query["actor_id"] = actor_id
|
|
350
|
+
if event_type is not None:
|
|
351
|
+
query["event_type"] = event_type
|
|
352
|
+
if outcome is not None:
|
|
353
|
+
query["outcome"] = outcome
|
|
354
|
+
cursor = self._db.audit_logs.find(query).sort("created_at", -1).skip(offset)
|
|
355
|
+
if limit is not None:
|
|
356
|
+
cursor = cursor.limit(limit)
|
|
357
|
+
return [_to_doc(doc) async for doc in cursor]
|
|
358
|
+
|
|
359
|
+
async def count_audit_logs(
|
|
360
|
+
self,
|
|
361
|
+
*,
|
|
362
|
+
actor_id: str | None = None,
|
|
363
|
+
event_type: str | None = None,
|
|
364
|
+
outcome: str | None = None,
|
|
365
|
+
) -> int:
|
|
366
|
+
query: dict[str, Any] = {}
|
|
367
|
+
if actor_id is not None:
|
|
368
|
+
query["actor_id"] = actor_id
|
|
369
|
+
if event_type is not None:
|
|
370
|
+
query["event_type"] = event_type
|
|
371
|
+
if outcome is not None:
|
|
372
|
+
query["outcome"] = outcome
|
|
373
|
+
return int(await self._db.audit_logs.count_documents(query))
|
|
374
|
+
|
|
375
|
+
async def get_audit_summary(
|
|
376
|
+
self,
|
|
377
|
+
*,
|
|
378
|
+
actor_id: str | None = None,
|
|
379
|
+
event_type: str | None = None,
|
|
380
|
+
outcome: str | None = None,
|
|
381
|
+
group_by: str = "event_type",
|
|
382
|
+
) -> dict[str, int]:
|
|
383
|
+
match_query: dict[str, Any] = {}
|
|
384
|
+
if actor_id is not None:
|
|
385
|
+
match_query["actor_id"] = actor_id
|
|
386
|
+
if event_type is not None:
|
|
387
|
+
match_query["event_type"] = event_type
|
|
388
|
+
if outcome is not None:
|
|
389
|
+
match_query["outcome"] = outcome
|
|
390
|
+
|
|
391
|
+
pipeline = []
|
|
392
|
+
if match_query:
|
|
393
|
+
pipeline.append({"$match": match_query})
|
|
394
|
+
|
|
395
|
+
pipeline.append({"$group": {"_id": f"${group_by}", "count": {"$sum": 1}}})
|
|
396
|
+
|
|
397
|
+
cursor = self._db.audit_logs.aggregate(pipeline)
|
|
398
|
+
result: dict[str, int] = {}
|
|
399
|
+
async for doc in cursor:
|
|
400
|
+
label = doc["_id"] or "unknown"
|
|
401
|
+
result[label] = doc["count"]
|
|
402
|
+
return result
|
|
403
|
+
|
|
404
|
+
# ------------------------------------------------------------------
|
|
405
|
+
# Skill
|
|
406
|
+
# ------------------------------------------------------------------
|
|
407
|
+
|
|
408
|
+
async def create_skill(self, **kwargs: Any) -> _MongoDoc:
|
|
409
|
+
if "id" not in kwargs:
|
|
410
|
+
kwargs["id"] = _uuid_to_str(uuid.uuid4())
|
|
411
|
+
else:
|
|
412
|
+
kwargs["id"] = _uuid_to_str(kwargs["id"])
|
|
413
|
+
kwargs.setdefault("company_id", "default")
|
|
414
|
+
kwargs.setdefault("usage_count", 0)
|
|
415
|
+
kwargs.setdefault("quality_score", 0.0)
|
|
416
|
+
kwargs.setdefault("tags", [])
|
|
417
|
+
kwargs.setdefault("embedding", None)
|
|
418
|
+
kwargs.setdefault("source_metadata", None)
|
|
419
|
+
kwargs.setdefault("excerpt_kind", "none")
|
|
420
|
+
kwargs.setdefault("created_at", _now())
|
|
421
|
+
kwargs.setdefault("updated_at", _now())
|
|
422
|
+
kwargs["_id"] = kwargs.pop("id")
|
|
423
|
+
await self._db.skills.insert_one(kwargs)
|
|
424
|
+
return _to_doc(kwargs)
|
|
425
|
+
|
|
426
|
+
async def get_skill_by_id(self, skill_id: uuid.UUID) -> _MongoDoc | None:
|
|
427
|
+
doc = await self._db.skills.find_one({"_id": _uuid_to_str(skill_id)})
|
|
428
|
+
return _to_doc(doc) if doc else None
|
|
429
|
+
|
|
430
|
+
async def list_skills(self) -> list[_MongoDoc]:
|
|
431
|
+
cursor = self._db.skills.find()
|
|
432
|
+
return [_to_doc(doc) async for doc in cursor]
|
|
433
|
+
|
|
434
|
+
async def update_skill(
|
|
435
|
+
self, skill_id: uuid.UUID, **kwargs: Any
|
|
436
|
+
) -> _MongoDoc | None:
|
|
437
|
+
if not kwargs:
|
|
438
|
+
return await self.get_skill_by_id(skill_id)
|
|
439
|
+
kwargs["updated_at"] = _now()
|
|
440
|
+
await self._db.skills.update_one(
|
|
441
|
+
{"_id": _uuid_to_str(skill_id)},
|
|
442
|
+
{"$set": kwargs},
|
|
443
|
+
)
|
|
444
|
+
return await self.get_skill_by_id(skill_id)
|
|
445
|
+
|
|
446
|
+
async def delete_skill(self, skill_id: uuid.UUID) -> None:
|
|
447
|
+
await self._db.skills.delete_one({"_id": _uuid_to_str(skill_id)})
|
|
448
|
+
|
|
449
|
+
# ------------------------------------------------------------------
|
|
450
|
+
# Admin Jobs
|
|
451
|
+
# ------------------------------------------------------------------
|
|
452
|
+
|
|
453
|
+
async def create_admin_job(self, **kwargs: Any) -> _MongoDoc:
|
|
454
|
+
if "id" not in kwargs:
|
|
455
|
+
kwargs["id"] = _uuid_to_str(uuid.uuid4())
|
|
456
|
+
else:
|
|
457
|
+
kwargs["id"] = _uuid_to_str(kwargs["id"])
|
|
458
|
+
if "requested_by_user_id" in kwargs and isinstance(
|
|
459
|
+
kwargs["requested_by_user_id"], uuid.UUID
|
|
460
|
+
):
|
|
461
|
+
kwargs["requested_by_user_id"] = _uuid_to_str(
|
|
462
|
+
kwargs["requested_by_user_id"]
|
|
463
|
+
)
|
|
464
|
+
kwargs.setdefault("company_id", "default")
|
|
465
|
+
kwargs.setdefault("status", "queued")
|
|
466
|
+
kwargs.setdefault("payload", {})
|
|
467
|
+
kwargs.setdefault("result_payload", None)
|
|
468
|
+
kwargs.setdefault("error_message", None)
|
|
469
|
+
kwargs.setdefault("progress_current", 0)
|
|
470
|
+
kwargs.setdefault("progress_total", 0)
|
|
471
|
+
kwargs.setdefault("message", None)
|
|
472
|
+
kwargs.setdefault("events", [])
|
|
473
|
+
kwargs.setdefault("created_at", _now())
|
|
474
|
+
kwargs.setdefault("updated_at", _now())
|
|
475
|
+
kwargs.setdefault("started_at", None)
|
|
476
|
+
kwargs.setdefault("finished_at", None)
|
|
477
|
+
kwargs["_id"] = kwargs.pop("id")
|
|
478
|
+
await self._db.admin_jobs.insert_one(kwargs)
|
|
479
|
+
return _to_doc(kwargs)
|
|
480
|
+
|
|
481
|
+
async def get_admin_job_by_id(self, job_id: uuid.UUID) -> _MongoDoc | None:
|
|
482
|
+
doc = await self._db.admin_jobs.find_one({"_id": _uuid_to_str(job_id)})
|
|
483
|
+
return _to_doc(doc) if doc else None
|
|
484
|
+
|
|
485
|
+
async def list_admin_jobs(
|
|
486
|
+
self,
|
|
487
|
+
*,
|
|
488
|
+
job_type: str | None = None,
|
|
489
|
+
status: str | None = None,
|
|
490
|
+
requested_by_user_id: uuid.UUID | None = None,
|
|
491
|
+
limit: int | None = None,
|
|
492
|
+
offset: int = 0,
|
|
493
|
+
) -> list[_MongoDoc]:
|
|
494
|
+
query: dict[str, Any] = {}
|
|
495
|
+
if job_type:
|
|
496
|
+
query["job_type"] = job_type
|
|
497
|
+
if status:
|
|
498
|
+
query["status"] = status
|
|
499
|
+
if requested_by_user_id is not None:
|
|
500
|
+
query["requested_by_user_id"] = _uuid_to_str(requested_by_user_id)
|
|
501
|
+
cursor = self._db.admin_jobs.find(query).sort("created_at", -1).skip(offset)
|
|
502
|
+
if limit is not None:
|
|
503
|
+
cursor = cursor.limit(limit)
|
|
504
|
+
return [_to_doc(doc) async for doc in cursor]
|
|
505
|
+
|
|
506
|
+
async def update_admin_job(
|
|
507
|
+
self, job_id: uuid.UUID, **kwargs: Any
|
|
508
|
+
) -> _MongoDoc | None:
|
|
509
|
+
if not kwargs:
|
|
510
|
+
return await self.get_admin_job_by_id(job_id)
|
|
511
|
+
if "requested_by_user_id" in kwargs and isinstance(
|
|
512
|
+
kwargs["requested_by_user_id"], uuid.UUID
|
|
513
|
+
):
|
|
514
|
+
kwargs["requested_by_user_id"] = _uuid_to_str(
|
|
515
|
+
kwargs["requested_by_user_id"]
|
|
516
|
+
)
|
|
517
|
+
kwargs["updated_at"] = _now()
|
|
518
|
+
await self._db.admin_jobs.update_one(
|
|
519
|
+
{"_id": _uuid_to_str(job_id)},
|
|
520
|
+
{"$set": kwargs},
|
|
521
|
+
)
|
|
522
|
+
return await self.get_admin_job_by_id(job_id)
|
|
523
|
+
|
|
524
|
+
# ------------------------------------------------------------------
|
|
525
|
+
# Session
|
|
526
|
+
# ------------------------------------------------------------------
|
|
527
|
+
|
|
528
|
+
async def create_session(self, **kwargs: Any) -> _MongoDoc:
|
|
529
|
+
if "id" not in kwargs:
|
|
530
|
+
kwargs["id"] = _uuid_to_str(uuid.uuid4())
|
|
531
|
+
else:
|
|
532
|
+
kwargs["id"] = _uuid_to_str(kwargs["id"])
|
|
533
|
+
kwargs.setdefault("company_id", "default")
|
|
534
|
+
for uuid_field in ("user_id", "client_id", "repo_id"):
|
|
535
|
+
if uuid_field in kwargs and isinstance(kwargs[uuid_field], uuid.UUID):
|
|
536
|
+
kwargs[uuid_field] = _uuid_to_str(kwargs[uuid_field])
|
|
537
|
+
kwargs.setdefault("name", None)
|
|
538
|
+
kwargs.setdefault("project_context", {})
|
|
539
|
+
kwargs.setdefault("active_skills", {})
|
|
540
|
+
kwargs.setdefault("state", {})
|
|
541
|
+
kwargs.setdefault("ttl", 86400)
|
|
542
|
+
kwargs.setdefault("created_at", _now())
|
|
543
|
+
kwargs.setdefault("last_active", _now())
|
|
544
|
+
kwargs["_id"] = kwargs.pop("id")
|
|
545
|
+
await self._db.sessions.insert_one(kwargs)
|
|
546
|
+
return _to_doc(kwargs)
|
|
547
|
+
|
|
548
|
+
async def get_session_by_id(self, session_id: uuid.UUID) -> _MongoDoc | None:
|
|
549
|
+
doc = await self._db.sessions.find_one({"_id": _uuid_to_str(session_id)})
|
|
550
|
+
return _to_doc(doc) if doc else None
|
|
551
|
+
|
|
552
|
+
async def get_sessions_by_user(self, user_id: uuid.UUID) -> list[_MongoDoc]:
|
|
553
|
+
cursor = self._db.sessions.find({"user_id": _uuid_to_str(user_id)}).sort(
|
|
554
|
+
"last_active", -1
|
|
555
|
+
)
|
|
556
|
+
return [_to_doc(doc) async for doc in cursor]
|
|
557
|
+
|
|
558
|
+
async def get_sessions_by_client(self, client_id: uuid.UUID) -> list[_MongoDoc]:
|
|
559
|
+
cursor = self._db.sessions.find({"client_id": _uuid_to_str(client_id)}).sort(
|
|
560
|
+
"last_active", -1
|
|
561
|
+
)
|
|
562
|
+
return [_to_doc(doc) async for doc in cursor]
|
|
563
|
+
|
|
564
|
+
async def find_session_by_name(
|
|
565
|
+
self,
|
|
566
|
+
name: str,
|
|
567
|
+
*,
|
|
568
|
+
user_id: uuid.UUID | None = None,
|
|
569
|
+
client_id: uuid.UUID | None = None,
|
|
570
|
+
) -> _MongoDoc | None:
|
|
571
|
+
query: dict[str, Any] = {"name": name}
|
|
572
|
+
if client_id is not None:
|
|
573
|
+
query["client_id"] = _uuid_to_str(client_id)
|
|
574
|
+
elif user_id is not None:
|
|
575
|
+
query["user_id"] = _uuid_to_str(user_id)
|
|
576
|
+
doc = await self._db.sessions.find_one(query, sort=[("last_active", -1)])
|
|
577
|
+
return _to_doc(doc) if doc else None
|
|
578
|
+
|
|
579
|
+
async def update_session(
|
|
580
|
+
self, session_id: uuid.UUID, **kwargs: Any
|
|
581
|
+
) -> _MongoDoc | None:
|
|
582
|
+
if not kwargs:
|
|
583
|
+
return await self.get_session_by_id(session_id)
|
|
584
|
+
kwargs["last_active"] = _now()
|
|
585
|
+
await self._db.sessions.update_one(
|
|
586
|
+
{"_id": _uuid_to_str(session_id)}, {"$set": kwargs}
|
|
587
|
+
)
|
|
588
|
+
return await self.get_session_by_id(session_id)
|
|
589
|
+
|
|
590
|
+
async def delete_session(self, session_id: uuid.UUID) -> None:
|
|
591
|
+
await self._db.sessions.delete_one({"_id": _uuid_to_str(session_id)})
|
|
592
|
+
|
|
593
|
+
async def cleanup_expired_sessions(
|
|
594
|
+
self,
|
|
595
|
+
*,
|
|
596
|
+
now: datetime | None = None,
|
|
597
|
+
user_id: uuid.UUID | None = None,
|
|
598
|
+
client_id: uuid.UUID | None = None,
|
|
599
|
+
) -> dict[str, int]:
|
|
600
|
+
reference_time = _normalize_datetime(now) or _now()
|
|
601
|
+
query: dict[str, Any] = {}
|
|
602
|
+
if user_id is not None:
|
|
603
|
+
query["user_id"] = _uuid_to_str(user_id)
|
|
604
|
+
if client_id is not None:
|
|
605
|
+
query["client_id"] = _uuid_to_str(client_id)
|
|
606
|
+
|
|
607
|
+
cursor = self._db.sessions.find(query)
|
|
608
|
+
expired_session_ids: list[str] = []
|
|
609
|
+
async for session in cursor:
|
|
610
|
+
ttl = int(session.get("ttl", 0) or 0)
|
|
611
|
+
if ttl <= 0:
|
|
612
|
+
continue
|
|
613
|
+
base = _normalize_datetime(
|
|
614
|
+
session.get("last_active") or session.get("created_at")
|
|
615
|
+
)
|
|
616
|
+
if base is not None and base + timedelta(seconds=ttl) <= reference_time:
|
|
617
|
+
expired_session_ids.append(str(session["_id"]))
|
|
618
|
+
|
|
619
|
+
if not expired_session_ids:
|
|
620
|
+
return {"deleted_sessions": 0, "deleted_history": 0}
|
|
621
|
+
|
|
622
|
+
history_result = await self._db.history.delete_many(
|
|
623
|
+
{"session_id": {"$in": expired_session_ids}}
|
|
624
|
+
)
|
|
625
|
+
session_result = await self._db.sessions.delete_many(
|
|
626
|
+
{"_id": {"$in": expired_session_ids}}
|
|
627
|
+
)
|
|
628
|
+
return {
|
|
629
|
+
"deleted_sessions": int(session_result.deleted_count),
|
|
630
|
+
"deleted_history": int(history_result.deleted_count),
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
# ------------------------------------------------------------------
|
|
634
|
+
# Workflow
|
|
635
|
+
# ------------------------------------------------------------------
|
|
636
|
+
|
|
637
|
+
async def create_workflow(self, **kwargs: Any) -> _MongoDoc:
|
|
638
|
+
if "id" not in kwargs:
|
|
639
|
+
kwargs["id"] = _uuid_to_str(uuid.uuid4())
|
|
640
|
+
else:
|
|
641
|
+
kwargs["id"] = _uuid_to_str(kwargs["id"])
|
|
642
|
+
kwargs.setdefault("company_id", "default")
|
|
643
|
+
kwargs.setdefault("version", 1)
|
|
644
|
+
kwargs.setdefault("steps", [])
|
|
645
|
+
kwargs.setdefault("policies", {})
|
|
646
|
+
kwargs.setdefault("default_for_repo", False)
|
|
647
|
+
kwargs.setdefault("created_at", _now())
|
|
648
|
+
kwargs.setdefault("updated_at", _now())
|
|
649
|
+
kwargs["_id"] = kwargs.pop("id")
|
|
650
|
+
await self._db.workflows.insert_one(kwargs)
|
|
651
|
+
return _to_doc(kwargs)
|
|
652
|
+
|
|
653
|
+
async def get_workflow_by_id(self, workflow_id: uuid.UUID) -> _MongoDoc | None:
|
|
654
|
+
doc = await self._db.workflows.find_one({"_id": _uuid_to_str(workflow_id)})
|
|
655
|
+
return _to_doc(doc) if doc else None
|
|
656
|
+
|
|
657
|
+
async def get_workflow_by_name(self, name: str) -> _MongoDoc | None:
|
|
658
|
+
doc = await self._db.workflows.find_one({"name": name})
|
|
659
|
+
return _to_doc(doc) if doc else None
|
|
660
|
+
|
|
661
|
+
async def list_workflows(self) -> list[_MongoDoc]:
|
|
662
|
+
cursor = self._db.workflows.find()
|
|
663
|
+
return [_to_doc(doc) async for doc in cursor]
|
|
664
|
+
|
|
665
|
+
async def update_workflow(
|
|
666
|
+
self, workflow_id: uuid.UUID, **kwargs: Any
|
|
667
|
+
) -> _MongoDoc | None:
|
|
668
|
+
if not kwargs:
|
|
669
|
+
return await self.get_workflow_by_id(workflow_id)
|
|
670
|
+
kwargs["updated_at"] = _now()
|
|
671
|
+
await self._db.workflows.update_one(
|
|
672
|
+
{"_id": _uuid_to_str(workflow_id)}, {"$set": kwargs}
|
|
673
|
+
)
|
|
674
|
+
return await self.get_workflow_by_id(workflow_id)
|
|
675
|
+
|
|
676
|
+
async def delete_workflow(self, workflow_id: uuid.UUID) -> None:
|
|
677
|
+
await self._db.workflows.delete_one({"_id": _uuid_to_str(workflow_id)})
|
|
678
|
+
|
|
679
|
+
# ------------------------------------------------------------------
|
|
680
|
+
# Repository
|
|
681
|
+
# ------------------------------------------------------------------
|
|
682
|
+
|
|
683
|
+
async def create_repository(self, **kwargs: Any) -> _MongoDoc:
|
|
684
|
+
if "id" not in kwargs:
|
|
685
|
+
kwargs["id"] = _uuid_to_str(uuid.uuid4())
|
|
686
|
+
else:
|
|
687
|
+
kwargs["id"] = _uuid_to_str(kwargs["id"])
|
|
688
|
+
kwargs.setdefault("company_id", "default")
|
|
689
|
+
for uuid_field in ("workflow_id",):
|
|
690
|
+
if uuid_field in kwargs and isinstance(kwargs[uuid_field], uuid.UUID):
|
|
691
|
+
kwargs[uuid_field] = _uuid_to_str(kwargs[uuid_field])
|
|
692
|
+
kwargs.setdefault("state_path", ".minder")
|
|
693
|
+
kwargs.setdefault("context_snapshot", {})
|
|
694
|
+
kwargs.setdefault("relationships", {})
|
|
695
|
+
kwargs.setdefault("created_at", _now())
|
|
696
|
+
kwargs.setdefault("updated_at", _now())
|
|
697
|
+
kwargs["_id"] = kwargs.pop("id")
|
|
698
|
+
await self._db.repositories.insert_one(kwargs)
|
|
699
|
+
return _to_doc(kwargs)
|
|
700
|
+
|
|
701
|
+
async def get_repository_by_id(self, repo_id: uuid.UUID) -> _MongoDoc | None:
|
|
702
|
+
doc = await self._db.repositories.find_one({"_id": _uuid_to_str(repo_id)})
|
|
703
|
+
return _to_doc(doc) if doc else None
|
|
704
|
+
|
|
705
|
+
async def get_repository_by_name(self, repo_name: str) -> _MongoDoc | None:
|
|
706
|
+
doc = await self._db.repositories.find_one({"repo_name": repo_name})
|
|
707
|
+
return _to_doc(doc) if doc else None
|
|
708
|
+
|
|
709
|
+
async def list_repositories(self) -> list[_MongoDoc]:
|
|
710
|
+
cursor = self._db.repositories.find()
|
|
711
|
+
return [_to_doc(doc) async for doc in cursor]
|
|
712
|
+
|
|
713
|
+
async def update_repository(
|
|
714
|
+
self, repo_id: uuid.UUID, **kwargs: Any
|
|
715
|
+
) -> _MongoDoc | None:
|
|
716
|
+
if not kwargs:
|
|
717
|
+
return await self.get_repository_by_id(repo_id)
|
|
718
|
+
kwargs["updated_at"] = _now()
|
|
719
|
+
await self._db.repositories.update_one(
|
|
720
|
+
{"_id": _uuid_to_str(repo_id)}, {"$set": kwargs}
|
|
721
|
+
)
|
|
722
|
+
return await self.get_repository_by_id(repo_id)
|
|
723
|
+
|
|
724
|
+
async def delete_repository(self, repo_id: uuid.UUID) -> None:
|
|
725
|
+
await self._db.repositories.delete_one({"_id": _uuid_to_str(repo_id)})
|
|
726
|
+
|
|
727
|
+
# ------------------------------------------------------------------
|
|
728
|
+
# RepositoryWorkflowState
|
|
729
|
+
# ------------------------------------------------------------------
|
|
730
|
+
|
|
731
|
+
async def create_workflow_state(self, **kwargs: Any) -> _MongoDoc:
|
|
732
|
+
if "id" not in kwargs:
|
|
733
|
+
kwargs["id"] = _uuid_to_str(uuid.uuid4())
|
|
734
|
+
else:
|
|
735
|
+
kwargs["id"] = _uuid_to_str(kwargs["id"])
|
|
736
|
+
for uuid_field in ("repo_id", "session_id"):
|
|
737
|
+
if uuid_field in kwargs and isinstance(kwargs[uuid_field], uuid.UUID):
|
|
738
|
+
kwargs[uuid_field] = _uuid_to_str(kwargs[uuid_field])
|
|
739
|
+
kwargs.setdefault("completed_steps", [])
|
|
740
|
+
kwargs.setdefault("blocked_by", [])
|
|
741
|
+
kwargs.setdefault("artifacts", {})
|
|
742
|
+
kwargs.setdefault("next_step", None)
|
|
743
|
+
kwargs.setdefault("updated_at", _now())
|
|
744
|
+
kwargs["_id"] = kwargs.pop("id")
|
|
745
|
+
await self._db.repository_workflow_states.insert_one(kwargs)
|
|
746
|
+
return _to_doc(kwargs)
|
|
747
|
+
|
|
748
|
+
async def get_workflow_state_by_id(self, state_id: uuid.UUID) -> _MongoDoc | None:
|
|
749
|
+
doc = await self._db.repository_workflow_states.find_one(
|
|
750
|
+
{"_id": _uuid_to_str(state_id)}
|
|
751
|
+
)
|
|
752
|
+
return _to_doc(doc) if doc else None
|
|
753
|
+
|
|
754
|
+
async def get_workflow_state_by_repo(self, repo_id: uuid.UUID) -> _MongoDoc | None:
|
|
755
|
+
doc = await self._db.repository_workflow_states.find_one(
|
|
756
|
+
{"repo_id": _uuid_to_str(repo_id)}
|
|
757
|
+
)
|
|
758
|
+
return _to_doc(doc) if doc else None
|
|
759
|
+
|
|
760
|
+
async def update_workflow_state(
|
|
761
|
+
self, state_id: uuid.UUID, **kwargs: Any
|
|
762
|
+
) -> _MongoDoc | None:
|
|
763
|
+
if not kwargs:
|
|
764
|
+
return await self.get_workflow_state_by_id(state_id)
|
|
765
|
+
kwargs["updated_at"] = _now()
|
|
766
|
+
await self._db.repository_workflow_states.update_one(
|
|
767
|
+
{"_id": _uuid_to_str(state_id)}, {"$set": kwargs}
|
|
768
|
+
)
|
|
769
|
+
return await self.get_workflow_state_by_id(state_id)
|
|
770
|
+
|
|
771
|
+
async def delete_workflow_state(self, state_id: uuid.UUID) -> None:
|
|
772
|
+
await self._db.repository_workflow_states.delete_one(
|
|
773
|
+
{"_id": _uuid_to_str(state_id)}
|
|
774
|
+
)
|
|
775
|
+
|
|
776
|
+
# ------------------------------------------------------------------
|
|
777
|
+
# Document (metadata — not vector store)
|
|
778
|
+
# ------------------------------------------------------------------
|
|
779
|
+
|
|
780
|
+
async def create_document(
|
|
781
|
+
self,
|
|
782
|
+
title: str,
|
|
783
|
+
content: str,
|
|
784
|
+
doc_type: str,
|
|
785
|
+
source_path: str,
|
|
786
|
+
project: str,
|
|
787
|
+
*,
|
|
788
|
+
chunks: dict[str, Any] | None = None,
|
|
789
|
+
embedding: list[float] | None = None,
|
|
790
|
+
) -> _MongoDoc:
|
|
791
|
+
doc_id = _uuid_to_str(uuid.uuid4())
|
|
792
|
+
document: dict[str, Any] = {
|
|
793
|
+
"_id": doc_id,
|
|
794
|
+
"title": title,
|
|
795
|
+
"content": content,
|
|
796
|
+
"doc_type": doc_type,
|
|
797
|
+
"source_path": source_path,
|
|
798
|
+
"project": project,
|
|
799
|
+
"chunks": chunks or {},
|
|
800
|
+
"embedding": embedding,
|
|
801
|
+
"created_at": _now(),
|
|
802
|
+
"updated_at": _now(),
|
|
803
|
+
}
|
|
804
|
+
await self._db.documents.insert_one(document)
|
|
805
|
+
return _to_doc(document)
|
|
806
|
+
|
|
807
|
+
async def get_document_by_path(
|
|
808
|
+
self, source_path: str, *, project: str | None = None
|
|
809
|
+
) -> _MongoDoc | None:
|
|
810
|
+
query: dict[str, Any] = {"source_path": source_path}
|
|
811
|
+
if project is not None:
|
|
812
|
+
query["project"] = project
|
|
813
|
+
doc = await self._db.documents.find_one(query)
|
|
814
|
+
return _to_doc(doc) if doc else None
|
|
815
|
+
|
|
816
|
+
async def get_documents_by_ids(self, doc_ids: list[uuid.UUID]) -> list[_MongoDoc]:
|
|
817
|
+
if not doc_ids:
|
|
818
|
+
return []
|
|
819
|
+
cursor = self._db.documents.find(
|
|
820
|
+
{"_id": {"$in": [_uuid_to_str(doc_id) for doc_id in doc_ids]}}
|
|
821
|
+
)
|
|
822
|
+
return [_to_doc(doc) async for doc in cursor]
|
|
823
|
+
|
|
824
|
+
async def list_documents(self, project: str | None = None) -> list[_MongoDoc]:
|
|
825
|
+
query: dict[str, Any] = {}
|
|
826
|
+
if project is not None:
|
|
827
|
+
query["project"] = project
|
|
828
|
+
cursor = self._db.documents.find(query)
|
|
829
|
+
return [_to_doc(doc) async for doc in cursor]
|
|
830
|
+
|
|
831
|
+
async def upsert_document(
|
|
832
|
+
self,
|
|
833
|
+
*,
|
|
834
|
+
title: str,
|
|
835
|
+
content: str,
|
|
836
|
+
doc_type: str,
|
|
837
|
+
source_path: str,
|
|
838
|
+
project: str,
|
|
839
|
+
chunks: dict[str, Any] | None = None,
|
|
840
|
+
embedding: list[float] | None = None,
|
|
841
|
+
) -> _MongoDoc:
|
|
842
|
+
existing = await self.get_document_by_path(source_path, project=project)
|
|
843
|
+
if existing is None:
|
|
844
|
+
return await self.create_document(
|
|
845
|
+
title=title,
|
|
846
|
+
content=content,
|
|
847
|
+
doc_type=doc_type,
|
|
848
|
+
source_path=source_path,
|
|
849
|
+
project=project,
|
|
850
|
+
chunks=chunks,
|
|
851
|
+
embedding=embedding,
|
|
852
|
+
)
|
|
853
|
+
await self._db.documents.update_one(
|
|
854
|
+
{"_id": _uuid_to_str(existing.id)},
|
|
855
|
+
{
|
|
856
|
+
"$set": {
|
|
857
|
+
"title": title,
|
|
858
|
+
"content": content,
|
|
859
|
+
"doc_type": doc_type,
|
|
860
|
+
"chunks": chunks or {},
|
|
861
|
+
"embedding": embedding,
|
|
862
|
+
"project": project,
|
|
863
|
+
"updated_at": _now(),
|
|
864
|
+
}
|
|
865
|
+
},
|
|
866
|
+
)
|
|
867
|
+
updated = await self._db.documents.find_one({"_id": _uuid_to_str(existing.id)})
|
|
868
|
+
return _to_doc(updated) # type: ignore[arg-type]
|
|
869
|
+
|
|
870
|
+
async def delete_documents_not_in_paths(
|
|
871
|
+
self, *, project: str, keep_paths: set[str]
|
|
872
|
+
) -> None:
|
|
873
|
+
query: dict[str, Any] = {"project": project}
|
|
874
|
+
if keep_paths:
|
|
875
|
+
query["source_path"] = {"$nin": list(keep_paths)}
|
|
876
|
+
await self._db.documents.delete_many(query)
|
|
877
|
+
|
|
878
|
+
# ------------------------------------------------------------------
|
|
879
|
+
# History
|
|
880
|
+
# ------------------------------------------------------------------
|
|
881
|
+
|
|
882
|
+
async def create_history(
|
|
883
|
+
self,
|
|
884
|
+
session_id: uuid.UUID,
|
|
885
|
+
role: str,
|
|
886
|
+
content: str,
|
|
887
|
+
reasoning_trace: str | None = None,
|
|
888
|
+
tool_calls: dict[str, Any] | None = None,
|
|
889
|
+
tokens_used: int = 0,
|
|
890
|
+
latency_ms: int = 0,
|
|
891
|
+
) -> _MongoDoc:
|
|
892
|
+
doc_id = _uuid_to_str(uuid.uuid4())
|
|
893
|
+
document: dict[str, Any] = {
|
|
894
|
+
"_id": doc_id,
|
|
895
|
+
"session_id": _uuid_to_str(session_id),
|
|
896
|
+
"role": role,
|
|
897
|
+
"content": content,
|
|
898
|
+
"reasoning_trace": reasoning_trace,
|
|
899
|
+
"tool_calls": tool_calls or {},
|
|
900
|
+
"tokens_used": tokens_used,
|
|
901
|
+
"latency_ms": latency_ms,
|
|
902
|
+
"created_at": _now(),
|
|
903
|
+
}
|
|
904
|
+
await self._db.history.insert_one(document)
|
|
905
|
+
return _to_doc(document)
|
|
906
|
+
|
|
907
|
+
async def list_history_for_session(self, session_id: uuid.UUID) -> list[_MongoDoc]:
|
|
908
|
+
cursor = self._db.history.find({"session_id": _uuid_to_str(session_id)})
|
|
909
|
+
return [_to_doc(doc) async for doc in cursor]
|
|
910
|
+
|
|
911
|
+
async def list_history_for_user(self, user_id: uuid.UUID) -> list[_MongoDoc]:
|
|
912
|
+
# Get all session IDs for this user, then get history for those sessions
|
|
913
|
+
user_id_str = _uuid_to_str(user_id)
|
|
914
|
+
session_cursor = self._db.sessions.find({"user_id": user_id_str}, {"_id": 1})
|
|
915
|
+
session_ids = [doc["_id"] async for doc in session_cursor]
|
|
916
|
+
if not session_ids:
|
|
917
|
+
return []
|
|
918
|
+
cursor = self._db.history.find({"session_id": {"$in": session_ids}})
|
|
919
|
+
return [_to_doc(doc) async for doc in cursor]
|
|
920
|
+
|
|
921
|
+
async def delete_history_for_session(self, session_id: uuid.UUID) -> int:
|
|
922
|
+
result = await self._db.history.delete_many(
|
|
923
|
+
{"session_id": _uuid_to_str(session_id)}
|
|
924
|
+
)
|
|
925
|
+
return int(result.deleted_count)
|
|
926
|
+
|
|
927
|
+
# ------------------------------------------------------------------
|
|
928
|
+
# Error
|
|
929
|
+
# ------------------------------------------------------------------
|
|
930
|
+
|
|
931
|
+
async def create_error(
|
|
932
|
+
self,
|
|
933
|
+
error_code: str,
|
|
934
|
+
error_message: str,
|
|
935
|
+
stack_trace: str | None = None,
|
|
936
|
+
context: dict[str, Any] | None = None,
|
|
937
|
+
resolution: str | None = None,
|
|
938
|
+
embedding: list[float] | None = None,
|
|
939
|
+
resolved: bool = False,
|
|
940
|
+
) -> _MongoDoc:
|
|
941
|
+
doc_id = _uuid_to_str(uuid.uuid4())
|
|
942
|
+
document: dict[str, Any] = {
|
|
943
|
+
"_id": doc_id,
|
|
944
|
+
"error_code": error_code,
|
|
945
|
+
"error_message": error_message,
|
|
946
|
+
"stack_trace": stack_trace,
|
|
947
|
+
"context": context or {},
|
|
948
|
+
"resolution": resolution,
|
|
949
|
+
"embedding": embedding,
|
|
950
|
+
"resolved": resolved,
|
|
951
|
+
"created_at": _now(),
|
|
952
|
+
}
|
|
953
|
+
await self._db.errors.insert_one(document)
|
|
954
|
+
return _to_doc(document)
|
|
955
|
+
|
|
956
|
+
async def list_errors(self) -> list[_MongoDoc]:
|
|
957
|
+
cursor = self._db.errors.find()
|
|
958
|
+
return [_to_doc(doc) async for doc in cursor]
|
|
959
|
+
|
|
960
|
+
async def search_errors(self, query: str, limit: int = 5) -> list[dict[str, Any]]:
|
|
961
|
+
"""Simple TF-based error search (same as SQLite adapter for now)."""
|
|
962
|
+
rows = await self.list_errors()
|
|
963
|
+
query_vector = self._text_vector(query)
|
|
964
|
+
ranked: list[dict[str, Any]] = []
|
|
965
|
+
for row in rows:
|
|
966
|
+
text = f"{row.error_code} {row.error_message} {row.context}"
|
|
967
|
+
score = self._cosine_similarity(query_vector, self._text_vector(text))
|
|
968
|
+
ranked.append(
|
|
969
|
+
{
|
|
970
|
+
"id": row.id,
|
|
971
|
+
"error_code": row.error_code,
|
|
972
|
+
"error_message": row.error_message,
|
|
973
|
+
"resolution": row.resolution,
|
|
974
|
+
"score": round(score, 4),
|
|
975
|
+
}
|
|
976
|
+
)
|
|
977
|
+
ranked.sort(key=lambda item: cast(float, item["score"]), reverse=True)
|
|
978
|
+
return ranked[:limit]
|
|
979
|
+
|
|
980
|
+
@staticmethod
|
|
981
|
+
def _text_vector(text: str) -> Counter[str]:
|
|
982
|
+
return Counter(token for token in text.lower().split() if len(token) > 2)
|
|
983
|
+
|
|
984
|
+
@staticmethod
|
|
985
|
+
def _cosine_similarity(left: Counter[str], right: Counter[str]) -> float:
|
|
986
|
+
if not left or not right:
|
|
987
|
+
return 0.0
|
|
988
|
+
numerator = sum(left[key] * right[key] for key in left.keys() & right.keys())
|
|
989
|
+
left_norm = math.sqrt(sum(value * value for value in left.values()))
|
|
990
|
+
right_norm = math.sqrt(sum(value * value for value in right.values()))
|
|
991
|
+
if left_norm == 0 or right_norm == 0:
|
|
992
|
+
return 0.0
|
|
993
|
+
return numerator / (left_norm * right_norm)
|