soliplex 0.70.2__tar.gz → 0.72__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 (97) hide show
  1. {soliplex-0.70.2/src/soliplex.egg-info → soliplex-0.72}/PKG-INFO +2 -2
  2. {soliplex-0.70.2 → soliplex-0.72}/pyproject.toml +2 -5
  3. {soliplex-0.70.2 → soliplex-0.72}/src/soliplex/agents.py +12 -3
  4. {soliplex-0.70.2 → soliplex-0.72}/src/soliplex/agui/__init__.py +5 -1
  5. {soliplex-0.70.2 → soliplex-0.72}/src/soliplex/agui/parser.py +4 -4
  6. {soliplex-0.70.2 → soliplex-0.72}/src/soliplex/agui/persistence.py +168 -31
  7. {soliplex-0.70.2 → soliplex-0.72}/src/soliplex/agui/schema.py +4 -4
  8. {soliplex-0.70.2 → soliplex-0.72}/src/soliplex/authz/__init__.py +138 -23
  9. soliplex-0.72/src/soliplex/authz/persistence.py +506 -0
  10. {soliplex-0.70.2 → soliplex-0.72}/src/soliplex/authz/schema.py +47 -24
  11. {soliplex-0.70.2 → soliplex-0.72}/src/soliplex/cli/admin_users.py +80 -117
  12. soliplex-0.72/src/soliplex/cli/ask.py +311 -0
  13. {soliplex-0.70.2 → soliplex-0.72}/src/soliplex/cli/audit.py +48 -44
  14. soliplex-0.72/src/soliplex/cli/cli_util.py +243 -0
  15. {soliplex-0.70.2 → soliplex-0.72}/src/soliplex/cli/main.py +3 -3
  16. {soliplex-0.70.2 → soliplex-0.72}/src/soliplex/cli/room_authz.py +185 -397
  17. {soliplex-0.70.2 → soliplex-0.72}/src/soliplex/cli/serve.py +23 -6
  18. {soliplex-0.70.2 → soliplex-0.72}/src/soliplex/config/exceptions.py +8 -0
  19. {soliplex-0.70.2 → soliplex-0.72}/src/soliplex/config/installation.py +80 -22
  20. {soliplex-0.70.2 → soliplex-0.72}/src/soliplex/config/meta.py +3 -7
  21. soliplex-0.72/src/soliplex/config/middleware.py +115 -0
  22. {soliplex-0.70.2 → soliplex-0.72}/src/soliplex/config/rooms.py +5 -0
  23. {soliplex-0.70.2 → soliplex-0.72}/src/soliplex/config/routing.py +3 -1
  24. {soliplex-0.70.2 → soliplex-0.72}/src/soliplex/config/skills.py +18 -36
  25. {soliplex-0.70.2 → soliplex-0.72}/src/soliplex/examples.py +50 -3
  26. {soliplex-0.70.2 → soliplex-0.72}/src/soliplex/installation.py +29 -27
  27. soliplex-0.72/src/soliplex/loggers.py +705 -0
  28. {soliplex-0.70.2 → soliplex-0.72}/src/soliplex/main.py +43 -49
  29. {soliplex-0.70.2 → soliplex-0.72}/src/soliplex/models.py +53 -18
  30. soliplex-0.72/src/soliplex/rag_audit.py +125 -0
  31. soliplex-0.72/src/soliplex/sandbox_audit.py +65 -0
  32. {soliplex-0.70.2 → soliplex-0.72}/src/soliplex/skills/bwrap_sandbox/__init__.py +106 -19
  33. {soliplex-0.70.2 → soliplex-0.72}/src/soliplex/titles.py +9 -6
  34. {soliplex-0.70.2 → soliplex-0.72}/src/soliplex/tools/agui_run_feedback.py +3 -3
  35. {soliplex-0.70.2 → soliplex-0.72}/src/soliplex/tools/rag.py +18 -9
  36. {soliplex-0.70.2 → soliplex-0.72}/src/soliplex/views/__init__.py +40 -3
  37. {soliplex-0.70.2 → soliplex-0.72}/src/soliplex/views/agui.py +120 -174
  38. {soliplex-0.70.2 → soliplex-0.72}/src/soliplex/views/authz.py +38 -25
  39. {soliplex-0.70.2 → soliplex-0.72}/src/soliplex/views/installation.py +72 -13
  40. {soliplex-0.70.2 → soliplex-0.72}/src/soliplex/views/quizzes.py +19 -16
  41. soliplex-0.72/src/soliplex/views/room_file_uploads.py +209 -0
  42. {soliplex-0.70.2 → soliplex-0.72}/src/soliplex/views/rooms.py +74 -34
  43. soliplex-0.72/src/soliplex/views/sandbox_workdirs.py +139 -0
  44. {soliplex-0.70.2 → soliplex-0.72}/src/soliplex/views/stats.py +12 -12
  45. {soliplex-0.70.2 → soliplex-0.72}/src/soliplex/views/streaming.py +43 -23
  46. soliplex-0.72/src/soliplex/views/thread_file_uploads.py +202 -0
  47. {soliplex-0.70.2 → soliplex-0.72/src/soliplex.egg-info}/PKG-INFO +2 -2
  48. {soliplex-0.70.2 → soliplex-0.72}/src/soliplex.egg-info/SOURCES.txt +8 -3
  49. {soliplex-0.70.2 → soliplex-0.72}/src/soliplex.egg-info/requires.txt +1 -1
  50. soliplex-0.70.2/src/soliplex/auth.py +0 -10
  51. soliplex-0.70.2/src/soliplex/authz/persistence.py +0 -232
  52. soliplex-0.70.2/src/soliplex/cli/cli_util.py +0 -127
  53. soliplex-0.70.2/src/soliplex/loggers.py +0 -172
  54. soliplex-0.70.2/src/soliplex/views/file_uploads.py +0 -486
  55. {soliplex-0.70.2 → soliplex-0.72}/LICENSE +0 -0
  56. {soliplex-0.70.2 → soliplex-0.72}/MANIFEST.in +0 -0
  57. {soliplex-0.70.2 → soliplex-0.72}/README.md +0 -0
  58. {soliplex-0.70.2 → soliplex-0.72}/example/skills/bare-bones/SKILL.md +0 -0
  59. {soliplex-0.70.2 → soliplex-0.72}/setup.cfg +0 -0
  60. {soliplex-0.70.2 → soliplex-0.72}/skills/soliplex-docs/SKILL.md +0 -0
  61. {soliplex-0.70.2 → soliplex-0.72}/src/soliplex/agui/util.py +0 -0
  62. {soliplex-0.70.2 → soliplex-0.72}/src/soliplex/authn.py +0 -0
  63. {soliplex-0.70.2 → soliplex-0.72}/src/soliplex/cli/ollama.py +0 -0
  64. {soliplex-0.70.2 → soliplex-0.72}/src/soliplex/cli/types.py +0 -0
  65. {soliplex-0.70.2 → soliplex-0.72}/src/soliplex/completions.py +0 -0
  66. {soliplex-0.70.2 → soliplex-0.72}/src/soliplex/config/__init__.py +0 -0
  67. {soliplex-0.70.2 → soliplex-0.72}/src/soliplex/config/_utils.py +0 -0
  68. {soliplex-0.70.2 → soliplex-0.72}/src/soliplex/config/agents.py +0 -0
  69. {soliplex-0.70.2 → soliplex-0.72}/src/soliplex/config/agui.py +0 -0
  70. {soliplex-0.70.2 → soliplex-0.72}/src/soliplex/config/authsystem.py +0 -0
  71. {soliplex-0.70.2 → soliplex-0.72}/src/soliplex/config/completions.py +0 -0
  72. {soliplex-0.70.2 → soliplex-0.72}/src/soliplex/config/logfire.py +0 -0
  73. {soliplex-0.70.2 → soliplex-0.72}/src/soliplex/config/quizzes.py +0 -0
  74. {soliplex-0.70.2 → soliplex-0.72}/src/soliplex/config/rag.py +0 -0
  75. {soliplex-0.70.2 → soliplex-0.72}/src/soliplex/config/secrets.py +0 -0
  76. {soliplex-0.70.2 → soliplex-0.72}/src/soliplex/config/tools.py +0 -0
  77. {soliplex-0.70.2 → soliplex-0.72}/src/soliplex/log_ingest.py +0 -0
  78. {soliplex-0.70.2 → soliplex-0.72}/src/soliplex/mcp_auth.py +0 -0
  79. {soliplex-0.70.2 → soliplex-0.72}/src/soliplex/mcp_client.py +0 -0
  80. {soliplex-0.70.2 → soliplex-0.72}/src/soliplex/mcp_server.py +0 -0
  81. {soliplex-0.70.2 → soliplex-0.72}/src/soliplex/ollama.py +0 -0
  82. {soliplex-0.70.2 → soliplex-0.72}/src/soliplex/quizzes.py +0 -0
  83. {soliplex-0.70.2 → soliplex-0.72}/src/soliplex/secrets.py +0 -0
  84. {soliplex-0.70.2 → soliplex-0.72}/src/soliplex/skills/bwrap_sandbox/SKILL.md +0 -0
  85. {soliplex-0.70.2 → soliplex-0.72}/src/soliplex/tools/__init__.py +0 -0
  86. {soliplex-0.70.2 → soliplex-0.72}/src/soliplex/tools/file_uploads.py +0 -0
  87. {soliplex-0.70.2 → soliplex-0.72}/src/soliplex/tui/cli.py +0 -0
  88. {soliplex-0.70.2 → soliplex-0.72}/src/soliplex/tui/main.py +0 -0
  89. {soliplex-0.70.2 → soliplex-0.72}/src/soliplex/tui/rest_api.py +0 -0
  90. {soliplex-0.70.2 → soliplex-0.72}/src/soliplex/tui/serve.py +0 -0
  91. {soliplex-0.70.2 → soliplex-0.72}/src/soliplex/util.py +0 -0
  92. {soliplex-0.70.2 → soliplex-0.72}/src/soliplex/views/authn.py +0 -0
  93. {soliplex-0.70.2 → soliplex-0.72}/src/soliplex/views/completions.py +0 -0
  94. {soliplex-0.70.2 → soliplex-0.72}/src/soliplex/views/log_ingest.py +0 -0
  95. {soliplex-0.70.2 → soliplex-0.72}/src/soliplex.egg-info/dependency_links.txt +0 -0
  96. {soliplex-0.70.2 → soliplex-0.72}/src/soliplex.egg-info/entry_points.txt +0 -0
  97. {soliplex-0.70.2 → soliplex-0.72}/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.70.2
