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
minder/tools/search.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from minder.config import MinderConfig
|
|
6
|
+
from minder.store.interfaces import IOperationalStore
|
|
7
|
+
from minder.tools.memory import MemoryTools
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SearchTools:
|
|
11
|
+
def __init__(self, store: IOperationalStore, config: MinderConfig) -> None:
|
|
12
|
+
self._memory = MemoryTools(store, config)
|
|
13
|
+
|
|
14
|
+
async def minder_search(self, query: str, *, limit: int = 5) -> list[dict[str, Any]]:
|
|
15
|
+
return await self._memory.minder_memory_recall(query, limit=limit)
|
minder/tools/session.py
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
"""Session Tools — MCP surface for per-client LLM context persistence.
|
|
2
|
+
|
|
3
|
+
Design rationale
|
|
4
|
+
----------------
|
|
5
|
+
A session is the server-side checkpoint for a single LLM work context. It is
|
|
6
|
+
keyed by a **server-assigned UUID** but is *also* addressable by a human-readable
|
|
7
|
+
**name** so the same LLM can resume the exact session from any machine using the
|
|
8
|
+
same client API key:
|
|
9
|
+
|
|
10
|
+
Machine A: minder_session_create(name="omi-channel-phase5")
|
|
11
|
+
→ {session_id: "a1b2..."}
|
|
12
|
+
Machine B: minder_session_find(name="omi-channel-phase5")
|
|
13
|
+
→ {session_id: "a1b2...", state: {...}, ...}
|
|
14
|
+
|
|
15
|
+
The `session_id` UUID is returned in every response so the LLM can cache it in
|
|
16
|
+
its context for faster subsequent calls while the session is active. After a
|
|
17
|
+
``/compact`` or machine switch the LLM calls ``minder_session_find`` with the
|
|
18
|
+
project name and immediately regains full context.
|
|
19
|
+
|
|
20
|
+
Access control
|
|
21
|
+
--------------
|
|
22
|
+
Sessions are owned by the creating principal (user or client). The ``session_id``
|
|
23
|
+
UUID acts as a bearer token for ``save``/``restore``/``context`` operations — the
|
|
24
|
+
server validates existence but does not re-check ownership on every call.
|
|
25
|
+
``minder_session_find`` enforces ownership by filtering on the caller's
|
|
26
|
+
principal_id automatically.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import uuid
|
|
32
|
+
from datetime import UTC, datetime, timedelta
|
|
33
|
+
from typing import Any
|
|
34
|
+
|
|
35
|
+
from minder.continuity import build_continuity_brief, build_instruction_envelope
|
|
36
|
+
from minder.observability.metrics import record_continuity_packet
|
|
37
|
+
from minder.store.interfaces import IOperationalStore
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class SessionTools:
|
|
41
|
+
def __init__(self, store: IOperationalStore) -> None:
|
|
42
|
+
self._store = store
|
|
43
|
+
|
|
44
|
+
def _normalize_datetime(self, value: datetime | None) -> datetime | None:
|
|
45
|
+
if value is None:
|
|
46
|
+
return None
|
|
47
|
+
if value.tzinfo is None:
|
|
48
|
+
return value.replace(tzinfo=UTC)
|
|
49
|
+
return value.astimezone(UTC)
|
|
50
|
+
|
|
51
|
+
def _expires_at(self, session: Any) -> datetime | None:
|
|
52
|
+
ttl = int(getattr(session, "ttl", 0) or 0)
|
|
53
|
+
if ttl <= 0:
|
|
54
|
+
return None
|
|
55
|
+
base = self._normalize_datetime(
|
|
56
|
+
getattr(session, "last_active", None)
|
|
57
|
+
or getattr(session, "created_at", None)
|
|
58
|
+
)
|
|
59
|
+
if base is None:
|
|
60
|
+
return None
|
|
61
|
+
return base + timedelta(seconds=ttl)
|
|
62
|
+
|
|
63
|
+
def _is_expired(self, session: Any, *, now: datetime | None = None) -> bool:
|
|
64
|
+
expires_at = self._expires_at(session)
|
|
65
|
+
if expires_at is None:
|
|
66
|
+
return False
|
|
67
|
+
reference_time = self._normalize_datetime(now) or datetime.now(UTC)
|
|
68
|
+
return expires_at <= reference_time
|
|
69
|
+
|
|
70
|
+
async def _cleanup_session(self, session_id: uuid.UUID) -> dict[str, int]:
|
|
71
|
+
deleted_history = await self._store.delete_history_for_session(session_id)
|
|
72
|
+
await self._store.delete_session(session_id)
|
|
73
|
+
return {"deleted_sessions": 1, "deleted_history": deleted_history}
|
|
74
|
+
|
|
75
|
+
async def _require_active_session(self, session_id: uuid.UUID) -> Any:
|
|
76
|
+
session = await self._store.get_session_by_id(session_id)
|
|
77
|
+
if session is None:
|
|
78
|
+
raise ValueError(f"Session not found: {session_id}")
|
|
79
|
+
if self._is_expired(session):
|
|
80
|
+
await self._cleanup_session(session_id)
|
|
81
|
+
raise ValueError(f"Session expired: {session_id}")
|
|
82
|
+
return session
|
|
83
|
+
|
|
84
|
+
# ------------------------------------------------------------------
|
|
85
|
+
# Create
|
|
86
|
+
# ------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
async def minder_session_create(
|
|
89
|
+
self,
|
|
90
|
+
*,
|
|
91
|
+
user_id: uuid.UUID | None = None,
|
|
92
|
+
client_id: uuid.UUID | None = None,
|
|
93
|
+
name: str | None = None,
|
|
94
|
+
repo_id: uuid.UUID | None = None,
|
|
95
|
+
project_context: dict[str, Any] | None = None,
|
|
96
|
+
) -> dict[str, Any]:
|
|
97
|
+
"""Create a new persisted session for the calling principal.
|
|
98
|
+
|
|
99
|
+
One of ``user_id`` (human admin) or ``client_id`` (MCP client) must be
|
|
100
|
+
provided. ``name`` is an optional project slug — use a stable, memorable
|
|
101
|
+
name so the session can be found again from any machine with the same
|
|
102
|
+
client API key. Example: ``"omi-channel-phase5-dev"``.
|
|
103
|
+
"""
|
|
104
|
+
if user_id is None and client_id is None:
|
|
105
|
+
raise ValueError("Either user_id or client_id must be provided")
|
|
106
|
+
session = await self._store.create_session(
|
|
107
|
+
id=uuid.uuid4(),
|
|
108
|
+
user_id=user_id,
|
|
109
|
+
client_id=client_id,
|
|
110
|
+
name=name,
|
|
111
|
+
repo_id=repo_id,
|
|
112
|
+
project_context=project_context or {},
|
|
113
|
+
active_skills={},
|
|
114
|
+
state={},
|
|
115
|
+
ttl=86400, # 24 h — long enough for multi-day work continuity
|
|
116
|
+
)
|
|
117
|
+
return {
|
|
118
|
+
"session_id": str(session.id),
|
|
119
|
+
"name": session.name,
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
# ------------------------------------------------------------------
|
|
123
|
+
# Find / list
|
|
124
|
+
# ------------------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
async def minder_session_find(
|
|
127
|
+
self,
|
|
128
|
+
*,
|
|
129
|
+
name: str,
|
|
130
|
+
user_id: uuid.UUID | None = None,
|
|
131
|
+
client_id: uuid.UUID | None = None,
|
|
132
|
+
) -> dict[str, Any]:
|
|
133
|
+
"""Find a session by name for the calling principal.
|
|
134
|
+
|
|
135
|
+
This is the primary **cross-environment recovery** entry point. The LLM
|
|
136
|
+
calls this on any machine using the same client API key and immediately
|
|
137
|
+
recovers full session state without needing to remember the UUID.
|
|
138
|
+
|
|
139
|
+
Returns the full session payload (same shape as ``minder_session_restore``)
|
|
140
|
+
or raises ``ValueError`` if no matching session is found.
|
|
141
|
+
"""
|
|
142
|
+
session = await self._store.find_session_by_name(
|
|
143
|
+
name,
|
|
144
|
+
user_id=user_id,
|
|
145
|
+
client_id=client_id,
|
|
146
|
+
)
|
|
147
|
+
if session is not None and not self._is_expired(session):
|
|
148
|
+
return {
|
|
149
|
+
"session_id": str(session.id),
|
|
150
|
+
"name": session.name,
|
|
151
|
+
"state": session.state,
|
|
152
|
+
"active_skills": session.active_skills,
|
|
153
|
+
"project_context": session.project_context,
|
|
154
|
+
"last_active": session.last_active.isoformat(),
|
|
155
|
+
}
|
|
156
|
+
raise ValueError(
|
|
157
|
+
f"No session named '{name}' found for the current principal. "
|
|
158
|
+
"Use minder_session_list to see all sessions or minder_session_create to start one."
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
async def minder_session_list(
|
|
162
|
+
self,
|
|
163
|
+
*,
|
|
164
|
+
user_id: uuid.UUID | None = None,
|
|
165
|
+
client_id: uuid.UUID | None = None,
|
|
166
|
+
) -> dict[str, Any]:
|
|
167
|
+
"""Return all sessions for the calling principal, newest-first.
|
|
168
|
+
|
|
169
|
+
Use this when you need to browse sessions or when you do not remember the
|
|
170
|
+
session name. Prefer ``minder_session_find`` when you know the name.
|
|
171
|
+
"""
|
|
172
|
+
if client_id is not None:
|
|
173
|
+
sessions = await self._store.get_sessions_by_client(client_id)
|
|
174
|
+
elif user_id is not None:
|
|
175
|
+
sessions = await self._store.get_sessions_by_user(user_id)
|
|
176
|
+
else:
|
|
177
|
+
raise ValueError("Either user_id or client_id must be provided")
|
|
178
|
+
active_sessions = [
|
|
179
|
+
session for session in sessions if not self._is_expired(session)
|
|
180
|
+
]
|
|
181
|
+
return {
|
|
182
|
+
"sessions": [
|
|
183
|
+
{
|
|
184
|
+
"session_id": str(s.id),
|
|
185
|
+
"name": s.name,
|
|
186
|
+
"repo_id": str(s.repo_id) if s.repo_id else None,
|
|
187
|
+
"project_context": s.project_context,
|
|
188
|
+
"last_active": s.last_active.isoformat(),
|
|
189
|
+
"created_at": s.created_at.isoformat(),
|
|
190
|
+
}
|
|
191
|
+
for s in sorted(
|
|
192
|
+
active_sessions, key=lambda s: s.last_active, reverse=True
|
|
193
|
+
)
|
|
194
|
+
]
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
# ------------------------------------------------------------------
|
|
198
|
+
# Save / restore / context
|
|
199
|
+
# ------------------------------------------------------------------
|
|
200
|
+
|
|
201
|
+
async def minder_session_save(
|
|
202
|
+
self,
|
|
203
|
+
session_id: uuid.UUID,
|
|
204
|
+
*,
|
|
205
|
+
state: dict[str, Any] | None = None,
|
|
206
|
+
active_skills: dict[str, Any] | None = None,
|
|
207
|
+
) -> dict[str, Any]:
|
|
208
|
+
"""Persist the LLM's current task state and active skill set.
|
|
209
|
+
|
|
210
|
+
Call this after every significant wave of work so the context survives
|
|
211
|
+
``/compact``, machine switches, or unexpected session drops. The ``state``
|
|
212
|
+
dict should capture:
|
|
213
|
+
|
|
214
|
+
- Current task description and phase
|
|
215
|
+
- Key decisions made
|
|
216
|
+
- Files modified / in progress
|
|
217
|
+
- Next planned steps
|
|
218
|
+
- Open questions / blockers
|
|
219
|
+
"""
|
|
220
|
+
await self._require_active_session(session_id)
|
|
221
|
+
session = await self._store.update_session(
|
|
222
|
+
session_id,
|
|
223
|
+
state=state or {},
|
|
224
|
+
active_skills=active_skills or {},
|
|
225
|
+
last_active=datetime.now(UTC),
|
|
226
|
+
)
|
|
227
|
+
if session is None:
|
|
228
|
+
raise ValueError(f"Session not found: {session_id}")
|
|
229
|
+
return {
|
|
230
|
+
"session_id": str(session.id),
|
|
231
|
+
"name": session.name,
|
|
232
|
+
"state": session.state,
|
|
233
|
+
"active_skills": session.active_skills,
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async def minder_session_restore(self, session_id: uuid.UUID) -> dict[str, Any]:
|
|
237
|
+
"""Restore a session checkpoint by UUID.
|
|
238
|
+
|
|
239
|
+
Use ``minder_session_find`` instead when you know the session name —
|
|
240
|
+
it performs owner-scoped lookup and returns the same payload.
|
|
241
|
+
"""
|
|
242
|
+
session = await self._require_active_session(session_id)
|
|
243
|
+
|
|
244
|
+
continuity_packet: dict[str, Any] | None = None
|
|
245
|
+
if session.repo_id is not None:
|
|
246
|
+
repo = await self._store.get_repository_by_id(session.repo_id)
|
|
247
|
+
workflow_state = await self._store.get_workflow_state_by_repo(
|
|
248
|
+
session.repo_id
|
|
249
|
+
)
|
|
250
|
+
workflow = None
|
|
251
|
+
if repo is not None and repo.workflow_id is not None:
|
|
252
|
+
workflow = await self._store.get_workflow_by_id(repo.workflow_id)
|
|
253
|
+
if workflow is not None and workflow_state is not None:
|
|
254
|
+
continuity_packet = {
|
|
255
|
+
"instruction_envelope": build_instruction_envelope(
|
|
256
|
+
workflow=workflow,
|
|
257
|
+
workflow_state=workflow_state,
|
|
258
|
+
),
|
|
259
|
+
"session_brief": build_continuity_brief(
|
|
260
|
+
session=session,
|
|
261
|
+
workflow_state=workflow_state,
|
|
262
|
+
workflow=workflow,
|
|
263
|
+
),
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
payload = {
|
|
267
|
+
"session_id": str(session.id),
|
|
268
|
+
"name": session.name,
|
|
269
|
+
"state": session.state,
|
|
270
|
+
"active_skills": session.active_skills,
|
|
271
|
+
"project_context": session.project_context,
|
|
272
|
+
}
|
|
273
|
+
if continuity_packet is not None:
|
|
274
|
+
record_continuity_packet("session_restore")
|
|
275
|
+
payload["continuity_packet"] = continuity_packet
|
|
276
|
+
return payload
|
|
277
|
+
|
|
278
|
+
async def minder_session_context(
|
|
279
|
+
self,
|
|
280
|
+
session_id: uuid.UUID,
|
|
281
|
+
*,
|
|
282
|
+
branch: str,
|
|
283
|
+
open_files: list[str],
|
|
284
|
+
) -> dict[str, Any]:
|
|
285
|
+
"""Update the repository context for an existing session.
|
|
286
|
+
|
|
287
|
+
Call after a branch switch or when the set of actively edited files
|
|
288
|
+
changes so that future session restores include current context.
|
|
289
|
+
"""
|
|
290
|
+
session = await self._require_active_session(session_id)
|
|
291
|
+
project_context = dict(session.project_context)
|
|
292
|
+
project_context.update({"branch": branch, "open_files": open_files})
|
|
293
|
+
updated = await self._store.update_session(
|
|
294
|
+
session_id, project_context=project_context
|
|
295
|
+
)
|
|
296
|
+
if updated is None:
|
|
297
|
+
raise ValueError(f"Session not found: {session_id}")
|
|
298
|
+
return {
|
|
299
|
+
"session_id": str(updated.id),
|
|
300
|
+
"name": updated.name,
|
|
301
|
+
"branch": branch,
|
|
302
|
+
"open_files": open_files,
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async def minder_session_cleanup(
|
|
306
|
+
self,
|
|
307
|
+
*,
|
|
308
|
+
user_id: uuid.UUID | None = None,
|
|
309
|
+
client_id: uuid.UUID | None = None,
|
|
310
|
+
) -> dict[str, int]:
|
|
311
|
+
if client_id is None and user_id is None:
|
|
312
|
+
raise ValueError("Either user_id or client_id must be provided")
|
|
313
|
+
return await self._store.cleanup_expired_sessions(
|
|
314
|
+
user_id=user_id,
|
|
315
|
+
client_id=client_id,
|
|
316
|
+
)
|