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.
Files changed (68) hide show
  1. {fred_sdk-3.3.4 → fred_sdk-3.3.5}/PKG-INFO +1 -1
  2. {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/authoring/__init__.py +4 -0
  3. {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/contracts/capability/__init__.py +2 -1
  4. {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/contracts/capability/base.py +61 -9
  5. {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/contracts/capability/manifest.py +39 -0
  6. {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/contracts/models.py +31 -2
  7. {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/contracts/runtime.py +201 -1
  8. {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk.egg-info/PKG-INFO +1 -1
  9. {fred_sdk-3.3.4 → fred_sdk-3.3.5}/pyproject.toml +1 -1
  10. {fred_sdk-3.3.4 → fred_sdk-3.3.5}/tests/test_capability_contracts.py +91 -0
  11. {fred_sdk-3.3.4 → fred_sdk-3.3.5}/tests/test_document_search_port_1906.py +2 -0
  12. {fred_sdk-3.3.4 → fred_sdk-3.3.5}/README.md +0 -0
  13. {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/__init__.py +0 -0
  14. {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/authoring/api.py +0 -0
  15. {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/authoring/authored_tool_runtime.py +0 -0
  16. {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/authoring/inspection.py +0 -0
  17. {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/authoring/knowledge_flow_mcp.py +0 -0
  18. {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/authoring/tool_args_schema.py +0 -0
  19. {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/authoring/tool_context_helpers.py +0 -0
  20. {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/contracts/__init__.py +0 -0
  21. {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/contracts/capability/chat_controls.py +0 -0
  22. {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/contracts/capability/context.py +0 -0
  23. {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/contracts/capability/hitl.py +0 -0
  24. {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/contracts/context.py +0 -0
  25. {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/contracts/eval.py +0 -0
  26. {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/contracts/execution.py +0 -0
  27. {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/contracts/openai_compat.py +0 -0
  28. {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/contracts/prompt_utils.py +0 -0
  29. {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/contracts/react_contract.py +0 -0
  30. {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/contracts/ui_part_union.py +0 -0
  31. {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/deep/__init__.py +0 -0
  32. {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/graph/__init__.py +0 -0
  33. {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/graph/authoring/__init__.py +0 -0
  34. {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/graph/authoring/api.py +0 -0
  35. {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/graph/authoring/team_api.py +0 -0
  36. {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/graph/runtime.py +0 -0
  37. {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/hello.py +0 -0
  38. {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/react/__init__.py +0 -0
  39. {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/resources/__init__.py +0 -0
  40. {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/resources/packaged.py +0 -0
  41. {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/resources/prompts/mermaid_output_contract.md +0 -0
  42. {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/resources/prompts.py +0 -0
  43. {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/runtime_support/__init__.py +0 -0
  44. {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/runtime_support/remote_agent_invoker.py +0 -0
  45. {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/support/__init__.py +0 -0
  46. {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/support/authored_toolsets.py +0 -0
  47. {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/support/builtins/__init__.py +0 -0
  48. {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/support/builtins/catalog.py +0 -0
  49. {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk/support/mcp_utils.py +0 -0
  50. {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk.egg-info/SOURCES.txt +0 -0
  51. {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk.egg-info/dependency_links.txt +0 -0
  52. {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk.egg-info/requires.txt +0 -0
  53. {fred_sdk-3.3.4 → fred_sdk-3.3.5}/fred_sdk.egg-info/top_level.txt +0 -0
  54. {fred_sdk-3.3.4 → fred_sdk-3.3.5}/setup.cfg +0 -0
  55. {fred_sdk-3.3.4 → fred_sdk-3.3.5}/tests/test_conversational_memory.py +0 -0
  56. {fred_sdk-3.3.4 → fred_sdk-3.3.5}/tests/test_execution_contracts.py +0 -0
  57. {fred_sdk-3.3.4 → fred_sdk-3.3.5}/tests/test_graph_choice_step.py +0 -0
  58. {fred_sdk-3.3.4 → fred_sdk-3.3.5}/tests/test_graph_models.py +0 -0
  59. {fred_sdk-3.3.4 → fred_sdk-3.3.5}/tests/test_hello.py +0 -0
  60. {fred_sdk-3.3.4 → fred_sdk-3.3.5}/tests/test_mcp_utils.py +0 -0
  61. {fred_sdk-3.3.4 → fred_sdk-3.3.5}/tests/test_openai_compat.py +0 -0
  62. {fred_sdk-3.3.4 → fred_sdk-3.3.5}/tests/test_prompt_bundles.py +0 -0
  63. {fred_sdk-3.3.4 → fred_sdk-3.3.5}/tests/test_prompt_utils.py +0 -0
  64. {fred_sdk-3.3.4 → fred_sdk-3.3.5}/tests/test_react_contract.py +0 -0
  65. {fred_sdk-3.3.4 → fred_sdk-3.3.5}/tests/test_sdk_purity.py +0 -0
  66. {fred_sdk-3.3.4 → fred_sdk-3.3.5}/tests/test_tool_args_schema.py +0 -0
  67. {fred_sdk-3.3.4 → fred_sdk-3.3.5}/tests/test_tool_context_workspace_fs.py +0 -0
  68. {fred_sdk-3.3.4 → fred_sdk-3.3.5}/tests/test_ui_part_union_1977.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fred-sdk
3
- Version: 3.3.4
3
+ Version: 3.3.5
4
4
  Summary: Authoring SDK for Fred agents — graph, ReAct, and team agent primitives.
5
5
  Author-email: Thales <noreply@thalesgroup.com>
6
6
  License: Apache-2.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; only `middleware()` is abstract::
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 middleware(self, ctx): ...
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, abstractmethod
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 langchain.agents.middleware import AgentMiddleware
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
- @abstractmethod
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 runtime half (RFC §3.2, §5): the LangChain middleware STACK
177
- carrying this capability's tools and hooks, bound to the turn's
178
- context. Authored list order is preserved within this capability's
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 | None = Field(
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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fred-sdk
3
- Version: 3.3.4
3
+ Version: 3.3.5
4
4
  Summary: Authoring SDK for Fred agents — graph, ReAct, and team agent primitives.
5
5
  Author-email: Thales <noreply@thalesgroup.com>
6
6
  License: Apache-2.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "fred-sdk"
3
- version = "3.3.4"
3
+ version = "3.3.5"
4
4
  description = "Authoring SDK for Fred agents — graph, ReAct, and team agent primitives."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12,<3.13"
@@ -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