soliplex 0.77.2__tar.gz → 0.78__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 (95) hide show
  1. {soliplex-0.77.2/src/soliplex.egg-info → soliplex-0.78}/PKG-INFO +1 -1
  2. {soliplex-0.77.2 → soliplex-0.78}/pyproject.toml +1 -1
  3. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/agents.py +5 -12
  4. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/config/agents.py +15 -4
  5. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/config/skills.py +86 -11
  6. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/installation.py +1 -1
  7. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/models.py +2 -2
  8. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/quizzes.py +1 -1
  9. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/skills/bwrap_sandbox/SKILL.md +44 -18
  10. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/skills/bwrap_sandbox/__init__.py +194 -10
  11. {soliplex-0.77.2 → soliplex-0.78/src/soliplex.egg-info}/PKG-INFO +1 -1
  12. {soliplex-0.77.2 → soliplex-0.78}/LICENSE +0 -0
  13. {soliplex-0.77.2 → soliplex-0.78}/MANIFEST.in +0 -0
  14. {soliplex-0.77.2 → soliplex-0.78}/README.md +0 -0
  15. {soliplex-0.77.2 → soliplex-0.78}/example/skills/bare-bones/SKILL.md +0 -0
  16. {soliplex-0.77.2 → soliplex-0.78}/example/skills/stanzas/SKILL.md +0 -0
  17. {soliplex-0.77.2 → soliplex-0.78}/setup.cfg +0 -0
  18. {soliplex-0.77.2 → soliplex-0.78}/skills/soliplex-docs/SKILL.md +0 -0
  19. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/agui/__init__.py +0 -0
  20. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/agui/parser.py +0 -0
  21. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/agui/persistence.py +0 -0
  22. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/agui/schema.py +0 -0
  23. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/agui/util.py +0 -0
  24. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/authn.py +0 -0
  25. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/authz/__init__.py +0 -0
  26. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/authz/persistence.py +0 -0
  27. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/authz/schema.py +0 -0
  28. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/capabilities/filesystem.py +0 -0
  29. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/capabilities/rag_audit.py +0 -0
  30. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/cli/admin_users.py +0 -0
  31. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/cli/ask.py +0 -0
  32. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/cli/audit.py +0 -0
  33. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/cli/cli_util.py +0 -0
  34. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/cli/main.py +0 -0
  35. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/cli/ollama.py +0 -0
  36. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/cli/room_authz.py +0 -0
  37. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/cli/serve.py +0 -0
  38. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/cli/types.py +0 -0
  39. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/completions.py +0 -0
  40. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/config/__init__.py +0 -0
  41. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/config/_utils.py +0 -0
  42. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/config/agui.py +0 -0
  43. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/config/authsystem.py +0 -0
  44. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/config/completions.py +0 -0
  45. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/config/exceptions.py +0 -0
  46. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/config/installation.py +0 -0
  47. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/config/logfire.py +0 -0
  48. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/config/meta.py +0 -0
  49. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/config/middleware.py +0 -0
  50. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/config/quizzes.py +0 -0
  51. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/config/rag.py +0 -0
  52. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/config/rooms.py +0 -0
  53. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/config/routing.py +0 -0
  54. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/config/secrets.py +0 -0
  55. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/config/tools.py +0 -0
  56. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/examples.py +0 -0
  57. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/log_ingest.py +0 -0
  58. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/loggers.py +0 -0
  59. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/main.py +0 -0
  60. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/mcp_auth.py +0 -0
  61. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/mcp_client.py +0 -0
  62. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/mcp_server.py +0 -0
  63. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/ollama.py +0 -0
  64. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/rag_audit.py +0 -0
  65. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/sandbox_audit.py +0 -0
  66. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/secrets.py +0 -0
  67. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/titles.py +0 -0
  68. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/tools/__init__.py +0 -0
  69. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/tools/agui_run_feedback.py +0 -0
  70. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/tools/file_uploads.py +0 -0
  71. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/tools/rag.py +0 -0
  72. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/tui/cli.py +0 -0
  73. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/tui/main.py +0 -0
  74. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/tui/rest_api.py +0 -0
  75. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/tui/serve.py +0 -0
  76. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/util.py +0 -0
  77. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/views/__init__.py +0 -0
  78. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/views/agui.py +0 -0
  79. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/views/authn.py +0 -0
  80. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/views/authz.py +0 -0
  81. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/views/completions.py +0 -0
  82. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/views/installation.py +0 -0
  83. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/views/log_ingest.py +0 -0
  84. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/views/quizzes.py +0 -0
  85. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/views/room_file_uploads.py +0 -0
  86. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/views/rooms.py +0 -0
  87. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/views/sandbox_workdirs.py +0 -0
  88. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/views/stats.py +0 -0
  89. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/views/streaming.py +0 -0
  90. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex/views/thread_file_uploads.py +0 -0
  91. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex.egg-info/SOURCES.txt +0 -0
  92. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex.egg-info/dependency_links.txt +0 -0
  93. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex.egg-info/entry_points.txt +0 -0
  94. {soliplex-0.77.2 → soliplex-0.78}/src/soliplex.egg-info/requires.txt +0 -0
  95. {soliplex-0.77.2 → soliplex-0.78}/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.77.2
