fred-capability-documents 4.4.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fred_capability_documents/__init__.py +21 -0
- fred_capability_documents/document_extract/__init__.py +28 -0
- fred_capability_documents/document_extract/capability.py +208 -0
- fred_capability_documents/document_label_search/__init__.py +30 -0
- fred_capability_documents/document_label_search/capability.py +249 -0
- fred_capability_documents/document_read_common.py +263 -0
- fred_capability_documents/document_similarity/__init__.py +27 -0
- fred_capability_documents/document_similarity/capability.py +280 -0
- fred_capability_documents/document_summarize/__init__.py +38 -0
- fred_capability_documents/document_summarize/capability.py +382 -0
- fred_capability_documents/document_verbatim/__init__.py +27 -0
- fred_capability_documents/document_verbatim/capability.py +128 -0
- fred_capability_documents/py.typed +0 -0
- fred_capability_documents-4.4.3.dist-info/METADATA +58 -0
- fred_capability_documents-4.4.3.dist-info/RECORD +18 -0
- fred_capability_documents-4.4.3.dist-info/WHEEL +5 -0
- fred_capability_documents-4.4.3.dist-info/entry_points.txt +6 -0
- fred_capability_documents-4.4.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,382 @@
|
|
|
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
|
+
`DocumentSummarizeCapability` (RFC §3, §10) — on-demand document summarization.
|
|
17
|
+
|
|
18
|
+
Why this module exists:
|
|
19
|
+
- `summarize_document` used to be bundled inside `document_access`, but its
|
|
20
|
+
invocation is decided purely by the LLM from the tool docstring — there is
|
|
21
|
+
no query-shaped config condition (unlike `list_document_tree`'s
|
|
22
|
+
`team_documents`) that tells the runtime when the tool should even exist
|
|
23
|
+
for an agent. This capability is its own admin-gated, per-agent opt-in (an
|
|
24
|
+
agent selecting `document_access` does NOT get this tool for free) — the
|
|
25
|
+
admission control that governs it today.
|
|
26
|
+
- commonly paired with `document_access` (its `list_document_tree` /
|
|
27
|
+
`search_documents_using_vectorization` hits are the usual source of the
|
|
28
|
+
`document_uid` this tool's caller needs), but there is no runtime coupling:
|
|
29
|
+
the uid can equally come from the conversation's attached-files list.
|
|
30
|
+
|
|
31
|
+
`hitl_specs()` (RFC §5.4) gates `summarize_document` behind human approval by
|
|
32
|
+
default — defense in depth alongside admission control (`ADMIN_GATED` below):
|
|
33
|
+
admission decides whether an agent can select this tool at all, HITL decides
|
|
34
|
+
whether each individual call still needs a human's proceed/cancel. The gate
|
|
35
|
+
is configurable per agent instance via `require_confirmation` (default
|
|
36
|
+
`True`), not hardcoded — an admin who has already reviewed and trusts this
|
|
37
|
+
agent's usage can turn it off. (#2179, ReAct V2 HITL resume dead-ending in a
|
|
38
|
+
409, blocked this until it was fixed — see `RUNTIME-EXECUTION-CONTRACT.md`.)
|
|
39
|
+
|
|
40
|
+
Doctrine (RFC §3.5, §3.8, §10) — same as `document_access`:
|
|
41
|
+
- the capability reaches the platform ONLY through the typed
|
|
42
|
+
`RuntimeServices.document_summarize` port; the per-turn binding and the raw
|
|
43
|
+
access token NEVER enter `CapabilityContext`
|
|
44
|
+
- the tool signature exposes ONLY LLM arguments; scope and identity reach the
|
|
45
|
+
tool through the middleware closure, never the tool schema
|
|
46
|
+
|
|
47
|
+
Identifier hygiene (hard rule, shared with `document_access`): the
|
|
48
|
+
`document_uid` argument is an internal working identifier for the agent's own
|
|
49
|
+
tool calls. The docstring instructs the model to NEVER repeat it to the end
|
|
50
|
+
user — answers refer to documents by display name only.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
from __future__ import annotations
|
|
54
|
+
|
|
55
|
+
import time
|
|
56
|
+
from collections.abc import Sequence
|
|
57
|
+
|
|
58
|
+
from fred_sdk.contracts.capability import (
|
|
59
|
+
AgentCapability,
|
|
60
|
+
CapabilityContext,
|
|
61
|
+
CapabilityManifest,
|
|
62
|
+
EmptyModel,
|
|
63
|
+
HitlGateRequest,
|
|
64
|
+
HitlSpec,
|
|
65
|
+
)
|
|
66
|
+
from fred_sdk.contracts.context import (
|
|
67
|
+
ToolContentBlock,
|
|
68
|
+
ToolContentKind,
|
|
69
|
+
ToolInvocationResult,
|
|
70
|
+
)
|
|
71
|
+
from fred_sdk.contracts.models import FieldSpec, UIHints
|
|
72
|
+
from fred_sdk.contracts.runtime import (
|
|
73
|
+
DocumentScopeRefusedError,
|
|
74
|
+
DocumentSummaryResult,
|
|
75
|
+
unwrap_run_stop_error,
|
|
76
|
+
)
|
|
77
|
+
from langchain_core.tools import BaseTool, tool
|
|
78
|
+
from pydantic import BaseModel, Field
|
|
79
|
+
|
|
80
|
+
from fred_capability_documents.document_read_common import document_scope_refusal
|
|
81
|
+
|
|
82
|
+
_KF_SERVICE = "Knowledge Flow"
|
|
83
|
+
|
|
84
|
+
# Built-in default summary length when neither the caller (the LLM) nor the
|
|
85
|
+
# capability config specifies one.
|
|
86
|
+
DEFAULT_SUMMARIZE_MAX_CHARS = 5000
|
|
87
|
+
# Wire bounds of the Knowledge Flow endpoint's `max_chars` validation — clamp
|
|
88
|
+
# client-side so an out-of-range LLM value degrades gracefully instead of 422ing.
|
|
89
|
+
_SUMMARIZE_MAX_CHARS_BOUNDS = (200, 20_000)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _clamp(value: int, bounds: tuple[int, int]) -> int:
|
|
93
|
+
low, high = bounds
|
|
94
|
+
return max(low, min(value, high))
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def resolve_summarize_max_chars(cap: int | None, requested: int | None) -> int:
|
|
98
|
+
"""
|
|
99
|
+
Resolve the effective summary length from the configured cap and the
|
|
100
|
+
caller's request.
|
|
101
|
+
|
|
102
|
+
The per-agent cap (`summarize_max_chars` config) is both the default (when
|
|
103
|
+
the caller asks for nothing) and a hard upper bound on whatever the caller
|
|
104
|
+
requests; without one, the built-in default applies and the caller's
|
|
105
|
+
request is honored verbatim (within wire bounds).
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
default = cap if cap is not None else DEFAULT_SUMMARIZE_MAX_CHARS
|
|
109
|
+
effective = requested if requested is not None else default
|
|
110
|
+
if cap is not None:
|
|
111
|
+
effective = min(effective, cap)
|
|
112
|
+
return _clamp(effective, _SUMMARIZE_MAX_CHARS_BOUNDS)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _document_tool_failure(
|
|
116
|
+
*,
|
|
117
|
+
tool_ref: str,
|
|
118
|
+
action: str,
|
|
119
|
+
exc: Exception,
|
|
120
|
+
elapsed_s: float,
|
|
121
|
+
document_uid: str | None = None,
|
|
122
|
+
) -> tuple[str, ToolInvocationResult]:
|
|
123
|
+
"""Turn any document tool-call failure into a non-empty, actionable error
|
|
124
|
+
message plus an ``is_error=True`` artifact.
|
|
125
|
+
|
|
126
|
+
The v2 ReAct runtime surfaces ``ToolInvocationResult.is_error`` directly to
|
|
127
|
+
the user (and suppresses LLM hallucination), so a failing tool MUST return
|
|
128
|
+
such a result instead of raising — a raised exception is re-raised by the
|
|
129
|
+
default ``ToolNode`` handler, which leaves the tool call pending in the
|
|
130
|
+
trace and yields an empty error detail to the UI.
|
|
131
|
+
|
|
132
|
+
Transport detail (timeout, HTTP status) arrives via the SDK-typed
|
|
133
|
+
`DocumentPortCallError` attributes the adapters stamp — this module never
|
|
134
|
+
imports the adapter's HTTP stack.
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
err_type = type(exc).__name__
|
|
138
|
+
raw = str(exc).strip()
|
|
139
|
+
timed_out = bool(getattr(exc, "timed_out", False))
|
|
140
|
+
status_code = getattr(exc, "status_code", None)
|
|
141
|
+
|
|
142
|
+
if timed_out:
|
|
143
|
+
cause = f"the {_KF_SERVICE} service timed out after {elapsed_s:.0f}s"
|
|
144
|
+
elif status_code is not None:
|
|
145
|
+
cause = f"the {_KF_SERVICE} service returned HTTP {status_code}"
|
|
146
|
+
else:
|
|
147
|
+
cause = f"the {_KF_SERVICE} service call failed after {elapsed_s:.0f}s"
|
|
148
|
+
|
|
149
|
+
target = f" (document_uid={document_uid})" if document_uid else ""
|
|
150
|
+
detail = f": {raw}" if raw else ""
|
|
151
|
+
message = f"Could not {action}{target}: {cause} [{err_type}{detail}]."
|
|
152
|
+
return message, ToolInvocationResult(
|
|
153
|
+
tool_ref=tool_ref,
|
|
154
|
+
is_error=True,
|
|
155
|
+
blocks=(ToolContentBlock(kind=ToolContentKind.TEXT, text=message),),
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class DocumentSummarizeConfig(BaseModel):
|
|
160
|
+
"""
|
|
161
|
+
Agent-creation / stored config of the document-summarize capability
|
|
162
|
+
(RFC §3.2, #2177).
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
# Per-agent default AND hard cap for summarize_document's summary length
|
|
166
|
+
# (chars). None = built-in default, caller's request honored verbatim.
|
|
167
|
+
summarize_max_chars: int | None = Field(default=None, ge=200, le=20_000)
|
|
168
|
+
# Whether summarize_document pauses for human proceed/cancel before each
|
|
169
|
+
# call (RFC §5.4). Defaults to on — an admin who trusts this agent's
|
|
170
|
+
# usage can turn it off per instance; see `hitl_specs()`.
|
|
171
|
+
require_confirmation: bool = True
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class DocumentSummarizeCapability(
|
|
175
|
+
AgentCapability[DocumentSummarizeConfig, DocumentSummarizeConfig, EmptyModel]
|
|
176
|
+
):
|
|
177
|
+
"""
|
|
178
|
+
On-demand document summarization, wired through the `document_summarize`
|
|
179
|
+
port. Single tool, no chat controls, no turn options — see the module
|
|
180
|
+
docstring for the admission/execution split with `document_access`.
|
|
181
|
+
"""
|
|
182
|
+
|
|
183
|
+
manifest = CapabilityManifest(
|
|
184
|
+
id="document_summarize",
|
|
185
|
+
# Pre-GA: version stays 0.1.0 while the platform has not shipped —
|
|
186
|
+
# config-surface changes land without bumps.
|
|
187
|
+
version="0.1.0",
|
|
188
|
+
name="capability.document_summarize.name",
|
|
189
|
+
description="capability.document_summarize.description",
|
|
190
|
+
icon="summarize",
|
|
191
|
+
config_fields=[
|
|
192
|
+
FieldSpec(
|
|
193
|
+
key="require_confirmation",
|
|
194
|
+
type="boolean",
|
|
195
|
+
title="capability.document_summarize.fields.require_confirmation.title",
|
|
196
|
+
description="capability.document_summarize.fields.require_confirmation.description",
|
|
197
|
+
default=True,
|
|
198
|
+
ui=UIHints(group="safety"),
|
|
199
|
+
),
|
|
200
|
+
FieldSpec(
|
|
201
|
+
key="summarize_max_chars",
|
|
202
|
+
type="integer",
|
|
203
|
+
title="capability.document_summarize.fields.summarize_max_chars.title",
|
|
204
|
+
description="capability.document_summarize.fields.summarize_max_chars.description",
|
|
205
|
+
min=200,
|
|
206
|
+
max=20_000,
|
|
207
|
+
ui=UIHints(group="retrieval", advanced=True),
|
|
208
|
+
),
|
|
209
|
+
],
|
|
210
|
+
# No new chat part / side panel / router / owned table.
|
|
211
|
+
# team_scope intentionally left at the class default (ADMIN_GATED,
|
|
212
|
+
# RFC §7, §8.3): unlike document_access's baseline search/tree, this
|
|
213
|
+
# tool has no config-shaped trigger a user can reason about — a team
|
|
214
|
+
# admin must explicitly enable it before any agent in that team can
|
|
215
|
+
# even select it.
|
|
216
|
+
)
|
|
217
|
+
ConfigModel = DocumentSummarizeConfig
|
|
218
|
+
|
|
219
|
+
def tools(
|
|
220
|
+
self,
|
|
221
|
+
ctx: CapabilityContext[DocumentSummarizeConfig, EmptyModel],
|
|
222
|
+
) -> Sequence[BaseTool]:
|
|
223
|
+
"""
|
|
224
|
+
Build the single summarize tool, bound to the turn's typed context
|
|
225
|
+
(RFC §3.2, §5). `AgentCapability.middleware()`'s default wraps this
|
|
226
|
+
for `create_agent()`; no ReAct-loop-specific hook is needed.
|
|
227
|
+
|
|
228
|
+
Return-convention note:
|
|
229
|
+
kept as `@tool(..., response_format="content_and_artifact")` returning
|
|
230
|
+
a `(content, ToolInvocationResult)` tuple — see `document_access`'s
|
|
231
|
+
`tools()` docstring for the full rationale; identical constraint here.
|
|
232
|
+
"""
|
|
233
|
+
|
|
234
|
+
config = ctx.config
|
|
235
|
+
services = ctx.services
|
|
236
|
+
summarize_cap = config.summarize_max_chars
|
|
237
|
+
|
|
238
|
+
@tool("summarize_document", response_format="content_and_artifact")
|
|
239
|
+
async def summarize_document(
|
|
240
|
+
document_uid: str,
|
|
241
|
+
instruction: str | None = None,
|
|
242
|
+
max_chars: int | None = None,
|
|
243
|
+
) -> tuple[str, ToolInvocationResult]:
|
|
244
|
+
"""Generate a fresh, on-demand summary of one document by its uid.
|
|
245
|
+
|
|
246
|
+
Use this when you need to understand a document's content in depth —
|
|
247
|
+
e.g. to decide whether it's relevant, or to extract specific
|
|
248
|
+
information — without pulling its full text into your own context. A
|
|
249
|
+
fresh model reads the whole document (using map-reduce for large
|
|
250
|
+
documents) and returns just the summary.
|
|
251
|
+
|
|
252
|
+
`document_uid` MUST be the document's opaque uid, not its name or
|
|
253
|
+
title. Get it from a prior search_documents_using_vectorization
|
|
254
|
+
hit's 'uid' field, from list_document_tree (the value shown in
|
|
255
|
+
'[...]' after each document name), or from the conversation's
|
|
256
|
+
attached-files list (the bracketed value after the file name). If
|
|
257
|
+
you only know a document's name, resolve its uid with one of those
|
|
258
|
+
first — never pass the name here. The uid is an internal working
|
|
259
|
+
identifier for YOUR tool calls only: NEVER repeat it in your answer
|
|
260
|
+
to the user — always refer to the document by its display name.
|
|
261
|
+
|
|
262
|
+
Pass `instruction` to steer the summary: focus area, what to look
|
|
263
|
+
for, audience, tone, desired length — e.g. "focus on financial risks
|
|
264
|
+
and list every action item". Without it, you get a generic abstract.
|
|
265
|
+
|
|
266
|
+
`max_chars` bounds the returned summary length; raise it for a more
|
|
267
|
+
detailed summary, lower it for a terse one. Leave it unset to use
|
|
268
|
+
the agent's configured default. The agent may also impose a hard
|
|
269
|
+
maximum, in which case a larger request is clamped down to it.
|
|
270
|
+
"""
|
|
271
|
+
|
|
272
|
+
port = services.document_summarize
|
|
273
|
+
if port is None:
|
|
274
|
+
raise RuntimeError(
|
|
275
|
+
"document_summarize: RuntimeServices.document_summarize is "
|
|
276
|
+
"not available on this execution path."
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
effective_max_chars = resolve_summarize_max_chars(summarize_cap, max_chars)
|
|
280
|
+
started = time.monotonic()
|
|
281
|
+
try:
|
|
282
|
+
result: DocumentSummaryResult = await port.summarize(
|
|
283
|
+
document_uid,
|
|
284
|
+
instruction=instruction,
|
|
285
|
+
max_chars=effective_max_chars,
|
|
286
|
+
)
|
|
287
|
+
except DocumentScopeRefusedError as exc:
|
|
288
|
+
return document_scope_refusal(
|
|
289
|
+
tool_ref="summarize_document",
|
|
290
|
+
action="summarize the document",
|
|
291
|
+
exc=exc,
|
|
292
|
+
)
|
|
293
|
+
except Exception as exc:
|
|
294
|
+
run_stop = unwrap_run_stop_error(exc)
|
|
295
|
+
if run_stop is not None:
|
|
296
|
+
raise run_stop from None
|
|
297
|
+
message, artifact = _document_tool_failure(
|
|
298
|
+
tool_ref="summarize_document",
|
|
299
|
+
action="summarize the document",
|
|
300
|
+
exc=exc,
|
|
301
|
+
elapsed_s=time.monotonic() - started,
|
|
302
|
+
document_uid=document_uid,
|
|
303
|
+
)
|
|
304
|
+
# 403/404 almost always means the model passed something that
|
|
305
|
+
# is not a document uid — a file NAME, a stale/foreign uid, or
|
|
306
|
+
# a FOLDER's tag id from a list_document_tree folder line
|
|
307
|
+
# (observed live, issue #2244) — the backend fails closed on
|
|
308
|
+
# unknown resources. Tell the model how to recover instead of
|
|
309
|
+
# letting it give up and echo the error.
|
|
310
|
+
if getattr(exc, "status_code", None) in (403, 404):
|
|
311
|
+
message += (
|
|
312
|
+
" Likely cause: document_uid was not a document's "
|
|
313
|
+
"opaque uid. If you passed a file name, resolve the "
|
|
314
|
+
"uid first: it is in the conversation's attached-files "
|
|
315
|
+
"list (the bracketed value after the file name), in a "
|
|
316
|
+
"search hit's 'uid' field, or on a DOCUMENT line of "
|
|
317
|
+
"list_document_tree. If you took the id from a FOLDER "
|
|
318
|
+
"line (it ends with '/', id shown as [folder:...]), "
|
|
319
|
+
"that is a folder id, not a document — summarize each "
|
|
320
|
+
"document listed under that folder instead. Then call "
|
|
321
|
+
"summarize_document again with a real document uid; "
|
|
322
|
+
"if other documents already summarized fine, continue "
|
|
323
|
+
"with those results. Do not repeat the uid to the "
|
|
324
|
+
"user."
|
|
325
|
+
)
|
|
326
|
+
# CAPAB-02: `_document_tool_failure` already baked the
|
|
327
|
+
# (shorter) pre-hint message into `artifact.blocks`; the
|
|
328
|
+
# recovery hint appended above must reach `blocks` too, or
|
|
329
|
+
# a Graph agent — which keeps only the artifact half of
|
|
330
|
+
# this return — loses exactly the guidance a model needs
|
|
331
|
+
# to self-correct and retry.
|
|
332
|
+
artifact = artifact.model_copy(
|
|
333
|
+
update={
|
|
334
|
+
"blocks": (
|
|
335
|
+
ToolContentBlock(
|
|
336
|
+
kind=ToolContentKind.TEXT, text=message
|
|
337
|
+
),
|
|
338
|
+
)
|
|
339
|
+
}
|
|
340
|
+
)
|
|
341
|
+
return message, artifact
|
|
342
|
+
# `blocks` carries the same summary text as `content` (CAPAB-02):
|
|
343
|
+
# a Graph agent's plain-dict invocation keeps only the artifact
|
|
344
|
+
# half of a `content_and_artifact` return.
|
|
345
|
+
artifact = ToolInvocationResult(
|
|
346
|
+
tool_ref="summarize_document",
|
|
347
|
+
blocks=(
|
|
348
|
+
ToolContentBlock(kind=ToolContentKind.TEXT, text=result.summary),
|
|
349
|
+
),
|
|
350
|
+
)
|
|
351
|
+
return result.summary, artifact
|
|
352
|
+
|
|
353
|
+
return [summarize_document]
|
|
354
|
+
|
|
355
|
+
def hitl_specs(self) -> Sequence[HitlSpec]:
|
|
356
|
+
"""
|
|
357
|
+
`summarize_document` is gated by default, per instance configurable
|
|
358
|
+
(#2177) — `require=False` defers to `_confirmation_required` instead
|
|
359
|
+
of hardcoding the decision, the same `when`-predicate mechanism
|
|
360
|
+
`document_access` already relies on for its own conditional scope
|
|
361
|
+
fields (RFC §5.4). The predicate reads the resolved instance config
|
|
362
|
+
fresh at gate time (`request.context.config`), so it always reflects
|
|
363
|
+
the CURRENT `require_confirmation` value, not whatever was true when
|
|
364
|
+
`hitl_specs()` itself was called (assembly time, no per-turn context
|
|
365
|
+
yet).
|
|
366
|
+
"""
|
|
367
|
+
|
|
368
|
+
return [
|
|
369
|
+
HitlSpec(
|
|
370
|
+
tool="summarize_document",
|
|
371
|
+
require=False,
|
|
372
|
+
when=_confirmation_required,
|
|
373
|
+
)
|
|
374
|
+
]
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def _confirmation_required(request: HitlGateRequest) -> bool:
|
|
378
|
+
"""Whether this agent instance still wants a human's proceed/cancel
|
|
379
|
+
before `summarize_document` runs — the resolved `require_confirmation`
|
|
380
|
+
config value (default `True`, see `DocumentSummarizeConfig`)."""
|
|
381
|
+
|
|
382
|
+
return bool(request.context.config.require_confirmation)
|
|
@@ -0,0 +1,27 @@
|
|
|
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
|
+
`DocumentVerbatimCapability` (DOCREAD-01) — verbatim, paginated document read.
|
|
17
|
+
|
|
18
|
+
Installing fred-capability-documents registers it via the `fred.capabilities` entry point
|
|
19
|
+
(`document_verbatim`). Pairs with `document_extract` over the shared
|
|
20
|
+
`document_markdown` port; see `document_read_common`.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from .capability import DocumentVerbatimCapability
|
|
26
|
+
|
|
27
|
+
__all__ = ["DocumentVerbatimCapability"]
|
|
@@ -0,0 +1,128 @@
|
|
|
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
|
+
`DocumentVerbatimCapability` (DOCREAD-01) — read a document's exact text
|
|
17
|
+
verbatim, one bounded page at a time, through the `document_markdown` port.
|
|
18
|
+
|
|
19
|
+
Half of the document-reading pair (see `document_read_common`). This capability
|
|
20
|
+
answers positional/literal questions ("what does the first paragraph say?",
|
|
21
|
+
"read section 3") — distinct from `document_extract` (exhaustive enumeration by
|
|
22
|
+
criterion) and `document_summarize` (a lossy overview). All three can coexist on
|
|
23
|
+
one agent; each tool's docstring draws the boundary so the model picks the right
|
|
24
|
+
one.
|
|
25
|
+
|
|
26
|
+
Installing fred-capability-documents registers it via the `fred.capabilities` entry point
|
|
27
|
+
(`document_verbatim`); it is `ADMIN_GATED` (class default), a per-agent opt-in a
|
|
28
|
+
team admin must enable.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
from collections.abc import Sequence
|
|
34
|
+
|
|
35
|
+
from fred_sdk.contracts.capability import (
|
|
36
|
+
AgentCapability,
|
|
37
|
+
CapabilityContext,
|
|
38
|
+
CapabilityManifest,
|
|
39
|
+
EmptyModel,
|
|
40
|
+
)
|
|
41
|
+
from fred_sdk.contracts.context import ToolInvocationResult
|
|
42
|
+
from fred_sdk.contracts.models import FieldSpec, UIHints
|
|
43
|
+
from langchain_core.tools import BaseTool, tool
|
|
44
|
+
|
|
45
|
+
from fred_capability_documents.document_read_common import (
|
|
46
|
+
DocumentReadConfig,
|
|
47
|
+
read_document_page,
|
|
48
|
+
resolve_page_max_chars,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class DocumentVerbatimCapability(
|
|
53
|
+
AgentCapability[DocumentReadConfig, DocumentReadConfig, EmptyModel]
|
|
54
|
+
):
|
|
55
|
+
"""Verbatim, paginated document read. Single tool, no chat controls, no turn
|
|
56
|
+
options, no HITL (read-only)."""
|
|
57
|
+
|
|
58
|
+
manifest = CapabilityManifest(
|
|
59
|
+
id="document_verbatim",
|
|
60
|
+
# Pre-GA: version stays 0.1.0 while the platform has not shipped.
|
|
61
|
+
version="0.1.0",
|
|
62
|
+
name="capability.document_verbatim.name",
|
|
63
|
+
description="capability.document_verbatim.description",
|
|
64
|
+
icon="description",
|
|
65
|
+
config_fields=[
|
|
66
|
+
FieldSpec(
|
|
67
|
+
key="page_max_chars",
|
|
68
|
+
type="integer",
|
|
69
|
+
title="capability.document_verbatim.fields.page_max_chars.title",
|
|
70
|
+
description="capability.document_verbatim.fields.page_max_chars.description",
|
|
71
|
+
min=200,
|
|
72
|
+
max=50_000,
|
|
73
|
+
ui=UIHints(group="retrieval", advanced=True),
|
|
74
|
+
),
|
|
75
|
+
],
|
|
76
|
+
# team_scope left at the class default (ADMIN_GATED).
|
|
77
|
+
)
|
|
78
|
+
ConfigModel = DocumentReadConfig
|
|
79
|
+
|
|
80
|
+
def tools(
|
|
81
|
+
self,
|
|
82
|
+
ctx: CapabilityContext[DocumentReadConfig, EmptyModel],
|
|
83
|
+
) -> Sequence[BaseTool]:
|
|
84
|
+
config = ctx.config
|
|
85
|
+
services = ctx.services
|
|
86
|
+
page_cap = config.page_max_chars
|
|
87
|
+
|
|
88
|
+
@tool("read_document", response_format="content_and_artifact")
|
|
89
|
+
async def read_document(
|
|
90
|
+
document_uid: str,
|
|
91
|
+
offset: int = 0,
|
|
92
|
+
max_chars: int | None = None,
|
|
93
|
+
) -> tuple[str, ToolInvocationResult]:
|
|
94
|
+
"""Read a document's exact text VERBATIM, one page at a time.
|
|
95
|
+
|
|
96
|
+
Use this to see what a document literally says at a given position —
|
|
97
|
+
e.g. "what does the first paragraph say?", "read the introduction",
|
|
98
|
+
"show me section 3" — or to read a whole short document. Returns the
|
|
99
|
+
document's text unchanged, NOT a summary.
|
|
100
|
+
|
|
101
|
+
When to use a different tool instead:
|
|
102
|
+
- for a short, lossy overview of the whole document → summarize_document;
|
|
103
|
+
- to enumerate EVERY item matching a criterion across the whole
|
|
104
|
+
document ("all the requirements") → extract_from_document.
|
|
105
|
+
|
|
106
|
+
`document_uid` MUST be the document's opaque uid, not its name — get
|
|
107
|
+
it from a search hit's 'uid', the document tree, or the
|
|
108
|
+
conversation's attached-files list. The uid is an internal working
|
|
109
|
+
identifier for YOUR tool calls only: NEVER repeat it in your answer;
|
|
110
|
+
always refer to the document by its display name.
|
|
111
|
+
|
|
112
|
+
`offset` is the character position to start from (0 = the very
|
|
113
|
+
beginning). `max_chars` bounds this page (leave unset for the agent's
|
|
114
|
+
default). If the returned result says more text remains, call again
|
|
115
|
+
with the next offset it gives you to keep reading.
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
effective = resolve_page_max_chars(page_cap, max_chars)
|
|
119
|
+
return await read_document_page(
|
|
120
|
+
port=services.document_markdown,
|
|
121
|
+
tool_ref="read_document",
|
|
122
|
+
document_uid=document_uid,
|
|
123
|
+
offset=offset,
|
|
124
|
+
max_chars=effective,
|
|
125
|
+
exhaustive=False,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
return [read_document]
|
|
File without changes
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fred-capability-documents
|
|
3
|
+
Version: 4.4.3
|
|
4
|
+
Summary: Fred agent capabilities over the Knowledge Flow document ports: summarize, similarity, label search, extract, verbatim read.
|
|
5
|
+
Author-email: Thales <noreply@thalesgroup.com>
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://site.fredlab.dev
|
|
8
|
+
Project-URL: Repository, https://github.com/ThalesGroup/fred
|
|
9
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Requires-Python: <3.13,>=3.12
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
Requires-Dist: fred-core>=4.4.3
|
|
16
|
+
Requires-Dist: fred-sdk[agents]>=4.4.3
|
|
17
|
+
Requires-Dist: pydantic<3.0.0,>=2.7.0
|
|
18
|
+
Requires-Dist: langchain-core>=0.3.0
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: bandit>=1.8.6; extra == "dev"
|
|
21
|
+
Requires-Dist: basedpyright==1.31.0; extra == "dev"
|
|
22
|
+
Requires-Dist: detect-secrets>=1.5.0; extra == "dev"
|
|
23
|
+
Requires-Dist: pytest>=8.4.2; extra == "dev"
|
|
24
|
+
Requires-Dist: pytest-asyncio>=1.2.0; extra == "dev"
|
|
25
|
+
Requires-Dist: pytest-cov>=6.2.1; extra == "dev"
|
|
26
|
+
Requires-Dist: pytest-socket>=0.7.0; extra == "dev"
|
|
27
|
+
Requires-Dist: ruff<0.16,>=0.15.22; extra == "dev"
|
|
28
|
+
|
|
29
|
+
# fred-capability-documents
|
|
30
|
+
|
|
31
|
+
Fred agent capabilities over the Knowledge Flow document ports. One package,
|
|
32
|
+
one `fred.capabilities` entry point per capability, each admin-gated and
|
|
33
|
+
independently grantable by a team admin.
|
|
34
|
+
|
|
35
|
+
## Capabilities
|
|
36
|
+
|
|
37
|
+
- **`document_summarize`** — on-demand summary of one document
|
|
38
|
+
(`RuntimeServices.document_summarize`).
|
|
39
|
+
- **`document_similarity`** — targeted document-to-document comparison
|
|
40
|
+
(`RuntimeServices.document_similarity`).
|
|
41
|
+
- **`document_label_search`** — exhaustive, paginated resolution of a
|
|
42
|
+
business label to documents (`RuntimeServices.document_tree`).
|
|
43
|
+
- **`document_verbatim`** — verbatim, paginated read of one document
|
|
44
|
+
(`RuntimeServices.document_markdown`).
|
|
45
|
+
- **`document_extract`** — exhaustive extraction from one document
|
|
46
|
+
(`RuntimeServices.document_extraction`).
|
|
47
|
+
|
|
48
|
+
`document_read_common.py` holds what the reading pair and the error shaping
|
|
49
|
+
share. The capabilities reach the platform only through the typed fred-sdk
|
|
50
|
+
ports — no URL, credential or client lives here.
|
|
51
|
+
|
|
52
|
+
## Registration
|
|
53
|
+
|
|
54
|
+
Installing this package *is* the registration: the fred-agents pod
|
|
55
|
+
auto-discovers each capability at boot via the `fred.capabilities` entry
|
|
56
|
+
points declared in `pyproject.toml`. Nothing else to wire.
|
|
57
|
+
|
|
58
|
+
Authoring guide: `docs/swift/capabilities/AUTHORING.md`.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
fred_capability_documents/__init__.py,sha256=udb_Xsa7gosduxzU9BcG3YSYR4u5KWoATb3CQs-lTtA,888
|
|
2
|
+
fred_capability_documents/document_read_common.py,sha256=5fZ-G2RyRCrR47nUTNHiHf7zM55rwobobUQiBXqySqg,10527
|
|
3
|
+
fred_capability_documents/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
fred_capability_documents/document_extract/__init__.py,sha256=nv_AHyUNjE113JFs_6aB7c1TXSP47fedLBYgW4U7H6Y,1009
|
|
5
|
+
fred_capability_documents/document_extract/capability.py,sha256=VaFHh4cG1n_Cb_KAPVsy3YCl_u9qL3Z4I5jL7sMb2iQ,8582
|
|
6
|
+
fred_capability_documents/document_label_search/__init__.py,sha256=Cc11flcKLFpQ8ql3ledXzstx3DN5T3I_9zp7hbepz5U,1059
|
|
7
|
+
fred_capability_documents/document_label_search/capability.py,sha256=cOy4LIxE_LeAm375MEviHx0LKrVSeEpMMAQnAJJvaF0,10686
|
|
8
|
+
fred_capability_documents/document_similarity/__init__.py,sha256=5rhnxUqtobPeHGWIzdtWBLpoaPfIb7b6xmnnUGDb5uI,1034
|
|
9
|
+
fred_capability_documents/document_similarity/capability.py,sha256=jqXDeKzScDpAHEO77EJG5vIj_6n9kHFPD4AeSRXceBE,11852
|
|
10
|
+
fred_capability_documents/document_summarize/__init__.py,sha256=x1zu5IvC2i6HY_oplpbKXwrSqVPXhbOuiN5T4qFT6-g,1232
|
|
11
|
+
fred_capability_documents/document_summarize/capability.py,sha256=jF19DJrEgZMnqjqr3ruI2mDfBCPoz9JxjanhMuTk9x8,17342
|
|
12
|
+
fred_capability_documents/document_verbatim/__init__.py,sha256=hjC3-1jiX1_RibS_K7uo7qTGfCM8esuWTCUf9qDyS2A,1004
|
|
13
|
+
fred_capability_documents/document_verbatim/capability.py,sha256=UArZNEW9kXrxI8XUQId6vSm_c10CHeHnNxVzRvijkds,5149
|
|
14
|
+
fred_capability_documents-4.4.3.dist-info/METADATA,sha256=xZC2rVLoyL2NJ9bjoLXkMYp7z6CXv2LhIsXMY1nkY00,2530
|
|
15
|
+
fred_capability_documents-4.4.3.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
16
|
+
fred_capability_documents-4.4.3.dist-info/entry_points.txt,sha256=BsOBCx2hpgmrOsuRNlTQ4ZRMOWmLD6AZU-2E3DZIHng,492
|
|
17
|
+
fred_capability_documents-4.4.3.dist-info/top_level.txt,sha256=5pzhsSaq21Qyqzp9Hlv95AjRZvR1_0Pd_OB9UQx71Zw,26
|
|
18
|
+
fred_capability_documents-4.4.3.dist-info/RECORD,,
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
[fred.capabilities]
|
|
2
|
+
document_extract = fred_capability_documents.document_extract:DocumentExtractCapability
|
|
3
|
+
document_label_search = fred_capability_documents.document_label_search:DocumentLabelSearchCapability
|
|
4
|
+
document_similarity = fred_capability_documents.document_similarity:DocumentSimilarityCapability
|
|
5
|
+
document_summarize = fred_capability_documents.document_summarize:DocumentSummarizeCapability
|
|
6
|
+
document_verbatim = fred_capability_documents.document_verbatim:DocumentVerbatimCapability
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
fred_capability_documents
|