soliplex 0.75.4__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.
Files changed (96) hide show
  1. {soliplex-0.75.4/src/soliplex.egg-info → soliplex-0.76}/PKG-INFO +34 -34
  2. {soliplex-0.75.4 → soliplex-0.76}/pyproject.toml +3 -3
  3. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/agents.py +24 -6
  4. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/config/agents.py +99 -43
  5. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/config/installation.py +38 -8
  6. soliplex-0.76/src/soliplex/config/meta.py +687 -0
  7. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/config/middleware.py +17 -5
  8. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/config/rag.py +78 -19
  9. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/config/rooms.py +24 -13
  10. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/config/routing.py +78 -37
  11. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/config/secrets.py +54 -3
  12. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/config/skills.py +133 -11
  13. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/config/tools.py +50 -12
  14. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/views/agui.py +2 -2
  15. {soliplex-0.75.4 → soliplex-0.76/src/soliplex.egg-info}/PKG-INFO +34 -34
  16. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex.egg-info/requires.txt +2 -2
  17. soliplex-0.75.4/src/soliplex/config/meta.py +0 -398
  18. {soliplex-0.75.4 → soliplex-0.76}/LICENSE +0 -0
  19. {soliplex-0.75.4 → soliplex-0.76}/MANIFEST.in +0 -0
  20. {soliplex-0.75.4 → soliplex-0.76}/README.md +31 -31
  21. {soliplex-0.75.4 → soliplex-0.76}/example/skills/bare-bones/SKILL.md +0 -0
  22. {soliplex-0.75.4 → soliplex-0.76}/example/skills/stanzas/SKILL.md +0 -0
  23. {soliplex-0.75.4 → soliplex-0.76}/setup.cfg +0 -0
  24. {soliplex-0.75.4 → soliplex-0.76}/skills/soliplex-docs/SKILL.md +0 -0
  25. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/agui/__init__.py +0 -0
  26. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/agui/parser.py +0 -0
  27. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/agui/persistence.py +0 -0
  28. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/agui/schema.py +0 -0
  29. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/agui/util.py +0 -0
  30. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/authn.py +0 -0
  31. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/authz/__init__.py +0 -0
  32. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/authz/persistence.py +0 -0
  33. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/authz/schema.py +0 -0
  34. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/capabilities/filesystem.py +0 -0
  35. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/capabilities/rag_audit.py +0 -0
  36. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/cli/admin_users.py +0 -0
  37. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/cli/ask.py +0 -0
  38. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/cli/audit.py +0 -0
  39. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/cli/cli_util.py +0 -0
  40. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/cli/main.py +0 -0
  41. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/cli/ollama.py +0 -0
  42. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/cli/room_authz.py +0 -0
  43. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/cli/serve.py +0 -0
  44. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/cli/types.py +0 -0
  45. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/completions.py +0 -0
  46. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/config/__init__.py +0 -0
  47. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/config/_utils.py +0 -0
  48. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/config/agui.py +0 -0
  49. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/config/authsystem.py +0 -0
  50. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/config/completions.py +0 -0
  51. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/config/exceptions.py +0 -0
  52. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/config/logfire.py +0 -0
  53. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/config/quizzes.py +0 -0
  54. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/examples.py +0 -0
  55. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/installation.py +0 -0
  56. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/log_ingest.py +0 -0
  57. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/loggers.py +0 -0
  58. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/main.py +0 -0
  59. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/mcp_auth.py +0 -0
  60. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/mcp_client.py +0 -0
  61. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/mcp_server.py +0 -0
  62. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/models.py +0 -0
  63. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/ollama.py +0 -0
  64. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/quizzes.py +0 -0
  65. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/rag_audit.py +0 -0
  66. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/sandbox_audit.py +0 -0
  67. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/secrets.py +0 -0
  68. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/skills/bwrap_sandbox/SKILL.md +0 -0
  69. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/skills/bwrap_sandbox/__init__.py +0 -0
  70. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/titles.py +0 -0
  71. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/tools/__init__.py +0 -0
  72. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/tools/agui_run_feedback.py +0 -0
  73. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/tools/file_uploads.py +0 -0
  74. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/tools/rag.py +0 -0
  75. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/tui/cli.py +0 -0
  76. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/tui/main.py +0 -0
  77. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/tui/rest_api.py +0 -0
  78. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/tui/serve.py +0 -0
  79. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/util.py +0 -0
  80. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/views/__init__.py +0 -0
  81. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/views/authn.py +0 -0
  82. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/views/authz.py +0 -0
  83. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/views/completions.py +0 -0
  84. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/views/installation.py +0 -0
  85. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/views/log_ingest.py +0 -0
  86. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/views/quizzes.py +0 -0
  87. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/views/room_file_uploads.py +0 -0
  88. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/views/rooms.py +0 -0
  89. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/views/sandbox_workdirs.py +0 -0
  90. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/views/stats.py +0 -0
  91. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/views/streaming.py +0 -0
  92. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex/views/thread_file_uploads.py +0 -0
  93. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex.egg-info/SOURCES.txt +0 -0
  94. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex.egg-info/dependency_links.txt +0 -0
  95. {soliplex-0.75.4 → soliplex-0.76}/src/soliplex.egg-info/entry_points.txt +0 -0
  96. {soliplex-0.75.4 → 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.75.4
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.11,>=0.10.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.74,>=0.73.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.75.4"
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.10.0, < 0.11",
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.73.0, < 0.74",
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
@@ -4,8 +4,10 @@ import dataclasses
4
4
  import typing
5
5
 
6
6
  import pydantic_ai
7
- from haiku.rag.capabilities import AnalysisCapability
8
- from haiku.rag.capabilities import RAGCapability
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, (RAGCapability, AnalysisCapability)):
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
- # multiple stay deferred so the model routes between them via
122
- # load_capability. The audit capability is hook-only and always eager.
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, cap_rag_audit.RAGAccessAuditCapability)
144
+ if not isinstance(capability, HookOnlyCapabilities)
127
145
  ]
