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.
- {fred_sdk-3.4.2 → fred_sdk-3.4.3}/PKG-INFO +2 -2
- {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/authoring/authored_tool_runtime.py +1 -1
- {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/contracts/__init__.py +2 -8
- {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/contracts/capability/manifest.py +12 -0
- {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/contracts/context.py +23 -3
- fred_sdk-3.4.3/fred_sdk/contracts/prompt_utils.py +55 -0
- {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/contracts/runtime.py +177 -2
- {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/contracts/ui_part_union.py +1 -0
- {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk.egg-info/PKG-INFO +2 -2
- {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk.egg-info/SOURCES.txt +1 -0
- {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk.egg-info/requires.txt +1 -1
- {fred_sdk-3.4.2 → fred_sdk-3.4.3}/pyproject.toml +2 -2
- fred_sdk-3.4.3/tests/test_final_runtime_event_finish_reason.py +43 -0
- fred_sdk-3.4.3/tests/test_prompt_utils.py +80 -0
- fred_sdk-3.4.2/fred_sdk/contracts/prompt_utils.py +0 -112
- fred_sdk-3.4.2/tests/test_prompt_utils.py +0 -167
- {fred_sdk-3.4.2 → fred_sdk-3.4.3}/README.md +0 -0
- {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/__init__.py +0 -0
- {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/authoring/__init__.py +0 -0
- {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/authoring/api.py +0 -0
- {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/authoring/inspection.py +0 -0
- {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/authoring/knowledge_flow_mcp.py +0 -0
- {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/authoring/tool_args_schema.py +0 -0
- {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/authoring/tool_context_helpers.py +0 -0
- {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/contracts/capability/__init__.py +0 -0
- {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/contracts/capability/base.py +0 -0
- {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/contracts/capability/chat_controls.py +0 -0
- {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/contracts/capability/context.py +0 -0
- {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/contracts/capability/hitl.py +0 -0
- {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/contracts/eval.py +0 -0
- {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/contracts/execution.py +0 -0
- {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/contracts/models.py +0 -0
- {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/contracts/openai_compat.py +0 -0
- {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/contracts/react_contract.py +0 -0
- {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/deep/__init__.py +0 -0
- {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/graph/__init__.py +0 -0
- {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/graph/authoring/__init__.py +0 -0
- {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/graph/authoring/api.py +0 -0
- {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/graph/authoring/team_api.py +0 -0
- {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/graph/runtime.py +0 -0
- {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/hello.py +0 -0
- {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/react/__init__.py +0 -0
- {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/resources/__init__.py +0 -0
- {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/resources/packaged.py +0 -0
- {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/resources/prompts/mermaid_output_contract.md +0 -0
- {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/resources/prompts.py +0 -0
- {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/runtime_support/__init__.py +0 -0
- {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/runtime_support/remote_agent_invoker.py +0 -0
- {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/support/__init__.py +0 -0
- {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/support/authored_toolsets.py +0 -0
- {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/support/builtins/__init__.py +0 -0
- {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/support/builtins/catalog.py +0 -0
- {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/support/mcp_utils.py +0 -0
- {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk/testing.py +0 -0
- {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk.egg-info/dependency_links.txt +0 -0
- {fred_sdk-3.4.2 → fred_sdk-3.4.3}/fred_sdk.egg-info/top_level.txt +0 -0
- {fred_sdk-3.4.2 → fred_sdk-3.4.3}/setup.cfg +0 -0
- {fred_sdk-3.4.2 → fred_sdk-3.4.3}/tests/test_capability_contracts.py +0 -0
- {fred_sdk-3.4.2 → fred_sdk-3.4.3}/tests/test_conversational_memory.py +0 -0
- {fred_sdk-3.4.2 → fred_sdk-3.4.3}/tests/test_document_search_port_1906.py +0 -0
- {fred_sdk-3.4.2 → fred_sdk-3.4.3}/tests/test_execution_contracts.py +0 -0
- {fred_sdk-3.4.2 → fred_sdk-3.4.3}/tests/test_graph_choice_step.py +0 -0
- {fred_sdk-3.4.2 → fred_sdk-3.4.3}/tests/test_graph_models.py +0 -0
- {fred_sdk-3.4.2 → fred_sdk-3.4.3}/tests/test_hello.py +0 -0
- {fred_sdk-3.4.2 → fred_sdk-3.4.3}/tests/test_hitl_resume_identity_contracts.py +0 -0
- {fred_sdk-3.4.2 → fred_sdk-3.4.3}/tests/test_mcp_utils.py +0 -0
- {fred_sdk-3.4.2 → fred_sdk-3.4.3}/tests/test_openai_compat.py +0 -0
- {fred_sdk-3.4.2 → fred_sdk-3.4.3}/tests/test_prompt_bundles.py +0 -0
- {fred_sdk-3.4.2 → fred_sdk-3.4.3}/tests/test_react_contract.py +0 -0
- {fred_sdk-3.4.2 → fred_sdk-3.4.3}/tests/test_sdk_purity.py +0 -0
- {fred_sdk-3.4.2 → fred_sdk-3.4.3}/tests/test_testing_fakes.py +0 -0
- {fred_sdk-3.4.2 → fred_sdk-3.4.3}/tests/test_tool_args_schema.py +0 -0
- {fred_sdk-3.4.2 → fred_sdk-3.4.3}/tests/test_tool_context_workspace_fs.py +0 -0
- {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.
|
|
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.
|
|
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
|
|
@@ -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
|
|
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:
|
|
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:
|
|
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.
|
|
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.
|
|
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,13 +1,13 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "fred-sdk"
|
|
3
|
-
version = "3.4.
|
|
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.
|
|
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
|
|
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
|