3
+ Version: 0.72
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
@@ -19,7 +19,7 @@ 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.64,>=0.63.1
22
+ Requires-Dist: haiku.rag-slim<0.66,>=0.65
23
23
  Requires-Dist: haiku-skills<0.19,>=0.18.0
24
24
  Requires-Dist: idna>=3.15
25
25
  Requires-Dist: itsdangerous
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "soliplex"
7
- version = "0.70.2"
7
+ version = "0.72"
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"
@@ -25,7 +25,7 @@ dependencies = [
25
25
  "fastmcp >= 3.3.0, < 3.4",
26
26
  "fakeredis != 2.35.0", # brownbag
27
27
  "greenlet",
28
- "haiku.rag-slim >= 0.63.1, < 0.64",
28
+ "haiku.rag-slim >= 0.65, < 0.66",
29
29
  "haiku-skills >= 0.18.0, < 0.19",
30
30
  "idna >= 3.15",
31
31
  "itsdangerous",
@@ -92,9 +92,6 @@ markers = [
92
92
  [tool.coverage.run]
93
93
  omit = [
94
94
  "src/soliplex/cli/__init__.py",
95
- "src/soliplex/cli/serve.py",
96
-
97
- "src/soliplex/examples.py",
98
95
  "src/soliplex/tui.py",
99
96
  ]
100
97
 
@@ -11,7 +11,7 @@ from pydantic_ai import agent as ai_agent
11
11
  from pydantic_ai import tools as ai_tools
12
12
  from pydantic_ai import toolsets as ai_toolsets
13
13
 
14
- from soliplex import agui as agui_package
14
+ from soliplex import agui
15
15
  from soliplex import mcp_client
16
16
  from soliplex import models
17
17
  from soliplex.config import agents as config_agents
@@ -41,8 +41,8 @@ class AgentDependencies:
41
41
  """
42
42
 
43
43
  the_installation: typing.Any # installation.Installation
44
- the_threads: agui_package.ThreadStorage = None
45
- state: agui_package.AGUI_State = dataclasses.field(default_factory=dict)
44
+ the_threads: agui.ThreadStorage = None
45
+ state: agui.AGUI_State = dataclasses.field(default_factory=dict)
46
46
  room_id: str | None = None
47
47
  thread_id: str | None = None
48
48
  run_id: str | None = None
@@ -150,3 +150,12 @@ def get_agent_from_configs(
150
150
  mcp_client_toolset_configs=mcp_client_toolset_configs,
151
151
  skill_toolset_config=skill_toolset_config,
152
152
  )
153
+
154
+
155
+ def find_skill_toolset(
156
+ agent: pydantic_ai.Agent,
157
+ ) -> hs_agent.SkillToolset | None:
158
+ for toolset in getattr(agent, "toolsets", ()):
159
+ if isinstance(toolset, hs_agent.SkillToolset):
160
+ return toolset
161
+ return None
@@ -587,7 +587,11 @@ async def get_the_threads(request: fastapi.Request) -> ThreadStorage:
587
587
 
588
588
  engine = request.state.threads_engine
589
589
  async with sqla_asyncio.AsyncSession(bind=engine) as session:
590
- yield persistence.ThreadStorage(session)
590
+ # One transaction per request: the storage methods no longer
591
+ # commit, so the request owns the unit of work -- committed on a
592
+ # clean response, rolled back if the handler raises.
593
+ async with session.begin():
594
+ yield persistence.ThreadStorage(session)
591
595
 
592
596
 
593
597
  depend_the_threads = fastapi.Depends(get_the_threads)
@@ -10,7 +10,7 @@ from collections import abc
10
10
  import jsonpatch
11
11
  from ag_ui import core as agui_core
12
12
 
13
- from soliplex import agui as agui_package
13
+ from soliplex import agui
14
14
 
15
15
  AGUI_EventIterator = abc.AsyncIterator[agui_core.Event]
16
16
  AGUI_EventDictIterator = abc.AsyncIterator[agui_core.Event]
@@ -230,12 +230,12 @@ DEFAULT_STRIPPED_MESSAGE_TYPES: StrippedMessageTypes = None
230
230
  @dataclasses.dataclass
231
231
  class EventStreamParser:
232
232
  run_agent_input: dataclasses.InitVar[agui_core.RunAgentInput] = None
233
- run: dataclasses.InitVar[agui_package.Run] = None
233
+ run: dataclasses.InitVar[agui.Run] = None
234
234
 
235
235
  _ = dataclasses.KW_ONLY
236
236
 
237
237
  _run_input: agui_core.RunAgentInput = None
238
- _the_run: agui_package.Run = None
238
+ _the_run: agui.Run = None
239
239
 
240
240
  run_status: RunStatus = RunStatus.INITIALIZED
241
241
  active_steps: set[str] = dataclasses.field(default_factory=set)
@@ -282,7 +282,7 @@ class EventStreamParser:
282
282
  self.messages_by_id = {msg.id: msg for msg in value.messages}
283
283
 
284
284
  @property
285
- def the_run(self) -> agui_package.Run:
285
+ def the_run(self) -> agui.Run:
286
286
  return self._the_run
287
287
 
288
288
  def _assert_running(self, event):
@@ -1,14 +1,20 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import collections.abc
3
4
  import contextlib
4
5
  import datetime
5
6
  import typing
6
7
 
8
+ import logfire
7
9
  from ag_ui import core as agui_core
10
+ from haiku.skills import agent as hs_agent
11
+ from sqlalchemy import exc as sqla_exc
8
12
  from sqlalchemy import sql as sqla_sql
9
13
  from sqlalchemy.ext import asyncio as sqla_asyncio
10
14
 
11
- from soliplex import agui as agui_package
15
+ from soliplex import agui
16
+ from soliplex import loggers
17
+ from soliplex import rag_audit
12
18
  from soliplex.agui import persistence as agui_persistence
13
19
  from soliplex.agui import schema as agui_schema
14
20
  from soliplex.agui import util as agui_util
@@ -16,7 +22,7 @@ from soliplex.agui import util as agui_util
16
22
  # Temporary backward-compatibility: to be removed in 'v0.45'
17
23
  from soliplex.agui.schema import * # noqa F403
18
24
 
19
- FeedbackReviewStatus = agui_package.FeedbackReviewStatus
25
+ FeedbackReviewStatus = agui.FeedbackReviewStatus
20
26
 
21
27
 
22
28
  class NoFeedbackFound(ValueError):
@@ -48,15 +54,19 @@ _LAST_ACTIVITY = sqla_sql.func.max(
48
54
  )
49
55
 
50
56
 
51
- class ThreadStorage(agui_package.ThreadStorage):
57
+ class ThreadStorage(agui.ThreadStorage):
52
58
  def __init__(self, session: sqla_asyncio.AsyncSession):
53
59
  self._session = session
54
60
 
55
61
  @property
56
62
  @contextlib.asynccontextmanager
57
63
  async def session(self):
58
- async with self._session.begin():
59
- yield self._session
64
+ # Yield the caller-owned session as-is: the session owner (a
65
+ # FastAPI dependency, CLI context manager, or streaming helper)
66
+ # owns the transaction boundary and commits the unit of work
67
+ # exactly once. Methods here never open their own transaction or
68
+ # commit, so callers -- and tests -- need no interleaved commits.
69
+ yield self._session
60
70
 
61
71
  async def _find_user_thread(
62
72
  self,
@@ -74,12 +84,12 @@ class ThreadStorage(agui_package.ThreadStorage):
74
84
  thread = (await session.scalars(query)).first()
75
85
 
76
86
  if thread is None:
77
- raise agui_package.UnknownThread(user_name, thread_id)
87
+ raise agui.UnknownThread(user_name, thread_id)
78
88
 
79
89
  t_room_id = await thread.awaitable_attrs.room_id
80
90
 
81
91
  if t_room_id != room_id:
82
- raise agui_package.ThreadRoomMismatch(room_id, t_room_id)
92
+ raise agui.ThreadRoomMismatch(room_id, t_room_id)
83
93
 
84
94
  return thread
85
95
 
@@ -91,7 +101,7 @@ class ThreadStorage(agui_package.ThreadStorage):
91
101
  thread_id: str,
92
102
  run_id: str,
93
103
  session,
94
- exc_type=agui_package.UnknownRun,
104
+ exc_type=agui.UnknownRun,
95
105
  ):
96
106
  thread = await self._find_user_thread(
97
107
  user_name=user_name,
@@ -294,7 +304,7 @@ class ThreadStorage(agui_package.ThreadStorage):
294
304
  thread_id=thread_id,
295
305
  run_id=parent_run_id,
296
306
  session=session,
297
- exc_type=agui_package.MissingParentRun,
307
+ exc_type=agui.MissingParentRun,
298
308
  )
299
309
  else:
300
310
  parent = None
@@ -362,7 +372,7 @@ class ThreadStorage(agui_package.ThreadStorage):
362
372
  already = await run.awaitable_attrs.run_agent_input
363
373
 
364
374
  if already is not None:
365
- raise agui_package.RunAlreadyStarted(
375
+ raise agui.RunAlreadyStarted(
366
376
  user_name,
367
377
  thread_id,
368
378
  run_id,
@@ -420,8 +430,6 @@ class ThreadStorage(agui_package.ThreadStorage):
420
430
  event: agui_core.Event,
421
431
  ) -> None:
422
432
  """Save a single event for a run (incremental persistence)"""
423
- await self._session.commit()
424
-
425
433
  async with self.session as session:
426
434
  run = await self._find_thread_run(
427
435
  user_name=user_name,
@@ -448,8 +456,6 @@ class ThreadStorage(agui_package.ThreadStorage):
448
456
  run_id: str,
449
457
  ) -> None:
450
458
  """Mark a run as finished"""
451
- await self._session.commit()
452
-
453
459
  async with self.session as session:
454
460
  run = await self._find_thread_run(
455
461
  user_name=user_name,
@@ -475,8 +481,6 @@ class ThreadStorage(agui_package.ThreadStorage):
475
481
 
476
482
  Each element is a (event_index, event) tuple.
477
483
  """
478
- await self._session.commit()
479
-
480
484
  async with self.session as session:
481
485
  run = await self._find_thread_run(
482
486
  user_name=user_name,
@@ -504,8 +508,6 @@ class ThreadStorage(agui_package.ThreadStorage):
504
508
  run_id: str,
505
509
  ) -> bool:
506
510
  """Return True if the run has a 'finished' timestamp"""
507
- await self._session.commit()
508
-
509
511
  async with self.session as session:
510
512
  run = await self._find_thread_run(
511
513
  user_name=user_name,
@@ -524,11 +526,9 @@ class ThreadStorage(agui_package.ThreadStorage):
524
526
  room_id: str,
525
527
  thread_id: str,
526
528
  run_id: str,
527
- events: agui_package.AGUI_Events,
528
- ) -> agui_package.AGUI_Events:
529
+ events: agui.AGUI_Events,
530
+ ) -> agui.AGUI_Events:
529
531
  """Save the events for a gven run"""
530
- await self._session.commit()
531
-
532
532
  async with self.session as session:
533
533
  run = await self._find_thread_run(
534
534
  user_name=user_name,
@@ -560,8 +560,6 @@ class ThreadStorage(agui_package.ThreadStorage):
560
560
  tool_calls: int,
561
561
  ):
562
562
  """Save the run usage statistics"""
563
- await self._session.commit()
564
-
565
563
  async with self.session as session:
566
564
  run = await self._find_thread_run(
567
565
  user_name=user_name,
@@ -591,8 +589,6 @@ class ThreadStorage(agui_package.ThreadStorage):
591
589
  reason: str,
592
590
  ):
593
591
  """Save the run feedback"""
594
- await self._session.commit()
595
-
596
592
  async with self.session as session:
597
593
  run = await self._find_thread_run(
598
594
  user_name=user_name,
@@ -624,10 +620,8 @@ class ThreadStorage(agui_package.ThreadStorage):
624
620
  room_id: str,
625
621
  thread_id: str,
626
622
  run_id: str,
627
- ) -> agui_package.RunFeedbackType | None:
623
+ ) -> agui.RunFeedbackType | None:
628
624
  """Get the run feedback"""
