spot-sdk-python 1.2.1__tar.gz → 2.0.0b2__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.
- spot_sdk_python-2.0.0b2/PKG-INFO +117 -0
- spot_sdk_python-2.0.0b2/README.md +93 -0
- {spot_sdk_python-1.2.1 → spot_sdk_python-2.0.0b2}/pyproject.toml +11 -7
- spot_sdk_python-2.0.0b2/spot_sdk/__init__.py +152 -0
- spot_sdk_python-2.0.0b2/spot_sdk/_json.py +46 -0
- spot_sdk_python-2.0.0b2/spot_sdk/app.py +332 -0
- spot_sdk_python-2.0.0b2/spot_sdk/clients.py +211 -0
- {spot_sdk_python-1.2.1 → spot_sdk_python-2.0.0b2}/spot_sdk/config.py +0 -8
- spot_sdk_python-2.0.0b2/spot_sdk/email.py +167 -0
- {spot_sdk_python-1.2.1 → spot_sdk_python-2.0.0b2}/spot_sdk/knowledge.py +113 -115
- spot_sdk_python-2.0.0b2/spot_sdk/llm.py +514 -0
- spot_sdk_python-2.0.0b2/spot_sdk/manifest.py +122 -0
- spot_sdk_python-2.0.0b2/spot_sdk/mime.py +2381 -0
- spot_sdk_python-2.0.0b2/spot_sdk/orchestrator.py +92 -0
- spot_sdk_python-2.0.0b2/spot_sdk/py.typed +0 -0
- spot_sdk_python-2.0.0b2/spot_sdk/retriever.py +108 -0
- spot_sdk_python-2.0.0b2/spot_sdk/settings.py +79 -0
- spot_sdk_python-2.0.0b2/spot_sdk/signals.py +42 -0
- spot_sdk_python-2.0.0b2/spot_sdk/testing/README.md +29 -0
- spot_sdk_python-2.0.0b2/spot_sdk/testing/__init__.py +17 -0
- spot_sdk_python-2.0.0b2/spot_sdk/testing/contract.py +51 -0
- {spot_sdk_python-1.2.1 → spot_sdk_python-2.0.0b2}/spot_sdk/testing/fake_knowledge_client.py +42 -18
- spot_sdk_python-2.0.0b2/spot_sdk/testing/fake_llm.py +70 -0
- spot_sdk_python-2.0.0b2/spot_sdk/testing/fake_spot.py +108 -0
- spot_sdk_python-2.0.0b2/spot_sdk/testing/views.py +47 -0
- spot_sdk_python-2.0.0b2/spot_sdk/verdict.py +53 -0
- {spot_sdk_python-1.2.1 → spot_sdk_python-2.0.0b2}/spot_sdk/workflow.py +79 -14
- spot_sdk_python-1.2.1/PKG-INFO +0 -353
- spot_sdk_python-1.2.1/README.md +0 -331
- spot_sdk_python-1.2.1/spot_sdk/__init__.py +0 -99
- spot_sdk_python-1.2.1/spot_sdk/analysis_context.py +0 -99
- spot_sdk_python-1.2.1/spot_sdk/analyzer.py +0 -106
- spot_sdk_python-1.2.1/spot_sdk/analyzer_base.py +0 -323
- spot_sdk_python-1.2.1/spot_sdk/api_gateway.py +0 -271
- spot_sdk_python-1.2.1/spot_sdk/config_client.py +0 -203
- spot_sdk_python-1.2.1/spot_sdk/config_helpers.py +0 -34
- spot_sdk_python-1.2.1/spot_sdk/email.py +0 -136
- spot_sdk_python-1.2.1/spot_sdk/ollama.py +0 -58
- spot_sdk_python-1.2.1/spot_sdk/orchestrator.py +0 -79
- spot_sdk_python-1.2.1/spot_sdk/plugin.py +0 -35
- spot_sdk_python-1.2.1/spot_sdk/results.py +0 -129
- spot_sdk_python-1.2.1/spot_sdk/retriever.py +0 -267
- spot_sdk_python-1.2.1/spot_sdk/settings_schema.py +0 -56
- spot_sdk_python-1.2.1/spot_sdk/testing/README.md +0 -83
- spot_sdk_python-1.2.1/spot_sdk/testing/__init__.py +0 -22
- spot_sdk_python-1.2.1/spot_sdk/testing/factories.py +0 -177
- spot_sdk_python-1.2.1/spot_sdk/threat_levels.py +0 -33
- {spot_sdk_python-1.2.1 → spot_sdk_python-2.0.0b2}/spot_sdk/errors.py +0 -0
- {spot_sdk_python-1.2.1 → spot_sdk_python-2.0.0b2}/spot_sdk/knowledge_tags.py +0 -0
- {spot_sdk_python-1.2.1 → spot_sdk_python-2.0.0b2}/spot_sdk/logging.py +0 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: spot-sdk-python
|
|
3
|
+
Version: 2.0.0b2
|
|
4
|
+
Summary: SPOT plugin SDK: the plugin contract, the plugin runtime, its clients and test helpers
|
|
5
|
+
License: Apache-2.0
|
|
6
|
+
Author: SPOT Project
|
|
7
|
+
Author-email: spot@sonn.lu
|
|
8
|
+
Requires-Python: >=3.11,<3.16
|
|
9
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
16
|
+
Requires-Dist: fastapi (>=0.115.3,<1)
|
|
17
|
+
Requires-Dist: httpx (>=0.25,<1)
|
|
18
|
+
Requires-Dist: pydantic (>=2.10,<3.0)
|
|
19
|
+
Requires-Dist: pydantic-settings (>=2.0.0,<3.0.0)
|
|
20
|
+
Requires-Dist: selectolax (>=1.0,<1.1)
|
|
21
|
+
Project-URL: Homepage, https://sonn.lu
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# spot-sdk-python
|
|
25
|
+
|
|
26
|
+
The SDK for writing plugins of SPOT, the Spear-Phishing Overwatching
|
|
27
|
+
Tool: analyzers, context providers, mail retrievers and responders.
|
|
28
|
+
|
|
29
|
+
SPOT runs each plugin as a container and calls its `/internal/*` routes
|
|
30
|
+
with the plugin's own token. This package holds what SPOT's core and its
|
|
31
|
+
plugins share:
|
|
32
|
+
|
|
33
|
+
- **the contract** (Pydantic models): the email view, verdicts and
|
|
34
|
+
signals, workflow policies, results, and the plugin manifest with its
|
|
35
|
+
OCI labels;
|
|
36
|
+
- **the parser** core runs on every message at intake
|
|
37
|
+
(`spot_sdk.mime.parse`), bounded against hostile input, which records
|
|
38
|
+
what mail clients may read differently as anomalies;
|
|
39
|
+
- **the plugin runtime**: `create_plugin_app` builds the FastAPI app with
|
|
40
|
+
the routes core calls, token-checked; `PluginSettings` reads the token
|
|
41
|
+
and secrets from `/run/secrets` and the settings from the environment;
|
|
42
|
+
- **the clients**: `SpotClient` (ingest, message content, results),
|
|
43
|
+
`KnowledgeClient` (the Knowledge Store) and `LLMClient`
|
|
44
|
+
(OpenAI-compatible chat models, with the email kept apart from the
|
|
45
|
+
instructions as untrusted data);
|
|
46
|
+
- **test helpers**: `view_from_eml`, `make_view`, `FakeSpot`,
|
|
47
|
+
`FakeKnowledgeClient`, `FakeLLM` and `assert_contract`.
|
|
48
|
+
|
|
49
|
+
## Install
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install spot-sdk-python==2.0.0b2
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Python 3.11 to 3.15. Version 2 implements plugin contract 2, for
|
|
56
|
+
SPOT 2.0.
|
|
57
|
+
|
|
58
|
+
## An analyzer
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from spot_sdk import (
|
|
62
|
+
AnalyzerVerdict,
|
|
63
|
+
AnalyzeRequest,
|
|
64
|
+
Egress,
|
|
65
|
+
Manifest,
|
|
66
|
+
PluginKind,
|
|
67
|
+
PluginSettings,
|
|
68
|
+
Signal,
|
|
69
|
+
VerdictClass,
|
|
70
|
+
create_plugin_app,
|
|
71
|
+
)
|
|
72
|
+
from spot_sdk.signals import AUTH_DMARC_FAIL
|
|
73
|
+
|
|
74
|
+
MANIFEST = Manifest(
|
|
75
|
+
id="analyzer-dmarc",
|
|
76
|
+
version="1.0.0",
|
|
77
|
+
kinds=[PluginKind.ANALYZER],
|
|
78
|
+
egress=Egress.NONE,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def analyze(request: AnalyzeRequest, settings: PluginSettings) -> AnalyzerVerdict:
|
|
83
|
+
failed = [
|
|
84
|
+
result
|
|
85
|
+
for result in request.email.authentication
|
|
86
|
+
if result.method == "dmarc" and result.result == "fail"
|
|
87
|
+
]
|
|
88
|
+
if not failed:
|
|
89
|
+
return AnalyzerVerdict(verdict=VerdictClass.BENIGN, score=10, confidence=60)
|
|
90
|
+
return AnalyzerVerdict(
|
|
91
|
+
verdict=VerdictClass.SUSPICIOUS,
|
|
92
|
+
score=70,
|
|
93
|
+
confidence=80,
|
|
94
|
+
signals=[Signal(name=AUTH_DMARC_FAIL, weight=60, evidence="dmarc=fail")],
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
app = create_plugin_app(MANIFEST, PluginSettings, analyze=analyze)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Serve it with `uvicorn analyzer_dmarc:app --host 0.0.0.0 --port 8000`,
|
|
102
|
+
and test it with `spot_sdk.testing.assert_contract(app, "tests/phish.eml",
|
|
103
|
+
token=...)`.
|
|
104
|
+
|
|
105
|
+
## Documentation
|
|
106
|
+
|
|
107
|
+
- [Documentation](https://spot-project.codeberg.page/documentation/sdk/):
|
|
108
|
+
the contract, writing each kind of plugin, the reference.
|
|
109
|
+
- [Migrating from 1.x](https://spot-project.codeberg.page/documentation/sdk/MIGRATION-2/):
|
|
110
|
+
every removed name and its replacement. SDK 2 has no compatibility
|
|
111
|
+
with contract 1.
|
|
112
|
+
- [Source and changelog](https://codeberg.org/SPOT_Project/sdk).
|
|
113
|
+
|
|
114
|
+
## License
|
|
115
|
+
|
|
116
|
+
Apache License 2.0.
|
|
117
|
+
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# spot-sdk-python
|
|
2
|
+
|
|
3
|
+
The SDK for writing plugins of SPOT, the Spear-Phishing Overwatching
|
|
4
|
+
Tool: analyzers, context providers, mail retrievers and responders.
|
|
5
|
+
|
|
6
|
+
SPOT runs each plugin as a container and calls its `/internal/*` routes
|
|
7
|
+
with the plugin's own token. This package holds what SPOT's core and its
|
|
8
|
+
plugins share:
|
|
9
|
+
|
|
10
|
+
- **the contract** (Pydantic models): the email view, verdicts and
|
|
11
|
+
signals, workflow policies, results, and the plugin manifest with its
|
|
12
|
+
OCI labels;
|
|
13
|
+
- **the parser** core runs on every message at intake
|
|
14
|
+
(`spot_sdk.mime.parse`), bounded against hostile input, which records
|
|
15
|
+
what mail clients may read differently as anomalies;
|
|
16
|
+
- **the plugin runtime**: `create_plugin_app` builds the FastAPI app with
|
|
17
|
+
the routes core calls, token-checked; `PluginSettings` reads the token
|
|
18
|
+
and secrets from `/run/secrets` and the settings from the environment;
|
|
19
|
+
- **the clients**: `SpotClient` (ingest, message content, results),
|
|
20
|
+
`KnowledgeClient` (the Knowledge Store) and `LLMClient`
|
|
21
|
+
(OpenAI-compatible chat models, with the email kept apart from the
|
|
22
|
+
instructions as untrusted data);
|
|
23
|
+
- **test helpers**: `view_from_eml`, `make_view`, `FakeSpot`,
|
|
24
|
+
`FakeKnowledgeClient`, `FakeLLM` and `assert_contract`.
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install spot-sdk-python==2.0.0b2
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Python 3.11 to 3.15. Version 2 implements plugin contract 2, for
|
|
33
|
+
SPOT 2.0.
|
|
34
|
+
|
|
35
|
+
## An analyzer
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from spot_sdk import (
|
|
39
|
+
AnalyzerVerdict,
|
|
40
|
+
AnalyzeRequest,
|
|
41
|
+
Egress,
|
|
42
|
+
Manifest,
|
|
43
|
+
PluginKind,
|
|
44
|
+
PluginSettings,
|
|
45
|
+
Signal,
|
|
46
|
+
VerdictClass,
|
|
47
|
+
create_plugin_app,
|
|
48
|
+
)
|
|
49
|
+
from spot_sdk.signals import AUTH_DMARC_FAIL
|
|
50
|
+
|
|
51
|
+
MANIFEST = Manifest(
|
|
52
|
+
id="analyzer-dmarc",
|
|
53
|
+
version="1.0.0",
|
|
54
|
+
kinds=[PluginKind.ANALYZER],
|
|
55
|
+
egress=Egress.NONE,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def analyze(request: AnalyzeRequest, settings: PluginSettings) -> AnalyzerVerdict:
|
|
60
|
+
failed = [
|
|
61
|
+
result
|
|
62
|
+
for result in request.email.authentication
|
|
63
|
+
if result.method == "dmarc" and result.result == "fail"
|
|
64
|
+
]
|
|
65
|
+
if not failed:
|
|
66
|
+
return AnalyzerVerdict(verdict=VerdictClass.BENIGN, score=10, confidence=60)
|
|
67
|
+
return AnalyzerVerdict(
|
|
68
|
+
verdict=VerdictClass.SUSPICIOUS,
|
|
69
|
+
score=70,
|
|
70
|
+
confidence=80,
|
|
71
|
+
signals=[Signal(name=AUTH_DMARC_FAIL, weight=60, evidence="dmarc=fail")],
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
app = create_plugin_app(MANIFEST, PluginSettings, analyze=analyze)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Serve it with `uvicorn analyzer_dmarc:app --host 0.0.0.0 --port 8000`,
|
|
79
|
+
and test it with `spot_sdk.testing.assert_contract(app, "tests/phish.eml",
|
|
80
|
+
token=...)`.
|
|
81
|
+
|
|
82
|
+
## Documentation
|
|
83
|
+
|
|
84
|
+
- [Documentation](https://spot-project.codeberg.page/documentation/sdk/):
|
|
85
|
+
the contract, writing each kind of plugin, the reference.
|
|
86
|
+
- [Migrating from 1.x](https://spot-project.codeberg.page/documentation/sdk/MIGRATION-2/):
|
|
87
|
+
every removed name and its replacement. SDK 2 has no compatibility
|
|
88
|
+
with contract 1.
|
|
89
|
+
- [Source and changelog](https://codeberg.org/SPOT_Project/sdk).
|
|
90
|
+
|
|
91
|
+
## License
|
|
92
|
+
|
|
93
|
+
Apache License 2.0.
|
|
@@ -4,8 +4,8 @@ build-backend = "poetry.core.masonry.api"
|
|
|
4
4
|
|
|
5
5
|
[tool.poetry]
|
|
6
6
|
name = "spot-sdk-python"
|
|
7
|
-
version = "
|
|
8
|
-
description = "
|
|
7
|
+
version = "2.0.0b2"
|
|
8
|
+
description = "SPOT plugin SDK: the plugin contract, the plugin runtime, its clients and test helpers"
|
|
9
9
|
authors = ["SPOT Project <spot@sonn.lu>"]
|
|
10
10
|
license = "Apache-2.0"
|
|
11
11
|
homepage = "https://sonn.lu"
|
|
@@ -15,18 +15,22 @@ packages = [
|
|
|
15
15
|
]
|
|
16
16
|
|
|
17
17
|
[tool.poetry.dependencies]
|
|
18
|
-
python = "
|
|
19
|
-
pydantic =
|
|
18
|
+
python = ">=3.11,<3.16"
|
|
19
|
+
pydantic = "^2.10"
|
|
20
20
|
pydantic-settings = "^2.0.0"
|
|
21
|
-
httpx = "
|
|
22
|
-
|
|
21
|
+
httpx = ">=0.25,<1"
|
|
22
|
+
selectolax = ">=1.0,<1.1"
|
|
23
|
+
fastapi = ">=0.115.3,<1"
|
|
23
24
|
|
|
24
25
|
[tool.poetry.group.dev.dependencies]
|
|
25
26
|
pytest = "^7.4.0"
|
|
26
27
|
pytest-asyncio = "^0.21.0"
|
|
28
|
+
pytest-cov = "^4.1.0"
|
|
27
29
|
mypy = "^1.5.0"
|
|
28
30
|
|
|
29
31
|
[tool.ruff]
|
|
32
|
+
target-version = "py311"
|
|
33
|
+
|
|
34
|
+
[tool.ruff.lint]
|
|
30
35
|
select = ["E", "F", "I", "N", "W"]
|
|
31
36
|
ignore = ["E501"] # Line too long
|
|
32
|
-
target-version = "py311"
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""SPOT SDK: the plugin contract v2, the plugin runtime and its clients."""
|
|
2
|
+
|
|
3
|
+
from importlib import import_module
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
5
|
+
|
|
6
|
+
from .config import ConfigOption, ConfigStatus, ConfigUpdateRequest
|
|
7
|
+
from .email import Address, Anomaly, AuthResult, EmailView, Envelope, Header, Part, Url
|
|
8
|
+
from .errors import ErrorResponse
|
|
9
|
+
from .knowledge_tags import KnowledgeTag
|
|
10
|
+
from .logging import configure_logging, get_logger, get_logging_config
|
|
11
|
+
from .manifest import (
|
|
12
|
+
CONTRACT_VERSION,
|
|
13
|
+
Egress,
|
|
14
|
+
Manifest,
|
|
15
|
+
PluginKind,
|
|
16
|
+
Resources,
|
|
17
|
+
SettingField,
|
|
18
|
+
SettingType,
|
|
19
|
+
)
|
|
20
|
+
from .orchestrator import (
|
|
21
|
+
AnalyzeRequest,
|
|
22
|
+
AnalyzerOutcome,
|
|
23
|
+
AttributedSignal,
|
|
24
|
+
OrchestrationResult,
|
|
25
|
+
RespondRequest,
|
|
26
|
+
StageOutcome,
|
|
27
|
+
)
|
|
28
|
+
from .retriever import IngestionRequest, IngestionResult, IngestionStatus
|
|
29
|
+
from .settings import PluginSettings
|
|
30
|
+
from .verdict import AnalyzerVerdict, Score, Signal, VerdictClass
|
|
31
|
+
from .workflow import (
|
|
32
|
+
AggregationMethod,
|
|
33
|
+
AnalyzerConfig,
|
|
34
|
+
DecisiveSignal,
|
|
35
|
+
FailureStrategy,
|
|
36
|
+
ResponseMode,
|
|
37
|
+
RetrievalLimits,
|
|
38
|
+
RetryConfig,
|
|
39
|
+
VerdictPolicy,
|
|
40
|
+
Workflow,
|
|
41
|
+
WorkflowStage,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
if TYPE_CHECKING:
|
|
45
|
+
from .app import SyncState, create_plugin_app
|
|
46
|
+
from .clients import SpotAPIError, SpotClient
|
|
47
|
+
from .knowledge import KnowledgeClient, KnowledgeDocument, chunk_text, content_hash
|
|
48
|
+
from .llm import LLMClient, LLMOutputError, LLMToolCall, LLMToolCalls, LLMToolRound
|
|
49
|
+
|
|
50
|
+
# The modules that import FastAPI, Starlette or httpx load on first use, so importing
|
|
51
|
+
# `spot_sdk.mime` or the models stays light (PEP 562).
|
|
52
|
+
_LAZY = {
|
|
53
|
+
**dict.fromkeys(("SyncState", "create_plugin_app"), ".app"),
|
|
54
|
+
**dict.fromkeys(("SpotAPIError", "SpotClient"), ".clients"),
|
|
55
|
+
**dict.fromkeys(
|
|
56
|
+
("KnowledgeClient", "KnowledgeDocument", "chunk_text", "content_hash"),
|
|
57
|
+
".knowledge",
|
|
58
|
+
),
|
|
59
|
+
**dict.fromkeys(
|
|
60
|
+
("LLMClient", "LLMOutputError", "LLMToolCall", "LLMToolCalls", "LLMToolRound"),
|
|
61
|
+
".llm",
|
|
62
|
+
),
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def __getattr__(name: str) -> Any:
|
|
67
|
+
module = _LAZY.get(name)
|
|
68
|
+
if module is None:
|
|
69
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
70
|
+
value = getattr(import_module(module, __name__), name)
|
|
71
|
+
globals()[name] = value
|
|
72
|
+
return value
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def __dir__() -> list[str]:
|
|
76
|
+
return sorted({*globals(), *_LAZY})
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
__all__ = [
|
|
80
|
+
# Email view
|
|
81
|
+
"Address",
|
|
82
|
+
"Anomaly",
|
|
83
|
+
"AuthResult",
|
|
84
|
+
"EmailView",
|
|
85
|
+
"Envelope",
|
|
86
|
+
"Header",
|
|
87
|
+
"Part",
|
|
88
|
+
"Url",
|
|
89
|
+
# Verdicts
|
|
90
|
+
"AnalyzerVerdict",
|
|
91
|
+
"Score",
|
|
92
|
+
"Signal",
|
|
93
|
+
"VerdictClass",
|
|
94
|
+
# Workflow
|
|
95
|
+
"AggregationMethod",
|
|
96
|
+
"AnalyzerConfig",
|
|
97
|
+
"DecisiveSignal",
|
|
98
|
+
"FailureStrategy",
|
|
99
|
+
"ResponseMode",
|
|
100
|
+
"RetrievalLimits",
|
|
101
|
+
"RetryConfig",
|
|
102
|
+
"VerdictPolicy",
|
|
103
|
+
"Workflow",
|
|
104
|
+
"WorkflowStage",
|
|
105
|
+
# Analyzer and responder calls, results
|
|
106
|
+
"AnalyzeRequest",
|
|
107
|
+
"AnalyzerOutcome",
|
|
108
|
+
"AttributedSignal",
|
|
109
|
+
"OrchestrationResult",
|
|
110
|
+
"RespondRequest",
|
|
111
|
+
"StageOutcome",
|
|
112
|
+
# Ingestion
|
|
113
|
+
"IngestionRequest",
|
|
114
|
+
"IngestionResult",
|
|
115
|
+
"IngestionStatus",
|
|
116
|
+
# Plugin manifest
|
|
117
|
+
"CONTRACT_VERSION",
|
|
118
|
+
"Egress",
|
|
119
|
+
"Manifest",
|
|
120
|
+
"PluginKind",
|
|
121
|
+
"Resources",
|
|
122
|
+
"SettingField",
|
|
123
|
+
"SettingType",
|
|
124
|
+
# Plugin runtime
|
|
125
|
+
"PluginSettings",
|
|
126
|
+
"SyncState",
|
|
127
|
+
"create_plugin_app",
|
|
128
|
+
# Clients
|
|
129
|
+
"LLMClient",
|
|
130
|
+
"LLMOutputError",
|
|
131
|
+
"LLMToolCall",
|
|
132
|
+
"LLMToolCalls",
|
|
133
|
+
"LLMToolRound",
|
|
134
|
+
"SpotAPIError",
|
|
135
|
+
"SpotClient",
|
|
136
|
+
# Knowledge Store
|
|
137
|
+
"KnowledgeClient",
|
|
138
|
+
"KnowledgeDocument",
|
|
139
|
+
"KnowledgeTag",
|
|
140
|
+
"chunk_text",
|
|
141
|
+
"content_hash",
|
|
142
|
+
# Config models
|
|
143
|
+
"ConfigOption",
|
|
144
|
+
"ConfigStatus",
|
|
145
|
+
"ConfigUpdateRequest",
|
|
146
|
+
# Error response
|
|
147
|
+
"ErrorResponse",
|
|
148
|
+
# Logging
|
|
149
|
+
"configure_logging",
|
|
150
|
+
"get_logger",
|
|
151
|
+
"get_logging_config",
|
|
152
|
+
]
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Strings measured and cut by the size of their JSON.
|
|
2
|
+
|
|
3
|
+
The view and the LLM client's rendering are sent as JSON, where escapes make
|
|
4
|
+
a string larger than its text: a control character takes six bytes
|
|
5
|
+
(``\\u0001``). ``json.dumps`` with ``ensure_ascii=False`` and pydantic's
|
|
6
|
+
``model_dump_json`` escape strings alike, so sizes here are theirs.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
|
|
13
|
+
_MAX_CHARACTER_BYTES = 6
|
|
14
|
+
"""The most bytes one character takes: a control character's escape."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def json_size(text: str) -> int:
|
|
18
|
+
"""The UTF-8 bytes of ``text``'s JSON string between its quotes, escapes
|
|
19
|
+
included; a lone surrogate (possible in a string built in Python) counts
|
|
20
|
+
as three."""
|
|
21
|
+
escaped = json.dumps(text, ensure_ascii=False)[1:-1]
|
|
22
|
+
return len(escaped.encode("utf-8", "surrogatepass"))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def cut_json(text: str, max_bytes: int) -> tuple[str, bool]:
|
|
26
|
+
"""The longest start of ``text`` whose JSON string takes at most
|
|
27
|
+
``max_bytes`` bytes between its quotes (``json_size``), and whether that
|
|
28
|
+
cut anything.
|
|
29
|
+
|
|
30
|
+
A character is kept or dropped whole, so neither an escape nor a
|
|
31
|
+
character's UTF-8 bytes are split. The text is measured in chunks that
|
|
32
|
+
surely fit, a sixth of the room left each, then character by character:
|
|
33
|
+
the work follows ``max_bytes``, whatever the text's length.
|
|
34
|
+
"""
|
|
35
|
+
if len(text) * _MAX_CHARACTER_BYTES <= max_bytes:
|
|
36
|
+
return text, False # it surely fits
|
|
37
|
+
kept = 0
|
|
38
|
+
room = max_bytes
|
|
39
|
+
while kept < len(text):
|
|
40
|
+
chunk = text[kept : kept + max(room // _MAX_CHARACTER_BYTES, 1)]
|
|
41
|
+
used = json_size(chunk)
|
|
42
|
+
if used > room: # one character, which does not fit
|
|
43
|
+
break
|
|
44
|
+
kept += len(chunk)
|
|
45
|
+
room -= used
|
|
46
|
+
return text[:kept], kept < len(text)
|