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,9 @@
1
+ from .base import EmbeddingProvider
2
+ from .local import LocalEmbeddingProvider
3
+ from .openai import OpenAIEmbeddingProvider
4
+
5
+ __all__ = [
6
+ "EmbeddingProvider",
7
+ "LocalEmbeddingProvider",
8
+ "OpenAIEmbeddingProvider",
9
+ ]
@@ -0,0 +1,7 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Protocol
4
+
5
+
6
+ class EmbeddingProvider(Protocol):
7
+ def embed(self, text: str) -> list[float]: ...
@@ -0,0 +1,65 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ from minder.runtime import load_attr, module_available
8
+
9
+
10
+ class LocalEmbeddingProvider:
11
+ def __init__(self, model_path: str, dimensions: int = 768, runtime: str = "mock") -> None:
12
+ self._model_path = model_path
13
+ self._dimensions = dimensions
14
+ self._runtime = runtime
15
+ self._client: Any | None = None
16
+
17
+ @property
18
+ def runtime(self) -> str:
19
+ runtime = self._runtime
20
+ if runtime == "auto":
21
+ if Path(self._model_path).expanduser().exists() and module_available("llama_cpp"):
22
+ return "llama_cpp"
23
+ return "mock"
24
+ return runtime
25
+
26
+ def embed(self, text: str) -> list[float]:
27
+ if self.runtime == "llama_cpp":
28
+ embedded = self._embed_with_llama_cpp(text)
29
+ if embedded is not None:
30
+ return embedded[: self._dimensions]
31
+ digest = hashlib.sha256(text.encode("utf-8")).digest()
32
+ values: list[float] = []
33
+ for index in range(self._dimensions):
34
+ byte = digest[index % len(digest)]
35
+ values.append(round(byte / 255.0, 6))
36
+ return values
37
+
38
+ def _embed_with_llama_cpp(self, text: str) -> list[float] | None:
39
+ client = self._llama_client()
40
+ if client is None:
41
+ return None
42
+ response = client.embed(text)
43
+ if isinstance(response, list):
44
+ return [float(value) for value in response]
45
+ data = response.get("data", []) if isinstance(response, dict) else []
46
+ if not data:
47
+ return None
48
+ embedding = data[0].get("embedding", [])
49
+ return [float(value) for value in embedding]
50
+
51
+ def _llama_client(self) -> Any | None:
52
+ if self._client is not None:
53
+ return self._client
54
+ llama_cls = load_attr("llama_cpp", "Llama")
55
+ if llama_cls is None:
56
+ return None
57
+ try:
58
+ self._client = llama_cls(
59
+ model_path=str(Path(self._model_path).expanduser()),
60
+ embedding=True,
61
+ verbose=False,
62
+ )
63
+ except Exception:
64
+ return None
65
+ return self._client
@@ -0,0 +1,7 @@
1
+ from __future__ import annotations
2
+
3
+ from minder.embedding.local import LocalEmbeddingProvider
4
+
5
+
6
+ class OpenAIEmbeddingProvider(LocalEmbeddingProvider):
7
+ pass
@@ -0,0 +1,11 @@
1
+ from .state import GraphState
2
+
3
+ __all__ = ["GraphState", "MinderGraph"]
4
+
5
+
6
+ def __getattr__(name: str):
7
+ if name == "MinderGraph":
8
+ from .graph import MinderGraph
9
+
10
+ return MinderGraph
11
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
minder/graph/edges.py ADDED
@@ -0,0 +1,13 @@
1
+ from __future__ import annotations
2
+
3
+ from minder.graph.state import GraphState
4
+
5
+
6
+ def determine_next_edge(state: GraphState) -> str:
7
+ if state.metadata.get("fallback_used") is True:
8
+ return "fallback_complete"
9
+ if state.guard_result.get("passed") is False:
10
+ return "guard_failed"
11
+ if state.verification_result.get("passed") is False:
12
+ return "verification_failed"
13
+ return "complete"
@@ -0,0 +1,127 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+
5
+ from minder.graph.edges import determine_next_edge
6
+ from minder.graph.nodes import (
7
+ EvaluatorNode,
8
+ GuardNode,
9
+ LLMNode,
10
+ PlanningNode,
11
+ ReasoningNode,
12
+ RerankerNode,
13
+ RetrieverNode,
14
+ VerificationNode,
15
+ WorkflowPlannerNode,
16
+ )
17
+ from minder.graph.runtime import graph_runtime_name, load_langgraph_state_graph
18
+ from minder.graph.state import GraphState
19
+
20
+
21
+ @dataclass
22
+ class GraphNodes:
23
+ workflow_planner: WorkflowPlannerNode
24
+ planning: PlanningNode
25
+ retriever: RetrieverNode
26
+ reasoning: ReasoningNode
27
+ llm: LLMNode
28
+ guard: GuardNode
29
+ verification: VerificationNode
30
+ evaluator: EvaluatorNode
31
+ reranker: RerankerNode | None = field(default=None)
32
+
33
+
34
+ class InternalGraphExecutor:
35
+ def __init__(self, nodes: GraphNodes) -> None:
36
+ self._nodes = nodes
37
+
38
+ async def run(self, state: GraphState) -> GraphState:
39
+ max_attempts = int(state.metadata.get("max_attempts", 1))
40
+ state.metadata.setdefault("attempt_failures", [])
41
+ state.metadata["orchestration_runtime"] = "internal"
42
+ state = await self._nodes.workflow_planner.run(state)
43
+ state = self._nodes.planning.run(state)
44
+ state = await self._nodes.retriever.run(state)
45
+ if self._nodes.reranker is not None:
46
+ state = await self._nodes.reranker.run(state)
47
+
48
+ attempt = 0
49
+ while True:
50
+ attempt += 1
51
+ state.retry_count = attempt - 1
52
+ state = self._nodes.reasoning.run(state)
53
+ state = self._nodes.llm.run(state)
54
+ state = self._nodes.guard.run(state)
55
+ state = self._nodes.verification.run(state)
56
+ edge = determine_next_edge(state)
57
+ state.transition_log.append(
58
+ {
59
+ "attempt": attempt,
60
+ "edge": edge,
61
+ "provider": state.llm_output.get("provider"),
62
+ "fallback_used": state.metadata.get("fallback_used", False),
63
+ }
64
+ )
65
+ if (
66
+ edge not in {"verification_failed", "guard_failed"}
67
+ or attempt >= max_attempts
68
+ ):
69
+ break
70
+ retry_reason = (
71
+ "; ".join(
72
+ str(reason)
73
+ for reason in state.guard_result.get("reasons", [])
74
+ if reason
75
+ )
76
+ if edge == "guard_failed"
77
+ else state.verification_result.get("stderr", "verification failed")
78
+ )
79
+ state.metadata["attempt_failures"].append(
80
+ {
81
+ "attempt": attempt,
82
+ "reason": retry_reason,
83
+ "provider": state.llm_output.get("provider"),
84
+ "edge": edge,
85
+ }
86
+ )
87
+ state.metadata["retry_reason"] = retry_reason
88
+
89
+ state = self._nodes.evaluator.run(state)
90
+ state.metadata["edge"] = determine_next_edge(state)
91
+ return state
92
+
93
+
94
+ class LangGraphExecutorAdapter:
95
+ def __init__(self, nodes: GraphNodes) -> None:
96
+ self._nodes = nodes
97
+ self._internal = InternalGraphExecutor(nodes)
98
+ self._compiled_graph = None
99
+
100
+ async def run(self, state: GraphState) -> GraphState:
101
+ if graph_runtime_name() != "langgraph":
102
+ state = await self._internal.run(state)
103
+ state.metadata["orchestration_runtime"] = "internal"
104
+ return state
105
+
106
+ compiled = self._compiled_graph or self._build_compiled_graph()
107
+ self._compiled_graph = compiled
108
+ result = await compiled.ainvoke(state)
109
+ if isinstance(result, GraphState):
110
+ result.metadata["orchestration_runtime"] = "langgraph"
111
+ return result
112
+ validated = GraphState.model_validate(result)
113
+ validated.metadata["orchestration_runtime"] = "langgraph"
114
+ return validated
115
+
116
+ def _build_compiled_graph(self):
117
+ state_graph_cls = load_langgraph_state_graph()
118
+ if state_graph_cls is None:
119
+ raise RuntimeError(
120
+ "LangGraph runtime requested but StateGraph is unavailable"
121
+ )
122
+
123
+ workflow = state_graph_cls(GraphState)
124
+ workflow.add_node("internal_executor", self._internal.run)
125
+ workflow.set_entry_point("internal_executor")
126
+ workflow.set_finish_point("internal_executor")
127
+ return workflow.compile()
minder/graph/graph.py ADDED
@@ -0,0 +1,263 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import AsyncGenerator
4
+ from time import perf_counter
5
+
6
+ from minder.config import MinderConfig
7
+ from minder.embedding.local import LocalEmbeddingProvider
8
+ from minder.graph.edges import determine_next_edge
9
+ from minder.graph.executor import (
10
+ GraphNodes,
11
+ InternalGraphExecutor,
12
+ LangGraphExecutorAdapter,
13
+ )
14
+ from minder.graph.nodes import (
15
+ EvaluatorNode,
16
+ GuardNode,
17
+ LLMNode,
18
+ PlanningNode,
19
+ ReasoningNode,
20
+ RerankerNode,
21
+ RetrieverNode,
22
+ VerificationNode,
23
+ WorkflowPlannerNode,
24
+ )
25
+ from minder.graph.state import GraphState
26
+ from minder.llm.local import LocalModelLLM
27
+ from minder.llm.openai import OpenAIFallbackLLM
28
+ from minder.store.interfaces import (
29
+ IOperationalStore,
30
+ IErrorRepository,
31
+ IHistoryRepository,
32
+ )
33
+
34
+
35
+ class MinderGraph:
36
+ def __init__(
37
+ self,
38
+ store: IOperationalStore,
39
+ config: MinderConfig,
40
+ *,
41
+ workflow_planner: WorkflowPlannerNode | None = None,
42
+ planning: PlanningNode | None = None,
43
+ retriever: RetrieverNode | None = None,
44
+ reranker: RerankerNode | None = None,
45
+ reasoning: ReasoningNode | None = None,
46
+ llm: LLMNode | None = None,
47
+ guard: GuardNode | None = None,
48
+ verification: VerificationNode | None = None,
49
+ evaluator: EvaluatorNode | None = None,
50
+ history_store: IHistoryRepository | None = None,
51
+ error_store: IErrorRepository | None = None,
52
+ ) -> None:
53
+ from minder.store.vector import VectorStore
54
+
55
+ self._store = store
56
+ self._config = config
57
+ self._workflow_planner = workflow_planner or WorkflowPlannerNode(store)
58
+ self._planning = planning or PlanningNode()
59
+ vector_store = VectorStore(store, store)
60
+ embedder = LocalEmbeddingProvider(
61
+ config.embedding.model_path,
62
+ dimensions=config.embedding.dimensions,
63
+ runtime="auto",
64
+ )
65
+ self._retriever = retriever or RetrieverNode(
66
+ top_k=config.retrieval.top_k,
67
+ embedding_provider=embedder,
68
+ vector_store=vector_store,
69
+ score_threshold=config.retrieval.similarity_threshold,
70
+ )
71
+ self._reranker = reranker # None by default; pass RerankerNode(...) to activate
72
+ self._reasoning = reasoning or ReasoningNode()
73
+ self._llm = llm or LLMNode(
74
+ primary=LocalModelLLM(
75
+ config.llm.model_path,
76
+ runtime="auto",
77
+ context_length=config.llm.context_length,
78
+ ),
79
+ fallback=OpenAIFallbackLLM(
80
+ config.llm.openai_api_key,
81
+ config.llm.openai_model,
82
+ runtime="auto",
83
+ ),
84
+ )
85
+ self._guard = guard or GuardNode()
86
+ self._verification = verification or VerificationNode(
87
+ sandbox=config.verification.sandbox,
88
+ timeout_seconds=config.verification.timeout_seconds,
89
+ )
90
+ self._evaluator = evaluator or EvaluatorNode()
91
+ self._history_store = history_store or store
92
+ self._error_store = error_store or store
93
+ self._nodes = GraphNodes(
94
+ workflow_planner=self._workflow_planner,
95
+ planning=self._planning,
96
+ retriever=self._retriever,
97
+ reranker=self._reranker,
98
+ reasoning=self._reasoning,
99
+ llm=self._llm,
100
+ guard=self._guard,
101
+ verification=self._verification,
102
+ evaluator=self._evaluator,
103
+ )
104
+
105
+ async def run(self, state: GraphState) -> GraphState:
106
+ executor = self._select_executor()
107
+ state = await executor.run(state)
108
+
109
+ await self._persist_history(state)
110
+ await self._persist_error_if_needed(state)
111
+ await self._advance_workflow_if_needed(state)
112
+ return state
113
+
114
+ async def stream(
115
+ self, state: GraphState
116
+ ) -> AsyncGenerator[dict[str, object], None]:
117
+ max_attempts = int(state.metadata.get("max_attempts", 1))
118
+ state.metadata.setdefault("attempt_failures", [])
119
+ state.metadata["orchestration_runtime"] = "internal"
120
+ state = await self._nodes.workflow_planner.run(state)
121
+ state = self._nodes.planning.run(state)
122
+ state = await self._nodes.retriever.run(state)
123
+ if self._nodes.reranker is not None:
124
+ state = await self._nodes.reranker.run(state)
125
+
126
+ attempt = 0
127
+ while True:
128
+ attempt += 1
129
+ state.retry_count = attempt - 1
130
+ state = self._nodes.reasoning.run(state)
131
+ yield {"type": "attempt", "attempt": attempt}
132
+ for event in self._nodes.llm.stream(state):
133
+ if str(event.get("type")) == "result":
134
+ continue
135
+ yield {**event, "attempt": attempt}
136
+ state = self._nodes.guard.run(state)
137
+ state = self._nodes.verification.run(state)
138
+ edge = determine_next_edge(state)
139
+ state.transition_log.append(
140
+ {
141
+ "attempt": attempt,
142
+ "edge": edge,
143
+ "provider": state.llm_output.get("provider"),
144
+ "fallback_used": state.metadata.get("fallback_used", False),
145
+ }
146
+ )
147
+ if (
148
+ edge not in {"verification_failed", "guard_failed"}
149
+ or attempt >= max_attempts
150
+ ):
151
+ break
152
+ retry_reason = (
153
+ "; ".join(
154
+ str(reason)
155
+ for reason in state.guard_result.get("reasons", [])
156
+ if reason
157
+ )
158
+ if edge == "guard_failed"
159
+ else state.verification_result.get("stderr", "verification failed")
160
+ )
161
+ state.metadata["attempt_failures"].append(
162
+ {
163
+ "attempt": attempt,
164
+ "reason": retry_reason,
165
+ "provider": state.llm_output.get("provider"),
166
+ "edge": edge,
167
+ }
168
+ )
169
+ state.metadata["retry_reason"] = retry_reason
170
+ yield {
171
+ "type": "retry",
172
+ "attempt": attempt,
173
+ "reason": retry_reason,
174
+ "edge": edge,
175
+ }
176
+
177
+ state = self._nodes.evaluator.run(state)
178
+ state.metadata["edge"] = determine_next_edge(state)
179
+ await self._persist_history(state)
180
+ await self._persist_error_if_needed(state)
181
+ await self._advance_workflow_if_needed(state)
182
+ yield {"type": "final", "state": state}
183
+
184
+ def _select_executor(self) -> InternalGraphExecutor | LangGraphExecutorAdapter:
185
+ if self._config.workflow.orchestration_runtime == "langgraph":
186
+ return LangGraphExecutorAdapter(self._nodes)
187
+ return InternalGraphExecutor(self._nodes)
188
+
189
+ async def _persist_history(self, state: GraphState) -> None:
190
+ if state.session_id is None:
191
+ return
192
+ started = perf_counter()
193
+ await self._history_store.create_history(
194
+ session_id=state.session_id,
195
+ role="assistant",
196
+ content=str(state.llm_output.get("text", "")),
197
+ reasoning_trace=str(state.reasoning_output.get("prompt", "")),
198
+ tool_calls={
199
+ "pipeline": [
200
+ "workflow_planner",
201
+ "planning",
202
+ "retriever",
203
+ "reasoning",
204
+ "llm",
205
+ "guard",
206
+ "verification",
207
+ ],
208
+ "transition_log": state.transition_log,
209
+ "provider": state.llm_output.get("provider"),
210
+ "fallback_used": state.metadata.get("fallback_used", False),
211
+ },
212
+ latency_ms=int((perf_counter() - started) * 1000),
213
+ )
214
+
215
+ async def _persist_error_if_needed(self, state: GraphState) -> None:
216
+ reasons = list(state.guard_result.get("reasons", []))
217
+ if state.verification_result.get("passed") is False:
218
+ reasons.append(
219
+ str(state.verification_result.get("stderr", "verification failed"))
220
+ )
221
+ for attempt_failure in state.metadata.get("attempt_failures", []):
222
+ reasons.append(str(attempt_failure.get("reason", "attempt failed")))
223
+ if not reasons:
224
+ return
225
+ await self._error_store.create_error(
226
+ error_code="PIPELINE_FAILURE",
227
+ error_message="; ".join(reason for reason in reasons if reason),
228
+ context={
229
+ "query": state.query,
230
+ "repo_path": state.repo_path,
231
+ "transition_log": state.transition_log,
232
+ "provider": state.llm_output.get("provider"),
233
+ "retry_count": state.retry_count,
234
+ },
235
+ resolved=False,
236
+ )
237
+
238
+ async def _advance_workflow_if_needed(self, state: GraphState) -> None:
239
+ if state.repo_id is None:
240
+ return
241
+ if state.guard_result.get("passed") is not True:
242
+ return
243
+ if state.verification_result.get("passed") is not True:
244
+ return
245
+ workflow_state = await self._store.get_workflow_state_by_repo(state.repo_id)
246
+ if workflow_state is None:
247
+ return
248
+ current_step = workflow_state.current_step
249
+ completed_steps = list(workflow_state.completed_steps)
250
+ if current_step not in completed_steps:
251
+ completed_steps.append(current_step)
252
+ await self._store.update_workflow_state(
253
+ workflow_state.id,
254
+ completed_steps=completed_steps,
255
+ current_step=state.workflow_context.get("next_step") or current_step,
256
+ next_step=None,
257
+ artifacts={
258
+ **dict(workflow_state.artifacts),
259
+ "last_transition_reason": "guard+verification passed",
260
+ "last_provider": state.llm_output.get("provider"),
261
+ "last_edge": state.metadata.get("edge"),
262
+ },
263
+ )
@@ -0,0 +1,27 @@
1
+ from .evaluator import EvaluatorNode
2
+ from .guard import GuardNode
3
+ from .llm import LLMNode
4
+ from .planning import PlanningNode
5
+ from .reranker import RerankerNode
6
+ from .reasoning import ReasoningNode
7
+ from .retriever import RetrieverNode
8
+ from .verification import (
9
+ DockerSandboxRunner,
10
+ SubprocessVerificationRunner,
11
+ VerificationNode,
12
+ )
13
+ from .workflow_planner import WorkflowPlannerNode
14
+
15
+ __all__ = [
16
+ "DockerSandboxRunner",
17
+ "EvaluatorNode",
18
+ "GuardNode",
19
+ "LLMNode",
20
+ "PlanningNode",
21
+ "ReasoningNode",
22
+ "RerankerNode",
23
+ "RetrieverNode",
24
+ "SubprocessVerificationRunner",
25
+ "VerificationNode",
26
+ "WorkflowPlannerNode",
27
+ ]
@@ -0,0 +1,21 @@
1
+ from __future__ import annotations
2
+
3
+ from minder.graph.state import GraphState
4
+
5
+
6
+ class EvaluatorNode:
7
+ def run(self, state: GraphState) -> GraphState:
8
+ score = 1.0
9
+ if not state.guard_result.get("passed", False):
10
+ score -= 0.5
11
+ if not state.verification_result.get("passed", False):
12
+ score -= 0.3
13
+ if not state.reranked_docs:
14
+ score -= 0.1
15
+ score = max(score, 0.0)
16
+ state.evaluation = {
17
+ "quality_score": round(score, 2),
18
+ "correctness_score": round(score, 2),
19
+ "used_sources": len(state.reranked_docs),
20
+ }
21
+ return state
@@ -0,0 +1,64 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ import re
5
+ from re import Pattern
6
+
7
+ from minder.graph.state import GraphState
8
+
9
+ UNSAFE_PATTERNS = ("rm -rf", "DROP TABLE", "ignore all safety", "steal credentials")
10
+ SECRET_PATTERNS: tuple[Pattern[str], ...] = (
11
+ re.compile(r"AKIA[0-9A-Z]{16}"),
12
+ re.compile(r"sk-[A-Za-z0-9]{20,}"),
13
+ )
14
+
15
+
16
+ class GuardNode:
17
+ def run(self, state: GraphState) -> GraphState:
18
+ text = str(state.llm_output.get("text", ""))
19
+ reasons: list[str] = []
20
+ passed = True
21
+
22
+ lowered = text.lower()
23
+ for pattern in UNSAFE_PATTERNS:
24
+ if pattern.lower() in lowered:
25
+ passed = False
26
+ reasons.append(f"unsafe pattern detected: {pattern}")
27
+
28
+ for secret_pattern in SECRET_PATTERNS:
29
+ if secret_pattern.search(text):
30
+ passed = False
31
+ reasons.append("secret or token pattern detected")
32
+
33
+ if "```python" in text:
34
+ code = text.split("```python", 1)[1].split("```", 1)[0]
35
+ try:
36
+ ast.parse(code)
37
+ except SyntaxError as exc:
38
+ passed = False
39
+ reasons.append(f"python syntax error: {exc.msg}")
40
+
41
+ source_paths = [doc["path"] for doc in state.reranked_docs]
42
+ claimed_sources = state.llm_output.get("sources", [])
43
+ for source in claimed_sources:
44
+ if source not in source_paths:
45
+ passed = False
46
+ reasons.append(f"hallucinated source: {source}")
47
+
48
+ instruction_envelope = dict(
49
+ state.workflow_context.get("instruction_envelope", {}) or {}
50
+ )
51
+ output_contract = dict(instruction_envelope.get("output_contract", {}) or {})
52
+ required_markers = [
53
+ str(marker).strip().lower().replace("_", " ")
54
+ for marker in list(output_contract.get("must_include", []) or [])
55
+ if str(marker).strip()
56
+ ]
57
+ normalized_text = lowered.replace("_", " ")
58
+ for marker in required_markers:
59
+ if marker not in normalized_text:
60
+ passed = False
61
+ reasons.append(f"workflow output contract missing: {marker}")
62
+
63
+ state.guard_result = {"passed": passed, "reasons": reasons}
64
+ return state
@@ -0,0 +1,59 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Generator
4
+
5
+ from minder.graph.state import GraphState
6
+ from minder.llm.base import LLMClient
7
+ from minder.llm.openai import OpenAIFallbackLLM
8
+
9
+
10
+ class LLMNode:
11
+ def __init__(
12
+ self, primary: LLMClient, fallback: OpenAIFallbackLLM | None = None
13
+ ) -> None:
14
+ self._primary = primary
15
+ self._fallback = fallback
16
+
17
+ def run(self, state: GraphState) -> GraphState:
18
+ try:
19
+ state.llm_output = self._primary.generate(state)
20
+ state.metadata["fallback_used"] = False
21
+ state.metadata["llm_provider"] = state.llm_output.get("provider")
22
+ except Exception as exc:
23
+ state.metadata["llm_error"] = str(exc)
24
+ if self._fallback is None or not self._fallback.available():
25
+ raise
26
+ state.llm_output = self._fallback.generate(state)
27
+ state.metadata["fallback_used"] = True
28
+ state.metadata["llm_provider"] = state.llm_output.get("provider")
29
+ return state
30
+
31
+ def stream(self, state: GraphState) -> Generator[dict[str, object], None, None]:
32
+ try:
33
+ streamer = getattr(self._primary, "stream_generate", None)
34
+ if callable(streamer):
35
+ result = None
36
+ for event in streamer(state):
37
+ if str(event.get("type")) == "result":
38
+ result = dict(event.get("result", {}) or {})
39
+ continue
40
+ yield event
41
+ state.llm_output = result or self._primary.generate(state)
42
+ else:
43
+ state.llm_output = self._primary.generate(state)
44
+ text = str(state.llm_output.get("text", ""))
45
+ if text:
46
+ yield {"type": "chunk", "delta": text}
47
+ state.metadata["fallback_used"] = False
48
+ state.metadata["llm_provider"] = state.llm_output.get("provider")
49
+ except Exception as exc:
50
+ state.metadata["llm_error"] = str(exc)
51
+ if self._fallback is None or not self._fallback.available():
52
+ raise
53
+ state.llm_output = self._fallback.generate(state)
54
+ text = str(state.llm_output.get("text", ""))
55
+ if text:
56
+ yield {"type": "chunk", "delta": text}
57
+ state.metadata["fallback_used"] = True
58
+ state.metadata["llm_provider"] = state.llm_output.get("provider")
59
+ yield {"type": "result", "result": state.llm_output}