soliplex 0.76__tar.gz → 0.76.1__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.76/src/soliplex.egg-info → soliplex-0.76.1}/PKG-INFO +1 -1
  2. {soliplex-0.76 → soliplex-0.76.1}/pyproject.toml +1 -1
  3. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/agents.py +24 -14
  4. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/config/authsystem.py +23 -6
  5. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/config/completions.py +21 -0
  6. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/config/quizzes.py +2 -5
  7. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/config/skills.py +0 -80
  8. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/installation.py +3 -4
  9. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/models.py +0 -8
  10. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/tools/agui_run_feedback.py +64 -43
  11. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/views/agui.py +1 -9
  12. {soliplex-0.76 → soliplex-0.76.1/src/soliplex.egg-info}/PKG-INFO +1 -1
  13. {soliplex-0.76 → soliplex-0.76.1}/LICENSE +0 -0
  14. {soliplex-0.76 → soliplex-0.76.1}/MANIFEST.in +0 -0
  15. {soliplex-0.76 → soliplex-0.76.1}/README.md +0 -0
  16. {soliplex-0.76 → soliplex-0.76.1}/example/skills/bare-bones/SKILL.md +0 -0
  17. {soliplex-0.76 → soliplex-0.76.1}/example/skills/stanzas/SKILL.md +0 -0
  18. {soliplex-0.76 → soliplex-0.76.1}/setup.cfg +0 -0
  19. {soliplex-0.76 → soliplex-0.76.1}/skills/soliplex-docs/SKILL.md +0 -0
  20. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/agui/__init__.py +0 -0
  21. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/agui/parser.py +0 -0
  22. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/agui/persistence.py +0 -0
  23. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/agui/schema.py +0 -0
  24. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/agui/util.py +0 -0
  25. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/authn.py +0 -0
  26. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/authz/__init__.py +0 -0
  27. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/authz/persistence.py +0 -0
  28. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/authz/schema.py +0 -0
  29. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/capabilities/filesystem.py +0 -0
  30. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/capabilities/rag_audit.py +0 -0
  31. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/cli/admin_users.py +0 -0
  32. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/cli/ask.py +0 -0
  33. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/cli/audit.py +0 -0
  34. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/cli/cli_util.py +0 -0
  35. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/cli/main.py +0 -0
  36. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/cli/ollama.py +0 -0
  37. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/cli/room_authz.py +0 -0
  38. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/cli/serve.py +0 -0
  39. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/cli/types.py +0 -0
  40. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/completions.py +0 -0
  41. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/config/__init__.py +0 -0
  42. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/config/_utils.py +0 -0
  43. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/config/agents.py +0 -0
  44. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/config/agui.py +0 -0
  45. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/config/exceptions.py +0 -0
  46. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/config/installation.py +0 -0
  47. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/config/logfire.py +0 -0
  48. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/config/meta.py +0 -0
  49. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/config/middleware.py +0 -0
  50. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/config/rag.py +0 -0
  51. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/config/rooms.py +0 -0
  52. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/config/routing.py +0 -0
  53. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/config/secrets.py +0 -0
  54. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/config/tools.py +0 -0
  55. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/examples.py +0 -0
  56. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/log_ingest.py +0 -0
  57. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/loggers.py +0 -0
  58. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/main.py +0 -0
  59. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/mcp_auth.py +0 -0
  60. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/mcp_client.py +0 -0
  61. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/mcp_server.py +0 -0
  62. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/ollama.py +0 -0
  63. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/quizzes.py +0 -0
  64. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/rag_audit.py +0 -0
  65. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/sandbox_audit.py +0 -0
  66. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/secrets.py +0 -0
  67. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/skills/bwrap_sandbox/SKILL.md +0 -0
  68. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/skills/bwrap_sandbox/__init__.py +0 -0
  69. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/titles.py +0 -0
  70. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/tools/__init__.py +0 -0
  71. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/tools/file_uploads.py +0 -0
  72. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/tools/rag.py +0 -0
  73. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/tui/cli.py +0 -0
  74. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/tui/main.py +0 -0
  75. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/tui/rest_api.py +0 -0
  76. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/tui/serve.py +0 -0
  77. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/util.py +0 -0
  78. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/views/__init__.py +0 -0
  79. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/views/authn.py +0 -0
  80. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/views/authz.py +0 -0
  81. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/views/completions.py +0 -0
  82. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/views/installation.py +0 -0
  83. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/views/log_ingest.py +0 -0
  84. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/views/quizzes.py +0 -0
  85. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/views/room_file_uploads.py +0 -0
  86. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/views/rooms.py +0 -0
  87. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/views/sandbox_workdirs.py +0 -0
  88. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/views/stats.py +0 -0
  89. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/views/streaming.py +0 -0
  90. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex/views/thread_file_uploads.py +0 -0
  91. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex.egg-info/SOURCES.txt +0 -0
  92. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex.egg-info/dependency_links.txt +0 -0
  93. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex.egg-info/entry_points.txt +0 -0
  94. {soliplex-0.76 → soliplex-0.76.1}/src/soliplex.egg-info/requires.txt +0 -0
  95. {soliplex-0.76 → soliplex-0.76.1}/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.76