128
146
  defer_loading = len(routing_capabilities) > 1
129
147
  for capability in routing_capabilities:
@@ -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
- if _config_path is not None:
54
- super().__init__(
55
- f"Unknown capability name: {name} "
56
- f"(configured in {_config_path})"
57
- )
58
- else:
59
- super().__init__(f"Unknown capability name: {name}")
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
- extract_capability_config(cap, config_path)
256
+ AgentCapabilityConfig.from_yaml(config_path, cap)
214
257
  for cap in capabilities
215
258
  ]
216
259
 
@@ -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
- if self.provider_base_url is None:
286
- provider_base_url = self._installation_config.get_environment(
287
- "OLLAMA_BASE_URL"
288
- )
289
- else:
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
- if agent_kind is not None: # kind is a typing.ClassVar
406
- config_dict = {
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
- ac_class = AGENT_CONFIG_CLASSES_BY_KIND.get(agent_kind, AgentConfig)
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,
@@ -857,13 +857,14 @@ class InstallationConfig:
857
857
  for mwc_yaml in config_dict.get("middleware_stack", ())
858
858
  ]
859
859
 
860
- config_dict["app_router_operations"] = [
861
- config_routing.app_router_operation_from_yaml(
862
- config_path,
863
- ar_yaml,
864
- )
865
- for ar_yaml in config_dict.get("app_router_operations", ())
866
- ]
860
+ aro_klasses_by_kind = config_routing.APP_ROUTER_OPERATIONS_BY_KIND
861
+ aros, aros_cfg = config_dict.pop("app_router_operations", []), []
862
+
863
+ for aro in aros:
864
+ klass = aro_klasses_by_kind[aro["kind"]]
865
+ aros_cfg.append(klass.from_yaml(config_path, aro))
866
+
867
+ config_dict["app_router_operations"] = aros_cfg
867
868
 
868
869
  secret_configs = [
869
870
  config_secrets.SecretConfig.from_yaml(
@@ -1067,6 +1068,8 @@ class InstallationConfig:
1067
1068
  def as_yaml(self) -> dict:
1068
1069
  result = {
1069
1070
  "id": self.id,
1071
+ "server_name": self.server_name,
1072
+ "server_description": self.server_description,
1070
1073
  "meta": self.meta.as_yaml,
1071
1074
  "secrets": [secret.as_yaml for secret in self.secrets],
1072
1075
  # Dump the resolved version, not the original config
@@ -1076,13 +1079,31 @@ class InstallationConfig:
1076
1079
  "filesystem_skills_paths": [
1077
1080
  str(path) for path in self.filesystem_skills_paths
1078
1081
  ],
1079
- "logging_config_file": str(self._logging_config_file),
1080
1082
  "oidc_paths": [str(path) for path in self.oidc_paths],
1081
1083
  "room_paths": [str(path) for path in self.room_paths],
1082
1084
  "completion_paths": [str(path) for path in self.completion_paths],
1083
1085
  "quizzes_paths": [str(path) for path in self.quizzes_paths],
1086
+ "thread_persistence_dburi": {
1087
+ "sync": self._thread_persistence_dburi_sync,
1088
+ "async": self._thread_persistence_dburi_async,
1089
+ },
1090
+ "authorization_dburi": {
1091
+ "sync": self._authorization_dburi_sync,
1092
+ "async": self._authorization_dburi_async,
1093
+ },
1084
1094
  }
1085
1095
 
1096
+ if self.disable_dotenv:
1097
+ result["disable_dotenv"] = True
1098
+
1099
+ if self.middleware_stack:
1100
+ result["middleware_stack"] = [
1101
+ layer.as_yaml for layer in self.middleware_stack
1102
+ ]
1103
+
1104
+ if self._skill_configs is not None:
1105
+ result["skill_configs"] = self._skill_configs
1106
+
1086
1107
  if self.rooms_upload_path:
1087
1108
  result["rooms_upload_path"] = str(self.rooms_upload_path)
1088
1109
 
@@ -1098,6 +1119,15 @@ class InstallationConfig:
1098
1119
  if self.logfire_config is not None:
1099
1120
  result["logfire_config"] = self.logfire_config.as_yaml
1100
1121
 
1122
+ if self._logging_config_file is not None:
1123
+ result["logging_config_file"] = str(self._logging_config_file)
1124
+
1125
+ if self._logging_headers_map is not None:
1126
+ result["logging_headers_map"] = self.logging_headers_map
1127
+
1128
+ if self._logging_claims_map is not None:
1129
+ result["logging_claims_map"] = self.logging_claims_map
1130
+
1101
1131
  if self.app_router_operations:
1102
1132
  result["app_router_operations"] = [
1103
1133
  aro.as_yaml for aro in self.app_router_operations