629
- await self._session.commit()
630
-
631
625
  async with self.session as session:
632
626
  run = await self._find_thread_run(
633
627
  user_name=user_name,
@@ -658,8 +652,6 @@ class ThreadStorage(agui_package.ThreadStorage):
658
652
  if limit is None and since is None:
659
653
  limit = 20
660
654
 
661
- await self._session.commit()
662
-
663
655
  async with self.session as session:
664
656
  query = (
665
657
  sqla_sql.Select(
@@ -869,3 +861,148 @@ class ThreadStorage(agui_package.ThreadStorage):
869
861
  )
870
862
 
871
863
  return history_entry
864
+
865
+
866
+ # --------------------------------------------------------------------------
867
+ # Engine-owning run-persistence helpers
868
+ #
869
+ # Each builds its own short-lived session (the one bound to a request's
870
+ # lifetime may already be closed, e.g. after an early connection reset) and
871
+ # owns its commit: 'ThreadStorage' methods no longer commit on their own.
872
+ # Committing per event is what lets a reconnecting client's poll observe
873
+ # events incrementally.
874
+ # --------------------------------------------------------------------------
875
+
876
+
877
+ async def capture_usage_after_stream(
878
+ result,
879
+ *,
880
+ sqla_engine,
881
+ user_name: str,
882
+ room_id: str,
883
+ thread_id: str,
884
+ run_id: str,
885
+ ):
886
+ """Save the run usage to the database."""
887
+ usage = getattr(result, "usage", None)
888
+
889
+ if usage is not None:
890
+ async with sqla_asyncio.AsyncSession(bind=sqla_engine) as session:
891
+ the_threads = ThreadStorage(session)
892
+
893
+ async with session.begin():
894
+ await the_threads.save_run_usage(
895
+ user_name=user_name,
896
+ room_id=room_id,
897
+ thread_id=thread_id,
898
+ run_id=run_id,
899
+ input_tokens=usage.input_tokens,
900
+ output_tokens=usage.output_tokens,
901
+ requests=usage.requests,
902
+ tool_calls=usage.tool_calls,
903
+ )
904
+
905
+
906
+ async def save_single_event(
907
+ sqla_engine,
908
+ user_name: str,
909
+ room_id: str,
910
+ thread_id: str,
911
+ run_id: str,
912
+ event,
913
+ ):
914
+ """Save a single run event to the database (incremental persistence)."""
915
+ async with sqla_asyncio.AsyncSession(bind=sqla_engine) as session:
916
+ the_threads = ThreadStorage(session)
917
+
918
+ async with session.begin():
919
+ await the_threads.save_single_event(
920
+ user_name=user_name,
921
+ room_id=room_id,
922
+ thread_id=thread_id,
923
+ run_id=run_id,
924
+ event=event,
925
+ )
926
+
927
+
928
+ async def finish_run(
929
+ sqla_engine,
930
+ user_name: str,
931
+ room_id: str,
932
+ thread_id: str,
933
+ run_id: str,
934
+ ):
935
+ """Mark a run as finished in the database."""
936
+ async with sqla_asyncio.AsyncSession(bind=sqla_engine) as session:
937
+ the_threads = ThreadStorage(session)
938
+
939
+ async with session.begin():
940
+ await the_threads.finish_run(
941
+ user_name=user_name,
942
+ room_id=room_id,
943
+ thread_id=thread_id,
944
+ run_id=run_id,
945
+ )
946
+
947
+
948
+ async def drive_agui_turn(
949
+ *,
950
+ adapter,
951
+ skill_toolset: hs_agent.SkillToolset | None,
952
+ engine,
953
+ user_name: str,
954
+ room_id: str,
955
+ thread_id: str,
956
+ run_id: str,
957
+ claims: dict | None = None,
958
+ rag_db_paths: dict[str, str] | None = None,
959
+ run_stream_kwargs: dict | None = None,
960
+ ) -> collections.abc.AsyncIterator:
961
+ """Drive one room-agent turn as an AG-UI event stream.
962
+
963
+ An async generator: for each event it records any RAG access (skill
964
+ retrievals surface as AG-UI activity events) and persists the event
965
+ (via :func:`save_single_event`), then yields it. Callers own
966
+ presentation (an SSE queue, or collecting the assistant text) and any
967
+ post-run work such as :func:`finish_run`, usage capture, or title
968
+ generation.
969
+
970
+ ``run_stream_kwargs`` is forwarded verbatim to
971
+ ``haiku.skills.agent.run_agui_stream`` (``deps``, ``conversation_id``,
972
+ ``on_complete``).
973
+ """
974
+ auditor = rag_audit.RagAccessAuditor(
975
+ loggers.RAGAccessAuditLog(
976
+ claims=claims or {},
977
+ room_id=room_id,
978
+ thread_id=thread_id,
979
+ run_id=run_id,
980
+ ),
981
+ (rag_db_paths or {}).get,
982
+ )
983
+
984
+ async with hs_agent.run_agui_stream(
985
+ adapter,
986
+ toolset=skill_toolset,
987
+ **(run_stream_kwargs or {}),
988
+ ) as event_stream:
989
+ compacted = agui.compact_event_stream(event_stream)
990
+ async for event in compacted:
991
+ auditor.handle(event)
992
+
993
+ try:
994
+ await save_single_event(
995
+ engine,
996
+ user_name=user_name,
997
+ room_id=room_id,
998
+ thread_id=thread_id,
999
+ run_id=run_id,
1000
+ event=event,
1001
+ )
1002
+ except sqla_exc.SQLAlchemyError as sa_exc:
1003
+ logfire.error(
1004
+ "Error saving event: {error_message}",
1005
+ error_message=str(sa_exc),
1006
+ )
1007
+
1008
+ yield event
@@ -12,7 +12,7 @@ from sqlalchemy import schema as sqla_schema
12
12
  from sqlalchemy.ext import asyncio as sqla_asyncio
13
13
  from sqlalchemy.sql import sqltypes as sqla_sqltypes
14
14
 
15
- from soliplex import agui as agui_package
15
+ from soliplex import agui
16
16
  from soliplex import util
17
17
  from soliplex.agui import util as agui_util
18
18
  from soliplex.config import installation as config_installation
@@ -42,7 +42,7 @@ JSON_Mapped_From = dict[str, typing.Any]
42
42
 
43
43
  metadata = sqla_schema.MetaData(naming_convention=NAMING_CONVENTION)
44
44
 
45
- FeedbackReviewStatus = agui_package.FeedbackReviewStatus
45
+ FeedbackReviewStatus = agui.FeedbackReviewStatus
46
46
 
47
47
 
48
48
  class Base(AsyncAttrs, DeclarativeBase):
@@ -447,8 +447,8 @@ class RunUsage(Base):
447
447
  requests: Mapped[int] = mapped_column()
448
448
  tool_calls: Mapped[int] = mapped_column()
449
449
 
450
- def as_tuple(self) -> agui_package.RunUsageStats:
451
- return agui_package.RunUsageStats(
450
+ def as_tuple(self) -> agui.RunUsageStats:
451
+ return agui.RunUsageStats(
452
452
  self.input_tokens,
453
453
  self.output_tokens,
454
454
  self.requests,