3
+ Version: 0.76.1
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.76"
7
+ version = "0.76.1"
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"
@@ -5,13 +5,12 @@ import typing
5
5
 
6
6
  import pydantic_ai
7
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
8
  from haiku.rag.capabilities import rag as hr_caps_rag
11
9
  from pydantic_ai import agent as ai_agent
12
10
  from pydantic_ai import capabilities as ai_capabilities
13
11
  from pydantic_ai import tools as ai_tools
14
12
  from pydantic_ai import toolsets as ai_toolsets
13
+ from sqlalchemy.ext import asyncio as sqla_asyncio
15
14
 
16
15
  from soliplex import agui
17
16
  from soliplex import mcp_client
@@ -27,13 +26,6 @@ HR_VisionCapabilities = (
27
26
  hr_caps_rag.RAGCapability | hr_caps_analysis.AnalysisCapability
28
27
  )
29
28
 
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
-
37
29
 
38
30
  class CapabilityConfig(typing.Protocol):
39
31
  """Room-specific native capabilities and their audit targets."""
@@ -54,7 +46,7 @@ class AgentDependencies:
54
46
  """
55
47
 
56
48
  the_installation: typing.Any # installation.Installation
57
- the_threads: agui.ThreadStorage = None
49
+ thread_persistence_engine: sqla_asyncio.AsyncEngine = None
58
50
  state: agui.AGUI_State = dataclasses.field(default_factory=dict)
59
51
  room_id: str | None = None
60
52
  thread_id: str | None = None
@@ -94,6 +86,27 @@ def make_mcp_client_toolset(
94
86
  return factory(**toolset_config.tool_kwargs)
95
87
 
96
88
 
89
+ def _is_routing_capability(
90
+ capability: ai_capabilities.AbstractCapability,
91
+ ) -> bool:
92
+ """Whether the model has anything to load from this capability.
93
+
94
+ Tools or instructions are what there is to route between; a hook-only
95
+ capability offers neither.
96
+ """
97
+ if capability.get_instructions() or capability.get_native_tools():
98
+ return True
99
+
100
+ toolset = capability.get_toolset()
101
+
102
+ if toolset is None:
103
+ return False
104
+
105
+ # A capability registering no tools of its own still gets an empty
106
+ # 'FunctionToolset', so ask whether it is empty, not whether it exists.
107
+ return bool(getattr(toolset, "tools", True))
108
+
109
+
97
110
  def get_default_agent_from_configs(
98
111
  *,
99
112
  agent_config: config_agents.AgentConfig,
@@ -135,13 +148,10 @@ def get_default_agent_from_configs(
135
148
  #
136
149
  # Multiple stay deferred so the model routes between them via
137
150
  # 'load_capability'.
138
- #
139
- # Hook-only capabilities expose no tools to route
140
- # between, so they neither defer nor count.
141
151
  routing_capabilities = [
142
152
  capability
143
153
  for capability in capabilities
144
- if not isinstance(capability, HookOnlyCapabilities)
154
+ if _is_routing_capability(capability)
145
155
  ]
146
156
  defer_loading = len(routing_capabilities) > 1
147
157
  for capability in routing_capabilities:
@@ -9,7 +9,6 @@ from . import _utils
9
9
  from . import exceptions as config_exc
10
10
 
11
11
  _no_repr_no_compare_none = _utils._no_repr_no_compare_none
12
- _default_list_field = _utils._default_list_field
13
12
 
14
13
 
15
14
  # ============================================================================
@@ -62,6 +61,29 @@ class OIDCAuthSystemConfig:
62
61
  config_dict,
63
62
  ) from exc
64
63
 
64
+ @property
65
+ def as_yaml(self) -> dict:
66
+ result = {
67
+ "id": self.id,
68
+ "title": self.title,
69
+ "server_url": self.server_url,
70
+ "token_validation_pem": self.token_validation_pem,
71
+ "client_id": self.client_id,
72
+ }
73
+
74
+ if self.scope is not None:
75
+ result["scope"] = self.scope
76
+
77
+ if self.client_secret:
78
+ # Dumped raw: 'env:'/'secret:' interpolations are not resolved.
79
+ result["client_secret"] = self.client_secret
80
+
81
+ if self.oidc_client_pem_path is not None:
82
+ # Absolutized against the config dir on load. See #1228.
83
+ result["oidc_client_pem_path"] = str(self.oidc_client_pem_path)
84
+
85
+ return result
86
+
65
87
  @property
66
88
  def server_metadata_url(self):
67
89
  return f"{self.server_url}/{WELL_KNOWN_OPENID_CONFIGURATION}"
@@ -94,8 +116,3 @@ class OIDCAuthSystemConfig:
94
116
  # added by the auth setup
95
117
  # "authorize_state": main.SESSION_SECRET_KEY,
96
118
  }
97
-
98
-
99
- @dataclasses.dataclass(kw_only=True)
100
- class AvailableOIDCAuthSystemConfigs:
101
- systems: list[OIDCAuthSystemConfig] = _default_list_field()
@@ -81,5 +81,26 @@ class CompletionConfig:
81
81
 
82
82
  return cls(**config_dict)
83
83
 
84
+ @property
85
+ def as_yaml(self) -> dict:
86
+ result = {
87
+ "id": self.id,
88
+ "agent": self.agent_config.as_yaml,
89
+ }
90
+
91
+ if self.name is not None:
92
+ result["name"] = self.name
93
+
94
+ if self.tool_configs:
95
+ result["tools"] = [tc.as_yaml for tc in self.tool_configs.values()]
96
+
97
+ if self.mcp_client_toolset_configs:
98
+ result["mcp_client_toolsets"] = {
99
+ key: mctc.as_yaml
100
+ for key, mctc in self.mcp_client_toolset_configs.items()
101
+ }
102
+
103
+ return result
104
+
84
105
 
85
106
  CompletionConfigMap = dict[str, CompletionConfig]
@@ -145,12 +145,9 @@ class QuizConfig:
145
145
  kwargs = {
146
146
  "id": f"quiz-{self.id}-judge",
147
147
  "model_name": "gpt-oss:20b",
148
+ "_installation_config": self._installation_config,
149
+ "_config_path": self._config_path,
148
150
  }
149
- if self._installation_config is not None:
150
- i_config = self._installation_config
151
- kwargs["provider_base_url"] = i_config.get_environment(
152
- "OLLAMA_BASE_URL",
153
- )
154
151
  self.judge_agent = agents.AgentConfig(**kwargs)
155
152
 
156
153
  @property
@@ -175,22 +175,6 @@ class FilesystemSkillConfig:
175
175
  def errors(self) -> list[str]:
176
176
  return self._validation_errors
177
177
 
178
- @property
179
- def license(self) -> None:
180
- return None
181
-
182
- @property
183
- def compatibility(self) -> None:
184
- return None
185
-
186
- @property
187
- def allowed_tools(self) -> list[str]:
188
- return []
189
-
190
- @property
191
- def metadata(self) -> dict[str, str]:
192
- return {}
193
-
194
178
  @property
195
179
  def extra_parameters(self) -> dict[str, pathlib.Path]:
196
180
  return {"path": self.path}
@@ -264,22 +248,6 @@ class _HaikuRAGCapabilityConfig(
264
248
  )
265
249
  return result
266
250
 
267
- @property
268
- def license(self) -> None:
269
- return None
270
-
271
- @property
272
- def compatibility(self) -> None:
273
- return None
274
-
275
- @property
276
- def allowed_tools(self) -> list[str]:
277
- return []
278
-
279
- @property
280
- def metadata(self) -> dict[str, str]:
281
- return {}
282
-
283
251
  @property
284
252
  def extra_parameters(self) -> dict[str, typing.Any]:
285
253
  return self.get_extra_parameters()
@@ -364,22 +332,6 @@ class _HaikuRAGEvidenceSkillConfig:
364
332
  def as_yaml(self) -> dict:
365
333
  return {"kind": self.kind}
366
334
 
367
- @property
368
- def license(self) -> None:
369
- return None
370
-
371
- @property
372
- def compatibility(self) -> None:
373
- return None
374
-
375
- @property
376
- def allowed_tools(self) -> list[str]:
377
- return []
378
-
379
- @property
380
- def metadata(self) -> dict[str, str]:
381
- return {}
382
-
383
335
  @property
384
336
  def extra_parameters(self) -> dict[str, typing.Any]:
385
337
  return {}
@@ -499,22 +451,6 @@ class BwrapSandboxSkillConfig:
499
451
  }
500
452
  return result
501
453
 
502
- @property
503
- def license(self) -> None:
504
- return None
505
-
506
- @property
507
- def compatibility(self) -> None:
508
- return None
509
-
510
- @property
511
- def allowed_tools(self) -> list[str]:
512
- return []
513
-
514
- @property
515
- def metadata(self) -> dict[str, str]:
516
- return {}
517
-
518
454
  @property
519
455
  def extra_parameters(self) -> dict[str, typing.Any]:
520
456
  result = {"default_environment": self.default_environment}
@@ -609,22 +545,6 @@ class EntrypointCapabilityConfig:
609
545
  def as_yaml(self) -> dict:
610
546
  return {"kind": self.kind, "name": self.name, **self.params}
611
547
 
612
- @property
613
- def license(self) -> None:
614
- return None
615
-
616
- @property
617
- def compatibility(self) -> None:
618
- return None
619
-
620
- @property
621
- def allowed_tools(self) -> list[str]:
622
- return []
623
-
624
- @property
625
- def metadata(self) -> dict[str, str]:
626
- return {}
627
-
628
548
  @property
629
549
  def extra_parameters(self) -> dict[str, typing.Any]:
630
550
  return dict(self.params)
@@ -16,7 +16,6 @@ from sqlalchemy.ext import asyncio as sqla_asyncio
16
16
  from sqlalchemy.pool import NullPool
17
17
 
18
18
  from soliplex import agents
19
- from soliplex import agui
20
19
  from soliplex import authz
21
20
  from soliplex import loggers
22
21
  from soliplex import mcp_server
@@ -405,7 +404,7 @@ class Installation:
405
404
  *,
406
405
  room_id: str,
407
406
  user: dict,
408
- the_threads: agui.ThreadStorage = None,
407
+ thread_persistence_engine: sqla_asyncio.AsyncEngine = None,
409
408
  the_room_authz: authz.RoomAuthorizationPolicy = None,
410
409
  run_agent_input: agui_core.RunAgentInput = None,
411
410
  the_logger: loggers.LogWrapper = None,
@@ -424,8 +423,8 @@ class Installation:
424
423
  thread_id = run_agent_input.thread_id
425
424
  run_id = run_agent_input.run_id
426
425
 
427
- if the_threads is not None:
428
- kwargs["the_threads"] = the_threads
426
+ if thread_persistence_engine is not None:
427
+ kwargs["thread_persistence_engine"] = thread_persistence_engine
429
428
 
430
429
  return agents.AgentDependencies(
431
430
  the_installation=self,
@@ -143,10 +143,6 @@ class Skill(pydantic.BaseModel):
143
143
  source: config_skills.SkillKind | None = None
144
144
  name: str
145
145
  description: str
146
- license: str | None
147
- compatibility: str | None
148
- allowed_tools: SkillAllowedTools = None
149
- metadata: SkillMetadata = None
150
146
  state_type_schema: dict[str, typing.Any] | None = None
151
147
  state_namespace: str | None = None
152
148
  extra_parameters: dict[str, typing.Any] = {}
@@ -168,10 +164,6 @@ class Skill(pydantic.BaseModel):
168
164
  source=skill_config.source,
169
165
  name=skill_config.name,
170
166
  description=skill_config.description,
171
- license=skill_config.license,
172
- compatibility=skill_config.compatibility,
173
- allowed_tools=" ".join(skill_config.allowed_tools),
174
- metadata=skill_config.metadata,
175
167
  state_namespace=skill_config.state_namespace,
176
168
  **kwargs,
177
169
  )
@@ -4,6 +4,7 @@
4
4
  - Allow privileged users to mark feedback a as "reviewed", "resolved".
5
5
  """
