soliplex 0.75.3__tar.gz → 0.76__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.
- {soliplex-0.75.3/src/soliplex.egg-info → soliplex-0.76}/PKG-INFO +34 -34
- {soliplex-0.75.3 → soliplex-0.76}/pyproject.toml +4 -3
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/agents.py +24 -6
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/agui/__init__.py +29 -0
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/agui/parser.py +33 -2
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/agui/persistence.py +5 -1
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/cli/admin_users.py +2 -2
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/cli/audit.py +1 -1
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/cli/room_authz.py +2 -2
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/config/agents.py +100 -44
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/config/installation.py +40 -10
- soliplex-0.76/src/soliplex/config/meta.py +687 -0
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/config/middleware.py +17 -5
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/config/quizzes.py +3 -1
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/config/rag.py +78 -19
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/config/rooms.py +24 -13
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/config/routing.py +78 -37
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/config/secrets.py +54 -3
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/config/skills.py +133 -11
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/config/tools.py +50 -12
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/secrets.py +1 -1
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/tools/agui_run_feedback.py +95 -6
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/util.py +3 -3
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/views/agui.py +7 -14
- {soliplex-0.75.3 → soliplex-0.76/src/soliplex.egg-info}/PKG-INFO +34 -34
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex.egg-info/requires.txt +2 -2
- soliplex-0.75.3/src/soliplex/config/meta.py +0 -398
- {soliplex-0.75.3 → soliplex-0.76}/LICENSE +0 -0
- {soliplex-0.75.3 → soliplex-0.76}/MANIFEST.in +0 -0
- {soliplex-0.75.3 → soliplex-0.76}/README.md +31 -31
- {soliplex-0.75.3 → soliplex-0.76}/example/skills/bare-bones/SKILL.md +0 -0
- {soliplex-0.75.3 → soliplex-0.76}/example/skills/stanzas/SKILL.md +0 -0
- {soliplex-0.75.3 → soliplex-0.76}/setup.cfg +0 -0
- {soliplex-0.75.3 → soliplex-0.76}/skills/soliplex-docs/SKILL.md +0 -0
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/agui/schema.py +0 -0
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/agui/util.py +0 -0
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/authn.py +0 -0
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/authz/__init__.py +0 -0
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/authz/persistence.py +0 -0
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/authz/schema.py +0 -0
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/capabilities/filesystem.py +0 -0
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/capabilities/rag_audit.py +0 -0
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/cli/ask.py +0 -0
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/cli/cli_util.py +0 -0
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/cli/main.py +0 -0
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/cli/ollama.py +0 -0
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/cli/serve.py +0 -0
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/cli/types.py +0 -0
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/completions.py +0 -0
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/config/__init__.py +0 -0
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/config/_utils.py +0 -0
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/config/agui.py +0 -0
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/config/authsystem.py +0 -0
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/config/completions.py +0 -0
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/config/exceptions.py +0 -0
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/config/logfire.py +0 -0
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/examples.py +0 -0
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/installation.py +0 -0
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/log_ingest.py +0 -0
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/loggers.py +0 -0
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/main.py +0 -0
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/mcp_auth.py +0 -0
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/mcp_client.py +0 -0
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/mcp_server.py +0 -0
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/models.py +0 -0
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/ollama.py +0 -0
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/quizzes.py +0 -0
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/rag_audit.py +0 -0
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/sandbox_audit.py +0 -0
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/skills/bwrap_sandbox/SKILL.md +0 -0
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/skills/bwrap_sandbox/__init__.py +0 -0
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/titles.py +0 -0
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/tools/__init__.py +0 -0
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/tools/file_uploads.py +0 -0
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/tools/rag.py +0 -0
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/tui/cli.py +0 -0
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/tui/main.py +0 -0
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/tui/rest_api.py +0 -0
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/tui/serve.py +0 -0
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/views/__init__.py +0 -0
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/views/authn.py +0 -0
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/views/authz.py +0 -0
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/views/completions.py +0 -0
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/views/installation.py +0 -0
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/views/log_ingest.py +0 -0
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/views/quizzes.py +0 -0
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/views/room_file_uploads.py +0 -0
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/views/rooms.py +0 -0
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/views/sandbox_workdirs.py +0 -0
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/views/stats.py +0 -0
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/views/streaming.py +0 -0
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex/views/thread_file_uploads.py +0 -0
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex.egg-info/SOURCES.txt +0 -0
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex.egg-info/dependency_links.txt +0 -0
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex.egg-info/entry_points.txt +0 -0
- {soliplex-0.75.3 → soliplex-0.76}/src/soliplex.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: soliplex
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.76
|
|
4
4
|
Summary: An AI-powered Retrieval-Augmented Generation (RAG) system with a modern web interface.
|
|
5
5
|
Author-email: Enfold <info@enfoldsystems.net>
|
|
6
6
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -13,13 +13,13 @@ Requires-Dist: aiohttp<3.15,>=3.14.2
|
|
|
13
13
|
Requires-Dist: aiosqlite
|
|
14
14
|
Requires-Dist: alembic<1.19,>=1.18.4
|
|
15
15
|
Requires-Dist: authlib
|
|
16
|
-
Requires-Dist: bubble-sandbox<0.
|
|
16
|
+
Requires-Dist: bubble-sandbox<0.12,>=0.11.0
|
|
17
17
|
Requires-Dist: cryptography>=50.0.0
|
|
18
18
|
Requires-Dist: fastapi
|
|
19
19
|
Requires-Dist: fastmcp<3.4,>=3.3.0
|
|
20
20
|
Requires-Dist: fakeredis!=2.35.0
|
|
21
21
|
Requires-Dist: greenlet
|
|
22
|
-
Requires-Dist: haiku.rag-slim<0.
|
|
22
|
+
Requires-Dist: haiku.rag-slim<0.75,>=0.74.0
|
|
23
23
|
Requires-Dist: idna>=3.15
|
|
24
24
|
Requires-Dist: itsdangerous
|
|
25
25
|
Requires-Dist: joserfc<1.7,>=1.6.7
|
|
@@ -46,6 +46,17 @@ Dynamic: license-file
|
|
|
46
46
|
|
|
47
47
|
An AI-powered Retrieval-Augmented Generation (RAG) system with a modern web interface.
|
|
48
48
|
|
|
49
|
+
## Overview
|
|
50
|
+
|
|
51
|
+
Soliplex is a self-hosted, full-stack RAG/AI system: a FastAPI backend
|
|
52
|
+
(a thin configuration and authentication layer around
|
|
53
|
+
[pydantic-ai](https://ai.pydantic.dev/)), a hardened
|
|
54
|
+
[Flutter client](https://github.com/soliplex/frontend), and a terminal UI.
|
|
55
|
+
|
|
56
|
+
See the [Overview](docs/overview.md) for what Soliplex is, when (and when not)
|
|
57
|
+
to use it, how it compares to alternatives, and the full feature and
|
|
58
|
+
architecture breakdown.
|
|
59
|
+
|
|
49
60
|
## Quickstart
|
|
50
61
|
|
|
51
62
|
The fastest way to stand up and run your own Soliplex is to let a
|
|
@@ -123,17 +134,6 @@ and then *"What is a Soliplex installation?"*
|
|
|
123
134
|
> and developing on the backend itself. If you only want to *use* Soliplex,
|
|
124
135
|
> the two skills above are all you need.
|
|
125
136
|
|
|
126
|
-
## Overview
|
|
127
|
-
|
|
128
|
-
Soliplex is a self-hosted, full-stack RAG/AI system: a FastAPI backend
|
|
129
|
-
(a thin configuration and authentication layer around
|
|
130
|
-
[pydantic-ai](https://ai.pydantic.dev/)), a hardened
|
|
131
|
-
[Flutter client](https://github.com/soliplex/frontend), and a terminal UI.
|
|
132
|
-
|
|
133
|
-
See the [Overview](docs/overview.md) for what Soliplex is, when (and when not)
|
|
134
|
-
to use it, how it compares to alternatives, and the full feature and
|
|
135
|
-
architecture breakdown.
|
|
136
|
-
|
|
137
137
|
## Running from source
|
|
138
138
|
|
|
139
139
|
The skills-based [Quickstart](#quickstart) above is the recommended path for
|
|
@@ -150,6 +150,26 @@ most users. To run Soliplex from a checkout of this repository instead, see:
|
|
|
150
150
|
- [Client Setup](docs/client.md) -- run the Flutter web client.
|
|
151
151
|
- [Docker Deployment](docs/docker.md) -- run the full stack with Docker Compose.
|
|
152
152
|
|
|
153
|
+
## Documentation
|
|
154
|
+
|
|
155
|
+
The full documentation set lives in [`docs/`](docs/) and is published as a
|
|
156
|
+
[Zensical site](https://soliplex.github.io/soliplex/):
|
|
157
|
+
|
|
158
|
+
> **Tip:** the same documentation is also available as the `soliplex-docs`
|
|
159
|
+
> Agent Skill, so a skills-compatible agent can answer questions straight from
|
|
160
|
+
> it -- see the [Quickstart](#quickstart) above.
|
|
161
|
+
|
|
162
|
+
- [Overview](docs/overview.md)
|
|
163
|
+
- [Prerequisites](docs/prerequisites.md)
|
|
164
|
+
- [Server Setup](docs/server/index.md) and
|
|
165
|
+
[CLI Reference](docs/server/cli.md)
|
|
166
|
+
- [Client Setup](docs/client.md)
|
|
167
|
+
- [Docker Deployment](docs/docker.md)
|
|
168
|
+
- [RAG Database](docs/rag.md)
|
|
169
|
+
- [Documentation Skill](docs/docs-skill.md)
|
|
170
|
+
- [Configuration](docs/config/)
|
|
171
|
+
- [Usage](docs/usage.md)
|
|
172
|
+
|
|
153
173
|
## Configuration
|
|
154
174
|
|
|
155
175
|
Soliplex is configured by a tree of YAML files rooted at an
|
|
@@ -173,26 +193,6 @@ Soliplex is configured by a tree of YAML files rooted at an
|
|
|
173
193
|
[Console logging](docs/config/logging.md),
|
|
174
194
|
[SQLAlchemy DBURIs](docs/config/dburis.md)
|
|
175
195
|
|
|
176
|
-
## Documentation
|
|
177
|
-
|
|
178
|
-
The full documentation set lives in [`docs/`](docs/) and is published as a
|
|
179
|
-
[Zensical site](https://soliplex.github.io/soliplex/):
|
|
180
|
-
|
|
181
|
-
> **Tip:** the same documentation is also available as the `soliplex-docs`
|
|
182
|
-
> Agent Skill, so a skills-compatible agent can answer questions straight from
|
|
183
|
-
> it -- see the [Quickstart](#quickstart) above.
|
|
184
|
-
|
|
185
|
-
- [Overview](docs/overview.md)
|
|
186
|
-
- [Prerequisites](docs/prerequisites.md)
|
|
187
|
-
- [Server Setup](docs/server/index.md) and
|
|
188
|
-
[CLI Reference](docs/server/cli.md)
|
|
189
|
-
- [Client Setup](docs/client.md)
|
|
190
|
-
- [Docker Deployment](docs/docker.md)
|
|
191
|
-
- [RAG Database](docs/rag.md)
|
|
192
|
-
- [Documentation Skill](docs/docs-skill.md)
|
|
193
|
-
- [Configuration](docs/config/)
|
|
194
|
-
- [Usage](docs/usage.md)
|
|
195
|
-
|
|
196
196
|
## Development
|
|
197
197
|
|
|
198
198
|
Contributor guidance -- setup, build and test commands, code style, the
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "soliplex"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.76"
|
|
8
8
|
description = "An AI-powered Retrieval-Augmented Generation (RAG) system with a modern web interface."
|
|
9
9
|
authors = [{ name = "Enfold", email = "info@enfoldsystems.net" }]
|
|
10
10
|
readme = "README.md"
|
|
@@ -19,13 +19,13 @@ dependencies = [
|
|
|
19
19
|
"aiosqlite",
|
|
20
20
|
"alembic >= 1.18.4, < 1.19",
|
|
21
21
|
"authlib",
|
|
22
|
-
"bubble-sandbox >= 0.
|
|
22
|
+
"bubble-sandbox >= 0.11.0, < 0.12",
|
|
23
23
|
"cryptography >= 50.0.0", # PYSEC-2026-3552
|
|
24
24
|
"fastapi",
|
|
25
25
|
"fastmcp >= 3.3.0, < 3.4",
|
|
26
26
|
"fakeredis != 2.35.0", # brownbag
|
|
27
27
|
"greenlet",
|
|
28
|
-
"haiku.rag-slim >= 0.
|
|
28
|
+
"haiku.rag-slim >= 0.74.0, < 0.75",
|
|
29
29
|
"idna >= 3.15",
|
|
30
30
|
"itsdangerous",
|
|
31
31
|
"joserfc >= 1.6.7, < 1.7", # CVE-2026-48990
|
|
@@ -93,6 +93,7 @@ markers = [
|
|
|
93
93
|
|
|
94
94
|
[tool.coverage.run]
|
|
95
95
|
omit = [
|
|
96
|
+
"scripts/lint_textio.py", # runs its own '--self-test' during lint
|
|
96
97
|
"src/soliplex/cli/__init__.py",
|
|
97
98
|
"src/soliplex/tui.py",
|
|
98
99
|
]
|
|
@@ -4,8 +4,10 @@ import dataclasses
|
|
|
4
4
|
import typing
|
|
5
5
|
|
|
6
6
|
import pydantic_ai
|
|
7
|
-
from haiku.rag.capabilities import
|
|
8
|
-
from haiku.rag.capabilities import
|
|
7
|
+
from haiku.rag.capabilities import analysis as hr_caps_analysis
|
|
8
|
+
from haiku.rag.capabilities import compaction as hr_caps_compaction
|
|
9
|
+
from haiku.rag.capabilities import policy as hr_caps_policy
|
|
10
|
+
from haiku.rag.capabilities import rag as hr_caps_rag
|
|
9
11
|
from pydantic_ai import agent as ai_agent
|
|
10
12
|
from pydantic_ai import capabilities as ai_capabilities
|
|
11
13
|
from pydantic_ai import tools as ai_tools
|
|
@@ -20,6 +22,18 @@ from soliplex.config import tools as config_tools
|
|
|
20
22
|
|
|
21
23
|
ToolConfigMap = dict[str, typing.Any]
|
|
22
24
|
|
|
25
|
+
# Capabilities which take 'vision' from agent config's 'multimodal'
|
|
26
|
+
HR_VisionCapabilities = (
|
|
27
|
+
hr_caps_rag.RAGCapability | hr_caps_analysis.AnalysisCapability
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# Capabilities which can always be eagerly loaded
|
|
31
|
+
HookOnlyCapabilities = (
|
|
32
|
+
hr_caps_compaction.EvidenceCompactionCapability
|
|
33
|
+
| hr_caps_policy.CitationPolicyCapability
|
|
34
|
+
| cap_rag_audit.RAGAccessAuditCapability
|
|
35
|
+
)
|
|
36
|
+
|
|
23
37
|
|
|
24
38
|
class CapabilityConfig(typing.Protocol):
|
|
25
39
|
"""Room-specific native capabilities and their audit targets."""
|
|
@@ -114,16 +128,20 @@ def get_default_agent_from_configs(
|
|
|
114
128
|
# agent's model, not haiku.rag's configured model, so gate on the room
|
|
115
129
|
# agent's declared multimodality.
|
|
116
130
|
for capability in capabilities:
|
|
117
|
-
if isinstance(capability,
|
|
131
|
+
if isinstance(capability, HR_VisionCapabilities):
|
|
118
132
|
capability.vision = agent_config.multimodal
|
|
119
133
|
|
|
120
134
|
# A single model-facing capability loads eagerly so its tools are visible;
|
|
121
|
-
#
|
|
122
|
-
#
|
|
135
|
+
#
|
|
136
|
+
# Multiple stay deferred so the model routes between them via
|
|
137
|
+
# 'load_capability'.
|
|
138
|
+
#
|
|
139
|
+
# Hook-only capabilities expose no tools to route
|
|
140
|
+
# between, so they neither defer nor count.
|
|
123
141
|
routing_capabilities = [
|
|
124
142
|
capability
|
|
125
143
|
for capability in capabilities
|
|
126
|
-
if not isinstance(capability,
|
|
144
|
+
if not isinstance(capability, HookOnlyCapabilities)
|
|
127
145
|
]
|
|
128
146
|
defer_loading = len(routing_capabilities) > 1
|
|
129
147
|
for capability in routing_capabilities:
|
|
@@ -8,6 +8,7 @@ import typing
|
|
|
8
8
|
|
|
9
9
|
import fastapi
|
|
10
10
|
from ag_ui import core as agui_core
|
|
11
|
+
from pydantic_ai import ui as ai_ui
|
|
11
12
|
from sqlalchemy.ext import asyncio as sqla_asyncio
|
|
12
13
|
|
|
13
14
|
AGUI_Events = list[agui_core.Event]
|
|
@@ -22,6 +23,16 @@ _COMPACTIBLE_TYPES = { # event_type: compacting_attr
|
|
|
22
23
|
agui_core.EventType.TOOL_CALL_ARGS: "tool_call_id",
|
|
23
24
|
}
|
|
24
25
|
|
|
26
|
+
#
|
|
27
|
+
# Events which terminate a run's event stream
|
|
28
|
+
#
|
|
29
|
+
TERMINAL_EVENT_TYPES = frozenset(
|
|
30
|
+
[
|
|
31
|
+
agui_core.EventType.RUN_FINISHED,
|
|
32
|
+
agui_core.EventType.RUN_ERROR,
|
|
33
|
+
]
|
|
34
|
+
)
|
|
35
|
+
|
|
25
36
|
|
|
26
37
|
class AGUI_Exception(ValueError):
|
|
27
38
|
status_code = 400
|
|
@@ -582,6 +593,24 @@ async def compact_event_stream(stream: AGUI_EventStream):
|
|
|
582
593
|
yield event
|
|
583
594
|
|
|
584
595
|
|
|
596
|
+
async def with_final_state(
|
|
597
|
+
*,
|
|
598
|
+
stream: AGUI_EventStream,
|
|
599
|
+
deps: ai_ui.StateHandler | None,
|
|
600
|
+
) -> AGUI_EventStream:
|
|
601
|
+
"""Yield events from 'stream', adding a final AG-UI state snapshot.
|
|
602
|
+
|
|
603
|
+
The snapshot precedes the run's terminal event: AG-UI clients may stop
|
|
604
|
+
reading after a terminal event, and on replay the parser only accepts a
|
|
605
|
+
snapshot while the run is still in the RUNNING state.
|
|
606
|
+
"""
|
|
607
|
+
async for event in stream:
|
|
608
|
+
if deps is not None and event.type in TERMINAL_EVENT_TYPES:
|
|
609
|
+
yield agui_core.StateSnapshotEvent(snapshot=deps.state)
|
|
610
|
+
|
|
611
|
+
yield event
|
|
612
|
+
|
|
613
|
+
|
|
585
614
|
async def get_the_threads(request: fastapi.Request) -> ThreadStorage:
|
|
586
615
|
from . import persistence
|
|
587
616
|
|
|
@@ -244,6 +244,15 @@ class EventStreamParser:
|
|
|
244
244
|
result: typing.Any = None
|
|
245
245
|
|
|
246
246
|
state: dict = dataclasses.field(default_factory=dict)
|
|
247
|
+
|
|
248
|
+
# Currently-skipped deltas: non-empty means our 'state' is incomplete.
|
|
249
|
+
# See 'STATE_DELTA' handling below (#1257).
|
|
250
|
+
invalid_state_deltas: Events = dataclasses.field(default_factory=list)
|
|
251
|
+
|
|
252
|
+
# All skipped deltas, preserved even when a snapshot heals our 'state'.
|
|
253
|
+
# See 'STATE_DELTA' handling below (#1257).
|
|
254
|
+
skipped_state_deltas: Events = dataclasses.field(default_factory=list)
|
|
255
|
+
|
|
247
256
|
messages: Messages = dataclasses.field(default_factory=list)
|
|
248
257
|
|
|
249
258
|
messages_by_id: MessagesByID = dataclasses.field(default_factory=dict)
|
|
@@ -484,15 +493,37 @@ class EventStreamParser:
|
|
|
484
493
|
case agui_core.EventType.STATE_DELTA:
|
|
485
494
|
self._assert_running(event)
|
|
486
495
|
|
|
487
|
-
|
|
496
|
+
try:
|
|
497
|
+
new_state = jsonpatch.apply_patch(self.state, event.delta)
|
|
498
|
+
|
|
499
|
+
except (
|
|
500
|
+
jsonpatch.JsonPatchException,
|
|
501
|
+
jsonpatch.JsonPointerException,
|
|
502
|
+
):
|
|
503
|
+
# The delta was recorded against state we don't have,
|
|
504
|
+
# and therefore cannot be applied. Skip it, but record
|
|
505
|
+
# it twice:
|
|
506
|
+
#
|
|
507
|
+
# - 'invalid_state_deltas' signals that our current 'state'
|
|
508
|
+
# is incomplete.
|
|
509
|
+
#
|
|
510
|
+
# - 'skipped_state_deltas' preserves the skip for later
|
|
511
|
+
# debugging, even when a subsequent 'STATE_SNAPSHOT"
|
|
512
|
+
# clears the invalid list.
|
|
513
|
+
self.invalid_state_deltas.append(event)
|
|
514
|
+
self.skipped_state_deltas.append(event)
|
|
488
515
|
|
|
489
|
-
|
|
516
|
+
else:
|
|
517
|
+
self.state = new_state
|
|
490
518
|
|
|
491
519
|
case agui_core.EventType.STATE_SNAPSHOT:
|
|
492
520
|
self._assert_running(event)
|
|
493
521
|
|
|
494
522
|
self.state = event.snapshot
|
|
495
523
|
|
|
524
|
+
# Clear the invalid list, because our 'state' is now correct.
|
|
525
|
+
self.invalid_state_deltas.clear()
|
|
526
|
+
|
|
496
527
|
case agui_core.EventType.MESSAGES_SNAPSHOT:
|
|
497
528
|
self._assert_running(event)
|
|
498
529
|
|
|
@@ -962,7 +962,11 @@ async def drive_agui_turn(
|
|
|
962
962
|
|
|
963
963
|
``run_stream_kwargs`` is forwarded verbatim to the Pydantic AI adapter.
|
|
964
964
|
"""
|
|
965
|
-
|
|
965
|
+
stream_kwargs = run_stream_kwargs or {}
|
|
966
|
+
event_stream = agui.with_final_state(
|
|
967
|
+
stream=adapter.run_stream(**stream_kwargs),
|
|
968
|
+
deps=stream_kwargs.get("deps"),
|
|
969
|
+
)
|
|
966
970
|
compacted = agui.compact_event_stream(event_stream)
|
|
967
971
|
async for event in compacted:
|
|
968
972
|
try:
|
|
@@ -472,7 +472,7 @@ def admin_users_as_yaml(
|
|
|
472
472
|
yaml_text = _admin_users_as_yaml(discriminators)
|
|
473
473
|
|
|
474
474
|
if output is not None:
|
|
475
|
-
output.write_text(yaml_text)
|
|
475
|
+
output.write_text(yaml_text, encoding="utf-8")
|
|
476
476
|
else:
|
|
477
477
|
print(yaml_text, end="")
|
|
478
478
|
|
|
@@ -508,7 +508,7 @@ def admin_users_from_yaml(
|
|
|
508
508
|
cli_util._check_ram_dburi(dburi, "admin-users from-yaml")
|
|
509
509
|
|
|
510
510
|
if input_ is not None:
|
|
511
|
-
yaml_text = input_.read_text()
|
|
511
|
+
yaml_text = input_.read_text(encoding="utf-8")
|
|
512
512
|
else:
|
|
513
513
|
yaml_text = sys.stdin.read()
|
|
514
514
|
|
|
@@ -1060,7 +1060,7 @@ def _load_logging_config(the_installation):
|
|
|
1060
1060
|
pyl_config = the_installation._config.logging_config_file
|
|
1061
1061
|
if pyl_config is None:
|
|
1062
1062
|
return None
|
|
1063
|
-
with pyl_config.open() as f:
|
|
1063
|
+
with pyl_config.open(encoding="utf-8") as f:
|
|
1064
1064
|
return yaml.safe_load(f)
|
|
1065
1065
|
|
|
1066
1066
|
|
|
@@ -325,7 +325,7 @@ def room_authz_as_yaml(
|
|
|
325
325
|
yaml_text = _room_policy_as_yaml(policy)
|
|
326
326
|
|
|
327
327
|
if output is not None:
|
|
328
|
-
output.write_text(yaml_text)
|
|
328
|
+
output.write_text(yaml_text, encoding="utf-8")
|
|
329
329
|
else:
|
|
330
330
|
print(yaml_text, end="")
|
|
331
331
|
|
|
@@ -372,7 +372,7 @@ def room_authz_from_yaml(
|
|
|
372
372
|
cli_util._check_ram_dburi(dburi, "room-authz from-yaml")
|
|
373
373
|
|
|
374
374
|
if input_ is not None:
|
|
375
|
-
yaml_text = input_.read_text()
|
|
375
|
+
yaml_text = input_.read_text(encoding="utf-8")
|
|
376
376
|
else:
|
|
377
377
|
yaml_text = sys.stdin.read()
|
|
378
378
|
|
|
@@ -5,6 +5,7 @@ import enum
|
|
|
5
5
|
import functools
|
|
6
6
|
import pathlib
|
|
7
7
|
import typing
|
|
8
|
+
import warnings
|
|
8
9
|
from collections import abc
|
|
9
10
|
|
|
10
11
|
from pydantic_ai import capabilities as ai_capabilities
|
|
@@ -50,13 +51,29 @@ class UnknownCapability(KeyError):
|
|
|
50
51
|
def __init__(self, name, _config_path=None):
|
|
51
52
|
self.name = name
|
|
52
53
|
self._config_path = _config_path
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
54
|
+
|
|
55
|
+
super().__init__(
|
|
56
|
+
f"Unknown capability name: {name} (configured in {_config_path})"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class UnknownAgentConfigKind(KeyError):
|
|
61
|
+
def __init__(self, kind, _config_path=None):
|
|
62
|
+
self.kind = kind
|
|
63
|
+
self._config_path = _config_path
|
|
64
|
+
super().__init__(
|
|
65
|
+
f"Unknown agent config kind: {kind} (configured in {_config_path})"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class AgentConfigKindMismatch(ValueError):
|
|
70
|
+
def __init__(self, found_kind, expected_kind):
|
|
71
|
+
self.found_kind = found_kind
|
|
72
|
+
self.expected_kind = expected_kind
|
|
73
|
+
super().__init__(
|
|
74
|
+
"Agent config kind mismatch: "
|
|
75
|
+
f"found '{found_kind}', expected '{expected_kind}'"
|
|
76
|
+
)
|
|
60
77
|
|
|
61
78
|
|
|
62
79
|
class LLMProviderType(enum.StrEnum):
|
|
@@ -97,6 +114,13 @@ def _apply_agent_config_template(
|
|
|
97
114
|
return config_dict
|
|
98
115
|
|
|
99
116
|
|
|
117
|
+
ACC_BBB_NO_NAME_KEY_DEPRECATED = """\
|
|
118
|
+
AgentCapabilityConfig.from_yaml: '{<name>: <kwargs>}' form is deprecated;
|
|
119
|
+
use '{"name": <name>, "kwargs": <kwargs>}' instead. Support for the form
|
|
120
|
+
will be removed after 'v0.76'.
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
|
|
100
124
|
@dataclasses.dataclass(kw_only=True)
|
|
101
125
|
class AgentCapabilityConfig:
|
|
102
126
|
name: str
|
|
@@ -104,36 +128,47 @@ class AgentCapabilityConfig:
|
|
|
104
128
|
|
|
105
129
|
_config_path: pathlib.Path = None
|
|
106
130
|
|
|
131
|
+
@classmethod
|
|
132
|
+
def from_yaml(
|
|
133
|
+
cls,
|
|
134
|
+
config_path: pathlib.Path,
|
|
135
|
+
config_dict_or_str: str | dict,
|
|
136
|
+
):
|
|
137
|
+
if isinstance(config_dict_or_str, str):
|
|
138
|
+
name = config_dict_or_str
|
|
139
|
+
config_dict = {"name": name}
|
|
140
|
+
else:
|
|
141
|
+
if "name" not in config_dict_or_str: # BBB deprecated
|
|
142
|
+
warnings.warn(
|
|
143
|
+
ACC_BBB_NO_NAME_KEY_DEPRECATED,
|
|
144
|
+
DeprecationWarning,
|
|
145
|
+
stacklevel=2,
|
|
146
|
+
)
|
|
147
|
+
((name, kwargs),) = config_dict_or_str.items()
|
|
148
|
+
config_dict = {"name": name, "kwargs": kwargs}
|
|
149
|
+
else:
|
|
150
|
+
name = config_dict_or_str["name"]
|
|
151
|
+
config_dict = config_dict_or_str
|
|
152
|
+
|
|
153
|
+
if name not in AGENT_CAPABILITY_CLASSES_BY_NAME:
|
|
154
|
+
raise UnknownCapability(name, config_path)
|
|
155
|
+
|
|
156
|
+
return cls(**config_dict, _config_path=config_path)
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def as_yaml(self) -> dict:
|
|
160
|
+
return {"name": self.name, "kwargs": self.kwargs}
|
|
161
|
+
|
|
107
162
|
@property
|
|
108
163
|
def as_capability(self) -> ai_capabilities.AbstractCapability:
|
|
109
164
|
try:
|
|
110
165
|
cap_klass = AGENT_CAPABILITY_CLASSES_BY_NAME[self.name]
|
|
111
166
|
except KeyError:
|
|
112
|
-
raise UnknownCapability(self.name) from None
|
|
167
|
+
raise UnknownCapability(self.name, self._config_path) from None
|
|
113
168
|
|
|
114
169
|
return cap_klass(**self.kwargs)
|
|
115
170
|
|
|
116
171
|
|
|
117
|
-
def extract_capability_config(
|
|
118
|
-
cap_config: str | dict[str, typing.Any],
|
|
119
|
-
_config_path: pathlib.Path,
|
|
120
|
-
) -> AgentCapabilityConfig:
|
|
121
|
-
if isinstance(cap_config, str):
|
|
122
|
-
name = cap_config
|
|
123
|
-
kwargs = {}
|
|
124
|
-
else:
|
|
125
|
-
((name, kwargs),) = cap_config.items()
|
|
126
|
-
|
|
127
|
-
if name not in AGENT_CAPABILITY_CLASSES_BY_NAME:
|
|
128
|
-
raise UnknownCapability(name, _config_path)
|
|
129
|
-
|
|
130
|
-
return AgentCapabilityConfig(
|
|
131
|
-
name=name,
|
|
132
|
-
kwargs=kwargs,
|
|
133
|
-
_config_path=_config_path,
|
|
134
|
-
)
|
|
135
|
-
|
|
136
|
-
|
|
137
172
|
@dataclasses.dataclass(kw_only=True)
|
|
138
173
|
class AgentConfig:
|
|
139
174
|
#
|
|
@@ -177,6 +212,11 @@ class AgentConfig:
|
|
|
177
212
|
if system_prompt is not None:
|
|
178
213
|
self._system_prompt_text = system_prompt
|
|
179
214
|
|
|
215
|
+
@classmethod
|
|
216
|
+
def _check_kind(cls, kind):
|
|
217
|
+
if kind not in (None, cls.kind):
|
|
218
|
+
raise AgentConfigKindMismatch(kind, cls.kind)
|
|
219
|
+
|
|
180
220
|
@classmethod
|
|
181
221
|
def from_yaml(
|
|
182
222
|
cls,
|
|
@@ -185,6 +225,9 @@ class AgentConfig:
|
|
|
185
225
|
config_dict: dict,
|
|
186
226
|
):
|
|
187
227
|
try:
|
|
228
|
+
kind = config_dict.pop("kind", None)
|
|
229
|
+
cls._check_kind(kind)
|
|
230
|
+
|
|
188
231
|
config_dict["_installation_config"] = installation_config
|
|
189
232
|
config_dict["_config_path"] = config_path
|
|
190
233
|
|
|
@@ -210,7 +253,7 @@ class AgentConfig:
|
|
|
210
253
|
capabilities = config_dict.pop("capabilities", None)
|
|
211
254
|
if capabilities is not None:
|
|
212
255
|
config_dict["_capability_configs"] = [
|
|
213
|
-
|
|
256
|
+
AgentCapabilityConfig.from_yaml(config_path, cap)
|
|
214
257
|
for cap in capabilities
|
|
215
258
|
]
|
|
216
259
|
|
|
@@ -236,7 +279,7 @@ class AgentConfig:
|
|
|
236
279
|
system_prompt_file = (
|
|
237
280
|
self._config_path.parent / self._system_prompt_path
|
|
238
281
|
)
|
|
239
|
-
return system_prompt_file.read_text()
|
|
282
|
+
return system_prompt_file.read_text(encoding="utf-8")
|
|
240
283
|
|
|
241
284
|
else: # pragma: NO COVER
|
|
242
285
|
pass
|
|
@@ -282,24 +325,25 @@ class AgentConfig:
|
|
|
282
325
|
if self._system_prompt_text is None
|
|
283
326
|
else self._system_prompt_text
|
|
284
327
|
)
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
)
|
|
289
|
-
|
|
290
|
-
provider_base_url = self.provider_base_url
|
|
328
|
+
capabilities = {}
|
|
329
|
+
|
|
330
|
+
for cap_cfg in self._capability_configs:
|
|
331
|
+
cap_list = capabilities.setdefault("capabilities", [])
|
|
332
|
+
cap_list.append(cap_cfg.as_yaml)
|
|
291
333
|
|
|
292
334
|
return {
|
|
293
335
|
"id": self.id,
|
|
336
|
+
"kind": self.kind,
|
|
294
337
|
"model_name": self.model_name,
|
|
295
338
|
"retries": self.retries,
|
|
296
339
|
"system_prompt": prompt,
|
|
297
340
|
"model_settings": self.model_settings,
|
|
298
341
|
"multimodal": self.multimodal,
|
|
299
342
|
"provider_type": str(self.provider_type),
|
|
300
|
-
"provider_base_url": provider_base_url,
|
|
343
|
+
"provider_base_url": self.provider_base_url,
|
|
301
344
|
"provider_key": self.provider_key, # "secret:SECRET_NAME"
|
|
302
|
-
|
|
345
|
+
"agui_feature_names": self.agui_feature_names,
|
|
346
|
+
} | capabilities
|
|
303
347
|
|
|
304
348
|
|
|
305
349
|
AgentFactory = abc.Callable[[], ai_ag_abstract.AbstractAgent]
|
|
@@ -342,6 +386,11 @@ class FactoryAgentConfig:
|
|
|
342
386
|
|
|
343
387
|
return self._factory
|
|
344
388
|
|
|
389
|
+
@classmethod
|
|
390
|
+
def _check_kind(cls, kind):
|
|
391
|
+
if kind not in (None, cls.kind):
|
|
392
|
+
raise AgentConfigKindMismatch(kind, cls.kind)
|
|
393
|
+
|
|
345
394
|
@classmethod
|
|
346
395
|
def from_yaml(
|
|
347
396
|
cls,
|
|
@@ -350,6 +399,9 @@ class FactoryAgentConfig:
|
|
|
350
399
|
config_dict: dict,
|
|
351
400
|
):
|
|
352
401
|
try:
|
|
402
|
+
kind = config_dict.pop("kind", None)
|
|
403
|
+
cls._check_kind(kind)
|
|
404
|
+
|
|
353
405
|
config_dict["_installation_config"] = installation_config
|
|
354
406
|
config_dict["_config_path"] = config_path
|
|
355
407
|
|
|
@@ -379,6 +431,7 @@ class FactoryAgentConfig:
|
|
|
379
431
|
"factory_name": self.factory_name,
|
|
380
432
|
"with_agent_config": self.with_agent_config,
|
|
381
433
|
"extra_config": self.extra_config,
|
|
434
|
+
"agui_feature_names": self.agui_feature_names,
|
|
382
435
|
}
|
|
383
436
|
|
|
384
437
|
|
|
@@ -400,14 +453,17 @@ def extract_agent_config(
|
|
|
400
453
|
config_path: pathlib.Path,
|
|
401
454
|
config_dict: dict,
|
|
402
455
|
) -> AgentConfig: # or subclass
|
|
403
|
-
agent_kind = config_dict.get("kind")
|
|
404
456
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
key: value for key, value in config_dict.items() if key != "kind"
|
|
408
|
-
}
|
|
457
|
+
# YAML not required to specify 'kind'
|
|
458
|
+
agent_kind = config_dict.get("kind", AgentConfig.kind)
|
|
409
459
|
|
|
410
|
-
|
|
460
|
+
try:
|
|
461
|
+
ac_class = AGENT_CONFIG_CLASSES_BY_KIND[agent_kind]
|
|
462
|
+
except KeyError:
|
|
463
|
+
raise UnknownAgentConfigKind(
|
|
464
|
+
agent_kind,
|
|
465
|
+
config_path,
|
|
466
|
+
) from None
|
|
411
467
|
|
|
412
468
|
return ac_class.from_yaml(
|
|
413
469
|
installation_config,
|