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,208 @@
|
|
|
1
|
+
"""Module-side config schema validation (feat-012 §4.4).
|
|
2
|
+
|
|
3
|
+
Per spec §4.4, each module ships its own Pydantic config schema; the
|
|
4
|
+
framework composes them at load time so `modules.memory.config:`,
|
|
5
|
+
`modules.evaluators[*].config:`, etc. get validated against the
|
|
6
|
+
right shape.
|
|
7
|
+
|
|
8
|
+
Module convention: a class registered with the resolver MAY declare
|
|
9
|
+
a class-level attribute:
|
|
10
|
+
|
|
11
|
+
class PostgresMemoryStore(MemoryStore):
|
|
12
|
+
config_schema: ClassVar[type[BaseModel] | None] = PostgresMemoryConfig
|
|
13
|
+
|
|
14
|
+
The validator walks the resolved config's `modules.*` blocks, looks
|
|
15
|
+
each entry's class up in the resolver, reads `cls.config_schema`,
|
|
16
|
+
and runs `schema.model_validate(entry.config)` if present. Modules
|
|
17
|
+
without a schema (the common case) accept any dict — the resolver
|
|
18
|
+
still confirms the class is registered (fail-at-startup, P11).
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from typing import TYPE_CHECKING, Any
|
|
24
|
+
|
|
25
|
+
from pydantic import ValidationError
|
|
26
|
+
|
|
27
|
+
from agentforge_core.config.schema import (
|
|
28
|
+
AgentForgeConfig,
|
|
29
|
+
EvaluatorEntry,
|
|
30
|
+
ModuleEntry,
|
|
31
|
+
ObservabilityEntry,
|
|
32
|
+
PipelineTaskEntry,
|
|
33
|
+
RerankerEntry,
|
|
34
|
+
)
|
|
35
|
+
from agentforge_core.production.exceptions import ModuleError
|
|
36
|
+
|
|
37
|
+
if TYPE_CHECKING:
|
|
38
|
+
from agentforge_core.resolver import Resolver
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def validate_module_configs(
|
|
42
|
+
cfg: AgentForgeConfig,
|
|
43
|
+
*,
|
|
44
|
+
resolver: Resolver | None = None,
|
|
45
|
+
strict: bool = True,
|
|
46
|
+
) -> None:
|
|
47
|
+
"""Validate each `modules.*` block against its module's schema.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
cfg: A loaded `AgentForgeConfig` (post-`load_config`).
|
|
51
|
+
resolver: Resolver to look classes up in. Defaults to the
|
|
52
|
+
global resolver.
|
|
53
|
+
strict: When True (default), missing modules raise
|
|
54
|
+
`ModuleError`. When False, missing modules are skipped
|
|
55
|
+
(useful for `agentforge config validate` against a config
|
|
56
|
+
that references not-yet-installed packages).
|
|
57
|
+
|
|
58
|
+
Raises:
|
|
59
|
+
ModuleError: a referenced module isn't registered (strict
|
|
60
|
+
mode) or a config dict fails its module's schema.
|
|
61
|
+
"""
|
|
62
|
+
# Late import — avoids a load-order cycle with the values /
|
|
63
|
+
# contracts modules that the resolver pulls in.
|
|
64
|
+
from agentforge_core.resolver import Resolver as _Resolver # noqa: PLC0415
|
|
65
|
+
|
|
66
|
+
r = resolver if resolver is not None else _Resolver.global_()
|
|
67
|
+
|
|
68
|
+
if cfg.modules.memory is not None:
|
|
69
|
+
_validate_one(r, "memory", cfg.modules.memory, strict=strict)
|
|
70
|
+
if cfg.modules.graph is not None:
|
|
71
|
+
_validate_one(r, "graph", cfg.modules.graph, strict=strict)
|
|
72
|
+
if cfg.modules.retriever is not None:
|
|
73
|
+
_validate_one(r, "retriever", cfg.modules.retriever, strict=strict)
|
|
74
|
+
|
|
75
|
+
for eval_entry in cfg.modules.evaluators:
|
|
76
|
+
_validate_named(r, "evaluators", eval_entry, strict=strict)
|
|
77
|
+
for obs_entry in cfg.modules.observability:
|
|
78
|
+
_validate_named(r, "hooks", obs_entry, strict=strict)
|
|
79
|
+
for proto_entry in cfg.modules.protocols:
|
|
80
|
+
_validate_named(r, "protocols", proto_entry, strict=strict)
|
|
81
|
+
if cfg.modules.pipeline is not None:
|
|
82
|
+
for task_entry in cfg.modules.pipeline.tasks:
|
|
83
|
+
_validate_named(r, "tasks", task_entry, strict=strict)
|
|
84
|
+
if cfg.modules.chat is not None:
|
|
85
|
+
if cfg.modules.chat.history is not None:
|
|
86
|
+
_validate_driver(
|
|
87
|
+
r,
|
|
88
|
+
"chat.history",
|
|
89
|
+
cfg.modules.chat.history.driver,
|
|
90
|
+
cfg.modules.chat.history.config,
|
|
91
|
+
strict=strict,
|
|
92
|
+
)
|
|
93
|
+
if cfg.modules.chat.truncation is not None:
|
|
94
|
+
_validate_driver(
|
|
95
|
+
r,
|
|
96
|
+
"chat.truncation",
|
|
97
|
+
cfg.modules.chat.truncation.strategy,
|
|
98
|
+
cfg.modules.chat.truncation.config,
|
|
99
|
+
strict=strict,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
if cfg.retrieval is not None:
|
|
103
|
+
_validate_retrieval(r, cfg, strict=strict)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _validate_one(
|
|
107
|
+
resolver: Resolver,
|
|
108
|
+
category: str,
|
|
109
|
+
entry: ModuleEntry,
|
|
110
|
+
*,
|
|
111
|
+
strict: bool,
|
|
112
|
+
) -> None:
|
|
113
|
+
"""Validate a `driver + config`-shaped module entry."""
|
|
114
|
+
try:
|
|
115
|
+
cls = resolver.resolve(category, entry.driver)
|
|
116
|
+
except ModuleError:
|
|
117
|
+
if strict:
|
|
118
|
+
raise
|
|
119
|
+
return
|
|
120
|
+
schema = _read_config_schema(cls)
|
|
121
|
+
if schema is None:
|
|
122
|
+
return
|
|
123
|
+
try:
|
|
124
|
+
schema.model_validate(entry.config)
|
|
125
|
+
except ValidationError as exc:
|
|
126
|
+
raise ModuleError(
|
|
127
|
+
f"modules.{category}.config failed validation for driver "
|
|
128
|
+
f"{entry.driver!r}: {exc.errors(include_url=False)}"
|
|
129
|
+
) from exc
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _validate_named(
|
|
133
|
+
resolver: Resolver,
|
|
134
|
+
category: str,
|
|
135
|
+
entry: EvaluatorEntry | ObservabilityEntry | PipelineTaskEntry | RerankerEntry,
|
|
136
|
+
*,
|
|
137
|
+
strict: bool,
|
|
138
|
+
) -> None:
|
|
139
|
+
"""Validate a named-list entry (evaluators / observability /
|
|
140
|
+
protocols)."""
|
|
141
|
+
try:
|
|
142
|
+
cls = resolver.resolve(category, entry.name)
|
|
143
|
+
except ModuleError:
|
|
144
|
+
if strict:
|
|
145
|
+
raise
|
|
146
|
+
return
|
|
147
|
+
schema = _read_config_schema(cls)
|
|
148
|
+
if schema is None:
|
|
149
|
+
return
|
|
150
|
+
try:
|
|
151
|
+
schema.model_validate(entry.config)
|
|
152
|
+
except ValidationError as exc:
|
|
153
|
+
raise ModuleError(
|
|
154
|
+
f"modules.{category}[{entry.name!r}].config failed validation: "
|
|
155
|
+
f"{exc.errors(include_url=False)}"
|
|
156
|
+
) from exc
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _validate_retrieval(
|
|
160
|
+
resolver: Resolver,
|
|
161
|
+
cfg: AgentForgeConfig,
|
|
162
|
+
*,
|
|
163
|
+
strict: bool,
|
|
164
|
+
) -> None:
|
|
165
|
+
"""Validate the top-level `retrieval:` block (feat-021 follow-up)."""
|
|
166
|
+
assert cfg.retrieval is not None
|
|
167
|
+
_validate_one(resolver, "vector_stores", cfg.retrieval.vector_store, strict=strict)
|
|
168
|
+
_validate_one(resolver, "embeddings", cfg.retrieval.embedder, strict=strict)
|
|
169
|
+
if cfg.retrieval.reranker is not None:
|
|
170
|
+
_validate_named(resolver, "rerankers", cfg.retrieval.reranker, strict=strict)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _validate_driver(
|
|
174
|
+
resolver: Resolver,
|
|
175
|
+
category: str,
|
|
176
|
+
name: str,
|
|
177
|
+
config: dict[str, Any],
|
|
178
|
+
*,
|
|
179
|
+
strict: bool,
|
|
180
|
+
) -> None:
|
|
181
|
+
"""Validate a driver-by-name + config dict (feat-020 chat hook)."""
|
|
182
|
+
try:
|
|
183
|
+
cls = resolver.resolve(category, name)
|
|
184
|
+
except ModuleError:
|
|
185
|
+
if strict:
|
|
186
|
+
raise
|
|
187
|
+
return
|
|
188
|
+
schema = _read_config_schema(cls)
|
|
189
|
+
if schema is None:
|
|
190
|
+
return
|
|
191
|
+
try:
|
|
192
|
+
schema.model_validate(config)
|
|
193
|
+
except ValidationError as exc:
|
|
194
|
+
raise ModuleError(
|
|
195
|
+
f"modules.{category}.config failed validation for {name!r}: "
|
|
196
|
+
f"{exc.errors(include_url=False)}"
|
|
197
|
+
) from exc
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _read_config_schema(cls: type) -> Any:
|
|
201
|
+
"""Pull `cls.config_schema` if declared. Tolerant — modules without
|
|
202
|
+
one return `None` (any dict accepted)."""
|
|
203
|
+
schema = getattr(cls, "config_schema", None)
|
|
204
|
+
if schema is None:
|
|
205
|
+
return None
|
|
206
|
+
if not isinstance(schema, type):
|
|
207
|
+
return None
|
|
208
|
+
return schema
|
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
"""Locked root models for `agentforge.yaml` (feat-012).
|
|
2
|
+
|
|
3
|
+
feat-001 shipped a minimal schema (`agent` + `logging`). feat-012
|
|
4
|
+
widens it to the full §4.2 surface: nested `budget`, `modules`
|
|
5
|
+
section with sub-shapes (memory, graph, retriever, evaluators,
|
|
6
|
+
observability, tools, protocols), `providers` named registry, and
|
|
7
|
+
`output` config. Adding fields is additive (ADR-0007 minor bump).
|
|
8
|
+
|
|
9
|
+
The runtime `Agent(budget_usd=, max_iterations=)` kwargs from
|
|
10
|
+
feat-001 remain the locked Public API; they continue to drive
|
|
11
|
+
`BudgetPolicy` internally. The YAML field shape (`budget.usd` etc.)
|
|
12
|
+
is the data-side representation.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any, Literal
|
|
19
|
+
|
|
20
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class BudgetConfig(BaseModel):
|
|
24
|
+
"""`agent.budget:` — nested budget shape per spec §4.1.
|
|
25
|
+
|
|
26
|
+
All caps optional; runtime defaults preserved when omitted. The
|
|
27
|
+
flat `agent.budget_usd: float` form from feat-001 is no longer
|
|
28
|
+
valid in YAML — `agentforge config validate` will report it as
|
|
29
|
+
an unknown field (set `agent.budget.usd:` instead).
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
model_config = ConfigDict(strict=True, extra="forbid")
|
|
33
|
+
|
|
34
|
+
usd: float = Field(default=1.0, ge=0.0)
|
|
35
|
+
max_tokens: int | None = Field(default=None, ge=1)
|
|
36
|
+
error_streak_limit: int | None = Field(default=None, ge=1)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class AgentConfig(BaseModel):
|
|
40
|
+
"""`agent:` section — model, strategy, prompt, budget, etc."""
|
|
41
|
+
|
|
42
|
+
model_config = ConfigDict(strict=True, extra="forbid")
|
|
43
|
+
|
|
44
|
+
name: str | None = None
|
|
45
|
+
model: str | dict[str, Any] | None = None
|
|
46
|
+
strategy: str | dict[str, Any] | None = None
|
|
47
|
+
system_prompt: str | None = None
|
|
48
|
+
system_prompt_file: Path | None = None
|
|
49
|
+
tools: list[str | dict[str, Any]] = Field(default_factory=list)
|
|
50
|
+
budget: BudgetConfig = Field(default_factory=BudgetConfig)
|
|
51
|
+
max_iterations: int = Field(default=25, ge=1)
|
|
52
|
+
llm_options: dict[str, Any] = Field(default_factory=dict)
|
|
53
|
+
|
|
54
|
+
@field_validator("system_prompt_file", mode="before")
|
|
55
|
+
@classmethod
|
|
56
|
+
def _coerce_path(cls, value: Any) -> Any:
|
|
57
|
+
# Strict mode rejects strings for Path; YAML loads as `str`,
|
|
58
|
+
# so coerce here. None / Path stay as-is.
|
|
59
|
+
if isinstance(value, str):
|
|
60
|
+
return Path(value)
|
|
61
|
+
return value
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class LoggingConfig(BaseModel):
|
|
65
|
+
"""`logging:` — level, run-id filter toggle, format."""
|
|
66
|
+
|
|
67
|
+
model_config = ConfigDict(strict=True, extra="forbid")
|
|
68
|
+
|
|
69
|
+
level: str = "INFO"
|
|
70
|
+
run_id_filter: bool = True
|
|
71
|
+
format: str = "text" # "text" | "json"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class ModuleEntry(BaseModel):
|
|
75
|
+
"""Generic `driver + config` shape used by the `modules.memory`,
|
|
76
|
+
`modules.graph`, `modules.retriever` sections."""
|
|
77
|
+
|
|
78
|
+
model_config = ConfigDict(strict=True, extra="forbid")
|
|
79
|
+
|
|
80
|
+
driver: str = Field(min_length=1)
|
|
81
|
+
config: dict[str, Any] = Field(default_factory=dict)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class MemoryModuleConfig(ModuleEntry):
|
|
85
|
+
"""Alias of `ModuleEntry` for `modules.memory:` — distinct type so
|
|
86
|
+
schema docs can attach memory-specific guidance."""
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class GraphModuleConfig(ModuleEntry):
|
|
90
|
+
"""Alias of `ModuleEntry` for `modules.graph:`."""
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class RetrieverModuleConfig(ModuleEntry):
|
|
94
|
+
"""Alias of `ModuleEntry` for `modules.retriever:` (legacy).
|
|
95
|
+
|
|
96
|
+
.. deprecated:: 0.2
|
|
97
|
+
Use the top-level `retrieval:` block (see
|
|
98
|
+
:class:`RetrievalConfig`) instead. The legacy single-entry
|
|
99
|
+
form remains valid for v0.2 backward compatibility and may
|
|
100
|
+
be removed in v1.0.
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class RerankerEntry(BaseModel):
|
|
105
|
+
"""`retrieval.reranker:` — name + config for a `Reranker` impl.
|
|
106
|
+
|
|
107
|
+
Resolved against the `rerankers` entry-point category. Name
|
|
108
|
+
matches the registered entry-point name (e.g.
|
|
109
|
+
`sentence-transformers`).
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
model_config = ConfigDict(strict=True, extra="forbid")
|
|
113
|
+
|
|
114
|
+
name: str = Field(min_length=1)
|
|
115
|
+
config: dict[str, Any] = Field(default_factory=dict)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class GraphExpansionConfig(BaseModel):
|
|
119
|
+
"""`retrieval.graph_expansion:` — wiring for GraphRAG hybrid
|
|
120
|
+
retrieval (feat-023).
|
|
121
|
+
|
|
122
|
+
The `store` field resolves against the `graph_stores`
|
|
123
|
+
entry-point category. When set on a `RetrievalConfig`, the
|
|
124
|
+
builder constructs a `GraphExpansion` value and forwards it
|
|
125
|
+
into the `Retriever` constructor so vector / hybrid hits get
|
|
126
|
+
augmented with N-hop graph neighbours.
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
model_config = ConfigDict(strict=True, extra="forbid")
|
|
130
|
+
|
|
131
|
+
store: ModuleEntry
|
|
132
|
+
max_hops: int = Field(default=2, ge=1)
|
|
133
|
+
edge_types: list[str] | None = None
|
|
134
|
+
"""Edge-type filter. YAML lists deserialize to `list[str]`;
|
|
135
|
+
converted to a tuple by `build_retriever_from_config` before
|
|
136
|
+
constructing the `GraphExpansion` value."""
|
|
137
|
+
text_property: str = "text"
|
|
138
|
+
decay: float = Field(default=0.5, gt=0.0, le=1.0)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class RetrievalConfig(BaseModel):
|
|
142
|
+
"""Top-level `retrieval:` block (feat-021 follow-up).
|
|
143
|
+
|
|
144
|
+
Models the full retrieval pipeline: a `VectorStore` + an
|
|
145
|
+
`EmbeddingClient` + an optional `Reranker`, plus retrieval-
|
|
146
|
+
time knobs (`top_k`, `over_fetch_factor`, `batch_size`).
|
|
147
|
+
Lives at the root of `agentforge.yaml`, not nested under
|
|
148
|
+
`modules:`.
|
|
149
|
+
|
|
150
|
+
The legacy `modules.retriever` single-entry form still works
|
|
151
|
+
for v0.2 backward compat; the new `retrieval:` block
|
|
152
|
+
supersedes it. Both should not be set simultaneously — the
|
|
153
|
+
builder picks `retrieval:` when present.
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
model_config = ConfigDict(strict=True, extra="forbid")
|
|
157
|
+
|
|
158
|
+
vector_store: ModuleEntry
|
|
159
|
+
embedder: ModuleEntry
|
|
160
|
+
reranker: RerankerEntry | None = None
|
|
161
|
+
top_k: int = Field(default=5, ge=1)
|
|
162
|
+
over_fetch_factor: int = Field(default=3, ge=1)
|
|
163
|
+
batch_size: int = Field(default=32, ge=1)
|
|
164
|
+
mode: Literal["vector", "hybrid"] = "vector"
|
|
165
|
+
"""Retrieval mode (feat-022): ``"vector"`` for cosine-only or
|
|
166
|
+
``"hybrid"`` for BM25 + cosine fused via RRF. Hybrid requires the
|
|
167
|
+
underlying ``VectorStore`` to declare the ``"hybrid_search"``
|
|
168
|
+
capability."""
|
|
169
|
+
rrf_k: int = Field(default=60, ge=1)
|
|
170
|
+
"""RRF constant (Cormack 2009 default 60). Ignored when ``mode``
|
|
171
|
+
is ``"vector"``."""
|
|
172
|
+
graph_expansion: GraphExpansionConfig | None = None
|
|
173
|
+
"""Optional graph-augmented retrieval (feat-023). When set the
|
|
174
|
+
builder resolves the graph store, constructs a
|
|
175
|
+
:class:`GraphExpansion`, and forwards it to ``Retriever`` so
|
|
176
|
+
vector / hybrid hits get expanded with N-hop graph neighbours.
|
|
177
|
+
Composes orthogonally with ``mode`` and ``reranker``."""
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class EvaluatorEntry(BaseModel):
|
|
181
|
+
"""An entry in `modules.evaluators:`. Two YAML shapes are valid:
|
|
182
|
+
|
|
183
|
+
- String form: `- faithfulness` (just the name).
|
|
184
|
+
- Mapping form: `- faithfulness: {cost_cap_usd: 0.05, ...}`.
|
|
185
|
+
|
|
186
|
+
We model the mapping form here; the loader normalises strings to
|
|
187
|
+
`EvaluatorEntry(name=..., config={})` before validation.
|
|
188
|
+
"""
|
|
189
|
+
|
|
190
|
+
model_config = ConfigDict(strict=True, extra="forbid")
|
|
191
|
+
|
|
192
|
+
name: str = Field(min_length=1)
|
|
193
|
+
config: dict[str, Any] = Field(default_factory=dict)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class ObservabilityEntry(BaseModel):
|
|
197
|
+
"""An entry in `modules.observability:` — same shape as evaluator
|
|
198
|
+
entries (name + config)."""
|
|
199
|
+
|
|
200
|
+
model_config = ConfigDict(strict=True, extra="forbid")
|
|
201
|
+
|
|
202
|
+
name: str = Field(min_length=1)
|
|
203
|
+
config: dict[str, Any] = Field(default_factory=dict)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class GuardrailPolicy(BaseModel):
|
|
207
|
+
"""Framework-wide guardrail policy (feat-018).
|
|
208
|
+
|
|
209
|
+
Read from `agentforge.yaml` under the top-level
|
|
210
|
+
`guardrail_policy:` key. Defaults are conservative (per P6 —
|
|
211
|
+
loud defaults). Lives here rather than in `values/guardrails.py`
|
|
212
|
+
so the config-schema module doesn't have to reach into
|
|
213
|
+
`values` (avoids an import cycle through `values.state`).
|
|
214
|
+
The runtime `ValidationResult` remains in `values.guardrails`.
|
|
215
|
+
"""
|
|
216
|
+
|
|
217
|
+
model_config = ConfigDict(strict=True, extra="forbid")
|
|
218
|
+
|
|
219
|
+
on_input_violation: Literal["block", "redact", "warn", "allow"] = "block"
|
|
220
|
+
on_output_violation: Literal["block", "redact", "warn", "allow"] = "redact"
|
|
221
|
+
on_tool_violation: Literal["block", "warn"] = "block"
|
|
222
|
+
audit_channel: str = "agentforge.audit"
|
|
223
|
+
fail_open: bool = False
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class GuardrailEntry(BaseModel):
|
|
227
|
+
"""One entry inside `modules.guardrails.{input,output,tool_gates}`.
|
|
228
|
+
|
|
229
|
+
Two YAML shapes are valid (mirrors `EvaluatorEntry`):
|
|
230
|
+
|
|
231
|
+
- String form: `- prompt_injection_basic` (just the name).
|
|
232
|
+
- Mapping form: `- presidio: {entities: ["EMAIL_ADDRESS"]}`.
|
|
233
|
+
|
|
234
|
+
Both normalise to `GuardrailEntry(name=..., config={})` before
|
|
235
|
+
validation.
|
|
236
|
+
"""
|
|
237
|
+
|
|
238
|
+
model_config = ConfigDict(strict=True, extra="forbid")
|
|
239
|
+
|
|
240
|
+
name: str = Field(min_length=1)
|
|
241
|
+
config: dict[str, Any] = Field(default_factory=dict)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class ChatHistoryDriverConfig(BaseModel):
|
|
245
|
+
"""`modules.chat.history:` — driver + config for a chat history
|
|
246
|
+
store (feat-020)."""
|
|
247
|
+
|
|
248
|
+
model_config = ConfigDict(strict=True, extra="forbid")
|
|
249
|
+
|
|
250
|
+
driver: str = Field(min_length=1)
|
|
251
|
+
config: dict[str, Any] = Field(default_factory=dict)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
class ChatTruncationConfig(BaseModel):
|
|
255
|
+
"""`modules.chat.truncation:` — strategy + config (feat-020)."""
|
|
256
|
+
|
|
257
|
+
model_config = ConfigDict(strict=True, extra="forbid")
|
|
258
|
+
|
|
259
|
+
strategy: str = Field(min_length=1)
|
|
260
|
+
config: dict[str, Any] = Field(default_factory=dict)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
class ChatSessionConfig(BaseModel):
|
|
264
|
+
"""`modules.chat.session:` — per-session policy knobs (feat-020)."""
|
|
265
|
+
|
|
266
|
+
model_config = ConfigDict(strict=True, extra="forbid")
|
|
267
|
+
|
|
268
|
+
per_turn_budget_usd: float | None = Field(default=None, ge=0.0)
|
|
269
|
+
per_session_budget_usd: float | None = Field(default=None, ge=0.0)
|
|
270
|
+
idempotency_window_s: float = Field(default=60.0, ge=0.0)
|
|
271
|
+
concurrency: Literal["queue", "reject", "replace"] = "queue"
|
|
272
|
+
safety_mode: Literal["buffer-then-stream", "sentence-window", "stream-then-redact"] = (
|
|
273
|
+
"buffer-then-stream"
|
|
274
|
+
)
|
|
275
|
+
"""Output-guardrail policy on streamed assistant turns:
|
|
276
|
+
|
|
277
|
+
- ``"buffer-then-stream"`` (default) — agent runs to completion;
|
|
278
|
+
output validators see the full text once; the assembled response
|
|
279
|
+
is then sentence-segmented for the wire. Existing v0.2 behaviour.
|
|
280
|
+
- ``"sentence-window"`` (feat-020 v0.3) — for real per-token
|
|
281
|
+
streaming, buffer tokens until a sentence boundary, run
|
|
282
|
+
``check_output`` over each completed sentence, emit the
|
|
283
|
+
validated sentence as the next ``text`` chunk. Trades a small
|
|
284
|
+
latency hit (visible chunks at sentence boundaries) for
|
|
285
|
+
streaming-aware safety.
|
|
286
|
+
- ``"stream-then-redact"`` (deferred) — currently an alias for
|
|
287
|
+
``sentence-window``. A future v0.3+ pass may add inline regex
|
|
288
|
+
redaction without buffering.
|
|
289
|
+
"""
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
class ChatConfig(BaseModel):
|
|
293
|
+
"""`modules.chat:` — chat layer config (feat-020).
|
|
294
|
+
|
|
295
|
+
`history` may be ``None`` (defaults to in-memory). `truncation`
|
|
296
|
+
similarly defaults to `SlidingWindow(50)` when absent.
|
|
297
|
+
"""
|
|
298
|
+
|
|
299
|
+
model_config = ConfigDict(strict=True, extra="forbid")
|
|
300
|
+
|
|
301
|
+
history: ChatHistoryDriverConfig | None = None
|
|
302
|
+
truncation: ChatTruncationConfig | None = None
|
|
303
|
+
session: ChatSessionConfig = Field(default_factory=ChatSessionConfig)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
class PipelineTaskEntry(BaseModel):
|
|
307
|
+
"""One entry inside `modules.pipeline.tasks` (feat-015).
|
|
308
|
+
|
|
309
|
+
Mirrors `EvaluatorEntry` / `GuardrailEntry`: a name (resolver
|
|
310
|
+
lookup under the `"tasks"` category) plus optional kwargs the
|
|
311
|
+
task class receives at construction.
|
|
312
|
+
"""
|
|
313
|
+
|
|
314
|
+
model_config = ConfigDict(strict=True, extra="forbid")
|
|
315
|
+
|
|
316
|
+
name: str = Field(min_length=1)
|
|
317
|
+
config: dict[str, Any] = Field(default_factory=dict)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
class PipelineConfig(BaseModel):
|
|
321
|
+
"""`modules.pipeline:` — deterministic-task DAG (feat-015).
|
|
322
|
+
|
|
323
|
+
When ``enabled`` is True and ``tasks`` is non-empty, the runtime
|
|
324
|
+
resolves each entry against the global resolver's ``tasks``
|
|
325
|
+
category, builds a `Pipeline`, and wires it into the `Agent`.
|
|
326
|
+
"""
|
|
327
|
+
|
|
328
|
+
model_config = ConfigDict(strict=True, extra="forbid")
|
|
329
|
+
|
|
330
|
+
enabled: bool = True
|
|
331
|
+
max_concurrent: int = Field(default=4, ge=1)
|
|
332
|
+
on_task_error: Literal["continue", "fail"] = "continue"
|
|
333
|
+
tasks: list[PipelineTaskEntry] = Field(default_factory=list)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
class GuardrailsConfig(BaseModel):
|
|
337
|
+
"""`modules.guardrails:` — input / output / tool-call validators.
|
|
338
|
+
|
|
339
|
+
`defaults: true` keeps the framework's built-in basic validators
|
|
340
|
+
(prompt_injection_basic, pii_redact_basic, capability_check)
|
|
341
|
+
installed alongside whatever's listed here. Disabling them is
|
|
342
|
+
explicit and emits a startup warning per P6 (loud defaults).
|
|
343
|
+
"""
|
|
344
|
+
|
|
345
|
+
model_config = ConfigDict(strict=True, extra="forbid")
|
|
346
|
+
|
|
347
|
+
defaults: bool = True
|
|
348
|
+
input: list[GuardrailEntry] = Field(default_factory=list)
|
|
349
|
+
output: list[GuardrailEntry] = Field(default_factory=list)
|
|
350
|
+
tool_gates: list[GuardrailEntry] = Field(default_factory=list)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
class ModulesConfig(BaseModel):
|
|
354
|
+
"""`modules:` — every plug-and-play module the agent uses.
|
|
355
|
+
|
|
356
|
+
Each section accepts either a single driver-with-config or a list
|
|
357
|
+
of entries:
|
|
358
|
+
|
|
359
|
+
modules:
|
|
360
|
+
memory: {driver: postgres, config: {...}}
|
|
361
|
+
evaluators: [faithfulness, {geval: {rubric: "..."}}]
|
|
362
|
+
observability: [{name: otel, config: {endpoint: "..."}}]
|
|
363
|
+
guardrails:
|
|
364
|
+
input: [prompt_injection_basic]
|
|
365
|
+
output: [pii_redact_basic]
|
|
366
|
+
tool_gates: [capability_check]
|
|
367
|
+
"""
|
|
368
|
+
|
|
369
|
+
model_config = ConfigDict(strict=True, extra="forbid")
|
|
370
|
+
|
|
371
|
+
memory: MemoryModuleConfig | None = None
|
|
372
|
+
graph: GraphModuleConfig | None = None
|
|
373
|
+
retriever: RetrieverModuleConfig | None = None
|
|
374
|
+
evaluators: list[EvaluatorEntry] = Field(default_factory=list)
|
|
375
|
+
observability: list[ObservabilityEntry] = Field(default_factory=list)
|
|
376
|
+
tools: list[str | dict[str, Any]] = Field(default_factory=list)
|
|
377
|
+
protocols: list[ObservabilityEntry] = Field(default_factory=list)
|
|
378
|
+
guardrails: GuardrailsConfig = Field(default_factory=GuardrailsConfig)
|
|
379
|
+
pipeline: PipelineConfig | None = None
|
|
380
|
+
chat: ChatConfig | None = None
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
class ProviderConfig(BaseModel):
|
|
384
|
+
"""One entry in the `providers:` named registry.
|
|
385
|
+
|
|
386
|
+
`type` is the entry-point name (e.g. `"anthropic"`, `"bedrock"`).
|
|
387
|
+
`model` + extra kwargs are passed through to the provider's
|
|
388
|
+
constructor. `options` carries per-call LLM options.
|
|
389
|
+
"""
|
|
390
|
+
|
|
391
|
+
model_config = ConfigDict(strict=True, extra="forbid")
|
|
392
|
+
|
|
393
|
+
type: str = Field(min_length=1)
|
|
394
|
+
model: str | None = None
|
|
395
|
+
options: dict[str, Any] = Field(default_factory=dict)
|
|
396
|
+
config: dict[str, Any] = Field(default_factory=dict)
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
class OutputConfig(BaseModel):
|
|
400
|
+
"""`output:` — finding-variant defaults, renderer choice, thresholds."""
|
|
401
|
+
|
|
402
|
+
model_config = ConfigDict(strict=True, extra="forbid")
|
|
403
|
+
|
|
404
|
+
default_finding_variant: str = "simple"
|
|
405
|
+
default_renderer: str = "scorecard"
|
|
406
|
+
thresholds: dict[str, list[str]] = Field(default_factory=dict)
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
class AgentForgeConfig(BaseModel):
|
|
410
|
+
"""Root model — `agentforge.yaml` shape.
|
|
411
|
+
|
|
412
|
+
Adding a field to this model (or any submodel) is a minor bump
|
|
413
|
+
under ADR-0007; removing or renaming requires a major bump.
|
|
414
|
+
"""
|
|
415
|
+
|
|
416
|
+
model_config = ConfigDict(strict=True, extra="forbid")
|
|
417
|
+
|
|
418
|
+
agent: AgentConfig = Field(default_factory=AgentConfig)
|
|
419
|
+
modules: ModulesConfig = Field(default_factory=ModulesConfig)
|
|
420
|
+
retrieval: RetrievalConfig | None = None
|
|
421
|
+
providers: dict[str, ProviderConfig] = Field(default_factory=dict)
|
|
422
|
+
logging: LoggingConfig = Field(default_factory=LoggingConfig)
|
|
423
|
+
output: OutputConfig = Field(default_factory=OutputConfig)
|
|
424
|
+
guardrail_policy: GuardrailPolicy = Field(default_factory=GuardrailPolicy)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Locked contracts — ABCs and the `Finding` Protocol.
|
|
2
|
+
|
|
3
|
+
Per ADR-0007, these are the framework's stable surface. Adding a method
|
|
4
|
+
to an ABC is a major version bump. Modules implement these; the runtime
|
|
5
|
+
consumes them by reference to the abstraction, never the implementation.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from agentforge_core.contracts.auth import AuthPolicy
|
|
11
|
+
from agentforge_core.contracts.chat import ChatHistoryStore, HistoryTruncationStrategy
|
|
12
|
+
from agentforge_core.contracts.embedding import EmbeddingClient
|
|
13
|
+
from agentforge_core.contracts.evaluator import EvalResult, Evaluator
|
|
14
|
+
from agentforge_core.contracts.finding import Finding
|
|
15
|
+
from agentforge_core.contracts.graph_store import GraphStore
|
|
16
|
+
from agentforge_core.contracts.llm import LLMClient
|
|
17
|
+
from agentforge_core.contracts.memory import MemoryStore
|
|
18
|
+
from agentforge_core.contracts.migrator import (
|
|
19
|
+
Migration,
|
|
20
|
+
MigrationChecksumError,
|
|
21
|
+
MigrationStatus,
|
|
22
|
+
Migrator,
|
|
23
|
+
)
|
|
24
|
+
from agentforge_core.contracts.renderer import FindingRenderer
|
|
25
|
+
from agentforge_core.contracts.reranker import Reranker
|
|
26
|
+
from agentforge_core.contracts.strategy import ReasoningStrategy
|
|
27
|
+
from agentforge_core.contracts.task import Task
|
|
28
|
+
from agentforge_core.contracts.tool import Tool
|
|
29
|
+
from agentforge_core.contracts.vector_store import VectorStore
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"AuthPolicy",
|
|
33
|
+
"ChatHistoryStore",
|
|
34
|
+
"EmbeddingClient",
|
|
35
|
+
"EvalResult",
|
|
36
|
+
"Evaluator",
|
|
37
|
+
"Finding",
|
|
38
|
+
"FindingRenderer",
|
|
39
|
+
"GraphStore",
|
|
40
|
+
"HistoryTruncationStrategy",
|
|
41
|
+
"LLMClient",
|
|
42
|
+
"MemoryStore",
|
|
43
|
+
"Migration",
|
|
44
|
+
"MigrationChecksumError",
|
|
45
|
+
"MigrationStatus",
|
|
46
|
+
"Migrator",
|
|
47
|
+
"ReasoningStrategy",
|
|
48
|
+
"Reranker",
|
|
49
|
+
"Task",
|
|
50
|
+
"Tool",
|
|
51
|
+
"VectorStore",
|
|
52
|
+
]
|