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/embedding/base.py
ADDED
|
@@ -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
|
minder/graph/__init__.py
ADDED
|
@@ -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"
|
minder/graph/executor.py
ADDED
|
@@ -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}
|