6
6
 
7
+ import contextlib
7
8
  import datetime
8
9
  import enum
9
10
  import typing
@@ -12,10 +13,12 @@ import jsonpatch
12
13
  import pydantic
13
14
  import pydantic_ai
14
15
  from ag_ui import core as agui_core
16
+ from sqlalchemy.ext import asyncio as sqla_asyncio
15
17
 
16
18
  from soliplex import agents
17
19
  from soliplex import agui
18
20
  from soliplex.agui import parser as agui_parser
21
+ from soliplex.agui import persistence as agui_persistence
19
22
 
20
23
  FRS = agui.FeedbackReviewStatus
21
24
  STATE_NAMESPACE = "soliplex-agui-run-feedback"
@@ -241,11 +244,28 @@ class FeedbackResolution(pydantic.BaseModel):
241
244
  note: str | None = None
242
245
 
243
246
 
244
- async def _do_query(
247
+ @contextlib.asynccontextmanager
248
+ async def _get_threads_storage(
245
249
  ctx: pydantic_ai.RunContext[agents.AgentDependencies],
250
+ ):
251
+ """Context manager for tool-call local thread storage / session
252
+
253
+ Cannot be derived from the FastAPI request dependency, because
254
+ tool calls must run / complete in their own boundaries.
255
+ """
256
+ engine = ctx.deps.thread_persistence_engine
257
+ async with sqla_asyncio.AsyncSession(bind=engine) as session:
258
+ # One transaction per tool call: the storage methods no longer
259
+ # commit, so the tool call owns the unit of work -- committed on a
260
+ # clean response, rolled back if the handler raises.
261
+ async with session.begin():
262
+ yield agui_persistence.ThreadStorage(session)
263
+
264
+
265
+ async def _do_query(
266
+ the_threads: agui_persistence.ThreadStorage,
246
267
  query: RecentRunFeedbackQuery,
247
268
  ) -> RecentRunFeedback:
248
- the_threads = ctx.deps.the_threads
249
269
  runs_w_recent_fb = await the_threads.list_recent_run_feedback(
250
270
  **query.as_kwargs,
251
271
  )
@@ -328,7 +348,8 @@ async def query_recent_feedback(
328
348
  )
329
349
  before_state = {} if raw_state is None else {STATE_NAMESPACE: raw_state}
330
350
 
331
- entries = await _do_query(ctx, query)
351
+ async with _get_threads_storage(ctx) as the_threads:
352
+ entries = await _do_query(the_threads, query)
332
353
  our_state.query = query
333
354
  our_state.entries = entries
334
355
 
@@ -390,29 +411,29 @@ async def get_feedback_run_info(
390
411
 
391
412
  to_query, _ = _find_feedback_by_run_id(our_state, run_id)
392
413
 
393
- the_threads = ctx.deps.the_threads
394
- run = await the_threads.get_run(
395
- user_name=to_query.user_name,
396
- room_id=to_query.room_id,
397
- thread_id=to_query.thread_id,
398
- run_id=to_query.run_id,
399
- )
414
+ async with _get_threads_storage(ctx) as the_threads:
415
+ run = await the_threads.get_run(
416
+ user_name=to_query.user_name,
417
+ room_id=to_query.room_id,
418
+ thread_id=to_query.thread_id,
419
+ run_id=to_query.run_id,
420
+ )
400
421
 
401
- thread = await run.awaitable_attrs.thread
402
- email = await thread.awaitable_attrs.email
403
- run_input = await run.awaitable_attrs.run_agent_input
404
- parent_run_id = await run.awaitable_attrs.parent_run_id
405
- events = await run.awaitable_attrs.events
422
+ thread = await run.awaitable_attrs.thread
423
+ email = await thread.awaitable_attrs.email
424
+ run_input = await run.awaitable_attrs.run_agent_input
425
+ parent_run_id = await run.awaitable_attrs.parent_run_id
426
+ events = await run.awaitable_attrs.events
406
427
 
407
- esp = agui_parser.EventStreamParser(run_input)
428
+ esp = agui_parser.EventStreamParser(run_input)
408
429
 
409
- for event in events:
410
- try:
411
- e_model = event.to_agui_model()
412
- # agui_core.events.Event is not all-inclusive :<
413
- except pydantic.ValidationError: # pragma: NO COVER
414
- continue
415
- esp(e_model)
430
+ for event in events:
431
+ try:
432
+ e_model = event.to_agui_model()
433
+ # agui_core.events.Event is not all-inclusive :<
434
+ except pydantic.ValidationError: # pragma: NO COVER
435
+ continue
436
+ esp(e_model)
416
437
 
417
438
  user_prompts = [
418
439
  message.content for message in esp.messages if message.role == "user"
@@ -471,17 +492,17 @@ async def review_recent_feedback(
471
492
  our_state, review.run_id, [FromWhichAttrs.OPENED]
472
493
  )
473
494
 
474
- the_threads = ctx.deps.the_threads
475
495
  user = ctx.deps.user
476
- await the_threads.review_run_feedback(
477
- reviewer_user_name=user.preferred_username,
478
- reviewer_email=user.email,
479
- note=review.note,
480
- user_name=to_review.user_name,
481
- room_id=to_review.room_id,
482
- thread_id=to_review.thread_id,
483
- run_id=to_review.run_id,
484
- )
496
+ async with _get_threads_storage(ctx) as the_threads:
497
+ await the_threads.review_run_feedback(
498
+ reviewer_user_name=user.preferred_username,
499
+ reviewer_email=user.email,
500
+ note=review.note,
501
+ user_name=to_review.user_name,
502
+ room_id=to_review.room_id,
503
+ thread_id=to_review.thread_id,
504
+ run_id=to_review.run_id,
505
+ )
485
506
 
486
507
  to_review.status = FRS.REVIEWED
487
508
  to_review.note = review.note
@@ -515,17 +536,17 @@ async def resolve_recent_feedback(
515
536
  [FromWhichAttrs.OPENED, FromWhichAttrs.REVIEWED],
516
537
  )
517
538
 
518
- the_threads = ctx.deps.the_threads
519
539
  user = ctx.deps.user
520
- await the_threads.resolve_run_feedback(
521
- resolver_user_name=user.preferred_username,
522
- resolver_email=user.email,
523
- note=resolution.note,
524
- user_name=to_resolve.user_name,
525
- room_id=to_resolve.room_id,
526
- thread_id=to_resolve.thread_id,
527
- run_id=to_resolve.run_id,
528
- )
540
+ async with _get_threads_storage(ctx) as the_threads:
541
+ await the_threads.resolve_run_feedback(
542
+ resolver_user_name=user.preferred_username,
543
+ resolver_email=user.email,
544
+ note=resolution.note,
545
+ user_name=to_resolve.user_name,
546
+ room_id=to_resolve.room_id,
547
+ thread_id=to_resolve.thread_id,
548
+ run_id=to_resolve.run_id,
549
+ )
529
550
 
530
551
  to_resolve.status = FRS.RESOLVED
531
552
  to_resolve.note = resolution.note
@@ -698,7 +698,6 @@ async def post_room_agui_thread_id_run_id(
698
698
  thread_id: pydantic.UUID4,
699
699
  run_id: pydantic.UUID4,
700
700
  the_installation: installation.Installation = depend_the_installation,
701
- the_threads: agui.ThreadStorage = depend_the_threads,
702
701
  the_room_authz: authz.RoomAuthorizationPolicy = depend_the_room_authz,
703
702
  the_user_claims: authn.UserClaims = depend_the_user_claims,
704
703
  the_logger: loggers.LogWrapper = depend_the_logger,
@@ -788,13 +787,6 @@ async def post_room_agui_thread_id_run_id(
788
787
  agent=agent,
789
788
  )
790
789
 
791
- # Persist the run input in its own short-lived, committed session
792
- # rather than via the request-scoped 'the_threads'. This handler
793
- # returns a long-lived StreamingResponse, so the request session's
794
- # transaction stays open for the whole stream; committing the input
795
- # write here releases its lock immediately (and keeps the request
796
- # transaction from holding a writer lock behind the incremental
797
- # per-event background saves).
798
790
  try:
799
791
  async with sqla_asyncio.AsyncSession(
800
792
  bind=request.state.threads_engine,
@@ -818,7 +810,7 @@ async def post_room_agui_thread_id_run_id(
818
810
  room_id=room_id,
819
811
  user=user,
820
812
  run_agent_input=agui_adapter.run_input,
821
- the_threads=the_threads,
813
+ thread_persistence_engine=request.state.threads_engine,
822
814
  the_logger=the_logger,
823
815
  )
824
816
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: soliplex
3
- Version: 0.76
3
+ Version: 0.76.1
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