3
+ Version: 0.78
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "soliplex"
7
- version = "0.77.2"
7
+ version = "0.78"
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"
@@ -144,18 +144,11 @@ def get_default_agent_from_configs(
144
144
  if isinstance(capability, HR_VisionCapabilities):
145
145
  capability.vision = agent_config.multimodal
146
146
 
147
- # A single model-facing capability loads eagerly so its tools are visible;
148
- #
149
- # Multiple stay deferred so the model routes between them via
150
- # 'load_capability'.
151
- routing_capabilities = [
152
- capability
153
- for capability in capabilities
154
- if _is_routing_capability(capability)
155
- ]
156
- defer_loading = len(routing_capabilities) > 1
157
- for capability in routing_capabilities:
158
- capability.defer_loading = defer_loading
147
+ # A hook-only capability is never deferred: nothing would load it, and
148
+ # its hooks only fire once loaded.
149
+ for capability in capabilities:
150
+ if not _is_routing_capability(capability):
151
+ capability.defer_loading = False
159
152
 
160
153
  return pydantic_ai.Agent(
161
154
  model=model,
@@ -271,6 +271,16 @@ class AgentConfig:
271
271
  else: # pragma: NO COVER
272
272
  pass
273
273
 
274
+ @property
275
+ def llm_model_name(self) -> str | None:
276
+ model_name = self.model_name
277
+ i_config = self._installation_config
278
+
279
+ if i_config is not None:
280
+ model_name = i_config.interpolate_environment(model_name)
281
+
282
+ return model_name
283
+
274
284
  @property
275
285
  def llm_provider_base_url(self) -> str | None:
276
286
  ic = self._installation_config
@@ -324,7 +334,7 @@ class AgentConfig:
324
334
  return {
325
335
  "id": self.id,
326
336
  "kind": self.kind,
327
- "model_name": self.model_name,
337
+ "model_name": self.model_name, # not interpolated
328
338
  "retries": self.retries,
329
339
  "system_prompt": prompt,
330
340
  "model_settings": self.model_settings,
@@ -478,6 +488,7 @@ def get_model_from_config(
478
488
  provider_kw = agent_config.llm_provider_kw
479
489
 
480
490
  model_settings_kw = {}
491
+ model_name = agent_config.llm_model_name
481
492
 
482
493
  if agent_config.model_settings:
483
494
  model_settings_kw["settings"] = ai_settings.ModelSettings(
@@ -487,7 +498,7 @@ def get_model_from_config(
487
498
  if agent_config.provider_type == LLMProviderType.GOOGLE:
488
499
  provider = google_providers.GoogleProvider(**provider_kw)
489
500
  return google_models.GoogleModel(
490
- model_name=agent_config.model_name,
501
+ model_name=model_name,
491
502
  provider=provider,
492
503
  **model_settings_kw,
493
504
  )
@@ -496,7 +507,7 @@ def get_model_from_config(
496
507
  provider_kw["api_key"] = "dummy"
497
508
  provider = ollama_providers.OllamaProvider(**provider_kw)
498
509
  return openai_models.OpenAIChatModel(
499
- model_name=agent_config.model_name,
510
+ model_name=model_name,
500
511
  provider=provider,
501
512
  profile=_OPENAI_COMPAT_PROFILE,
502
513
  **model_settings_kw,
@@ -510,7 +521,7 @@ def get_model_from_config(
510
521
  )
511
522
  provider = openai_providers.OpenAIProvider(**provider_kw)
512
523
  return openai_models.OpenAIChatModel(
513
- model_name=agent_config.model_name,
524
+ model_name=model_name,
514
525
  provider=provider,
515
526
  **profile_kw,
516
527
  **model_settings_kw,
@@ -179,6 +179,22 @@ class FilesystemSkillConfig:
179
179
  def extra_parameters(self) -> dict[str, pathlib.Path]:
180
180
  return {"path": self.path}
181
181
 
182
+ def with_defer_loading(self, defer_loading: bool) -> FilesystemSkillConfig:
183
+ """A copy whose capability loads as a room asked.
184
+
185
+ Installation skill configs are shared between the rooms which name
186
+ them, so a room's choice cannot be written onto the capability.
187
+ """
188
+ if defer_loading == self._capability.defer_loading:
189
+ return self
190
+ return dataclasses.replace(
191
+ self,
192
+ _capability=dataclasses.replace(
193
+ self._capability,
194
+ defer_loading=defer_loading,
195
+ ),
196
+ )
197
+
182
198
 
183
199
  @dataclasses.dataclass(kw_only=True)
184
200
  class _HaikuRAGCapabilityConfig(
@@ -192,6 +208,8 @@ class _HaikuRAGCapabilityConfig(
192
208
  state_type: typing.ClassVar[type[pydantic.BaseModel]]
193
209
  source: typing.ClassVar[SkillKind] = SkillKind.NATIVE
194
210
 
211
+ defer_loading: bool = False
212
+
195
213
  @classmethod
196
214
  def from_yaml(
197
215
  cls,
@@ -230,7 +248,7 @@ class _HaikuRAGCapabilityConfig(
230
248
  return type(self).capability_factory(
231
249
  db_path=self.rag_lancedb_path,
232
250
  config=self.haiku_rag_config,
233
- defer_loading=True,
251
+ defer_loading=self.defer_loading,
234
252
  )
235
253
 
236
254
  @property
@@ -239,7 +257,10 @@ class _HaikuRAGCapabilityConfig(
239
257
 
240
258
  @property
241
259
  def as_yaml(self) -> dict:
242
- result = {"kind": self.kind}
260
+ result = {
261
+ "kind": self.kind,
262
+ "defer_loading": self.defer_loading,
263
+ }
243
264
  if self.rag_lancedb_stem is not None:
244
265
  result["rag_lancedb_stem"] = self.rag_lancedb_stem
245
266
  else:
@@ -383,6 +404,7 @@ class BwrapSandboxSkillConfig:
383
404
  allowed_environments: bwrap_sandbox.AllowedEnvironments = None
384
405
  sandbox_config: bs_config.Config = None
385
406
  volumes: bs_models.VolumeMap = _default_dict_field()
407
+ defer_loading: bool = False
386
408
 
387
409
  @classmethod
388
410
  def from_yaml(
@@ -420,6 +442,7 @@ class BwrapSandboxSkillConfig:
420
442
  sandbox_config=self.sandbox_config,
421
443
  volumes=self.volumes,
422
444
  installation_config=self._installation_config,
445
+ defer_loading=self.defer_loading,
423
446
  )
424
447
 
425
448
  @property
@@ -427,6 +450,7 @@ class BwrapSandboxSkillConfig:
427
450
  result = {
428
451
  "kind": self.kind,
429
452
  "default_environment": self.default_environment,
453
+ "defer_loading": self.defer_loading,
430
454
  }
431
455
  if self.id is not None:
432
456
  result["id"] = self.id
@@ -475,6 +499,7 @@ class EntrypointCapabilityConfig:
475
499
 
476
500
  name: str
477
501
  params: dict[str, typing.Any] = _default_dict_field()
502
+ defer_loading: bool = True
478
503
 
479
504
  def __post_init__(self) -> None:
480
505
  # Register the capability's typed AG-UI state feature so the room can
@@ -500,10 +525,13 @@ class EntrypointCapabilityConfig:
500
525
  params = dict(config_dict)
501
526
  params.pop("kind", None)
502
527
  name = params.pop("name", None)
528
+ # Soliplex's own flag, not one of the plugin's parameters.
529
+ defer_loading = params.pop("defer_loading", True)
503
530
  try:
504
531
  return cls(
505
532
  name=name,
506
533
  params=params,
534
+ defer_loading=defer_loading,
507
535
  _installation_config=installation_config,
508
536
  _config_path=config_path,
509
537
  )
@@ -521,7 +549,7 @@ class EntrypointCapabilityConfig:
521
549
  @property
522
550
  def capability(self) -> ai_capabilities.AbstractCapability:
523
551
  return self._module.create_capability(
524
- defer_loading=True, **self.params
552
+ defer_loading=self.defer_loading, **self.params
525
553
  )
526
554
 
527
555
  @property
@@ -543,7 +571,12 @@ class EntrypointCapabilityConfig:
543
571
 
544
572
  @property
545
573
  def as_yaml(self) -> dict:
546
- return {"kind": self.kind, "name": self.name, **self.params}
574
+ return {
575
+ "kind": self.kind,
576
+ "name": self.name,
577
+ "defer_loading": self.defer_loading,
578
+ **self.params,
579
+ }
547
580
 
548
581
  @property
549
582
  def extra_parameters(self) -> dict[str, typing.Any]:
@@ -611,24 +644,57 @@ def extract_skill_configs(
611
644
  return skill_configs
612
645
 
613
646
 
647
+ @dataclasses.dataclass(kw_only=True)
648
+ class InstallationSkillRef:
649
+ """A room's reference to one of the installation's skills.
650
+
651
+ Spelled as a bare name, or as a mapping when the room sets a flag.
652
+ """
653
+
654
+ name: str
655
+ defer_loading: bool = True
656
+
657
+ @classmethod
658
+ def from_yaml(cls, entry: str | dict | InstallationSkillRef):
659
+ if isinstance(entry, cls):
660
+ return entry
661
+ if isinstance(entry, str):
662
+ return cls(name=entry)
663
+ return cls(**entry)
664
+
665
+ @property
666
+ def as_yaml(self) -> str | dict:
667
+ return {"name": self.name, "defer_loading": self.defer_loading}
668
+
669
+
614
670
  @dataclasses.dataclass(kw_only=True)
615
671
  class RoomSkillsConfig:
616
672
  """Select native capabilities for a room."""
617
673
 
618
- installation_skill_names: list[str] = _default_list_field()
674
+ installation_skill_names: list[InstallationSkillRef] = (
675
+ _default_list_field()
676
+ )
619
677
  _skill_configs: SkillConfigMap = _default_dict_field()
620
678
  _installation_config: config_installation.InstallationConfig = (
621
679
  _no_repr_no_compare_none()
622
680
  )
623
681
  _config_path: pathlib.Path = None
624
682
 
683
+ def __post_init__(self) -> None:
684
+ # Callers which build the config directly, rather than via
685
+ # 'from_yaml', may spell the entries as bare names.
686
+ self.installation_skill_names = [
687
+ InstallationSkillRef.from_yaml(entry)
688
+ for entry in self.installation_skill_names
689
+ ]
690
+
625
691
  @staticmethod
626
692
  def _check_installation_skills(
627
693
  installation_config: config_installation.InstallationConfig,
628
694
  config_path: pathlib.Path,
629
- config_dict: dict,
695
+ refs: typing.Sequence[InstallationSkillRef],
630
696
  ) -> None:
631
- requested = set(config_dict.get("installation_skill_names", ()))
697
+ requested = {ref.name for ref in refs}
632
698
  available = set(installation_config.skill_configs)
633
699
  if missing := requested - available:
634
700
  raise MissingSkillNames(
@@ -645,11 +711,16 @@ class RoomSkillsConfig:
645
711
  config_dict: dict,
646
712
  ):
647
713
  try:
714
+ refs = [
715
+ InstallationSkillRef.from_yaml(entry)
716
+ for entry in config_dict.get("installation_skill_names", ())
717
+ ]
648
718
  cls._check_installation_skills(
649
719
  installation_config,
650
720
  config_path,
651
- config_dict,
721
+ refs,
652
722
  )
723
+ config_dict["installation_skill_names"] = refs
653
724
  config_dict["_skill_configs"] = extract_skill_configs(
654
725
  installation_config,
655
726
  config_path,
@@ -671,7 +742,9 @@ class RoomSkillsConfig:
671
742
  def as_yaml(self) -> dict:
672
743
  result = {}
673
744
  if self.installation_skill_names:
674
- result["installation_skill_names"] = self.installation_skill_names
745
+ result["installation_skill_names"] = [
746
+ ref.as_yaml for ref in self.installation_skill_names
747
+ ]
675
748
  if self._skill_configs:
676
749
  result["skill_configs"] = [
677
750
  config.as_yaml for config in self._skill_configs.values()
@@ -682,8 +755,10 @@ class RoomSkillsConfig:
682
755
  def skill_configs(self) -> SkillConfigMap:
683
756
  installation_configs = self._installation_config.skill_configs
684
757
  return {
685
- name: installation_configs[name]
686
- for name in self.installation_skill_names
758
+ ref.name: installation_configs[ref.name].with_defer_loading(
759
+ ref.defer_loading
760
+ )
761
+ for ref in self.installation_skill_names
687
762
  } | self._skill_configs
688
763
 
689
764
  @property
@@ -178,7 +178,7 @@ class Installation:
178
178
  type_info = found.setdefault(provider_type, {})
179
179
  base_url = agent_config.llm_provider_base_url
180
180
  url_models = type_info.setdefault(base_url, set())
181
- url_models.add(agent_config.model_name)
181
+ url_models.add(agent_config.llm_model_name)
182
182
 
183
183
  return found
184
184
 
@@ -174,7 +174,7 @@ ConfiguredSkills = dict[str, Skill]
174
174
 
175
175
  class DefaultAgent(pydantic.BaseModel):
176
176
  id: str
177
- model_name: str
177
+ model_name: str | None
178
178
  retries: int
179
179
  system_prompt: str | None
180
180
  provider_type: config_agents.LLMProviderType # enum, not dataclass
@@ -187,7 +187,7 @@ class DefaultAgent(pydantic.BaseModel):
187
187
  llm_provider_kw = agent_config.llm_provider_kw
188
188
  return cls(
189
189
  id=agent_config.id,
190
- model_name=agent_config.model_name,
190
+ model_name=agent_config.llm_model_name,
191
191
  retries=agent_config.retries,
192
192
  system_prompt=agent_config.get_system_prompt(),
193
193
  provider_type=agent_config.provider_type,
@@ -52,7 +52,7 @@ def get_quiz_judge_agent(quiz: config_quizzes.QuizConfig):
52
52
  model_provider = ollama_providers.OllamaProvider(**llm_provider_kw)
53
53
 
54
54
  ollama_model = openai_models.OpenAIChatModel(
55
- model_name=quiz.judge_agent.model_name,
55
+ model_name=quiz.judge_agent.llm_model_name,
56
56
  provider=model_provider,
57
57
  )
58
58
 
@@ -55,11 +55,30 @@ Do NOT use the sandbox if **any** of these is true:
55
55
  - `run_python(script, environment_name=None, timeout=None)` — run a Python
56
56
  script. `script` is the full source as one string.
57
57
 
58
- When calling `run` and `run_python`, `environment_name` and `timeout` are both
59
- optional and fall back to the values this skill was configured with.
60
- Always pass `environment_name` explicitly, using a name that
61
- `list_environments` returned — the configured default is not visible to you
62
- and may not be the environment you want.
58
+ ## Environment names come only from `list_environments`
59
+
60
+ **Never guess an environment name.** The only values `environment_name`
61
+ accepts are the exact `name` strings that a `list_environments` call
62
+ returned **to you, in this conversation**.
63
+
64
+ - Call `list_environments` before the first `run` or `run_python` of a
65
+ turn. It is cheap; skipping it is the most common way this skill fails.
66
+ - Every environment name written in this document is a placeholder —
67
+ `<name from list_environments>` — standing in for a name you have yet to
68
+ look up. No placeholder names a real environment.
69
+ - A name that appears in a task, a filename, a library you plan to import,
70
+ or an earlier conversation is not an environment name. Neither is the
71
+ name of a package: an environment's `name` and its `dependencies` are
72
+ different things, and are usually spelled differently.
73
+ - Passing a name that was not in the `list_environments` result fails the
74
+ call. The failure names the environments you may actually use — call
75
+ `list_environments` and retry with one of those, rather than guessing
76
+ again.
77
+
78
+ `environment_name` and `timeout` are both optional and fall back to the
79
+ values this skill was configured with. Always pass `environment_name`
80
+ explicitly: the configured default is not visible to you and may not be
81
+ the environment you want.
63
82
 
64
83
  ## Workflow
65
84
 
@@ -68,9 +87,9 @@ and may not be the environment you want.
68
87
 
69
88
  ```python
70
89
  {
71
- "name": "pandas",
72
- "description": "Minimal Python with Pandas",
73
- "dependencies": ["pandas"],
90
+ "name": "<name from list_environments>",
91
+ "description": "<what this environment is for>",
92
+ "dependencies": ["<installed package>"],
74
93
  }
75
94
  ```
76
95
 
@@ -78,10 +97,11 @@ and may not be the environment you want.
78
97
  - If the list is empty, stop and tell the user the skill
79
98
  is not configured — do not proceed.
80
99
  - If the list contains exactly one environment, use its `name`.
81
- - Otherwise, pick the `name` of the first environment whose `dependencies`
82
- include a library the task needs (e.g. `pandas` for tabular data,
83
- `numpy` for numeric work, `pillow` for images). If no environments
84
- match, use the `name` of the first entry in the list.
100
+ - Otherwise, compare the task against each entry's `dependencies` —
101
+ a `pandas` dependency suits tabular data, `numpy` numeric work,
102
+ `pillow` images — and use the `name` of the first entry that matches.
103
+ Use the entry's `name`, not the name of the dependency that matched.
104
+ If no entry matches, use the `name` of the first entry in the list.
85
105
 
86
106
  2. **List files in both volumes.** This step is mandatory — do not skip it,
87
107
  even if the task seems to involve one volume. Run both:
@@ -113,7 +133,7 @@ and may not be the environment you want.
113
133
 
114
134
  ```python
115
135
  run(command=["head", "-n", "5", "/sandbox/volumes/thread/orders.csv"],
116
- environment_name="<selected-environment>")
136
+ environment_name="<name from list_environments>")
117
137
  ```
118
138
 
119
139
  (or `["wc", "-l", <path>]`, `["file", <path>]`, etc.). For anything
@@ -125,7 +145,8 @@ and may not be the environment you want.
125
145
  argument to `run_python`:
126
146
 
127
147
  ```python
128
- run_python(script="<python source>", environment_name="<selected-environment>")
148
+ run_python(script="<python source>",
149
+ environment_name="<name from list_environments>")
129
150
  ```
130
151
 
131
152
  Write the whole program as a single string, and use real newlines between
@@ -147,7 +168,10 @@ and may not be the environment you want.
147
168
  ```
148
169
 
149
170
  5. **On failure.**
150
- - Change exactly one thing per retry.
171
+ - If the failure says the environment is not available, you guessed a
172
+ name. Call `list_environments` and retry with a name it returned —
173
+ do not try a different guess.
174
+ - Otherwise change exactly one thing per retry.
151
175
  - After 3 failed runs, stop. Report the error to the user (paste the
152
176
  `Exited with code: <N>` line and any traceback), instead of retrying
153
177
  further.
@@ -168,8 +192,10 @@ and may not be the environment you want.
168
192
  Task: user uploads `orders.csv` and asks "what's the total order value?".
169
193
  A full run looks like:
170
194
 
171
- 1. `list_environments()` — shows one environment named `pandas`
172
- with `pandas` in its dependencies. Use it.
195
+ 1. `list_environments()` — returns exactly one environment, whose
196
+ `dependencies` include `pandas`. Use that entry's `name`; in step 3 it
197
+ is written as `<name from list_environments>`, because what the name
198
+ actually is can only be read off this call's result.
173
199
 
174
200
  2. `list_volume_files("thread")` — returns
175
201
  `["/sandbox/volumes/thread/orders.csv"]`.
@@ -184,7 +210,7 @@ A full run looks like:
184
210
  df = pd.read_csv('/sandbox/volumes/thread/orders.csv')
185
211
  print(f"Total: {df['amount'].sum():.2f}")
186
212
  """,
187
- environment_name="pandas",
213
+ environment_name="<name from list_environments>",
188
214
  )
189
215
  ```
190
216
 
@@ -31,6 +31,10 @@ SANDBOX_WORKDIR_PATH = SKILL_PROPERTIES.metadata["sandbox_workdir_path"]
31
31
  LIST_ENVIRONMENTS_DESCRIPTION = """
32
32
  Return a list of information about available sandbox environments
33
33
 
34
+ Call this tool before the first ``run`` or ``run_python`` of a turn: \
35
+ their ``environment_name`` argument accepts nothing but the names \
36
+ returned here.
37
+
34
38
  Each entry will contain these fields:
35
39
  - 'name' (string) pass this value to the ``run`` and ``run_python`` \
36
40
  tools to run the tool in the environment.
@@ -55,6 +59,164 @@ async def skill_list_environments(
55
59
  return candidates
56
60
 
57
61
 
62
+ class UnknownEnvironment(pydantic_ai.ModelRetry):
63
+ def __init__(self, name, choices):
64
+ self.name = name
65
+ self.choices = choices
66
+ c_list = ", ".join(repr(name) for name in choices)
67
+ super().__init__(
68
+ f"{name!r} is not an available sandbox environment. "
69
+ "Environment names cannot be guessed or copied from examples: "
70
+ "call 'list_environments' and pass one of the names it returns. "
71
+ f"Available environments: {c_list}."
72
+ )
73
+
74
+
75
+ class NoEnvironmentsConfigured(pydantic_ai.ToolFailed):
76
+ def __init__(self, name):
77
+ self.name = name
78
+ super().__init__(
79
+ f"{name!r} is not an available sandbox environment, "
80
+ "and this room has none configured. "
81
+ "Tell the user the sandbox skill is not configured."
82
+ )
83
+
84
+
85
+ async def check_environment_name(
86
+ *,
87
+ bwrap_sandbox: bs_sandbox.BwrapSandbox,
88
+ environment_name: str | None,
89
+ allowed_environments: AllowedEnvironments = None,
90
+ ) -> None:
91
+ """Reject an 'environment_name' the model invented, before anything runs.
92
+
93
+ Guards 'run' / 'run_python' against a name the model guessed rather than
94
+ read from a 'list_environments' result. Without it, an unknown name
95
+ aborts the whole agent run, and a name which exists but is *not* in
96
+ 'allowed_environments' runs anyway -- 'allowed_environments' otherwise
97
+ only filters what 'list_environments' reports.
98
+
99
+ The two failures differ in whether the model can do anything about them,
100
+ so they are reported differently. A name outside the choices is fixable
101
+ by calling 'list_environments' and passing one of them, which is what
102
+ 'ModelRetry' asks for (and matches how 'pydantic_ai' itself reports an
103
+ unknown *tool* name). A room with no environments at all is the
104
+ operator's to fix: 'ToolFailed' shows the model a failed call to report
105
+ rather than one to retry, and spends none of the tool's retry budget.
106
+
107
+ 'None' passes through unchecked: it selects the environment the skill was
108
+ configured with, which is the operator's choice rather than the model's.
109
+ """
110
+ if environment_name is None:
111
+ return
112
+
113
+ available = [
114
+ env.name
115
+ for env in await skill_list_environments(
116
+ bwrap_sandbox=bwrap_sandbox,
117
+ allowed_environments=allowed_environments,
118
+ )
119
+ ]
120
+
121
+ if environment_name in available:
122
+ return
123
+
124
+ if not available:
125
+ raise NoEnvironmentsConfigured(name=environment_name)
126
+
127
+ raise UnknownEnvironment(
128
+ name=environment_name,
129
+ choices=available,
130
+ )
131
+
132
+
133
+ def _environment_label(environment_name):
134
+ """Name the environment a run asked for, without leaking host paths.
135
+
136
+ 'bubble_sandbox' builds its messages from the resolved filesystem path
137
+ ("Environment not found: PosixPath('/app/sandbox/environments/...')"),
138
+ which is meaningless inside the sandbox and not something to put in
139
+ front of the model. The requested name says the same thing.
140
+ """
141
+ if environment_name is None:
142
+ return "the environment this room uses by default"
143
+ return f"the {environment_name!r} environment"
144
+
145
+
146
+ class EnvironmentMissing(pydantic_ai.ToolFailed):
147
+ def __init__(self, environment_name):
148
+ self.environment_name = environment_name
149
+ super().__init__(
150
+ f"The sandbox cannot run: {_environment_label(environment_name)} "
151
+ "is not installed on this server. Report this to the user as a "
152
+ "sandbox configuration problem; retrying will not fix it."
153
+ )
154
+
155
+
156
+ class EnvironmentNotBuilt(pydantic_ai.ToolFailed):
157
+ def __init__(self, environment_name):
158
+ self.environment_name = environment_name
159
+ super().__init__(
160
+ f"The sandbox cannot run: {_environment_label(environment_name)} "
161
+ "is installed but its Python virtualenv is missing. Report this "
162
+ "to the user as a sandbox configuration problem; retrying will "
163
+ "not fix it."
164
+ )
165
+
166
+
167
+ class EnvironmentNameInvalid(pydantic_ai.ToolFailed):
168
+ def __init__(self, environment_name):
169
+ self.environment_name = environment_name
170
+ super().__init__(
171
+ f"The sandbox cannot run: {_environment_label(environment_name)} "
172
+ "is not a usable environment name. Report this to the user as a "
173
+ "sandbox configuration problem; retrying will not fix it."
174
+ )
175
+
176
+
177
+ class SandboxUnavailable(pydantic_ai.ToolFailed):
178
+ def __init__(self, environment_name, reason):
179
+ self.environment_name = environment_name
180
+ self.reason = reason
181
+ super().__init__(
182
+ "The sandbox could not be started in "
183
+ f"{_environment_label(environment_name)}: {reason}. Report this "
184
+ "to the user; retrying will not fix it."
185
+ )
186
+
187
+
188
+ # 'bubble_sandbox' reports a bad environment name with 'ValueError' and
189
+ # 'FileNotFoundError' subclasses, not 'RuntimeError'; letting one escape
190
+ # aborts the agent run (soliplex#1306) instead of handing the model an
191
+ # error it can act on.
192
+ EXECUTION_ERRORS = (
193
+ RuntimeError,
194
+ OSError,
195
+ bs_config.InvalidEnvironmentName,
196
+ )
197
+
198
+
199
+ def translate_execution_error(exc, *, environment_name):
200
+ """Return the error the model should see for a failed sandbox start.
201
+
202
+ Every case here is the operator's to fix rather than the model's --
203
+ 'check_environment_name' has already rejected any name the model could
204
+ have chosen differently -- so each maps to a 'ToolFailed' subclass: the
205
+ model sees a failed call to report, spends none of the tool's retry
206
+ budget, and is told plainly that retrying will not help.
207
+ """
208
+ if isinstance(exc, bs_config.EnvironmentNotFound):
209
+ return EnvironmentMissing(environment_name)
210
+
211
+ if isinstance(exc, bs_config.EnvironmentNotInitialized):
212
+ return EnvironmentNotBuilt(environment_name)
213
+
214
+ if isinstance(exc, bs_config.InvalidEnvironmentName):
215
+ return EnvironmentNameInvalid(environment_name)
216
+
217
+ return SandboxUnavailable(environment_name, str(exc))
218
+
219
+
58
220
  RUN_DESCRIPTION = f"""\
59
221
  Run a shell command inside the bubblewrap sandbox.
60
222
 
@@ -69,8 +231,10 @@ pass ``command`` as a single string; it is run via "sh -c".
69
231
  - Otherwise pass ``command`` as a list of strings: the executable name \
70
232
  or path first, then one element per argument. This form needs no \
71
233
  quoting and is the safer default.
72
- - ``environment_name`` selects the environment to run in; omit it to \
73
- use the configured default. Call ``list_environments`` for the choices.
234
+ - ``environment_name`` selects the environment to run in. Pass only a \
235
+ ``name`` that ``list_environments`` returned in this conversation -- \
236
+ never a guessed name, a package name, or a name copied from an example. \
237
+ Omit it to use the configured default.
74
238
  - ``timeout`` caps the run in seconds; omit it to use the configured \
75
239
  default.
76
240
  - Paths must be absolute and sandbox-visible: read inputs from \
@@ -159,8 +323,11 @@ async def skill_run(
159
323
  timeout=timeout,
160
324
  extra_volumes=extra_volumes,
161
325
  )
162
- except RuntimeError as e:
163
- return f"Error: {e}"
326
+ except EXECUTION_ERRORS as exc:
327
+ raise translate_execution_error(
328
+ exc,
329
+ environment_name=environment_name,
330
+ ) from exc
164
331
 
165
332
  output = result.output
166
333
  if result.truncated:
@@ -182,9 +349,10 @@ Do NOT pass shell commands — use the ``run`` tool for those.
182
349
  - Pass a complete, self-contained Python script as the ``script`` string.
183
350
  - The script runs via the Python interpreter built into the chosen \
184
351
  environment, with access to its pre-installed packages.
185
- - Use ``list_environments`` first to discover available environments \
186
- and their installed packages. ``environment_name`` selects one; omit \
187
- it to use the configured default.
352
+ - Call ``list_environments`` first to discover available environments \
353
+ and their installed packages. ``environment_name`` must be one of the \
354
+ ``name`` values it returned -- never a guessed name, a package name, or \
355
+ a name copied from an example. Omit it to use the configured default.
188
356
  - ``timeout`` caps the run in seconds; omit it to use the configured \
189
357
  default.
190
358
  - Print results to stdout — the output is captured and returned.
@@ -235,8 +403,11 @@ async def skill_run_python(
235
403
  timeout=timeout,
236
404
  extra_volumes=extra_volumes,
237
405
  )
238
- except RuntimeError as e:
239
- return f"Error: {e}"
406
+ except EXECUTION_ERRORS as exc:
407
+ raise translate_execution_error(
408
+ exc,
409
+ environment_name=environment_name,
410
+ ) from exc
240
411
 
241
412
  output = result.output
242
413
  if result.truncated:
@@ -434,6 +605,12 @@ def create_sandbox_toolset(
434
605
  environment_name: str | None = None,
435
606
  timeout: float | None = None, # seconds
436
607
  ) -> str:
608
+ await check_environment_name(
609
+ bwrap_sandbox=bwrap_sandbox,
610
+ environment_name=environment_name,
611
+ allowed_environments=allowed_environments,
612
+ )
613
+
437
614
  deps = ctx.deps
438
615
  workdir = get_workdir(
439
616
  workdirs_path,
@@ -486,6 +663,12 @@ def create_sandbox_toolset(
486
663
  environment_name: str | None = None,
487
664
  timeout: float | None = None, # seconds
488
665
  ) -> str:
666
+ await check_environment_name(
667
+ bwrap_sandbox=bwrap_sandbox,
668
+ environment_name=environment_name,
669
+ allowed_environments=allowed_environments,
670
+ )
671
+
489
672
  deps = ctx.deps
490
673
  workdir = get_workdir(
491
674
  workdirs_path,
@@ -570,11 +753,12 @@ def create_bwrap_sandbox_capability(
570
753
  volumes: bs_models.VolumeMap | None = None,
571
754
  max_retries: int = 1,
572
755
  installation_config=None, # noqa F821 cycles
756
+ defer_loading: bool = False,
573
757
  ) -> SandboxCapability:
574
758
  return SandboxCapability(
575
759
  id=id or SKILL_PROPERTIES.name,
576
760
  description=SKILL_PROPERTIES.description.strip(),
577
- defer_loading=True,
761
+ defer_loading=defer_loading,
578
762
  default_environment=default_environment,
579
763
  allowed_environments=allowed_environments,
580
764
  sandbox_config=sandbox_config,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: soliplex
3
- Version: 0.77.2
3
+ Version: 0.78
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
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes