fred-sdk 3.4.2__tar.gz → 3.4.3__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 (74) hide show
  1. {fred_sdk-3.4.2 → fred_sdk-3.4.3}/PKG-INFO +2 -2
  2. {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/authoring/authored_tool_runtime.py +1 -1
  3. {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/contracts/__init__.py +2 -8
  4. {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/contracts/capability/manifest.py +12 -0
  5. {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/contracts/context.py +23 -3
  6. fred_sdk-3.4.3/fred_sdk/contracts/prompt_utils.py +55 -0
  7. {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/contracts/runtime.py +177 -2
  8. {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/contracts/ui_part_union.py +1 -0
  9. {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk.egg-info/PKG-INFO +2 -2
  10. {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk.egg-info/SOURCES.txt +1 -0
  11. {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk.egg-info/requires.txt +1 -1
  12. {fred_sdk-3.4.2 → fred_sdk-3.4.3}/pyproject.toml +2 -2
  13. fred_sdk-3.4.3/tests/test_final_runtime_event_finish_reason.py +43 -0
  14. fred_sdk-3.4.3/tests/test_prompt_utils.py +80 -0
  15. fred_sdk-3.4.2/fred_sdk/contracts/prompt_utils.py +0 -112
  16. fred_sdk-3.4.2/tests/test_prompt_utils.py +0 -167
  17. {fred_sdk-3.4.2 → fred_sdk-3.4.3}/README.md +0 -0
  18. {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/__init__.py +0 -0
  19. {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/authoring/__init__.py +0 -0
  20. {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/authoring/api.py +0 -0
  21. {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/authoring/inspection.py +0 -0
  22. {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/authoring/knowledge_flow_mcp.py +0 -0
  23. {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/authoring/tool_args_schema.py +0 -0
  24. {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/authoring/tool_context_helpers.py +0 -0
  25. {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/contracts/capability/__init__.py +0 -0
  26. {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/contracts/capability/base.py +0 -0
  27. {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/contracts/capability/chat_controls.py +0 -0
  28. {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/contracts/capability/context.py +0 -0
  29. {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/contracts/capability/hitl.py +0 -0
  30. {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/contracts/eval.py +0 -0
  31. {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/contracts/execution.py +0 -0
  32. {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/contracts/models.py +0 -0
  33. {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/contracts/openai_compat.py +0 -0
  34. {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/contracts/react_contract.py +0 -0
  35. {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/deep/__init__.py +0 -0
  36. {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/graph/__init__.py +0 -0
  37. {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/graph/authoring/__init__.py +0 -0
  38. {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/graph/authoring/api.py +0 -0
  39. {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/graph/authoring/team_api.py +0 -0
  40. {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/graph/runtime.py +0 -0
  41. {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/hello.py +0 -0
  42. {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/react/__init__.py +0 -0
  43. {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/resources/__init__.py +0 -0
  44. {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/resources/packaged.py +0 -0
  45. {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/resources/prompts/mermaid_output_contract.md +0 -0
  46. {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/resources/prompts.py +0 -0
  47. {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/runtime_support/__init__.py +0 -0
  48. {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/runtime_support/remote_agent_invoker.py +0 -0
  49. {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/support/__init__.py +0 -0
  50. {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/support/authored_toolsets.py +0 -0
  51. {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/support/builtins/__init__.py +0 -0
  52. {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/support/builtins/catalog.py +0 -0
  53. {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/support/mcp_utils.py +0 -0
  54. {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/testing.py +0 -0
  55. {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk.egg-info/dependency_links.txt +0 -0
  56. {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk.egg-info/top_level.txt +0 -0
  57. {fred_sdk-3.4.2 → fred_sdk-3.4.3}/setup.cfg +0 -0
  58. {fred_sdk-3.4.2 → fred_sdk-3.4.3}/tests/test_capability_contracts.py +0 -0
  59. {fred_sdk-3.4.2 → fred_sdk-3.4.3}/tests/test_conversational_memory.py +0 -0
  60. {fred_sdk-3.4.2 → fred_sdk-3.4.3}/tests/test_document_search_port_1906.py +0 -0
  61. {fred_sdk-3.4.2 → fred_sdk-3.4.3}/tests/test_execution_contracts.py +0 -0
  62. {fred_sdk-3.4.2 → fred_sdk-3.4.3}/tests/test_graph_choice_step.py +0 -0
  63. {fred_sdk-3.4.2 → fred_sdk-3.4.3}/tests/test_graph_models.py +0 -0
  64. {fred_sdk-3.4.2 → fred_sdk-3.4.3}/tests/test_hello.py +0 -0
  65. {fred_sdk-3.4.2 → fred_sdk-3.4.3}/tests/test_hitl_resume_identity_contracts.py +0 -0
  66. {fred_sdk-3.4.2 → fred_sdk-3.4.3}/tests/test_mcp_utils.py +0 -0
  67. {fred_sdk-3.4.2 → fred_sdk-3.4.3}/tests/test_openai_compat.py +0 -0
  68. {fred_sdk-3.4.2 → fred_sdk-3.4.3}/tests/test_prompt_bundles.py +0 -0
  69. {fred_sdk-3.4.2 → fred_sdk-3.4.3}/tests/test_react_contract.py +0 -0
  70. {fred_sdk-3.4.2 → fred_sdk-3.4.3}/tests/test_sdk_purity.py +0 -0
  71. {fred_sdk-3.4.2 → fred_sdk-3.4.3}/tests/test_testing_fakes.py +0 -0
  72. {fred_sdk-3.4.2 → fred_sdk-3.4.3}/tests/test_tool_args_schema.py +0 -0
  73. {fred_sdk-3.4.2 → fred_sdk-3.4.3}/tests/test_tool_context_workspace_fs.py +0 -0
  74. {fred_sdk-3.4.2 → fred_sdk-3.4.3}/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.4.2
3
+ Version: 3.4.3
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
@@ -12,7 +12,7 @@ Classifier: Programming Language :: Python :: 3 :: Only
12
12
  Classifier: Operating System :: OS Independent
13
13
  Requires-Python: <3.13,>=3.12
14
14
  Description-Content-Type: text/markdown
15
- Requires-Dist: fred-core>=3.4.1
15
+ Requires-Dist: fred-core>=3.8.2
16
16
  Requires-Dist: pydantic<3.0.0,>=2.7.0
17
17
  Requires-Dist: langchain-core>=0.3.0
18
18
  Requires-Dist: langchain>=1.3.14
@@ -316,7 +316,7 @@ def _coerce_tool_return(
316
316
  *,
317
317
  tool_ref: str,
318
318
  value: object,
319
- context: "ToolContext",
319
+ context: ToolContext,
320
320
  success_message: str | None,
321
321
  ) -> ToolInvocationResult:
322
322
  """
@@ -63,11 +63,7 @@ from .openai_compat import (
63
63
  OpenAIToolCall,
64
64
  OpenAIToolCallFunction,
65
65
  )
66
- from .prompt_utils import (
67
- PROMPT_SAFE_TOKENS,
68
- PromptTemplateError,
69
- validate_prompt_template,
70
- )
66
+ from .prompt_utils import PROMPT_SAFE_TOKENS
71
67
  from .runtime import RuntimeErrorEvent, TurnPersistedEvent
72
68
  from .ui_part_union import (
73
69
  BASE_UI_PARTS,
@@ -120,8 +116,6 @@ __all__ = [
120
116
  "OpenAIModelList",
121
117
  "OpenAIToolCall",
122
118
  "OpenAIToolCallFunction",
123
- # Prompt template token registry and validation
119
+ # Prompt template token registry
124
120
  "PROMPT_SAFE_TOKENS",
125
- "PromptTemplateError",
126
- "validate_prompt_template",
127
121
  ]
@@ -391,6 +391,18 @@ class CapabilityCatalogEntry(BaseModel):
391
391
  # so the admin row shows no reasoning control at all (an administrator
392
392
  # cannot make a model reason). Always empty for kind="tool"/"agent".
393
393
  model_thinking_profile_ids: tuple[str, ...] = Field(default_factory=tuple)
394
+ # The ops-authored `settings.reasoning_effort` of the model's thinking
395
+ # profile — derived from the settings, the single source of truth (review
396
+ # 2026-08-12: no separate supported-efforts declaration). Displayed by the
397
+ # composer's reasoning menu as the level a reasoning turn actually runs
398
+ # with; None when no thinking profile ships the key. Only `kind="model"`
399
+ # entries populate it.
400
+ model_reasoning_effort: str | None = None
401
+ # The ops-authored `model_display_name` from `models_catalog.yaml` — the
402
+ # label the composer shows for this model. None = unnamed, and the
403
+ # frontend derives one from the capability id. Display only; nothing
404
+ # routes or authorizes on it. Only `kind="model"` entries populate it.
405
+ model_display_name: str | None = None
394
406
 
395
407
  @classmethod
396
408
  def from_manifest(
@@ -42,7 +42,7 @@ from enum import Enum
42
42
  from typing import Annotated, Any, Dict, Literal, Optional
43
43
 
44
44
  from fred_core.store import VectorSearchHit
45
- from pydantic import BaseModel, ConfigDict, Field
45
+ from pydantic import BaseModel, ConfigDict, Field, model_validator
46
46
 
47
47
  # NOTE: This module is the canonical home for portable context + UI parts.
48
48
  # Keep Link/Geo parts and RuntimeContext here to avoid circular imports.
@@ -99,7 +99,7 @@ class GeoPart(BaseModel):
99
99
  class TeamOperationRouteRule(BaseModel):
100
100
  """
101
101
  One team-authored model-routing override for a specific runtime operation
102
- (`TEAM-ROUTING-POLICY-RFC.md` §3/§8). Lives in `fred-sdk` — not
102
+ or agent (`TEAM-ROUTING-POLICY-RFC.md` §3/§8). Lives in `fred-sdk` — not
103
103
  `fred-runtime` or `control-plane-backend` — because both of those already
104
104
  depend on `fred_sdk.contracts` and this type is shared verbatim by both:
105
105
  control-plane stores/validates it (`TeamRoutingPolicy`), fred-runtime
@@ -109,10 +109,24 @@ class TeamOperationRouteRule(BaseModel):
109
109
  model_config = ConfigDict(frozen=True)
110
110
 
111
111
  rule_id: str = Field(..., min_length=1)
112
- operation: str = Field(..., min_length=1)
112
+ # Optional operation scope: when set, the rule only applies to this operation.
113
+ # None = applies to every operation (wildcard). Resolved by `resolve_team_override`
114
+ # alongside purpose/agent_id.
115
+ operation: str | None = None
113
116
  purpose: str | None = None
117
+ # Optional per-agent scope: when set, the rule only applies to this agent
118
+ # (matched against the request's `agent_id`). None = applies to every agent
119
+ # of the team (wildcard) — the historical behaviour, so existing rules are
120
+ # unchanged. Resolved by `resolve_team_override` alongside operation/purpose.
121
+ agent_id: str | None = None
114
122
  target_profile_id: str = Field(..., min_length=1)
115
123
 
124
+ @model_validator(mode="after")
125
+ def validate_at_least_one_criterion(self) -> "TeamOperationRouteRule":
126
+ if self.operation is None and self.agent_id is None:
127
+ raise ValueError("Rule must specify at least one of: operation, agent_id")
128
+ return self
129
+
116
130
 
117
131
  class RuntimeContext(BaseModel):
118
132
  """
@@ -239,6 +253,12 @@ class RuntimeContext(BaseModel):
239
253
  "guarantee: level 2 remains a ceiling this cannot raise (§5.3)."
240
254
  ),
241
255
  )
256
+ # NOTE (2026-08-12): a per-question `reasoning_effort` override was built
257
+ # and withdrawn the same day — providers disagree on accepted values
258
+ # (Mistral small 400s on low/medium), and the declaration machinery it
259
+ # required wasn't worth the surface. The effort a reasoning turn runs
260
+ # with is the ops-authored `settings.reasoning_effort` of the resolved
261
+ # profile, full stop; the user's choice is the boolean above.
242
262
 
243
263
  # Group D — Content and preferences (will migrate to proper homes over time)
244
264
  language: Optional[str] = None
@@ -0,0 +1,55 @@
1
+ # Copyright Thales 2026
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """
16
+ Canonical prompt-template token registry.
17
+
18
+ Why this module exists:
19
+ - define the single source of truth for which {tokens} are substituted in
20
+ user-authored system prompts
21
+ - let the renderer in fred-runtime import that token set, so the registry and
22
+ the substitution stay in sync automatically
23
+
24
+ Why there is no validator here (#2277):
25
+ - the renderer substitutes a token only when it is present in this registry and
26
+ otherwise leaves the matched text verbatim, so an unrecognized `{token}` is
27
+ already harmless — it renders as itself and cannot crash the agent
28
+ - rejecting unknown tokens at save time was therefore a false positive: it
29
+ blocked prompts such as `Hello {name}` that would have rendered correctly,
30
+ with no in-product documentation or UI hint to warn the author
31
+ - the accepted trade-off is that a typo (`{todya}`) now reaches the prompt as
32
+ literal text instead of being caught on write; that failure is visible to the
33
+ author in the agent's output
34
+
35
+ How to use:
36
+ - import PROMPT_SAFE_TOKENS to get the canonical {token} → description map
37
+ """
38
+
39
+ from __future__ import annotations
40
+
41
+ from typing import Final
42
+
43
+ # Canonical set of runtime tokens available in user-authored system prompts.
44
+ # Key = token name (used inside {…} in the prompt text).
45
+ # Value = human-readable description, intended for a prompt-editor UI hint.
46
+ #
47
+ # To add a new supported token: add one entry here plus its runtime value in
48
+ # `safe_prompt_token_map` (fred-runtime) — no other site needs to change.
49
+ PROMPT_SAFE_TOKENS: Final[dict[str, str]] = {
50
+ "today": "ISO-8601 date at execution time (e.g. 2026-05-07)",
51
+ "response_language": "Human-readable response language (e.g. English, français)",
52
+ "session_id": "Active session identifier",
53
+ "user_id": "Authenticated user identifier",
54
+ "agent_id": "Agent definition identifier",
55
+ }
@@ -33,10 +33,11 @@ from dataclasses import dataclass
33
33
  from enum import Enum
34
34
  from typing import Annotated, Any, Generic, List, Literal, Protocol, TypeAlias, TypeVar
35
35
 
36
+ from fred_core.history.history_schema import FinishReason, coerce_finish_reason
36
37
  from fred_core.kpi import BaseKPIWriter
37
38
  from fred_core.portable import MetricsProvider, Span, Tracer
38
39
  from fred_core.store import VectorSearchHit
39
- from pydantic import BaseModel, ConfigDict, Field
40
+ from pydantic import BaseModel, ConfigDict, Field, field_validator
40
41
 
41
42
  from .context import (
42
43
  AgentInvocationRequest,
@@ -310,7 +311,16 @@ class FinalRuntimeEvent(RuntimeEventBase):
310
311
  ui_parts: tuple[UiPart, ...] = ()
311
312
  model_name: str | None = None
312
313
  token_usage: dict[str, int] | None = None
313
- finish_reason: str | None = None
314
+ finish_reason: FinishReason | None = None
315
+
316
+ # Same tolerant coercion as `ChatMetadata.finish_reason` (fred-core): a raw
317
+ # provider value that isn't already normalized (or one this build has never
318
+ # seen) must never fail this frozen model's construction — it degrades to
319
+ # `other` instead.
320
+ @field_validator("finish_reason", mode="before")
321
+ @classmethod
322
+ def _normalize_finish_reason(cls, v: object) -> FinishReason | None:
323
+ return coerce_finish_reason(v)
314
324
 
315
325
 
316
326
  class TurnPersistedEvent(RuntimeEventBase):
@@ -785,6 +795,30 @@ class DocumentTreeResult(FrozenModel):
785
795
  truncated: bool = False
786
796
 
787
797
 
798
+ class DocumentLabelReference(FrozenModel):
799
+ """Lean document reference for a label-page result — just enough for a
800
+ capability to identify and chain into another tool (e.g. summarize by
801
+ uid), not a full document payload."""
802
+
803
+ document_uid: str
804
+ document_name: str
805
+
806
+
807
+ class DocumentLabelPageResult(FrozenModel):
808
+ """Typed result of one paginated, exhaustive label resolution — the
809
+ flat sibling of `DocumentTreeResult`, for a caller that needs EVERY
810
+ document carrying a label rather than a folder-oriented, size-budgeted
811
+ view (see `DocumentTreePort.list_by_label`)."""
812
+
813
+ label: str = ""
814
+ documents: tuple[DocumentLabelReference, ...] = ()
815
+ total: int = 0
816
+ offset: int = 0
817
+ limit: int = 50
818
+ next_offset: int | None = None
819
+ has_more: bool = False
820
+
821
+
788
822
  class DocumentSummaryResult(FrozenModel):
789
823
  """Typed result of one on-demand document summarization."""
790
824
 
@@ -824,6 +858,10 @@ class DocumentTreePort(ABC):
824
858
  only — never a caller-supplied context, identity, or access token. Auth
825
859
  and identity come solely from the adapter's privately-captured per-turn
826
860
  binding, which never enters `CapabilityContext`.
861
+
862
+ Read-only projection over already-ingested documents — never a workspace,
863
+ never free-form files, never byte storage. Distinct from `workspace_fs`
864
+ (`RuntimeServices`), which is the mutable, persistent file domain.
827
865
  """
828
866
 
829
867
  @abstractmethod
@@ -842,6 +880,37 @@ class DocumentTreePort(ABC):
842
880
  by the session binding before calling Knowledge Flow, completing
843
881
  `turn_option ⊆ capability_config ⊆ session_binding`. Raises
844
882
  `DocumentPortCallError` on transport failure.
883
+
884
+ No business-label filtering here, deliberately: a folder tree renders
885
+ every folder in scope, size-budgeted to fit one response — the wrong
886
+ shape for "give me every document labeled X", which needs an
887
+ exhaustive, paginated answer instead. See `list_by_label` for that
888
+ need.
889
+ """
890
+
891
+ @abstractmethod
892
+ async def list_by_label(
893
+ self,
894
+ *,
895
+ label: str,
896
+ offset: int = 0,
897
+ limit: int = 50,
898
+ ) -> DocumentLabelPageResult:
899
+ """
900
+ Return one deterministic page of the documents carrying `label` —
901
+ exhaustive, paginated business-label resolution. This is the ONLY
902
+ label-search surface on this port: `tree()` does no label filtering
903
+ at all (see its docstring) — a folder tree answers "where", this
904
+ method answers "give me every document with this label", and mixing
905
+ the two into one size-budgeted response was the exact design mistake
906
+ this split undoes.
907
+
908
+ KNOWN GAP: does NOT accept `library_tag_ids` — Knowledge Flow's label
909
+ resolution (`MetadataService.get_document_uids_with_any_label`)
910
+ narrows only by the caller's document-level READ permission, never by
911
+ folder/library scope. Flagged, not silently implemented as a no-op
912
+ parameter; narrowing this is follow-up work, not yet built. Raises
913
+ `DocumentPortCallError` on transport failure.
845
914
  """
846
915
 
847
916
 
@@ -872,6 +941,102 @@ class DocumentSummarizePort(ABC):
872
941
  """
873
942
 
874
943
 
944
+ class DocumentMarkdownResult(FrozenModel):
945
+ """
946
+ Typed result of one paginated read of a document's FULL parsed markdown
947
+ (DOCREAD-01).
948
+
949
+ Distinct from the two neighbouring shapes:
950
+ - `DocumentSummaryResult` is a lossy overview — this is the document
951
+ verbatim, nothing dropped;
952
+ - `DocumentRawContent` is the original uploaded bytes (a PDF/DOCX blob the
953
+ model cannot read as text) — this is the markdown Knowledge Flow parsed at
954
+ ingestion (`output.md`), directly usable by the model.
955
+
956
+ Pagination is the whole point (it is why `document_summarize`'s "half
957
+ answer" failure mode cannot recur here): `text` is one bounded window
958
+ `[offset : offset+max_chars]`, and `next_offset` is the offset to pass for
959
+ the next page or `None` once the end of the document has been reached — so
960
+ an exhaustive read always knows whether more text remains.
961
+ """
962
+
963
+ document_uid: str
964
+ text: str = ""
965
+ offset: int = 0
966
+ next_offset: int | None = None
967
+ total_chars: int = 0
968
+
969
+
970
+ class DocumentMarkdownPort(ABC):
971
+ """
972
+ Capability-safe paginated read of a corpus document's FULL parsed markdown
973
+ by uid (DOCREAD-01). Same doctrine as `DocumentSummarizePort`: takes SCOPE
974
+ PARAMETERS only (the uid plus the page window) and NEVER a caller-supplied
975
+ context, identity, or access token — auth/identity come solely from the
976
+ adapter's privately-captured per-turn binding, and Knowledge Flow's own
977
+ per-document ReBAC is the real authorization gate. Document uids are
978
+ internal working identifiers: tools use them freely but never surface them
979
+ to the end user.
980
+ """
981
+
982
+ @abstractmethod
983
+ async def fetch_markdown(
984
+ self,
985
+ document_uid: str,
986
+ *,
987
+ offset: int = 0,
988
+ max_chars: int = 8000,
989
+ ) -> DocumentMarkdownResult:
990
+ """
991
+ Return one bounded page `[offset : offset+max_chars]` of the document's
992
+ full markdown. The result's `next_offset` is the offset to pass to read
993
+ the following page, or `None` when the end has been reached. Raises
994
+ `DocumentPortCallError` on transport failure.
995
+ """
996
+
997
+
998
+ class DocumentExtractionResult(FrozenModel):
999
+ """Typed result of one server-side exhaustive extraction (DOCREAD-01 Phase 2).
1000
+
1001
+ `extraction` is the consolidated, de-duplicated list of every matching item
1002
+ (markdown bullets, in document order) — produced by a map over EVERY chunk of
1003
+ the document with a concatenate/de-dupe reduce, never a lossy summary.
1004
+ """
1005
+
1006
+ document_uid: str
1007
+ extraction: str = ""
1008
+ item_count: int = 0
1009
+ chunks_processed: int = 0
1010
+ truncated: bool = False
1011
+
1012
+
1013
+ class DocumentExtractionPort(ABC):
1014
+ """
1015
+ Capability-safe server-side exhaustive extraction (DOCREAD-01 Phase 2). Same
1016
+ doctrine as `DocumentSummarizePort`: the caller passes only the document uid
1017
+ and the extraction instruction; identity stays private to the adapter and
1018
+ Knowledge Flow's per-document ReBAC is the gate.
1019
+
1020
+ Unlike `DocumentMarkdownPort` (which streams pages for the AGENT to process,
1021
+ N model calls), this runs the whole map-reduce on the Knowledge Flow side in
1022
+ ONE call — so the agent never bursts many model calls of its own, and the
1023
+ provider throttling is absorbed server-side (bounded concurrency + 429
1024
+ backoff). Long-running; adapters use an extended read timeout.
1025
+ """
1026
+
1027
+ @abstractmethod
1028
+ async def extract(
1029
+ self,
1030
+ document_uid: str,
1031
+ *,
1032
+ instruction: str,
1033
+ ) -> DocumentExtractionResult:
1034
+ """
1035
+ Exhaustively extract every item matching `instruction` from the document.
1036
+ Raises `DocumentPortCallError` on transport failure.
1037
+ """
1038
+
1039
+
875
1040
  @dataclass(frozen=True, slots=True)
876
1041
  class RuntimeServices:
877
1042
  """
@@ -927,6 +1092,16 @@ class RuntimeServices:
927
1092
  # the positional meaning of any field an external positional-argument
928
1093
  # caller might already rely on.
929
1094
  kpi_writer: BaseKPIWriter | None = None
1095
+ # Paginated full-markdown read of a corpus document by uid (DOCREAD-01):
1096
+ # powers the `document_verbatim` / `document_extract` capabilities. Same
1097
+ # doctrine/optionality as the other document ports — the per-turn binding
1098
+ # and raw access token stay private to the adapter. Appended after
1099
+ # `kpi_writer` for the same positional-safety reason noted above.
1100
+ document_markdown: DocumentMarkdownPort | None = None
1101
+ # Server-side exhaustive extraction (DOCREAD-01 Phase 2): powers the
1102
+ # `document_extract` capability's single-call path. Same doctrine/optionality
1103
+ # as the other document ports.
1104
+ document_extraction: DocumentExtractionPort | None = None
930
1105
 
931
1106
 
932
1107
  InputModelT = TypeVar("InputModelT", bound=BaseModel)
@@ -97,6 +97,7 @@ def rebuild_ui_part_union(extra_parts: Sequence[type[BaseModel]] = ()) -> None:
97
97
  members.append(part)
98
98
  if tuple(members) == _current_members:
99
99
  return # already the registered union — keep validators/caches warm
100
+ # codeql[py/unused-global-variable]
100
101
  _current_members = tuple(members)
101
102
  union = members[0] if len(members) == 1 else Union[tuple(members)]
102
103
  new_alias = Annotated[union, Field(discriminator="type")]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fred-sdk
3
- Version: 3.4.2
3
+ Version: 3.4.3
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
@@ -12,7 +12,7 @@ Classifier: Programming Language :: Python :: 3 :: Only
12
12
  Classifier: Operating System :: OS Independent
13
13
  Requires-Python: <3.13,>=3.12
14
14
  Description-Content-Type: text/markdown
15
- Requires-Dist: fred-core>=3.4.1
15
+ Requires-Dist: fred-core>=3.8.2
16
16
  Requires-Dist: pydantic<3.0.0,>=2.7.0
17
17
  Requires-Dist: langchain-core>=0.3.0
18
18
  Requires-Dist: langchain>=1.3.14
@@ -53,6 +53,7 @@ tests/test_capability_contracts.py
53
53
  tests/test_conversational_memory.py
54
54
  tests/test_document_search_port_1906.py
55
55
  tests/test_execution_contracts.py
56
+ tests/test_final_runtime_event_finish_reason.py
56
57
  tests/test_graph_choice_step.py
57
58
  tests/test_graph_models.py
58
59
  tests/test_hello.py
@@ -1,4 +1,4 @@
1
- fred-core>=3.4.1
1
+ fred-core>=3.8.2
2
2
  pydantic<3.0.0,>=2.7.0
3
3
  langchain-core>=0.3.0
4
4
  langchain>=1.3.14
@@ -1,13 +1,13 @@
1
1
  [project]
2
2
  name = "fred-sdk"
3
- version = "3.4.2"
3
+ version = "3.4.3"
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"
7
7
  license = { text = "Apache-2.0" }
8
8
  authors = [{ name = "Thales", email = "noreply@thalesgroup.com" }]
9
9
  dependencies = [
10
- "fred-core>=3.4.1",
10
+ "fred-core>=3.8.2",
11
11
  "pydantic>=2.7.0,<3.0.0",
12
12
  "langchain-core>=0.3.0",
13
13
  "langchain>=1.3.14",
@@ -0,0 +1,43 @@
1
+ # Copyright Thales 2026
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """
16
+ Offline unit test: `FinalRuntimeEvent.finish_reason` — the live SSE contract
17
+ must normalize a raw provider finish-reason value the same way `ChatMetadata`
18
+ (fred-core, persisted history) does, and — being a frozen, `extra="forbid"`
19
+ model — must never fail construction over a value this build doesn't
20
+ recognize.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ from fred_core.history.history_schema import FinishReason
26
+ from fred_sdk.contracts.runtime import FinalRuntimeEvent
27
+
28
+
29
+ def test_final_runtime_event_normalizes_a_raw_provider_value() -> None:
30
+ event = FinalRuntimeEvent.model_validate({"kind": "final", "finish_reason": "STOP"})
31
+ assert event.finish_reason is FinishReason.stop
32
+
33
+
34
+ def test_final_runtime_event_coerces_an_unrecognized_value_to_other() -> None:
35
+ event = FinalRuntimeEvent.model_validate(
36
+ {"kind": "final", "finish_reason": "UNKNOWN_7"}
37
+ )
38
+ assert event.finish_reason is FinishReason.other
39
+
40
+
41
+ def test_final_runtime_event_finish_reason_defaults_to_none() -> None:
42
+ event = FinalRuntimeEvent.model_validate({"kind": "final"})
43
+ assert event.finish_reason is None
@@ -0,0 +1,80 @@
1
+ # Copyright Thales 2026
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """
16
+ Tests for fred_sdk.contracts.prompt_utils.
17
+
18
+ The module is now a pure registry: save-time validation of unknown {tokens}
19
+ was removed in #2277 because the fred-runtime renderer already preserves any
20
+ token outside PROMPT_SAFE_TOKENS verbatim, making rejection a false positive.
21
+
22
+ These tests pin the registry itself — its exact key set is a contract shared
23
+ with `safe_prompt_token_map` in fred-runtime, and adding a key here without
24
+ adding the matching runtime value would render that token as empty text.
25
+ """
26
+
27
+ import fred_sdk.contracts.prompt_utils as prompt_utils
28
+ from fred_sdk.contracts import PROMPT_SAFE_TOKENS as PROMPT_SAFE_TOKENS_REEXPORT
29
+
30
+ PROMPT_SAFE_TOKENS = prompt_utils.PROMPT_SAFE_TOKENS
31
+
32
+ # ---------------------------------------------------------------------------
33
+ # PROMPT_SAFE_TOKENS registry
34
+ # ---------------------------------------------------------------------------
35
+
36
+
37
+ def test_safe_tokens_contains_expected_keys() -> None:
38
+ assert set(PROMPT_SAFE_TOKENS.keys()) == {
39
+ "today",
40
+ "response_language",
41
+ "session_id",
42
+ "user_id",
43
+ "agent_id",
44
+ }
45
+
46
+
47
+ def test_safe_tokens_all_have_non_empty_descriptions() -> None:
48
+ for key, desc in PROMPT_SAFE_TOKENS.items():
49
+ assert desc, f"Token '{key}' has an empty description"
50
+
51
+
52
+ def test_safe_tokens_are_simple_identifiers() -> None:
53
+ """
54
+ Every registry key must be a bare identifier.
55
+
56
+ The renderer matches `\\{([a-zA-Z_][a-zA-Z0-9_]*)\\}`; a key that does not
57
+ fit that shape could never be substituted and would silently do nothing.
58
+ """
59
+ for key in PROMPT_SAFE_TOKENS:
60
+ assert key.isidentifier(), f"Token '{key}' is not a simple identifier"
61
+
62
+
63
+ def test_registry_is_re_exported_from_contracts_package() -> None:
64
+ assert PROMPT_SAFE_TOKENS_REEXPORT is PROMPT_SAFE_TOKENS
65
+
66
+
67
+ # ---------------------------------------------------------------------------
68
+ # Removed surface (#2277)
69
+ # ---------------------------------------------------------------------------
70
+
71
+
72
+ def test_validator_surface_is_gone() -> None:
73
+ """
74
+ The validator must not come back without a deliberate decision.
75
+
76
+ Re-adding save-time rejection would once again block prompts such as
77
+ `Hello {name}` that the renderer handles correctly.
78
+ """
79
+ assert not hasattr(prompt_utils, "validate_prompt_template")
80
+ assert not hasattr(prompt_utils, "PromptTemplateError")
@@ -1,112 +0,0 @@
1
- # Copyright Thales 2026
2
- #
3
- # Licensed under the Apache License, Version 2.0 (the "License");
4
- # you may not use this file except in compliance with the License.
5
- # You may obtain a copy of the License at
6
- #
7
- # http://www.apache.org/licenses/LICENSE-2.0
8
- #
9
- # Unless required by applicable law or agreed to in writing, software
10
- # distributed under the License is distributed on an "AS IS" BASIS,
11
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
- # See the License for the specific language governing permissions and
13
- # limitations under the License.
14
-
15
- """
16
- Canonical prompt-template token registry and validation utilities.
17
-
18
- Why this module exists:
19
- - define the single source of truth for which {tokens} are supported in
20
- user-authored system prompts
21
- - provide a validator that control-plane can call before persisting any
22
- prompt field value, so broken templates are rejected at save time rather
23
- than crashing the agent at execution time
24
- - let the renderer in fred-runtime import the same token set so the two
25
- surfaces stay in sync automatically
26
-
27
- How to use:
28
- - import PROMPT_SAFE_TOKENS to get the canonical {token} → description map
29
- - call validate_prompt_template(text) before persisting a prompt field;
30
- an empty return list means the template is safe to store
31
- """
32
-
33
- from __future__ import annotations
34
-
35
- import re
36
- from typing import Final
37
-
38
- from pydantic import BaseModel
39
-
40
- # Matches only {simple_identifier} — no dots, no digits, no spaces, not empty.
41
- # This is the only pattern that can ever be a valid template token.
42
- _SIMPLE_TOKEN_RE: Final = re.compile(r"\{([a-zA-Z_][a-zA-Z0-9_]*)\}")
43
-
44
- # Canonical set of runtime tokens available in user-authored system prompts.
45
- # Key = token name (used inside {…} in the prompt text).
46
- # Value = human-readable description forwarded to the frontend in error messages.
47
- #
48
- # To add a new supported token: add one entry here. The renderer and validator
49
- # pick it up automatically — no other site needs to change.
50
- PROMPT_SAFE_TOKENS: Final[dict[str, str]] = {
51
- "today": "ISO-8601 date at execution time (e.g. 2026-05-07)",
52
- "response_language": "Human-readable response language (e.g. English, français)",
53
- "session_id": "Active session identifier",
54
- "user_id": "Authenticated user identifier",
55
- "agent_id": "Agent definition identifier",
56
- }
57
-
58
- # Pre-built hint string included in error messages so users see the full list.
59
- _SUPPORTED_HINT: Final[str] = ", ".join(f"{{{k}}}" for k in PROMPT_SAFE_TOKENS)
60
-
61
-
62
- class PromptTemplateError(BaseModel):
63
- """One validation error found in a prompt template."""
64
-
65
- pattern: str
66
- reason: str
67
-
68
-
69
- def validate_prompt_template(text: str) -> list[PromptTemplateError]:
70
- """
71
- Validate a user-authored prompt template text against PROMPT_SAFE_TOKENS.
72
-
73
- Why this exists:
74
- - prompt fields submitted via the control-plane API are stored before the
75
- agent ever executes; without up-front validation a bad template silently
76
- breaks the agent on the first chat message
77
-
78
- How to use:
79
- - call before persisting any field whose FieldSpec.type == "prompt"
80
- - an empty return list means the template is safe to store
81
- - a non-empty list means the create/update should be rejected with 422
82
-
83
- Validation rule:
84
- - any {simple_identifier} not in PROMPT_SAFE_TOKENS is an error
85
- - non-simple brace patterns ({}, {0}, {toto.toto}, { x }) are not matched
86
- and are silently preserved as literals by the renderer — no error raised,
87
- because they cannot be confused with template tokens (they will never be
88
- substituted and they no longer crash the renderer)
89
-
90
- Example:
91
- - validate_prompt_template("Today is {today}.") → []
92
- - validate_prompt_template("Hello {name}!") → [PromptTemplateError(...)]
93
- """
94
- errors: list[PromptTemplateError] = []
95
- seen: set[str] = set()
96
-
97
- for match in _SIMPLE_TOKEN_RE.finditer(text):
98
- token = match.group(1)
99
- raw = match.group(0)
100
- if token not in PROMPT_SAFE_TOKENS and raw not in seen:
101
- seen.add(raw)
102
- errors.append(
103
- PromptTemplateError(
104
- pattern=raw,
105
- reason=(
106
- f"Unknown template token. "
107
- f"Supported tokens are: {_SUPPORTED_HINT}."
108
- ),
109
- )
110
- )
111
-
112
- return errors
@@ -1,167 +0,0 @@
1
- # Copyright Thales 2026
2
- #
3
- # Licensed under the Apache License, Version 2.0 (the "License");
4
- # you may not use this file except in compliance with the License.
5
- # You may obtain a copy of the License at
6
- #
7
- # http://www.apache.org/licenses/LICENSE-2.0
8
- #
9
- # Unless required by applicable law or agreed to in writing, software
10
- # distributed under the License is distributed on an "AS IS" BASIS,
11
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
- # See the License for the specific language governing permissions and
13
- # limitations under the License.
14
-
15
- """
16
- Tests for fred_sdk.contracts.prompt_utils.
17
-
18
- Ref: docs/backlog/BACKLOG.md §3d.9 (P1) — PROMPT_SAFE_TOKENS registry,
19
- validate_prompt_template (unknown tokens → error, non-simple braces preserved),
20
- crash-proof rendering for code braces and dotted-notation patterns.
21
- """
22
-
23
- import pytest
24
- from fred_sdk.contracts.prompt_utils import (
25
- PROMPT_SAFE_TOKENS,
26
- validate_prompt_template,
27
- )
28
-
29
- # ---------------------------------------------------------------------------
30
- # PROMPT_SAFE_TOKENS registry
31
- # ---------------------------------------------------------------------------
32
-
33
-
34
- def test_safe_tokens_contains_expected_keys() -> None:
35
- assert set(PROMPT_SAFE_TOKENS.keys()) == {
36
- "today",
37
- "response_language",
38
- "session_id",
39
- "user_id",
40
- "agent_id",
41
- }
42
-
43
-
44
- def test_safe_tokens_all_have_non_empty_descriptions() -> None:
45
- for key, desc in PROMPT_SAFE_TOKENS.items():
46
- assert desc, f"Token '{key}' has an empty description"
47
-
48
-
49
- # ---------------------------------------------------------------------------
50
- # Clean templates — no errors expected
51
- # ---------------------------------------------------------------------------
52
-
53
-
54
- @pytest.mark.parametrize(
55
- "text",
56
- [
57
- "You are a helpful assistant.",
58
- "Today is {today}.",
59
- "Respond in {response_language}.",
60
- "Session: {session_id}, User: {user_id}, Agent: {agent_id}.",
61
- "All tokens: {today} {response_language} {session_id} {user_id} {agent_id}.",
62
- # Code braces are not simple identifiers — preserved as literals, no error
63
- "function main(workbook: ExcelScript.Workbook) { doSomething(); }",
64
- # Dotted notation is not a simple identifier — left as literal, no error
65
- "{toto.toto} should not be flagged",
66
- # Empty brace not a simple identifier
67
- "Use {} carefully",
68
- # Positional placeholder not a simple identifier
69
- "Value: {0}",
70
- # Whitespace inside braces not a simple identifier
71
- "{ not a token }",
72
- ],
73
- )
74
- def test_clean_templates(text: str) -> None:
75
- assert validate_prompt_template(text) == []
76
-
77
-
78
- # ---------------------------------------------------------------------------
79
- # Invalid templates — errors expected
80
- # ---------------------------------------------------------------------------
81
-
82
-
83
- def test_unknown_simple_token_single() -> None:
84
- errors = validate_prompt_template("Hello {name}!")
85
- assert len(errors) == 1
86
- assert errors[0].pattern == "{name}"
87
- assert "Unknown template token" in errors[0].reason
88
- # Error message must list the supported tokens
89
- for token in PROMPT_SAFE_TOKENS:
90
- assert token in errors[0].reason
91
-
92
-
93
- def test_unknown_simple_token_multiple_distinct() -> None:
94
- errors = validate_prompt_template("{foo} and {bar} are unknown")
95
- patterns = {e.pattern for e in errors}
96
- assert patterns == {"{foo}", "{bar}"}
97
-
98
-
99
- def test_unknown_simple_token_repeated_deduplicated() -> None:
100
- errors = validate_prompt_template("{foo} again {foo}")
101
- assert len(errors) == 1
102
- assert errors[0].pattern == "{foo}"
103
-
104
-
105
- def test_mix_valid_and_invalid_tokens() -> None:
106
- errors = validate_prompt_template("Today is {today} and {bad_token} here.")
107
- assert len(errors) == 1
108
- assert errors[0].pattern == "{bad_token}"
109
-
110
-
111
- def test_all_valid_tokens_produce_no_errors() -> None:
112
- text = " ".join(f"{{{k}}}" for k in PROMPT_SAFE_TOKENS)
113
- assert validate_prompt_template(text) == []
114
-
115
-
116
- # ---------------------------------------------------------------------------
117
- # Non-simple brace patterns are NOT flagged (renderer preserves them safely)
118
- # ---------------------------------------------------------------------------
119
-
120
-
121
- def test_dotted_notation_not_flagged() -> None:
122
- assert validate_prompt_template("{object.attr}") == []
123
-
124
-
125
- def test_empty_braces_not_flagged() -> None:
126
- assert validate_prompt_template("{}") == []
127
-
128
-
129
- def test_positional_placeholder_not_flagged() -> None:
130
- assert validate_prompt_template("{0}") == []
131
-
132
-
133
- def test_whitespace_brace_not_flagged() -> None:
134
- assert validate_prompt_template("{ spaces }") == []
135
-
136
-
137
- def test_code_block_not_flagged() -> None:
138
- prompt = (
139
- "You help users write Excel scripts. Example:\n"
140
- "function main(workbook: ExcelScript.Workbook) {\n"
141
- " workbook.getActiveWorksheet().getCell(0, 0).setValue('Hello');\n"
142
- "}"
143
- )
144
- assert validate_prompt_template(prompt) == []
145
-
146
-
147
- # ---------------------------------------------------------------------------
148
- # Edge cases
149
- # ---------------------------------------------------------------------------
150
-
151
-
152
- def test_empty_string_is_valid() -> None:
153
- assert validate_prompt_template("") == []
154
-
155
-
156
- def test_no_braces_is_valid() -> None:
157
- assert validate_prompt_template("Plain text without any braces.") == []
158
-
159
-
160
- def test_token_adjacent_to_punctuation() -> None:
161
- errors = validate_prompt_template("Hi, {unknown}!")
162
- assert len(errors) == 1
163
- assert errors[0].pattern == "{unknown}"
164
-
165
-
166
- def test_known_token_at_start_and_end() -> None:
167
- assert validate_prompt_template("{today} is a great day, {user_id}.") == []
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes