fred-sdk 3.3.4__tar.gz → 3.3.5__tar.gz
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.
- {fred_sdk-3.3.4 → fred_sdk-3.3.5}/PKG-INFO +1 -1
- {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/authoring/__init__.py +4 -0
- {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/contracts/capability/__init__.py +2 -1
- {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/contracts/capability/base.py +61 -9
- {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/contracts/capability/manifest.py +39 -0
- {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/contracts/models.py +31 -2
- {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/contracts/runtime.py +201 -1
- {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk.egg-info/PKG-INFO +1 -1
- {fred_sdk-3.3.4 → fred_sdk-3.3.5}/pyproject.toml +1 -1
- {fred_sdk-3.3.4 → fred_sdk-3.3.5}/tests/test_capability_contracts.py +91 -0
- {fred_sdk-3.3.4 → fred_sdk-3.3.5}/tests/test_document_search_port_1906.py +2 -0
- {fred_sdk-3.3.4 → fred_sdk-3.3.5}/README.md +0 -0
- {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/__init__.py +0 -0
- {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/authoring/api.py +0 -0
- {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/authoring/authored_tool_runtime.py +0 -0
- {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/authoring/inspection.py +0 -0
- {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/authoring/knowledge_flow_mcp.py +0 -0
- {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/authoring/tool_args_schema.py +0 -0
- {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/authoring/tool_context_helpers.py +0 -0
- {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/contracts/__init__.py +0 -0
- {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/contracts/capability/chat_controls.py +0 -0
- {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/contracts/capability/context.py +0 -0
- {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/contracts/capability/hitl.py +0 -0
- {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/contracts/context.py +0 -0
- {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/contracts/eval.py +0 -0
- {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/contracts/execution.py +0 -0
- {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/contracts/openai_compat.py +0 -0
- {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/contracts/prompt_utils.py +0 -0
- {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/contracts/react_contract.py +0 -0
- {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/contracts/ui_part_union.py +0 -0
- {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/deep/__init__.py +0 -0
- {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/graph/__init__.py +0 -0
- {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/graph/authoring/__init__.py +0 -0
- {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/graph/authoring/api.py +0 -0
- {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/graph/authoring/team_api.py +0 -0
- {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/graph/runtime.py +0 -0
- {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/hello.py +0 -0
- {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/react/__init__.py +0 -0
- {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/resources/__init__.py +0 -0
- {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/resources/packaged.py +0 -0
- {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/resources/prompts/mermaid_output_contract.md +0 -0
- {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/resources/prompts.py +0 -0
- {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/runtime_support/__init__.py +0 -0
- {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/runtime_support/remote_agent_invoker.py +0 -0
- {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/support/__init__.py +0 -0
- {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/support/authored_toolsets.py +0 -0
- {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/support/builtins/__init__.py +0 -0
- {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/support/builtins/catalog.py +0 -0
- {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/support/mcp_utils.py +0 -0
- {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk.egg-info/SOURCES.txt +0 -0
- {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk.egg-info/dependency_links.txt +0 -0
- {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk.egg-info/requires.txt +0 -0
- {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk.egg-info/top_level.txt +0 -0
- {fred_sdk-3.3.4 → fred_sdk-3.3.5}/setup.cfg +0 -0
- {fred_sdk-3.3.4 → fred_sdk-3.3.5}/tests/test_conversational_memory.py +0 -0
- {fred_sdk-3.3.4 → fred_sdk-3.3.5}/tests/test_execution_contracts.py +0 -0
- {fred_sdk-3.3.4 → fred_sdk-3.3.5}/tests/test_graph_choice_step.py +0 -0
- {fred_sdk-3.3.4 → fred_sdk-3.3.5}/tests/test_graph_models.py +0 -0
- {fred_sdk-3.3.4 → fred_sdk-3.3.5}/tests/test_hello.py +0 -0
- {fred_sdk-3.3.4 → fred_sdk-3.3.5}/tests/test_mcp_utils.py +0 -0
- {fred_sdk-3.3.4 → fred_sdk-3.3.5}/tests/test_openai_compat.py +0 -0
- {fred_sdk-3.3.4 → fred_sdk-3.3.5}/tests/test_prompt_bundles.py +0 -0
- {fred_sdk-3.3.4 → fred_sdk-3.3.5}/tests/test_prompt_utils.py +0 -0
- {fred_sdk-3.3.4 → fred_sdk-3.3.5}/tests/test_react_contract.py +0 -0
- {fred_sdk-3.3.4 → fred_sdk-3.3.5}/tests/test_sdk_purity.py +0 -0
- {fred_sdk-3.3.4 → fred_sdk-3.3.5}/tests/test_tool_args_schema.py +0 -0
- {fred_sdk-3.3.4 → fred_sdk-3.3.5}/tests/test_tool_context_workspace_fs.py +0 -0
- {fred_sdk-3.3.4 → fred_sdk-3.3.5}/tests/test_ui_part_union_1977.py +0 -0
|
@@ -58,7 +58,9 @@ from .knowledge_flow_mcp import (
|
|
|
58
58
|
MCP_SERVER_KNOWLEDGE_FLOW_CORPUS,
|
|
59
59
|
MCP_SERVER_KNOWLEDGE_FLOW_FS,
|
|
60
60
|
MCP_SERVER_KNOWLEDGE_FLOW_OPENSEARCH_OPS,
|
|
61
|
+
MCP_SERVER_KNOWLEDGE_FLOW_PROMETHEUS_OPS,
|
|
61
62
|
MCP_SERVER_KNOWLEDGE_FLOW_TABULAR,
|
|
63
|
+
MCP_SERVER_KNOWLEDGE_FLOW_TEXT,
|
|
62
64
|
MCPServerRef,
|
|
63
65
|
)
|
|
64
66
|
|
|
@@ -68,7 +70,9 @@ __all__ = [
|
|
|
68
70
|
"MCP_SERVER_KNOWLEDGE_FLOW_CORPUS",
|
|
69
71
|
"MCP_SERVER_KNOWLEDGE_FLOW_FS",
|
|
70
72
|
"MCP_SERVER_KNOWLEDGE_FLOW_OPENSEARCH_OPS",
|
|
73
|
+
"MCP_SERVER_KNOWLEDGE_FLOW_PROMETHEUS_OPS",
|
|
71
74
|
"MCP_SERVER_KNOWLEDGE_FLOW_TABULAR",
|
|
75
|
+
"MCP_SERVER_KNOWLEDGE_FLOW_TEXT",
|
|
72
76
|
"ReActAgent",
|
|
73
77
|
"ToolContext",
|
|
74
78
|
"ToolInvocationError",
|
|
@@ -32,7 +32,7 @@ How to use:
|
|
|
32
32
|
from __future__ import annotations
|
|
33
33
|
|
|
34
34
|
from ..models import StoredCapabilityConfig
|
|
35
|
-
from .base import AgentCapability
|
|
35
|
+
from .base import AgentCapability, ToolCarrierMiddleware
|
|
36
36
|
from .chat_controls import (
|
|
37
37
|
ChatControlsRequest,
|
|
38
38
|
ChatControlsRequestItem,
|
|
@@ -80,6 +80,7 @@ __all__ = [
|
|
|
80
80
|
"SidePanelSpec",
|
|
81
81
|
"StoredCapabilityConfig",
|
|
82
82
|
"TeamScopePolicy",
|
|
83
|
+
"ToolCarrierMiddleware",
|
|
83
84
|
"UploadedFile",
|
|
84
85
|
"chat_part_kind",
|
|
85
86
|
]
|
|
@@ -23,7 +23,8 @@ Why this module exists:
|
|
|
23
23
|
|
|
24
24
|
How to use:
|
|
25
25
|
- subclass with the three generic parameters and declare `manifest` plus the
|
|
26
|
-
typed models;
|
|
26
|
+
typed models; implement `tools()` — the primary, execution-model-agnostic
|
|
27
|
+
authoring surface every simple capability needs::
|
|
27
28
|
|
|
28
29
|
class PptFillerCapability(AgentCapability[PptConfig, PptStored, PptTurn]):
|
|
29
30
|
manifest = CapabilityManifest(id="ppt_filler", ...)
|
|
@@ -31,7 +32,12 @@ How to use:
|
|
|
31
32
|
StoredConfigModel = PptStored # defaults to ConfigModel if omitted
|
|
32
33
|
TurnOptionsModel = PptTurn # defaults to EmptyModel if omitted
|
|
33
34
|
|
|
34
|
-
def
|
|
35
|
+
def tools(self, ctx): ...
|
|
36
|
+
|
|
37
|
+
`middleware()` has a default that wraps `tools()` for `create_agent()`
|
|
38
|
+
(ReAct); override `middleware()` directly only for ReAct-loop-specific hooks
|
|
39
|
+
a plain tool cannot express (prompt injection, model-call wrapping — see
|
|
40
|
+
`McpCapability`).
|
|
35
41
|
|
|
36
42
|
- installing the package IS the registration: declare a `fred.capabilities`
|
|
37
43
|
entry point pointing at the subclass (RFC §4, §7)
|
|
@@ -47,10 +53,11 @@ Contract rules (RFC §3.2, §5.3):
|
|
|
47
53
|
|
|
48
54
|
from __future__ import annotations
|
|
49
55
|
|
|
50
|
-
from abc import ABC
|
|
56
|
+
from abc import ABC
|
|
51
57
|
from collections.abc import Mapping, Sequence
|
|
52
58
|
from typing import TYPE_CHECKING, Any, ClassVar, Generic, TypeVar, cast
|
|
53
59
|
|
|
60
|
+
from langchain.agents.middleware import AgentMiddleware
|
|
54
61
|
from pydantic import BaseModel
|
|
55
62
|
|
|
56
63
|
from .context import CapabilityContext, EmptyModel, SaveContext
|
|
@@ -58,7 +65,7 @@ from .hitl import HitlSpec
|
|
|
58
65
|
from .manifest import CapabilityManifest, ChatControlSpec, UploadedFile
|
|
59
66
|
|
|
60
67
|
if TYPE_CHECKING:
|
|
61
|
-
from
|
|
68
|
+
from langchain_core.tools import BaseTool
|
|
62
69
|
|
|
63
70
|
ConfigT = TypeVar("ConfigT", bound=BaseModel)
|
|
64
71
|
StoredT = TypeVar("StoredT", bound=BaseModel)
|
|
@@ -168,13 +175,58 @@ class AgentCapability(ABC, Generic[ConfigT, StoredT, TurnOptionsT]):
|
|
|
168
175
|
|
|
169
176
|
return None
|
|
170
177
|
|
|
171
|
-
|
|
178
|
+
def tools(
|
|
179
|
+
self, ctx: CapabilityContext[StoredT, TurnOptionsT]
|
|
180
|
+
) -> Sequence["BaseTool"]:
|
|
181
|
+
"""
|
|
182
|
+
The runtime half, execution-model-agnostic (RFC §3.2, §5): the plain
|
|
183
|
+
LangChain tools this capability's turn exposes, bound to `ctx`. This is
|
|
184
|
+
the PRIMARY authoring surface — implement this, not `middleware()`,
|
|
185
|
+
unless the capability needs a ReAct-loop-specific hook `middleware()`
|
|
186
|
+
offers (prompt injection, model-call wrapping) that a plain tool
|
|
187
|
+
cannot express.
|
|
188
|
+
|
|
189
|
+
Default: no tools.
|
|
190
|
+
"""
|
|
191
|
+
|
|
192
|
+
del ctx
|
|
193
|
+
return ()
|
|
194
|
+
|
|
172
195
|
def middleware(
|
|
173
196
|
self, ctx: CapabilityContext[StoredT, TurnOptionsT]
|
|
174
197
|
) -> Sequence["AgentMiddleware"]:
|
|
175
198
|
"""
|
|
176
|
-
The
|
|
177
|
-
carrying this capability's tools and hooks, bound to
|
|
178
|
-
context. Authored list order is preserved within this
|
|
179
|
-
block (§5.3).
|
|
199
|
+
The ReAct-loop-only escape hatch (RFC §3.2, §5): the LangChain
|
|
200
|
+
middleware STACK carrying this capability's tools and hooks, bound to
|
|
201
|
+
the turn's context. Authored list order is preserved within this
|
|
202
|
+
capability's block (§5.3). Only `create_agent()` (ReAct) consumes
|
|
203
|
+
this; Graph agents never see it (Invariant A).
|
|
204
|
+
|
|
205
|
+
Default: wraps `tools()` in one generic tool-carrier middleware, which
|
|
206
|
+
is correct for every capability whose only runtime need is exposing
|
|
207
|
+
tools. Override directly only for hooks `tools()` cannot express (see
|
|
208
|
+
`McpCapability.middleware`, a prompt-fragment-only override).
|
|
180
209
|
"""
|
|
210
|
+
|
|
211
|
+
tools = self.tools(ctx)
|
|
212
|
+
if not tools:
|
|
213
|
+
return []
|
|
214
|
+
return [ToolCarrierMiddleware(tools)]
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class ToolCarrierMiddleware(AgentMiddleware):
|
|
218
|
+
"""
|
|
219
|
+
Carries one capability's `tools()` output for `create_agent()` to bind.
|
|
220
|
+
|
|
221
|
+
Public (not `_`-prefixed) so the runtime assembly loop
|
|
222
|
+
(`fred_runtime.capabilities.assembly.build_capability_agent_block`) can
|
|
223
|
+
build it directly from the SAME `tools(ctx)` call used for
|
|
224
|
+
`CapabilityAgentBlock.tools`/HITL binding, instead of letting the default
|
|
225
|
+
`middleware()` above call `tools(ctx)` a second, separate time (CAPAB-02)
|
|
226
|
+
— one `tools()` call per capability per assembly, one set of tool
|
|
227
|
+
instances shared by every consumer.
|
|
228
|
+
"""
|
|
229
|
+
|
|
230
|
+
def __init__(self, tools: Sequence["BaseTool"]) -> None:
|
|
231
|
+
super().__init__()
|
|
232
|
+
self.tools = list(tools)
|
|
@@ -244,6 +244,40 @@ class CapabilityManifest(BaseModel):
|
|
|
244
244
|
# ever authored; this discriminator exists on `CapabilityCatalogEntry` for
|
|
245
245
|
# that projection and is carried here only so the two models stay aligned.
|
|
246
246
|
kind: Literal["tool", "agent"] = "tool"
|
|
247
|
+
# Which execution models this capability's runtime actually works under
|
|
248
|
+
# (CAPAB-02, RFC §3.2/§3.9). Default = both, correct for any capability
|
|
249
|
+
# whose only runtime need is `tools()` (execution-model-agnostic by
|
|
250
|
+
# construction). A capability that overrides `middleware()` for a
|
|
251
|
+
# ReAct-specific hook (dynamic per-turn tools, `before_model` state edits,
|
|
252
|
+
# prompt injection) WITHOUT also implementing `tools()` MUST declare
|
|
253
|
+
# `("react",)` explicitly — selecting it on a Graph agent then fails
|
|
254
|
+
# loudly at assembly instead of silently contributing no tools.
|
|
255
|
+
execution_models: tuple[Literal["react", "graph"], ...] = ("react", "graph")
|
|
256
|
+
|
|
257
|
+
@model_validator(mode="after")
|
|
258
|
+
def _check_execution_models(self) -> "CapabilityManifest":
|
|
259
|
+
if not self.execution_models:
|
|
260
|
+
raise ValueError(
|
|
261
|
+
f"CapabilityManifest '{self.id}': execution_models must not be empty."
|
|
262
|
+
)
|
|
263
|
+
if "react" not in self.execution_models:
|
|
264
|
+
# No capability runtime need is Graph-only: `tools()` is the only
|
|
265
|
+
# Graph-visible surface, and it feeds the ReAct binding
|
|
266
|
+
# identically (via the default middleware() wrap) — there is no
|
|
267
|
+
# equivalent "ReAct-only escape hatch" mechanism in reverse. A
|
|
268
|
+
# declaration missing "react" cannot correspond to anything the
|
|
269
|
+
# runtime can actually build, so it fails at declaration time
|
|
270
|
+
# rather than silently being ignored (nothing today even reads
|
|
271
|
+
# this field on the ReAct assembly path, which would otherwise
|
|
272
|
+
# make ("graph",) a lie the runtime never catches).
|
|
273
|
+
raise ValueError(
|
|
274
|
+
f"CapabilityManifest '{self.id}': execution_models must "
|
|
275
|
+
'include "react" — there is no Graph-only capability shape '
|
|
276
|
+
"(every Graph-visible tool is also ReAct-visible, since "
|
|
277
|
+
'tools() feeds both). Use ("react",) or the default '
|
|
278
|
+
'("react", "graph").'
|
|
279
|
+
)
|
|
280
|
+
return self
|
|
247
281
|
|
|
248
282
|
|
|
249
283
|
class CapabilityCatalogEntry(BaseModel):
|
|
@@ -283,6 +317,10 @@ class CapabilityCatalogEntry(BaseModel):
|
|
|
283
317
|
# projection of an agent template into this catalog, CAPAB-01 RFC §8.6) —
|
|
284
318
|
# see `CapabilityManifest.kind`.
|
|
285
319
|
kind: Literal["tool", "agent"] = "tool"
|
|
320
|
+
# See `CapabilityManifest.execution_models` (CAPAB-02). Advertised so a
|
|
321
|
+
# future catalog/UI filter can hide a ReAct-only capability from a Graph
|
|
322
|
+
# template's picker instead of only failing loud at selection time.
|
|
323
|
+
execution_models: tuple[Literal["react", "graph"], ...] = ("react", "graph")
|
|
286
324
|
# Ingress-relative base URL of this capability's auto-mounted router
|
|
287
325
|
# (`{pod_base_url}/capabilities/{id}`), or None when the capability ships
|
|
288
326
|
# no `router` (#1979, RFC §9.1). The template-bound (pre-save) surface
|
|
@@ -327,5 +365,6 @@ class CapabilityCatalogEntry(BaseModel):
|
|
|
327
365
|
assets=[slot.model_copy(deep=True) for slot in manifest.assets],
|
|
328
366
|
team_scope=manifest.team_scope,
|
|
329
367
|
kind=manifest.kind,
|
|
368
|
+
execution_models=manifest.execution_models,
|
|
330
369
|
route_base_url=route_base_url if manifest.router is not None else None,
|
|
331
370
|
)
|
|
@@ -98,6 +98,35 @@ class UIHints(BaseModel):
|
|
|
98
98
|
textarea: bool = False
|
|
99
99
|
group: Optional[str] = None
|
|
100
100
|
hide: bool = False
|
|
101
|
+
widget: Optional[str] = Field(
|
|
102
|
+
default=None,
|
|
103
|
+
description=(
|
|
104
|
+
"Names a frontend form widget to render this field instead of "
|
|
105
|
+
"the type-derived default input. Resolved first against the "
|
|
106
|
+
"owning capability plugin's `configWidgets` (custom widgets, "
|
|
107
|
+
"AGENT-CAPABILITY-RFC §9 item 4, #1903), then against stock "
|
|
108
|
+
"widgets — known stock ids: 'document_libraries' "
|
|
109
|
+
"(library/document tree picker for an array of library tag ids). "
|
|
110
|
+
"Unknown ids fall back to the default input, so older frontends "
|
|
111
|
+
"degrade gracefully."
|
|
112
|
+
),
|
|
113
|
+
)
|
|
114
|
+
visible_when: Optional[str] = Field(
|
|
115
|
+
default=None,
|
|
116
|
+
description=(
|
|
117
|
+
"Key of a sibling field in the same form: this field is only "
|
|
118
|
+
"shown while that sibling's effective value (current input or its "
|
|
119
|
+
"declared default) is truthy. Display-only — the value is kept, "
|
|
120
|
+
"and backends must not rely on the field being hidden."
|
|
121
|
+
),
|
|
122
|
+
)
|
|
123
|
+
advanced: bool = Field(
|
|
124
|
+
default=False,
|
|
125
|
+
description=(
|
|
126
|
+
"Renders the field inside the form's collapsed 'Advanced "
|
|
127
|
+
"settings' disclosure instead of the main section. Display-only."
|
|
128
|
+
),
|
|
129
|
+
)
|
|
101
130
|
|
|
102
131
|
|
|
103
132
|
class FieldSpec(BaseModel):
|
|
@@ -223,7 +252,7 @@ class MCPServerRef(BaseModel):
|
|
|
223
252
|
"""
|
|
224
253
|
|
|
225
254
|
id: str = Field(..., validation_alias=AliasChoices("id", "name"))
|
|
226
|
-
require_tools: list[str] =
|
|
255
|
+
require_tools: list[str] = Field(default_factory=list)
|
|
227
256
|
locked: bool = Field(
|
|
228
257
|
default=False,
|
|
229
258
|
description=(
|
|
@@ -833,7 +862,7 @@ class ReActPolicy(FrozenModel):
|
|
|
833
862
|
- "Operations copilot": tools + explicit approval on risky actions
|
|
834
863
|
"""
|
|
835
864
|
|
|
836
|
-
system_prompt_template: str
|
|
865
|
+
system_prompt_template: str = Field(
|
|
837
866
|
...,
|
|
838
867
|
min_length=1,
|
|
839
868
|
description=(
|
|
@@ -192,6 +192,7 @@ class ToolResultRuntimeEvent(RuntimeEventBase):
|
|
|
192
192
|
is_error: bool = False
|
|
193
193
|
sources: tuple[VectorSearchHit, ...] = ()
|
|
194
194
|
ui_parts: tuple[UiPart, ...] = ()
|
|
195
|
+
latency_ms: int | None = None
|
|
195
196
|
|
|
196
197
|
|
|
197
198
|
class HumanChoiceOption(FrozenModel):
|
|
@@ -617,6 +618,7 @@ class DocumentSearchPort(ABC):
|
|
|
617
618
|
library_tag_ids: Sequence[str] | None = None,
|
|
618
619
|
document_uids: Sequence[str] | None = None,
|
|
619
620
|
search_policy: str | None = None,
|
|
621
|
+
attachments_only: bool = False,
|
|
620
622
|
) -> DocumentSearchResult:
|
|
621
623
|
"""
|
|
622
624
|
Run one scoped vector search and return typed hits.
|
|
@@ -624,7 +626,191 @@ class DocumentSearchPort(ABC):
|
|
|
624
626
|
`library_tag_ids` / `document_uids` are the capability's already-narrowed
|
|
625
627
|
scope (None = "no capability-side narrowing at this level"); the adapter
|
|
626
628
|
further bounds them by the session binding. `search_policy` overrides the
|
|
627
|
-
binding's default policy when provided.
|
|
629
|
+
binding's default policy when provided. `attachments_only=True` restricts
|
|
630
|
+
the search to the conversation's session-scoped documents (attached
|
|
631
|
+
files) and excludes the corpus entirely.
|
|
632
|
+
"""
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
class AgentAssetPort(ABC):
|
|
636
|
+
"""
|
|
637
|
+
Capability-safe storage for one agent instance's configuration assets
|
|
638
|
+
(#1903, RFC AGENT-CAPABILITY §3.4, §3.8).
|
|
639
|
+
|
|
640
|
+
Doctrine: `validate_config` must never persist an asset binary inside the
|
|
641
|
+
stored config — it stores the bytes through THIS port and keeps only the
|
|
642
|
+
returned key. The adapter privately binds the acting identity, the team,
|
|
643
|
+
and the agent instance (the KF path is
|
|
644
|
+
`teams/{team}/agents/{agent_instance_id}/config/{key}`); a capability only
|
|
645
|
+
ever names the slot-relative `key`. The per-turn binding and raw access
|
|
646
|
+
token never enter `CapabilityContext`.
|
|
647
|
+
|
|
648
|
+
Reads are team-membership-gated on the KF side, so ANY user chatting with
|
|
649
|
+
the agent can fetch the asset at tool time; writes require the same team
|
|
650
|
+
resource-update permission as the team-shared space.
|
|
651
|
+
"""
|
|
652
|
+
|
|
653
|
+
@abstractmethod
|
|
654
|
+
async def store(
|
|
655
|
+
self,
|
|
656
|
+
key: str,
|
|
657
|
+
content: bytes,
|
|
658
|
+
*,
|
|
659
|
+
content_type: str | None = None,
|
|
660
|
+
filename: str | None = None,
|
|
661
|
+
) -> str:
|
|
662
|
+
"""Store one asset under `key` and return the stored key."""
|
|
663
|
+
|
|
664
|
+
@abstractmethod
|
|
665
|
+
async def fetch(self, key: str) -> bytes:
|
|
666
|
+
"""Fetch one asset's bytes by `key`; raise if it does not exist."""
|
|
667
|
+
|
|
668
|
+
@abstractmethod
|
|
669
|
+
async def delete(self, key: str) -> None:
|
|
670
|
+
"""Delete one asset by `key` (missing keys are a no-op)."""
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
class DocumentRawContent(FrozenModel):
|
|
674
|
+
"""Original uploaded bytes of one corpus document (#1903 image support)."""
|
|
675
|
+
|
|
676
|
+
content: bytes
|
|
677
|
+
content_type: str = "application/octet-stream"
|
|
678
|
+
filename: str = ""
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
class DocumentContentPort(ABC):
|
|
682
|
+
"""
|
|
683
|
+
Capability-safe fetch of a corpus document's ORIGINAL bytes by uid
|
|
684
|
+
(#1903, PPT-filler image support; RFC AGENT-CAPABILITY §3.8 doctrine).
|
|
685
|
+
|
|
686
|
+
Takes only the document uid — auth and identity come from the adapter's
|
|
687
|
+
privately-captured per-turn binding, never from the caller.
|
|
688
|
+
"""
|
|
689
|
+
|
|
690
|
+
@abstractmethod
|
|
691
|
+
async def fetch_raw(self, document_uid: str) -> DocumentRawContent:
|
|
692
|
+
"""Fetch one document's original bytes; raise on missing/forbidden."""
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
class FolderDocumentEntry(FrozenModel):
|
|
696
|
+
"""One document inside a resolved folder (#1903 image support)."""
|
|
697
|
+
|
|
698
|
+
document_uid: str
|
|
699
|
+
document_name: str
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
class DocumentFolderPort(ABC):
|
|
703
|
+
"""
|
|
704
|
+
Capability-safe access to author folders (DOCUMENT tags) (#1903, PPT-filler
|
|
705
|
+
image support).
|
|
706
|
+
|
|
707
|
+
The adapter binds the space (team vs personal) privately; the capability
|
|
708
|
+
passes only the author's folder string (e.g. ``"images/flags"``) or an
|
|
709
|
+
already-resolved tag id.
|
|
710
|
+
"""
|
|
711
|
+
|
|
712
|
+
@abstractmethod
|
|
713
|
+
async def resolve_folder(self, folder: str) -> str | None:
|
|
714
|
+
"""Resolve one folder string to its DOCUMENT tag id, else None."""
|
|
715
|
+
|
|
716
|
+
@abstractmethod
|
|
717
|
+
async def list_folder_documents(
|
|
718
|
+
self, folder_tag_id: str
|
|
719
|
+
) -> tuple[FolderDocumentEntry, ...]:
|
|
720
|
+
"""List the documents carried by one folder tag (uid + display name)."""
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
class DocumentTreeResult(FrozenModel):
|
|
724
|
+
"""Typed result of one capability-scoped document tree listing."""
|
|
725
|
+
|
|
726
|
+
tree: str = ""
|
|
727
|
+
truncated: bool = False
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
class DocumentSummaryResult(FrozenModel):
|
|
731
|
+
"""Typed result of one on-demand document summarization."""
|
|
732
|
+
|
|
733
|
+
document_uid: str
|
|
734
|
+
summary: str = ""
|
|
735
|
+
shrunk_for_budget: bool = False
|
|
736
|
+
keywords: tuple[str, ...] = ()
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
class DocumentPortCallError(Exception):
|
|
740
|
+
"""
|
|
741
|
+
Typed transport failure raised by document port adapters.
|
|
742
|
+
|
|
743
|
+
Why this exists: a failing document tool must surface a clean `is_error`
|
|
744
|
+
tool result (actionable detail, never a raised exception reaching the
|
|
745
|
+
runtime's ToolNode) — but the capability must not import the adapter's
|
|
746
|
+
HTTP stack to introspect the failure. Adapters map their transport errors
|
|
747
|
+
(timeout, HTTP status) onto this exception; capabilities render it.
|
|
748
|
+
"""
|
|
749
|
+
|
|
750
|
+
def __init__(
|
|
751
|
+
self,
|
|
752
|
+
message: str,
|
|
753
|
+
*,
|
|
754
|
+
timed_out: bool = False,
|
|
755
|
+
status_code: int | None = None,
|
|
756
|
+
) -> None:
|
|
757
|
+
super().__init__(message)
|
|
758
|
+
self.timed_out = timed_out
|
|
759
|
+
self.status_code = status_code
|
|
760
|
+
|
|
761
|
+
|
|
762
|
+
class DocumentTreePort(ABC):
|
|
763
|
+
"""
|
|
764
|
+
Capability-safe folder/document tree listing over the platform corpus
|
|
765
|
+
(same doctrine as `DocumentSearchPort`): the port takes SCOPE PARAMETERS
|
|
766
|
+
only — never a caller-supplied context, identity, or access token. Auth
|
|
767
|
+
and identity come solely from the adapter's privately-captured per-turn
|
|
768
|
+
binding, which never enters `CapabilityContext`.
|
|
769
|
+
"""
|
|
770
|
+
|
|
771
|
+
@abstractmethod
|
|
772
|
+
async def tree(
|
|
773
|
+
self,
|
|
774
|
+
*,
|
|
775
|
+
working_directory: str | None = None,
|
|
776
|
+
library_tag_ids: Sequence[str] | None = None,
|
|
777
|
+
max_chars: int = 6000,
|
|
778
|
+
) -> DocumentTreeResult:
|
|
779
|
+
"""
|
|
780
|
+
Return the caller's authorized folder/document tree as rendered text.
|
|
781
|
+
|
|
782
|
+
`library_tag_ids` is the capability's already-narrowed library scope
|
|
783
|
+
(None = "no capability-side narrowing"); the adapter further bounds it
|
|
784
|
+
by the session binding before calling Knowledge Flow, completing
|
|
785
|
+
`turn_option ⊆ capability_config ⊆ session_binding`. Raises
|
|
786
|
+
`DocumentPortCallError` on transport failure.
|
|
787
|
+
"""
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
class DocumentSummarizePort(ABC):
|
|
791
|
+
"""
|
|
792
|
+
Capability-safe on-demand document summarization (same doctrine as
|
|
793
|
+
`DocumentSearchPort`): scope parameters only, identity stays private to
|
|
794
|
+
the adapter. No scope narrowing here — the caller already holds a concrete
|
|
795
|
+
`document_uid` (from a search hit, tree listing, or the conversation's
|
|
796
|
+
attached-files context) and Knowledge Flow's own per-document ReBAC is the
|
|
797
|
+
real authorization gate. Document uids are internal working identifiers:
|
|
798
|
+
tools use them freely, but they are never surfaced to the end user.
|
|
799
|
+
"""
|
|
800
|
+
|
|
801
|
+
@abstractmethod
|
|
802
|
+
async def summarize(
|
|
803
|
+
self,
|
|
804
|
+
document_uid: str,
|
|
805
|
+
*,
|
|
806
|
+
instruction: str | None = None,
|
|
807
|
+
max_chars: int = 2000,
|
|
808
|
+
) -> DocumentSummaryResult:
|
|
809
|
+
"""
|
|
810
|
+
Summarize one document by uid, optionally steered by `instruction`,
|
|
811
|
+
bounded by `max_chars`. Long-running server-side (map-reduce over the
|
|
812
|
+
whole document); adapters use an extended read timeout. Raises
|
|
813
|
+
`DocumentPortCallError` on transport failure.
|
|
628
814
|
"""
|
|
629
815
|
|
|
630
816
|
|
|
@@ -660,6 +846,20 @@ class RuntimeServices:
|
|
|
660
846
|
# `ctx.services.document_search`; the port takes scope parameters only — the
|
|
661
847
|
# per-turn binding and raw access token stay private to the adapter.
|
|
662
848
|
document_search: DocumentSearchPort | None = None
|
|
849
|
+
# Per-agent-instance config-asset storage (#1903, RFC §3.4/§3.8): the
|
|
850
|
+
# save-time path stores uploaded asset binaries here (keys only in the
|
|
851
|
+
# stored config); the turn-time path fetches them back. Same doctrine as
|
|
852
|
+
# `document_search` — identity/team/instance bind privately in the adapter.
|
|
853
|
+
agent_assets: AgentAssetPort | None = None
|
|
854
|
+
# Original-bytes fetch of a corpus document by uid (#1903 image support).
|
|
855
|
+
document_content: DocumentContentPort | None = None
|
|
856
|
+
# Author folder string → DOCUMENT tag id resolution (#1903 image support);
|
|
857
|
+
# used at save/analyze time to validate image folders.
|
|
858
|
+
document_folders: DocumentFolderPort | None = None
|
|
859
|
+
# Companion document-access ports (same doctrine and optionality):
|
|
860
|
+
# scoped folder/document tree listing and on-demand summarization.
|
|
861
|
+
document_tree: DocumentTreePort | None = None
|
|
862
|
+
document_summarize: DocumentSummarizePort | None = None
|
|
663
863
|
|
|
664
864
|
|
|
665
865
|
InputModelT = TypeVar("InputModelT", bound=BaseModel)
|
|
@@ -42,6 +42,7 @@ from fred_sdk.contracts.capability import (
|
|
|
42
42
|
SidePanelSpec,
|
|
43
43
|
StoredCapabilityConfig,
|
|
44
44
|
TeamScopePolicy,
|
|
45
|
+
ToolCarrierMiddleware,
|
|
45
46
|
UploadedFile,
|
|
46
47
|
chat_part_kind,
|
|
47
48
|
)
|
|
@@ -103,6 +104,24 @@ class _MinimalCapability(AgentCapability[_Config, _Config, EmptyModel]):
|
|
|
103
104
|
return []
|
|
104
105
|
|
|
105
106
|
|
|
107
|
+
class _ToolsOnlyCapability(AgentCapability[_Config, _Config, EmptyModel]):
|
|
108
|
+
"""Implements only `tools()` — relies on the default `middleware()` wrap
|
|
109
|
+
(CAPAB-02: the primary authoring surface, execution-model-agnostic)."""
|
|
110
|
+
|
|
111
|
+
manifest = _manifest(id="tools_only_cap")
|
|
112
|
+
ConfigModel = _Config
|
|
113
|
+
|
|
114
|
+
def tools(self, ctx: CapabilityContext[_Config, EmptyModel]) -> list[Any]:
|
|
115
|
+
from langchain_core.tools import tool
|
|
116
|
+
|
|
117
|
+
@tool
|
|
118
|
+
def echo(text: str) -> str:
|
|
119
|
+
"""Echo text back."""
|
|
120
|
+
return text
|
|
121
|
+
|
|
122
|
+
return [echo]
|
|
123
|
+
|
|
124
|
+
|
|
106
125
|
def _identity() -> CapabilityIdentity:
|
|
107
126
|
return CapabilityIdentity(user_id="user-1", session_id="session-1")
|
|
108
127
|
|
|
@@ -152,6 +171,50 @@ def test_default_chat_controls_and_hitl_specs_are_empty() -> None:
|
|
|
152
171
|
assert list(cap.hitl_specs()) == []
|
|
153
172
|
|
|
154
173
|
|
|
174
|
+
def test_default_tools_is_empty() -> None:
|
|
175
|
+
# `tools()` is the primary authoring surface (CAPAB-02); a capability that
|
|
176
|
+
# doesn't implement it (e.g. a middleware()-only, ReAct-specific one) gets
|
|
177
|
+
# the empty default, never an AttributeError.
|
|
178
|
+
cap = _FullCapability()
|
|
179
|
+
ctx = CapabilityContext(
|
|
180
|
+
identity=_identity(),
|
|
181
|
+
config=_StoredConfig(),
|
|
182
|
+
turn_options=_TurnOptions(),
|
|
183
|
+
services=RuntimeServices(),
|
|
184
|
+
)
|
|
185
|
+
assert list(cap.tools(ctx)) == []
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def test_default_middleware_wraps_tools_in_tool_carrier_middleware() -> None:
|
|
189
|
+
cap = _ToolsOnlyCapability()
|
|
190
|
+
ctx = CapabilityContext(
|
|
191
|
+
identity=_identity(),
|
|
192
|
+
config=_Config(),
|
|
193
|
+
turn_options=EmptyModel(),
|
|
194
|
+
services=RuntimeServices(),
|
|
195
|
+
)
|
|
196
|
+
(middleware,) = cap.middleware(ctx)
|
|
197
|
+
assert isinstance(middleware, ToolCarrierMiddleware)
|
|
198
|
+
assert [t.name for t in middleware.tools] == ["echo"]
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def test_default_middleware_is_empty_when_tools_is_empty() -> None:
|
|
202
|
+
# A capability implementing neither tools() nor middleware() (both base
|
|
203
|
+
# class defaults) contributes nothing — not a crash.
|
|
204
|
+
class _BareCapability(AgentCapability[_Config, _Config, EmptyModel]):
|
|
205
|
+
manifest = _manifest(id="bare_cap")
|
|
206
|
+
ConfigModel = _Config
|
|
207
|
+
|
|
208
|
+
cap = _BareCapability()
|
|
209
|
+
ctx = CapabilityContext(
|
|
210
|
+
identity=_identity(),
|
|
211
|
+
config=_Config(),
|
|
212
|
+
turn_options=EmptyModel(),
|
|
213
|
+
services=RuntimeServices(),
|
|
214
|
+
)
|
|
215
|
+
assert list(cap.middleware(ctx)) == []
|
|
216
|
+
|
|
217
|
+
|
|
155
218
|
def test_empty_model_forbids_fields() -> None:
|
|
156
219
|
with pytest.raises(ValidationError):
|
|
157
220
|
EmptyModel.model_validate({"unexpected": 1})
|
|
@@ -271,6 +334,34 @@ def test_manifest_defaults() -> None:
|
|
|
271
334
|
assert manifest.state_models == []
|
|
272
335
|
assert manifest.router is None
|
|
273
336
|
assert manifest.team_scope is TeamScopePolicy.ADMIN_GATED
|
|
337
|
+
assert manifest.execution_models == ("react", "graph")
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def test_manifest_execution_models_can_be_declared_react_only() -> None:
|
|
341
|
+
# CAPAB-02: a middleware()-only capability (a ReAct-specific hook tools()
|
|
342
|
+
# cannot express) must declare this explicitly — the default claims both.
|
|
343
|
+
manifest = _manifest(execution_models=("react",))
|
|
344
|
+
assert manifest.execution_models == ("react",)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def test_manifest_rejects_empty_execution_models() -> None:
|
|
348
|
+
with pytest.raises(ValidationError):
|
|
349
|
+
_manifest(execution_models=())
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def test_manifest_rejects_execution_models_without_react() -> None:
|
|
353
|
+
# No capability runtime need is Graph-only: tools() is the only
|
|
354
|
+
# Graph-visible surface and it feeds the ReAct binding identically —
|
|
355
|
+
# there is no equivalent "ReAct-only" mechanism in reverse.
|
|
356
|
+
with pytest.raises(ValidationError, match="react"):
|
|
357
|
+
_manifest(execution_models=("graph",))
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def test_catalog_entry_carries_execution_models() -> None:
|
|
361
|
+
manifest = _manifest(execution_models=("react",))
|
|
362
|
+
entry = CapabilityCatalogEntry.from_manifest(manifest)
|
|
363
|
+
assert entry.execution_models == ("react",)
|
|
364
|
+
assert CapabilityCatalogEntry.model_validate_json(entry.model_dump_json()) == entry
|
|
274
365
|
|
|
275
366
|
|
|
276
367
|
def test_manifest_rejects_blank_id() -> None:
|
|
@@ -54,6 +54,7 @@ class _FakePort(DocumentSearchPort):
|
|
|
54
54
|
library_tag_ids=None,
|
|
55
55
|
document_uids=None,
|
|
56
56
|
search_policy=None,
|
|
57
|
+
attachments_only: bool = False,
|
|
57
58
|
) -> DocumentSearchResult:
|
|
58
59
|
self.calls.append(
|
|
59
60
|
{
|
|
@@ -62,6 +63,7 @@ class _FakePort(DocumentSearchPort):
|
|
|
62
63
|
"library_tag_ids": library_tag_ids,
|
|
63
64
|
"document_uids": document_uids,
|
|
64
65
|
"search_policy": search_policy,
|
|
66
|
+
"attachments_only": attachments_only,
|
|
65
67
|
}
|
|
66
68
|
)
|
|
67
69
|
return DocumentSearchResult(hits=(_hit("d1"),))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|