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.
@@ -0,0 +1,263 @@
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
+ Shared plumbing for the document-reading capability pair (DOCREAD-01):
17
+ `document_verbatim` (verbatim positional read) and `document_extract`
18
+ (exhaustive extraction). Both sit on the SAME `RuntimeServices.document_markdown`
19
+ port and differ only in tool intent and how the pagination footer is worded, so
20
+ the config model, length resolution, error shaping, and page-to-tool-result
21
+ formatting live here once.
22
+
23
+ `document_tool_failure` has outgrown that pair - `document_similarity` uses it
24
+ too, since the error shaping is about the document ports in general, not about
25
+ paginated reading. It stays here rather than moving to a new module: two other
26
+ capabilities (`document_access`, `document_summarize`) still carry their own
27
+ private copies, and folding all four into one home is a cleanup of its own, not
28
+ something to smuggle into a feature change.
29
+
30
+ Doctrine (RFC §3.5, §3.8, §10), identical to `document_summarize`:
31
+ - the capability reaches the platform ONLY through the typed
32
+ `RuntimeServices.document_markdown` port; the per-turn binding and the raw
33
+ access token NEVER enter `CapabilityContext`;
34
+ - the tool signature exposes ONLY LLM arguments (uid + page window); identity
35
+ and config reach the tool through the middleware closure, never the schema;
36
+ - `document_uid` is an internal working identifier — tools instruct the model to
37
+ NEVER repeat it to the end user (answers refer to documents by display name).
38
+ """
39
+
40
+ from __future__ import annotations
41
+
42
+ import time
43
+
44
+ from fred_sdk.contracts.context import (
45
+ ToolContentBlock,
46
+ ToolContentKind,
47
+ ToolInvocationResult,
48
+ )
49
+ from fred_sdk.contracts.runtime import (
50
+ DocumentMarkdownResult,
51
+ DocumentScopeRefusedError,
52
+ unwrap_run_stop_error,
53
+ )
54
+ from pydantic import BaseModel, Field
55
+
56
+ _KF_SERVICE = "Knowledge Flow"
57
+
58
+ # Built-in default page length when neither the caller (the LLM) nor the
59
+ # capability config specifies one. Matches the adapter's default page size.
60
+ DEFAULT_PAGE_MAX_CHARS = 8000
61
+ # Wire bounds clamped client-side so an out-of-range LLM value degrades
62
+ # gracefully instead of a surprising page.
63
+ _PAGE_MAX_CHARS_BOUNDS = (200, 50_000)
64
+
65
+
66
+ def _clamp(value: int, bounds: tuple[int, int]) -> int:
67
+ low, high = bounds
68
+ return max(low, min(value, high))
69
+
70
+
71
+ def resolve_page_max_chars(cap: int | None, requested: int | None) -> int:
72
+ """
73
+ Resolve the effective page length from the configured cap and the caller's
74
+ request.
75
+
76
+ The per-agent cap (`page_max_chars` config) is both the default (when the
77
+ caller asks for nothing) and a hard upper bound on whatever the caller
78
+ requests; without one, the built-in default applies and the caller's request
79
+ is honored verbatim (within wire bounds).
80
+ """
81
+
82
+ default = cap if cap is not None else DEFAULT_PAGE_MAX_CHARS
83
+ effective = requested if requested is not None else default
84
+ if cap is not None:
85
+ effective = min(effective, cap)
86
+ return _clamp(effective, _PAGE_MAX_CHARS_BOUNDS)
87
+
88
+
89
+ class DocumentReadConfig(BaseModel):
90
+ """Shared agent-creation / stored config of the document-reading pair.
91
+
92
+ A single knob today: the default AND hard cap for the per-call page length
93
+ (chars). None = built-in default, caller's request honored verbatim.
94
+ """
95
+
96
+ page_max_chars: int | None = Field(default=None, ge=200, le=50_000)
97
+
98
+
99
+ def document_tool_failure(
100
+ *,
101
+ tool_ref: str,
102
+ action: str,
103
+ exc: Exception,
104
+ elapsed_s: float,
105
+ document_uid: str,
106
+ ) -> tuple[str, ToolInvocationResult]:
107
+ """Turn a document-read tool-call failure into a non-empty, actionable error
108
+ message plus an ``is_error=True`` artifact.
109
+
110
+ The v2 ReAct runtime surfaces ``ToolInvocationResult.is_error`` directly to
111
+ the user (and suppresses LLM hallucination), so a failing tool MUST return
112
+ such a result instead of raising. Transport detail (timeout, HTTP status)
113
+ arrives via the SDK-typed ``DocumentPortCallError`` attributes the adapters
114
+ stamp — this module never imports the adapter's HTTP stack.
115
+ """
116
+
117
+ err_type = type(exc).__name__
118
+ raw = str(exc).strip()
119
+ timed_out = bool(getattr(exc, "timed_out", False))
120
+ status_code = getattr(exc, "status_code", None)
121
+
122
+ if timed_out:
123
+ cause = f"the {_KF_SERVICE} service timed out after {elapsed_s:.0f}s"
124
+ elif status_code is not None:
125
+ cause = f"the {_KF_SERVICE} service returned HTTP {status_code}"
126
+ else:
127
+ cause = f"the {_KF_SERVICE} service call failed after {elapsed_s:.0f}s"
128
+
129
+ detail = f": {raw}" if raw else ""
130
+ message = (
131
+ f"Could not {action} (document_uid={document_uid}): {cause} "
132
+ f"[{err_type}{detail}]."
133
+ )
134
+ if status_code in (403, 404):
135
+ message += (
136
+ " Likely cause: document_uid was not a document's opaque uid. If you "
137
+ "passed a file NAME, resolve the uid first — it is in the "
138
+ "conversation's attached-files list (the bracketed value after the "
139
+ "file name), in a search hit's 'uid' field, or on a DOCUMENT line of "
140
+ "the document tree. Then retry with a real document uid. Do not "
141
+ "repeat the uid to the user."
142
+ )
143
+ return message, ToolInvocationResult(
144
+ tool_ref=tool_ref,
145
+ is_error=True,
146
+ blocks=(ToolContentBlock(kind=ToolContentKind.TEXT, text=message),),
147
+ )
148
+
149
+
150
+ def document_scope_refusal(
151
+ *,
152
+ tool_ref: str,
153
+ action: str,
154
+ exc: DocumentScopeRefusedError,
155
+ ) -> tuple[str, ToolInvocationResult]:
156
+ """Shape a turn-scope refusal for the model — never as a service failure.
157
+
158
+ Nothing failed downstream: the user narrowed this turn to a selection the
159
+ named document is not in. Told as a transport error the model retries;
160
+ told as an empty result it reports the document as empty.
161
+ """
162
+
163
+ uids = ", ".join(exc.requested_uids) or "that document"
164
+ message = (
165
+ f"Cannot {action} ({uids}): the user has restricted this conversation to "
166
+ "a specific set of documents, and this one is not in it. Nothing was "
167
+ "read - this is NOT an empty or unreadable document. Work from a "
168
+ "document that is in scope (search and the document tree return exactly "
169
+ "those), or tell the user the document they mean is outside the current "
170
+ "selection. Never repeat the identifier to the user."
171
+ )
172
+ return message, ToolInvocationResult(
173
+ tool_ref=tool_ref,
174
+ is_error=True,
175
+ blocks=(ToolContentBlock(kind=ToolContentKind.TEXT, text=message),),
176
+ )
177
+
178
+
179
+ def _pagination_footer(result: DocumentMarkdownResult, *, exhaustive: bool) -> str:
180
+ """Machine-readable page/continuation banner appended to the tool content.
181
+
182
+ This is the signal that structurally prevents `document_summarize`'s "half
183
+ answer" failure mode: the model is told, in-band, exactly how much of the
184
+ document it has seen and whether it MUST keep paging. `exhaustive` (the
185
+ `extract` tool) makes the "keep going" directive imperative; the verbatim
186
+ tool states it as a plain option.
187
+ """
188
+
189
+ end = result.offset + len(result.text)
190
+ seen = f"chars {result.offset}–{end} of {result.total_chars}"
191
+ if result.next_offset is not None:
192
+ if exhaustive:
193
+ return (
194
+ f"\n\n[MORE TEXT REMAINS — {seen} read. You have NOT seen the "
195
+ f"whole document yet; do not conclude. Call this tool again with "
196
+ f"offset={result.next_offset} and keep accumulating matches until "
197
+ f"the end is reached.]"
198
+ )
199
+ return (
200
+ f"\n\n[More text remains — {seen} read. You have NOT seen the "
201
+ f"whole document yet; do not present this page as the complete "
202
+ f"document. You may call this tool again with "
203
+ f"offset={result.next_offset} to continue reading.]"
204
+ )
205
+ if result.offset == 0 and result.total_chars == 0:
206
+ return "\n\n[The document has no readable text content.]"
207
+ if exhaustive:
208
+ return (
209
+ f"\n\n[END OF DOCUMENT reached ({result.total_chars} chars total). "
210
+ f"You now have the complete text — produce your exhaustive answer.]"
211
+ )
212
+ return f"\n\n[End of document ({result.total_chars} chars total).]"
213
+
214
+
215
+ async def read_document_page(
216
+ *,
217
+ port,
218
+ tool_ref: str,
219
+ document_uid: str,
220
+ offset: int,
221
+ max_chars: int,
222
+ exhaustive: bool,
223
+ ) -> tuple[str, ToolInvocationResult]:
224
+ """Fetch one page through the `document_markdown` port and format it as a
225
+ `(content, ToolInvocationResult)` return (shared by both tools).
226
+
227
+ `content_and_artifact` convention (see `document_summarize`): the artifact's
228
+ `blocks` carries the same text as `content` so a Graph agent — which keeps
229
+ only the artifact half — still sees the page and its continuation footer.
230
+ """
231
+
232
+ if port is None:
233
+ raise RuntimeError(
234
+ f"{tool_ref}: RuntimeServices.document_markdown is not available on "
235
+ "this execution path."
236
+ )
237
+
238
+ started = time.monotonic()
239
+ action = "extract from the document" if exhaustive else "read the document"
240
+ try:
241
+ result = await port.fetch_markdown(
242
+ document_uid, offset=offset, max_chars=max_chars
243
+ )
244
+ except DocumentScopeRefusedError as exc:
245
+ return document_scope_refusal(tool_ref=tool_ref, action=action, exc=exc)
246
+ except Exception as exc:
247
+ run_stop = unwrap_run_stop_error(exc)
248
+ if run_stop is not None:
249
+ raise run_stop from None
250
+ return document_tool_failure(
251
+ tool_ref=tool_ref,
252
+ action=action,
253
+ exc=exc,
254
+ elapsed_s=time.monotonic() - started,
255
+ document_uid=document_uid,
256
+ )
257
+
258
+ content = result.text + _pagination_footer(result, exhaustive=exhaustive)
259
+ artifact = ToolInvocationResult(
260
+ tool_ref=tool_ref,
261
+ blocks=(ToolContentBlock(kind=ToolContentKind.TEXT, text=content),),
262
+ )
263
+ return content, artifact
@@ -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
+ `DocumentSimilarityCapability` — targeted document-to-document comparison.
17
+
18
+ Installing fred-capability-documents registers it via the `fred.capabilities` entry point
19
+ (`document_similarity`). Sits on `RuntimeServices.document_similarity`, the
20
+ comparison counterpart of the `document_search` port `document_access` uses.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ from .capability import DocumentSimilarityCapability
26
+
27
+ __all__ = ["DocumentSimilarityCapability"]
@@ -0,0 +1,280 @@
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
+ `DocumentSimilarityCapability` — find the passages most similar to an anchor
17
+ passage, inside documents named on the call.
18
+
19
+ Separate from `document_access` because it COMPARES rather than answers, and
20
+ sits on its own port: its targeting comes from the model per call, not from
21
+ the conversation. Read-only, so no HITL gate; `ADMIN_GATED` by class default.
22
+ Full rationale: RUNTIME-EXECUTION-CONTRACT.md §8.60.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import json
28
+ import logging
29
+ import time
30
+ from collections.abc import Sequence
31
+
32
+ from fred_core.store.vector_search import select_citable_sources
33
+ from fred_sdk.contracts.capability import (
34
+ AgentCapability,
35
+ CapabilityContext,
36
+ CapabilityManifest,
37
+ EmptyModel,
38
+ )
39
+ from fred_sdk.contracts.context import (
40
+ ToolContentBlock,
41
+ ToolContentKind,
42
+ ToolInvocationResult,
43
+ )
44
+ from fred_sdk.contracts.models import FieldSpec, UIHints
45
+ from fred_sdk.contracts.runtime import (
46
+ DocumentScopeRefusedError,
47
+ DocumentSearchResult,
48
+ unwrap_run_stop_error,
49
+ )
50
+ from langchain_core.tools import BaseTool, tool
51
+ from pydantic import BaseModel, Field
52
+
53
+ from fred_capability_documents.document_read_common import document_tool_failure
54
+
55
+ DOCUMENT_SIMILARITY_TOOL_REF = "document_similarity"
56
+
57
+ # Same LLM-visible slice the sibling document tools use: citation and reasoning
58
+ # fields only, never URLs or operational paths the model could echo back. `uid`
59
+ # stays - it is the working identifier for chaining into the reading tools.
60
+ _LLM_FIELDS = frozenset(
61
+ {"uid", "title", "content", "file_name", "page", "section", "score"}
62
+ )
63
+
64
+ # Wire bounds, clamped capability-side so an out-of-range model value degrades
65
+ # instead of failing the call downstream.
66
+ _TOP_K_BOUNDS = (1, 50)
67
+
68
+ logger = logging.getLogger(__name__)
69
+
70
+
71
+ class DocumentSimilarityConfig(BaseModel):
72
+ """Per-agent-instance tuning. Targeting is deliberately absent: the whole
73
+ point of this mode is that the model names its targets per call, and the
74
+ adapter bounds them by the session binding."""
75
+
76
+ default_top_k: int = Field(default=10, ge=1, le=50)
77
+ rerank: bool = Field(default=True)
78
+ min_score: float | None = Field(default=None, ge=0.0, le=1.0)
79
+
80
+
81
+ class DocumentSimilarityCapability(
82
+ AgentCapability[DocumentSimilarityConfig, DocumentSimilarityConfig, EmptyModel]
83
+ ):
84
+ """Targeted comparison search. Single tool, no chat controls, no turn
85
+ options, no HITL (read-only)."""
86
+
87
+ manifest = CapabilityManifest(
88
+ id="document_similarity",
89
+ # Pre-GA: version stays 0.1.0 while the platform has not shipped.
90
+ version="0.1.0",
91
+ name="capability.document_similarity.name",
92
+ description="capability.document_similarity.description",
93
+ icon="sync_alt",
94
+ config_fields=[
95
+ FieldSpec(
96
+ key="default_top_k",
97
+ type="integer",
98
+ title="capability.document_similarity.fields.default_top_k.title",
99
+ description="capability.document_similarity.fields.default_top_k.description",
100
+ min=1,
101
+ max=50,
102
+ ui=UIHints(group="retrieval"),
103
+ ),
104
+ FieldSpec(
105
+ key="rerank",
106
+ type="boolean",
107
+ title="capability.document_similarity.fields.rerank.title",
108
+ description="capability.document_similarity.fields.rerank.description",
109
+ ui=UIHints(group="retrieval", advanced=True),
110
+ ),
111
+ FieldSpec(
112
+ key="min_score",
113
+ type="number",
114
+ title="capability.document_similarity.fields.min_score.title",
115
+ description="capability.document_similarity.fields.min_score.description",
116
+ min=0.0,
117
+ max=1.0,
118
+ ui=UIHints(group="retrieval", advanced=True),
119
+ ),
120
+ ],
121
+ # team_scope left at the class default (ADMIN_GATED).
122
+ )
123
+ ConfigModel = DocumentSimilarityConfig
124
+
125
+ def tools(
126
+ self,
127
+ ctx: CapabilityContext[DocumentSimilarityConfig, EmptyModel],
128
+ ) -> Sequence[BaseTool]:
129
+ config = ctx.config
130
+ services = ctx.services
131
+ default_top_k = config.default_top_k
132
+ rerank = config.rerank
133
+ min_score = config.min_score
134
+
135
+ @tool("find_similar_passages", response_format="content_and_artifact")
136
+ async def find_similar_passages(
137
+ anchor: str,
138
+ document_uids: list[str],
139
+ top_k: int | None = None,
140
+ ) -> tuple[str, ToolInvocationResult]:
141
+ """Find the passages most similar to an anchor passage, inside specific documents.
142
+
143
+ This is a COMPARISON tool, not a question-answering one. `anchor` is
144
+ a passage of text to match against - a sentence, a paragraph, a
145
+ requirement - NOT a question. Use it to compare one document against
146
+ another: "does the operations manual cover what the architecture
147
+ document requires here?", "what in document B corresponds to this
148
+ section of document A?", "is this requirement contradicted anywhere
149
+ in that spec?".
150
+
151
+ To compare two documents end to end, call this once per passage of
152
+ the first document with `document_uids` set to the second - each call
153
+ is aimed independently, which is what this tool is for.
154
+
155
+ When to use a different tool instead:
156
+ - to answer a question from the corpus at large -> use the vector
157
+ search tool, which is not restricted to named documents;
158
+ - to read a document's own text in order -> use the verbatim read
159
+ tool.
160
+
161
+ `document_uids` is REQUIRED and must name at least one document: the
162
+ search runs ONLY inside those documents. They must be opaque
163
+ document uids, not file names - take them from a search hit's 'uid'
164
+ or from the document tree. This tool searches the document corpus
165
+ only: a file attached to this conversation is NOT searchable here,
166
+ so its uid would return nothing. Those uids are internal working
167
+ identifiers for YOUR tool calls: NEVER repeat one in your answer,
168
+ always refer to a document by its display name.
169
+
170
+ `top_k` bounds how many matches come back, best-first (leave unset
171
+ for the agent's default). An empty result means nothing in those
172
+ documents resembles the anchor - report that, do not invent a match.
173
+ """
174
+
175
+ port = services.document_similarity
176
+ if port is None:
177
+ # No platform port injected (e.g. a bare test harness). Fail
178
+ # LOUD rather than silently returning nothing.
179
+ raise RuntimeError(
180
+ "document_similarity: RuntimeServices.document_similarity is "
181
+ "not available on this execution path."
182
+ )
183
+
184
+ if not isinstance(anchor, str) or not anchor.strip():
185
+ return _bad_call(
186
+ "`anchor` must be a non-empty passage of text to match "
187
+ "against (not a question, and not empty)."
188
+ )
189
+ if not isinstance(document_uids, list) or not document_uids:
190
+ return _bad_call(
191
+ "`document_uids` must name at least one document to search "
192
+ "inside - this tool never searches the whole corpus. Resolve "
193
+ "a document's uid from a search hit, the document tree, or "
194
+ "the conversation's attached files, then retry."
195
+ )
196
+
197
+ uids = [str(uid) for uid in document_uids]
198
+ effective_top_k = (
199
+ _clamp(top_k, _TOP_K_BOUNDS)
200
+ if isinstance(top_k, int) and not isinstance(top_k, bool) and top_k > 0
201
+ else default_top_k
202
+ )
203
+
204
+ started = time.monotonic()
205
+ try:
206
+ result: DocumentSearchResult = await port.find_similar(
207
+ anchor,
208
+ document_uids=uids,
209
+ top_k=effective_top_k,
210
+ rerank=rerank,
211
+ min_score=min_score,
212
+ )
213
+ except DocumentScopeRefusedError as exc:
214
+ # Nothing was searched, so this must never reach the model as an
215
+ # empty result - it would report "nothing matches" about a
216
+ # document it never looked at.
217
+ return _bad_call(
218
+ "Cannot search "
219
+ f"{', '.join(exc.requested_uids) or 'those documents'}: they "
220
+ "are not part of this conversation's document scope, so "
221
+ "nothing was searched. This is NOT a 'no matches' answer. "
222
+ "Use a document that is in scope - resolve one from a search "
223
+ "hit or the document tree - or tell the user the document is "
224
+ "out of scope."
225
+ )
226
+ except Exception as exc:
227
+ run_stop = unwrap_run_stop_error(exc)
228
+ if run_stop is not None:
229
+ raise run_stop from None
230
+ # Degrade rather than raise: the default ToolNode handler
231
+ # re-raises, killing the turn with an empty error detail.
232
+ return document_tool_failure(
233
+ tool_ref=DOCUMENT_SIMILARITY_TOOL_REF,
234
+ action="find similar passages",
235
+ exc=exc,
236
+ elapsed_s=time.monotonic() - started,
237
+ document_uid=", ".join(uids),
238
+ )
239
+
240
+ hits = result.hits
241
+ content = {
242
+ "anchor": anchor,
243
+ "document_uids": uids,
244
+ "hits": [
245
+ {
246
+ k: v
247
+ for k, v in hit.model_dump(mode="json").items()
248
+ if k in _LLM_FIELDS
249
+ }
250
+ for hit in hits
251
+ ],
252
+ }
253
+ # Keeps the dataset-pointer exclusion, drops the score-ratio one:
254
+ # the caller named these documents, so a weak match is a real
255
+ # finding, not corpus noise (RUNTIME-EXECUTION-CONTRACT.md §8.60).
256
+ artifact = ToolInvocationResult(
257
+ tool_ref=DOCUMENT_SIMILARITY_TOOL_REF,
258
+ blocks=(ToolContentBlock(kind=ToolContentKind.JSON, data=content),),
259
+ sources=select_citable_sources(hits, min_score_ratio=0.0),
260
+ )
261
+ return json.dumps(content), artifact
262
+
263
+ return [find_similar_passages]
264
+
265
+
266
+ def _clamp(value: int, bounds: tuple[int, int]) -> int:
267
+ low, high = bounds
268
+ return max(low, min(value, high))
269
+
270
+
271
+ def _bad_call(message: str) -> tuple[str, ToolInvocationResult]:
272
+ """A malformed tool call, answered as an `is_error` artifact the model can
273
+ act on. Not `document_tool_failure`: nothing failed downstream, so its
274
+ transport wording ("the Knowledge Flow service ...") would be a lie."""
275
+
276
+ return message, ToolInvocationResult(
277
+ tool_ref=DOCUMENT_SIMILARITY_TOOL_REF,
278
+ is_error=True,
279
+ blocks=(ToolContentBlock(kind=ToolContentKind.TEXT, text=message),),
280
+ )
@@ -0,0 +1,38 @@
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 §10) — on-demand document summarization,
17
+ split out of the `document_access` pilot.
18
+
19
+ Installing fred-capability-documents registers it via the `fred.capabilities` entry point
20
+ (`document_summarize`), separate from `document_access` so a team admin opts
21
+ into it explicitly.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ from .capability import (
27
+ DEFAULT_SUMMARIZE_MAX_CHARS,
28
+ DocumentSummarizeCapability,
29
+ DocumentSummarizeConfig,
30
+ resolve_summarize_max_chars,
31
+ )
32
+
33
+ __all__ = [
34
+ "DEFAULT_SUMMARIZE_MAX_CHARS",
35
+ "DocumentSummarizeCapability",
36
+ "DocumentSummarizeConfig",
37
+ "resolve_summarize_max_chars",
38
+ ]