agentforge-core 0.2.1__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.
- agentforge_core/__init__.py +228 -0
- agentforge_core/_bm25.py +132 -0
- agentforge_core/config/__init__.py +62 -0
- agentforge_core/config/loader.py +239 -0
- agentforge_core/config/module_schemas.py +208 -0
- agentforge_core/config/schema.py +424 -0
- agentforge_core/contracts/__init__.py +52 -0
- agentforge_core/contracts/auth.py +33 -0
- agentforge_core/contracts/chat.py +118 -0
- agentforge_core/contracts/embedding.py +71 -0
- agentforge_core/contracts/evaluator.py +56 -0
- agentforge_core/contracts/finding.py +39 -0
- agentforge_core/contracts/graph_store.py +180 -0
- agentforge_core/contracts/guardrails.py +129 -0
- agentforge_core/contracts/llm.py +152 -0
- agentforge_core/contracts/memory.py +113 -0
- agentforge_core/contracts/migrator.py +120 -0
- agentforge_core/contracts/renderer.py +57 -0
- agentforge_core/contracts/reranker.py +91 -0
- agentforge_core/contracts/strategy.py +70 -0
- agentforge_core/contracts/task.py +73 -0
- agentforge_core/contracts/tool.py +71 -0
- agentforge_core/contracts/vector_store.py +151 -0
- agentforge_core/migrations/__init__.py +14 -0
- agentforge_core/migrations/discover.py +77 -0
- agentforge_core/migrations/template.py +34 -0
- agentforge_core/observability/__init__.py +18 -0
- agentforge_core/observability/tracing.py +37 -0
- agentforge_core/production/__init__.py +77 -0
- agentforge_core/production/budget.py +134 -0
- agentforge_core/production/exceptions.py +136 -0
- agentforge_core/production/fallback.py +321 -0
- agentforge_core/production/log_filter.py +49 -0
- agentforge_core/production/log_format.py +117 -0
- agentforge_core/production/run_context.py +108 -0
- agentforge_core/py.typed +0 -0
- agentforge_core/resolver/__init__.py +38 -0
- agentforge_core/resolver/discover.py +145 -0
- agentforge_core/resolver/resolve.py +168 -0
- agentforge_core/testing/__init__.py +45 -0
- agentforge_core/testing/conformance.py +1138 -0
- agentforge_core/values/__init__.py +103 -0
- agentforge_core/values/auth.py +20 -0
- agentforge_core/values/chat.py +131 -0
- agentforge_core/values/claim.py +30 -0
- agentforge_core/values/graph.py +136 -0
- agentforge_core/values/guardrails.py +49 -0
- agentforge_core/values/manifest.py +129 -0
- agentforge_core/values/messages.py +153 -0
- agentforge_core/values/module.py +40 -0
- agentforge_core/values/pipeline.py +43 -0
- agentforge_core/values/retrieval.py +53 -0
- agentforge_core/values/state.py +118 -0
- agentforge_core/values/vector.py +59 -0
- agentforge_core-0.2.1.dist-info/METADATA +66 -0
- agentforge_core-0.2.1.dist-info/RECORD +58 -0
- agentforge_core-0.2.1.dist-info/WHEEL +4 -0
- agentforge_core-0.2.1.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""`AuthPolicy` — locked authentication contract (feat-014).
|
|
2
|
+
|
|
3
|
+
Both `agentforge-chat-http` (feat-020) and `agentforge-a2a`
|
|
4
|
+
(feat-014) need to validate incoming bearer tokens against
|
|
5
|
+
configured credentials. This contract unifies them.
|
|
6
|
+
|
|
7
|
+
Server-side validation only: `authenticate(bearer_token) ->
|
|
8
|
+
Principal | None`. Client-side credential attachment is
|
|
9
|
+
dict-driven (per-peer config carries `{type, token, cert,
|
|
10
|
+
key, ...}`) — no policy abstraction; outgoing transports build
|
|
11
|
+
the right httpx parameters from the dict.
|
|
12
|
+
|
|
13
|
+
Per ADR-0007 the methods on this ABC are locked once the
|
|
14
|
+
feature ships. Adding a method is a major-version bump.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from abc import ABC, abstractmethod
|
|
20
|
+
|
|
21
|
+
from agentforge_core.values.auth import Principal
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class AuthPolicy(ABC):
|
|
25
|
+
"""Validates incoming bearer credentials against configured
|
|
26
|
+
identities. Implementations are typically env-backed
|
|
27
|
+
(`EnvBearerAuth`) or registry-backed."""
|
|
28
|
+
|
|
29
|
+
@abstractmethod
|
|
30
|
+
async def authenticate(self, bearer_token: str | None) -> Principal | None:
|
|
31
|
+
"""Return a `Principal` when the token is valid, else
|
|
32
|
+
``None``. ``None`` input (missing header) must yield
|
|
33
|
+
``None``."""
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Chat-agent contracts (feat-020).
|
|
2
|
+
|
|
3
|
+
`ChatHistoryStore` and `HistoryTruncationStrategy` are the two locked
|
|
4
|
+
ABCs the chat layer ships against. Drivers (in-memory + sqlite +
|
|
5
|
+
postgres + redis) all implement the same `ChatHistoryStore` shape;
|
|
6
|
+
truncation strategies (sliding-window, token-budget, summarise-oldest,
|
|
7
|
+
hybrid) all implement the same `HistoryTruncationStrategy` shape.
|
|
8
|
+
|
|
9
|
+
Per ADR-0007, methods on these ABCs are locked once the feature
|
|
10
|
+
ships. Adding a method is a major version bump.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from abc import ABC, abstractmethod
|
|
16
|
+
from collections.abc import Mapping
|
|
17
|
+
from datetime import datetime
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from agentforge_core.values.chat import ChatTurn, SessionInfo
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ChatHistoryStore(ABC):
|
|
24
|
+
"""Persistent store for chat turns, isolated by `session_id`.
|
|
25
|
+
|
|
26
|
+
All read/write methods take `session_id`; cross-session access
|
|
27
|
+
is impossible without explicitly passing the id. Drivers
|
|
28
|
+
typically index on `(session_id, created_at)` so `load()` is
|
|
29
|
+
sub-linear w.r.t. total store size.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
@abstractmethod
|
|
33
|
+
async def append(self, turn: ChatTurn) -> None:
|
|
34
|
+
"""Persist a single chat turn."""
|
|
35
|
+
|
|
36
|
+
@abstractmethod
|
|
37
|
+
async def load(
|
|
38
|
+
self,
|
|
39
|
+
session_id: str,
|
|
40
|
+
*,
|
|
41
|
+
limit: int | None = None,
|
|
42
|
+
before: datetime | None = None,
|
|
43
|
+
after: datetime | None = None,
|
|
44
|
+
roles: list[str] | None = None,
|
|
45
|
+
) -> list[ChatTurn]:
|
|
46
|
+
"""Load turns for ``session_id`` in chronological order
|
|
47
|
+
(oldest first). Filters apply pre-limit."""
|
|
48
|
+
|
|
49
|
+
@abstractmethod
|
|
50
|
+
async def count(self, session_id: str) -> int:
|
|
51
|
+
"""Total turn count for ``session_id``."""
|
|
52
|
+
|
|
53
|
+
@abstractmethod
|
|
54
|
+
async def delete_session(self, session_id: str) -> int:
|
|
55
|
+
"""Delete every turn for ``session_id``. Returns the number
|
|
56
|
+
of turns removed."""
|
|
57
|
+
|
|
58
|
+
@abstractmethod
|
|
59
|
+
async def list_sessions(
|
|
60
|
+
self,
|
|
61
|
+
*,
|
|
62
|
+
owner: str | None = None,
|
|
63
|
+
limit: int = 100,
|
|
64
|
+
before: datetime | None = None,
|
|
65
|
+
) -> list[SessionInfo]:
|
|
66
|
+
"""List sessions, optionally filtered by owner. Ordered by
|
|
67
|
+
``last_active_at`` descending."""
|
|
68
|
+
|
|
69
|
+
@abstractmethod
|
|
70
|
+
async def update_session_metadata(self, session_id: str, metadata: Mapping[str, Any]) -> None:
|
|
71
|
+
"""Merge ``metadata`` into the session's metadata dict.
|
|
72
|
+
|
|
73
|
+
Implementations may overwrite top-level keys; nested merging
|
|
74
|
+
is the caller's responsibility.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
@abstractmethod
|
|
78
|
+
async def expire_before(self, cutoff: datetime) -> int:
|
|
79
|
+
"""TTL sweep: delete every session whose ``last_active_at <
|
|
80
|
+
cutoff``. Returns the number of sessions removed. Drivers
|
|
81
|
+
without TTL support return 0."""
|
|
82
|
+
|
|
83
|
+
@abstractmethod
|
|
84
|
+
async def close(self) -> None:
|
|
85
|
+
"""Release driver resources (DB pool, file handles, etc.)."""
|
|
86
|
+
|
|
87
|
+
def capabilities(self) -> set[str]:
|
|
88
|
+
"""Optional capability bag.
|
|
89
|
+
|
|
90
|
+
Subset of: ``"ttl"``, ``"encryption_at_rest"``,
|
|
91
|
+
``"full_text_search"``, ``"streaming_load"``.
|
|
92
|
+
"""
|
|
93
|
+
return set()
|
|
94
|
+
|
|
95
|
+
def supports(self, capability: str) -> bool:
|
|
96
|
+
return capability in self.capabilities()
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class HistoryTruncationStrategy(ABC):
|
|
100
|
+
"""Decides which prior turns to include in the next LLM call.
|
|
101
|
+
|
|
102
|
+
Truncation runs every turn, between `load()` and the agent call.
|
|
103
|
+
Returns a possibly-empty subset of ``all_turns`` (ordered).
|
|
104
|
+
Invariants every strategy honours (covered by the conformance
|
|
105
|
+
harness):
|
|
106
|
+
|
|
107
|
+
- Order-preserving (output is a subsequence of input).
|
|
108
|
+
- Tool-call / tool-result pairs are never split.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
@abstractmethod
|
|
112
|
+
async def select(
|
|
113
|
+
self,
|
|
114
|
+
all_turns: list[ChatTurn],
|
|
115
|
+
next_user_message: str,
|
|
116
|
+
context: Mapping[str, Any],
|
|
117
|
+
) -> list[ChatTurn]:
|
|
118
|
+
"""Return the subset of ``all_turns`` to feed to the LLM."""
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""`EmbeddingClient` — locked embeddings provider abstraction.
|
|
2
|
+
|
|
3
|
+
Embedding providers (Bedrock Titan / Cohere, OpenAI, etc.) implement
|
|
4
|
+
this ABC. The vector store / retrieval layer (feat-007) consumes
|
|
5
|
+
`EmbeddingClient`, never the concrete driver type, so swapping
|
|
6
|
+
providers is a string-id swap.
|
|
7
|
+
|
|
8
|
+
Per ADR-0007 the surface is locked at v0.1: adding a method is a
|
|
9
|
+
major version bump. Optional capabilities (e.g. multimodal embeddings)
|
|
10
|
+
are layered the same way as on `LLMClient` — declared in
|
|
11
|
+
`capabilities()` and gated via `supports()`.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from abc import ABC, abstractmethod
|
|
17
|
+
|
|
18
|
+
from agentforge_core.values.messages import EmbeddingResponse
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class EmbeddingClient(ABC):
|
|
22
|
+
"""Provider-agnostic text-embedding client.
|
|
23
|
+
|
|
24
|
+
Implementations:
|
|
25
|
+
- normalise the provider's response into `EmbeddingResponse`
|
|
26
|
+
- declare the model's vector dimensionality up front via
|
|
27
|
+
`dimensions()` so callers can size storage before the call
|
|
28
|
+
- compute `cost_usd` from token usage and a per-model price
|
|
29
|
+
table inside the driver (consistent with `LLMClient`)
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
@abstractmethod
|
|
33
|
+
async def embed(self, texts: list[str]) -> EmbeddingResponse:
|
|
34
|
+
"""Embed a batch of texts.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
texts: One or more texts to embed. Empty list raises
|
|
38
|
+
`ValueError` (no provider supports zero-length batches
|
|
39
|
+
and the cost would be ambiguous).
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
`EmbeddingResponse` carrying one vector per input text in
|
|
43
|
+
input order. Every vector has length `self.dimensions()`.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
@abstractmethod
|
|
47
|
+
async def close(self) -> None:
|
|
48
|
+
"""Release any resources (HTTP clients, connection pools)."""
|
|
49
|
+
|
|
50
|
+
@abstractmethod
|
|
51
|
+
def dimensions(self) -> int:
|
|
52
|
+
"""The vector dimensionality every `embed()` call returns.
|
|
53
|
+
|
|
54
|
+
Drivers declare this without a network round-trip — it is a
|
|
55
|
+
property of the configured model. Callers use this to size
|
|
56
|
+
storage (e.g. vector-store column widths) before the first
|
|
57
|
+
embed call.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def capabilities(self) -> set[str]:
|
|
61
|
+
"""Optional capabilities this driver supports.
|
|
62
|
+
|
|
63
|
+
Default empty set. Closed vocabulary (additions are minor
|
|
64
|
+
bumps): `"multimodal"` (image / audio inputs in addition to
|
|
65
|
+
text), `"matryoshka"` (truncatable variable-length vectors).
|
|
66
|
+
"""
|
|
67
|
+
return set()
|
|
68
|
+
|
|
69
|
+
def supports(self, capability: str) -> bool:
|
|
70
|
+
"""True if this client declares the given capability."""
|
|
71
|
+
return capability in self.capabilities()
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""`Evaluator` — the locked post-run evaluator ABC, plus `EvalResult`.
|
|
2
|
+
|
|
3
|
+
feat-001 ships only the contract and the result type. feat-006 ships
|
|
4
|
+
deterministic graders (coverage, consistency, regression-vs-baseline,
|
|
5
|
+
format-compliance) and LLM-judge graders (correctness, faithfulness,
|
|
6
|
+
groundedness, hallucination, relevance, helpfulness) via the
|
|
7
|
+
`agentforge-eval-geval` module.
|
|
8
|
+
|
|
9
|
+
Evaluators run *after* the reasoning loop completes and score the
|
|
10
|
+
agent's output (per `docs/features/feat-006-evaluators-and-benchmarks.md`).
|
|
11
|
+
This is distinct from real-time validators (feat-018) which block /
|
|
12
|
+
redact at the moment a violation happens.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from abc import ABC, abstractmethod
|
|
18
|
+
from typing import Any, ClassVar
|
|
19
|
+
|
|
20
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class EvalResult(BaseModel):
|
|
24
|
+
"""The outcome of evaluating one finding (or output)."""
|
|
25
|
+
|
|
26
|
+
model_config = ConfigDict(frozen=True, strict=True)
|
|
27
|
+
|
|
28
|
+
evaluator: str
|
|
29
|
+
score: float
|
|
30
|
+
"""Conventionally in [0, 1]; NaN allowed for "not applicable"."""
|
|
31
|
+
|
|
32
|
+
label: str | None = None
|
|
33
|
+
"""Optional discrete label such as "pass" / "fail" / "warn"."""
|
|
34
|
+
|
|
35
|
+
reasoning: str | None = None
|
|
36
|
+
"""LLM-judge rationale or rule-based explanation."""
|
|
37
|
+
|
|
38
|
+
raw: dict[str, Any] = Field(default_factory=dict)
|
|
39
|
+
"""Driver-specific extra detail — never required, never relied on."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class Evaluator(ABC):
|
|
43
|
+
"""Post-run quality scorer.
|
|
44
|
+
|
|
45
|
+
Subclasses declare:
|
|
46
|
+
|
|
47
|
+
name: str — identifier surfaced in EvalResult
|
|
48
|
+
cost_estimate_usd: float — per-evaluation cost (0 for non-LLM)
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
name: ClassVar[str]
|
|
52
|
+
cost_estimate_usd: ClassVar[float] = 0.0
|
|
53
|
+
|
|
54
|
+
@abstractmethod
|
|
55
|
+
async def evaluate(self, finding: Any, context: dict[str, Any]) -> EvalResult:
|
|
56
|
+
"""Score `finding` against this evaluator's rubric."""
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""`Finding` — structural Protocol the agent's output items satisfy.
|
|
2
|
+
|
|
3
|
+
Per feat-008 / ADR-0012, `Finding` is a `runtime_checkable` Protocol
|
|
4
|
+
rather than a single dataclass. Shipped variants (`SimpleFinding`,
|
|
5
|
+
`PatchFinding`, `NarrativeFinding`, `MultiSpanFinding`) live in the
|
|
6
|
+
runtime package; they satisfy this Protocol structurally without
|
|
7
|
+
needing to inherit from anything.
|
|
8
|
+
|
|
9
|
+
Custom variants from agent code or third-party packages also satisfy
|
|
10
|
+
the Protocol simply by declaring the required attributes — no
|
|
11
|
+
registration ceremony.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from typing import Any, Protocol, runtime_checkable
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@runtime_checkable
|
|
20
|
+
class Finding(Protocol):
|
|
21
|
+
"""Minimum shape any pipeline / agent output item satisfies.
|
|
22
|
+
|
|
23
|
+
The runtime checks `isinstance(x, Finding)` opportunistically (e.g.
|
|
24
|
+
when storing as a `Claim.payload`); the check is structural and
|
|
25
|
+
tolerant.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
severity: str
|
|
29
|
+
"""One of "critical" | "warning" | "suggestion" | "info"."""
|
|
30
|
+
|
|
31
|
+
category: str
|
|
32
|
+
"""Free-form categorisation: "style", "security", "answer", etc."""
|
|
33
|
+
|
|
34
|
+
message: str
|
|
35
|
+
"""Short human-readable summary (one or two sentences)."""
|
|
36
|
+
|
|
37
|
+
def to_dict(self) -> dict[str, Any]:
|
|
38
|
+
"""Serialise to a JSON-compatible dict for persistence / transport."""
|
|
39
|
+
...
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""`GraphStore` — locked graph-traversal ABC.
|
|
2
|
+
|
|
3
|
+
A graph store is distinct from `MemoryStore` (claim audit log) and
|
|
4
|
+
`VectorStore` (similarity search): the shapes don't unify cleanly.
|
|
5
|
+
`MemoryStore` filters by structured metadata; `VectorStore` ranks by
|
|
6
|
+
cosine similarity; `GraphStore` walks relationships — multi-hop
|
|
7
|
+
queries, pattern matching, ontology traversal. Forcing graph traversal
|
|
8
|
+
into either of the existing ABCs would degrade them; keeping
|
|
9
|
+
`GraphStore` separate respects the contract layer's purpose (one ABC
|
|
10
|
+
per concern, not one per backend).
|
|
11
|
+
|
|
12
|
+
Per ADR-0007 the surface is locked at v0.1: adding a method is a
|
|
13
|
+
major version bump. Optional capabilities (e.g. native Cypher
|
|
14
|
+
support, transactions, embedded vector search) layer the same way as
|
|
15
|
+
`LLMClient` capabilities — declared via `capabilities()` and gated via
|
|
16
|
+
`supports()`.
|
|
17
|
+
|
|
18
|
+
Conformance: every shipped or third-party driver must pass
|
|
19
|
+
`agentforge_core.testing.run_graph_conformance` (lands alongside this
|
|
20
|
+
contract).
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from abc import ABC, abstractmethod
|
|
26
|
+
from typing import Literal
|
|
27
|
+
|
|
28
|
+
from agentforge_core.values.graph import (
|
|
29
|
+
GraphEdge,
|
|
30
|
+
GraphNode,
|
|
31
|
+
GraphPattern,
|
|
32
|
+
Path,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class GraphStore(ABC):
|
|
37
|
+
"""Provider-agnostic property graph.
|
|
38
|
+
|
|
39
|
+
Implementations:
|
|
40
|
+
- treat `add_node` and `add_edge` as idempotent upserts
|
|
41
|
+
(re-adding the same `id` / `(src, dst, edge_type)` replaces
|
|
42
|
+
the prior record's `properties`)
|
|
43
|
+
- reject edges whose `src` or `dst` references an unknown node —
|
|
44
|
+
callers must `add_node` first; this keeps the graph
|
|
45
|
+
well-formed and matches Cypher / SurrealQL behaviour
|
|
46
|
+
- return `Path` results with `len(edges) == len(nodes) - 1` and
|
|
47
|
+
edges in chain order
|
|
48
|
+
|
|
49
|
+
Cross-driver invariants enforced by the conformance suite:
|
|
50
|
+
- round-trip: `add_node(N); get_node(N.id)` returns an equal node
|
|
51
|
+
- edge readback: `add_edge(E); get_edges(E.src)` includes E
|
|
52
|
+
- pattern match: a one-segment pattern returns paths of length 2
|
|
53
|
+
- traversal: depth-bounded BFS does not exceed `max_depth`
|
|
54
|
+
- cascade delete: `delete_node(id, cascade=True)` removes
|
|
55
|
+
adjacent edges; `cascade=False` raises if edges remain
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
@abstractmethod
|
|
59
|
+
async def add_node(self, node: GraphNode) -> None:
|
|
60
|
+
"""Insert or replace `node` (idempotent upsert by `node.id`)."""
|
|
61
|
+
|
|
62
|
+
@abstractmethod
|
|
63
|
+
async def add_edge(self, edge: GraphEdge) -> None:
|
|
64
|
+
"""Insert or replace `edge` (idempotent upsert by
|
|
65
|
+
`(src, dst, edge_type)`).
|
|
66
|
+
|
|
67
|
+
Raises:
|
|
68
|
+
ValueError: `edge.src` or `edge.dst` references an unknown
|
|
69
|
+
node. Callers must add nodes before edges.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
@abstractmethod
|
|
73
|
+
async def get_node(self, node_id: str) -> GraphNode | None:
|
|
74
|
+
"""Return the node with this id, or `None` if absent."""
|
|
75
|
+
|
|
76
|
+
@abstractmethod
|
|
77
|
+
async def get_edges(
|
|
78
|
+
self,
|
|
79
|
+
node_id: str,
|
|
80
|
+
*,
|
|
81
|
+
edge_type: str | None = None,
|
|
82
|
+
direction: Literal["out", "in", "any"] = "out",
|
|
83
|
+
) -> list[GraphEdge]:
|
|
84
|
+
"""Return edges incident on `node_id`.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
node_id: The node whose edges to fetch.
|
|
88
|
+
edge_type: If set, only edges of this type. `None` returns
|
|
89
|
+
all types.
|
|
90
|
+
direction: `"out"` returns edges where `src == node_id`;
|
|
91
|
+
`"in"` returns edges where `dst == node_id`; `"any"`
|
|
92
|
+
returns the union.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
@abstractmethod
|
|
96
|
+
async def match(
|
|
97
|
+
self,
|
|
98
|
+
pattern: GraphPattern,
|
|
99
|
+
*,
|
|
100
|
+
limit: int = 50,
|
|
101
|
+
) -> list[Path]:
|
|
102
|
+
"""Return paths matching `pattern`, capped at `limit`.
|
|
103
|
+
|
|
104
|
+
Drivers may evaluate the pattern via Cypher (Neo4j),
|
|
105
|
+
SurrealQL (SurrealDB), or in-memory walking (the reference
|
|
106
|
+
implementation). The return shape is the same.
|
|
107
|
+
|
|
108
|
+
Raises:
|
|
109
|
+
ValueError: `limit < 1`.
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
@abstractmethod
|
|
113
|
+
async def traverse(
|
|
114
|
+
self,
|
|
115
|
+
start_id: str,
|
|
116
|
+
*,
|
|
117
|
+
edge_types: tuple[str, ...] | None = None,
|
|
118
|
+
max_depth: int = 3,
|
|
119
|
+
limit: int = 50,
|
|
120
|
+
) -> list[Path]:
|
|
121
|
+
"""Breadth-first traversal from `start_id`.
|
|
122
|
+
|
|
123
|
+
Returns every path of length 1..`max_depth` starting from
|
|
124
|
+
`start_id`, restricted to `edge_types` if given. Useful for
|
|
125
|
+
knowledge-graph expansion (pull a neighbourhood for retrieval
|
|
126
|
+
augmentation).
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
start_id: The seed node. If absent, returns an empty list.
|
|
130
|
+
edge_types: If set, only traverse edges of these types.
|
|
131
|
+
max_depth: Hop limit (>= 1).
|
|
132
|
+
limit: Maximum number of paths to return (>= 1).
|
|
133
|
+
|
|
134
|
+
Raises:
|
|
135
|
+
ValueError: `max_depth < 1` or `limit < 1`.
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
@abstractmethod
|
|
139
|
+
async def delete_node(self, node_id: str, *, cascade: bool = False) -> bool:
|
|
140
|
+
"""Delete a node by id. Returns True if a node was removed.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
node_id: The node to delete.
|
|
144
|
+
cascade: If True, also delete every edge incident on the
|
|
145
|
+
node. If False (default) and the node still has edges,
|
|
146
|
+
raises `ValueError` — drivers must not orphan edges.
|
|
147
|
+
|
|
148
|
+
Raises:
|
|
149
|
+
ValueError: `cascade=False` and the node has incident edges.
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
@abstractmethod
|
|
153
|
+
async def delete_edge(self, src: str, dst: str, *, edge_type: str) -> bool:
|
|
154
|
+
"""Delete an edge by `(src, dst, edge_type)`. Returns True if
|
|
155
|
+
an edge was removed.
|
|
156
|
+
|
|
157
|
+
Unknown triples return False (no exception).
|
|
158
|
+
"""
|
|
159
|
+
|
|
160
|
+
@abstractmethod
|
|
161
|
+
async def close(self) -> None:
|
|
162
|
+
"""Release backing resources (connections, file handles)."""
|
|
163
|
+
|
|
164
|
+
def capabilities(self) -> set[str]:
|
|
165
|
+
"""Optional capabilities this driver supports.
|
|
166
|
+
|
|
167
|
+
Default empty set. Closed vocabulary (additions are minor
|
|
168
|
+
bumps): `"transactions"` (multi-statement atomic writes),
|
|
169
|
+
`"cypher"` (driver speaks Cypher natively),
|
|
170
|
+
`"surrealql"` (driver speaks SurrealQL natively),
|
|
171
|
+
`"vector"` (driver also indexes embeddings — typically also
|
|
172
|
+
implements `VectorStore`), `"live_query"` (driver pushes
|
|
173
|
+
change notifications), `"fulltext"` (driver indexes node /
|
|
174
|
+
edge property text).
|
|
175
|
+
"""
|
|
176
|
+
return set()
|
|
177
|
+
|
|
178
|
+
def supports(self, capability: str) -> bool:
|
|
179
|
+
"""True if this driver declares the given capability."""
|
|
180
|
+
return capability in self.capabilities()
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Guardrail ABCs (feat-018).
|
|
2
|
+
|
|
3
|
+
Three locked ABCs:
|
|
4
|
+
|
|
5
|
+
- `InputValidator.validate(content, context)` — runs before each
|
|
6
|
+
LLM call on the user-visible input.
|
|
7
|
+
- `OutputValidator.validate(content, context)` — runs after each
|
|
8
|
+
LLM call on the model's output.
|
|
9
|
+
- `ToolCallGate.authorize(tool_name, tool, args, context)` — runs
|
|
10
|
+
before tool dispatch.
|
|
11
|
+
|
|
12
|
+
All three return `ValidationResult`. Implementations are async so
|
|
13
|
+
they can integrate with HTTP-based validators (LLM Guard,
|
|
14
|
+
Presidio, Llama Guard) without blocking the event loop.
|
|
15
|
+
|
|
16
|
+
The `name: str` ClassVar identifies the validator in audit events
|
|
17
|
+
and config-resolution paths.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from abc import ABC, abstractmethod
|
|
23
|
+
from typing import TYPE_CHECKING, Any, ClassVar
|
|
24
|
+
|
|
25
|
+
from agentforge_core.values.guardrails import ValidationResult
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from agentforge_core.contracts.tool import Tool
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class InputValidator(ABC):
|
|
32
|
+
"""Validates user input before the agent's first LLM call.
|
|
33
|
+
|
|
34
|
+
Subclasses set ClassVars `name`, `description`, and
|
|
35
|
+
`cost_estimate_ms` (rough per-call latency in milliseconds).
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
name: ClassVar[str]
|
|
39
|
+
description: ClassVar[str]
|
|
40
|
+
cost_estimate_ms: ClassVar[int] = 0
|
|
41
|
+
|
|
42
|
+
@abstractmethod
|
|
43
|
+
async def validate(self, content: str, context: dict[str, Any]) -> ValidationResult:
|
|
44
|
+
"""Validate `content`. `context` carries `run_id`, `project`,
|
|
45
|
+
`agent`, and any per-call metadata."""
|
|
46
|
+
|
|
47
|
+
def __init_subclass__(cls, **kwargs: Any) -> None:
|
|
48
|
+
super().__init_subclass__(**kwargs)
|
|
49
|
+
_require_attrs(cls, ("name", "description"))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class OutputValidator(ABC):
|
|
53
|
+
"""Validates the model's output after each LLM call.
|
|
54
|
+
|
|
55
|
+
Output validators MAY redact: set `redacted_content` on the
|
|
56
|
+
returned `ValidationResult` to the post-redaction text. The
|
|
57
|
+
framework forwards that content downstream when
|
|
58
|
+
`policy.on_output_violation == "redact"`.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
name: ClassVar[str]
|
|
62
|
+
description: ClassVar[str]
|
|
63
|
+
cost_estimate_ms: ClassVar[int] = 0
|
|
64
|
+
|
|
65
|
+
@abstractmethod
|
|
66
|
+
async def validate(self, content: str, context: dict[str, Any]) -> ValidationResult:
|
|
67
|
+
"""Validate `content` (the LLM's text output)."""
|
|
68
|
+
|
|
69
|
+
def __init_subclass__(cls, **kwargs: Any) -> None:
|
|
70
|
+
super().__init_subclass__(**kwargs)
|
|
71
|
+
_require_attrs(cls, ("name", "description"))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class ToolCallGate(ABC):
|
|
75
|
+
"""Authorises a tool invocation before dispatch.
|
|
76
|
+
|
|
77
|
+
Receives the tool instance so gates can inspect `tool.capabilities`
|
|
78
|
+
or other static metadata.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
name: ClassVar[str]
|
|
82
|
+
description: ClassVar[str]
|
|
83
|
+
cost_estimate_ms: ClassVar[int] = 0
|
|
84
|
+
|
|
85
|
+
@abstractmethod
|
|
86
|
+
async def authorize(
|
|
87
|
+
self,
|
|
88
|
+
tool_name: str,
|
|
89
|
+
tool: Tool,
|
|
90
|
+
args: dict[str, Any],
|
|
91
|
+
context: dict[str, Any],
|
|
92
|
+
) -> ValidationResult:
|
|
93
|
+
"""Authorise the upcoming tool call."""
|
|
94
|
+
|
|
95
|
+
def __init_subclass__(cls, **kwargs: Any) -> None:
|
|
96
|
+
super().__init_subclass__(**kwargs)
|
|
97
|
+
_require_attrs(cls, ("name", "description"))
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _require_attrs(cls: type, attrs: tuple[str, ...]) -> None:
|
|
101
|
+
"""Enforce the ClassVar contract on concrete subclasses."""
|
|
102
|
+
import inspect # noqa: PLC0415
|
|
103
|
+
|
|
104
|
+
if inspect.isabstract(cls):
|
|
105
|
+
return
|
|
106
|
+
for attr in attrs:
|
|
107
|
+
if attr not in cls.__dict__ and not _inherited(cls, attr):
|
|
108
|
+
msg = (
|
|
109
|
+
f"{cls.__name__} must declare class attribute {attr!r} "
|
|
110
|
+
"(every guardrail validator carries a stable name and "
|
|
111
|
+
"human-readable description for audit events)."
|
|
112
|
+
)
|
|
113
|
+
raise TypeError(msg)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _inherited(cls: type, attr: str) -> bool:
|
|
117
|
+
for base in cls.__mro__[1:]:
|
|
118
|
+
if base is object:
|
|
119
|
+
continue
|
|
120
|
+
if attr in base.__dict__ and base.__module__ != cls.__module__:
|
|
121
|
+
return True
|
|
122
|
+
return False
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
__all__ = [
|
|
126
|
+
"InputValidator",
|
|
127
|
+
"OutputValidator",
|
|
128
|
+
"ToolCallGate",
|
|
129
|
+
]
|