soliplex 0.81__tar.gz → 0.82__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 (105) hide show
  1. {soliplex-0.81/src/soliplex.egg-info → soliplex-0.82}/PKG-INFO +1 -1
  2. {soliplex-0.81 → soliplex-0.82}/pyproject.toml +13 -4
  3. {soliplex-0.81 → soliplex-0.82}/src/soliplex/agui/__init__.py +18 -2
  4. {soliplex-0.81 → soliplex-0.82}/src/soliplex/agui/persistence.py +14 -6
  5. {soliplex-0.81 → soliplex-0.82}/src/soliplex/agui/schema.py +3 -14
  6. soliplex-0.82/src/soliplex/alembic_migrations/__init__.py +715 -0
  7. soliplex-0.82/src/soliplex/alembic_migrations/env.py +17 -0
  8. soliplex-0.82/src/soliplex/alembic_migrations/versions/216d48e1e2e5_soliplex_v0_53.py +66 -0
  9. soliplex-0.82/src/soliplex/alembic_migrations/versions/4b63e5f3f39e_soliplex_v0_51_1.py +74 -0
  10. soliplex-0.82/src/soliplex/alembic_migrations/versions/63edaa5987f6_soliplex_v0_67.py +459 -0
  11. soliplex-0.82/src/soliplex/alembic_migrations/versions/a1c7d3e90b42_soliplex_v0_80.py +60 -0
  12. soliplex-0.82/src/soliplex/alembic_migrations/versions/b7e2f41c9d05_soliplex_v0_82.py +56 -0
  13. soliplex-0.82/src/soliplex/alembic_migrations/versions/d5009d4f9874_soliplex_v0_44.py +240 -0
  14. {soliplex-0.81 → soliplex-0.82}/src/soliplex/authz/schema.py +0 -14
  15. {soliplex-0.81 → soliplex-0.82}/src/soliplex/cli/admin_users.py +51 -40
  16. {soliplex-0.81 → soliplex-0.82}/src/soliplex/cli/ask.py +8 -10
  17. {soliplex-0.81 → soliplex-0.82}/src/soliplex/cli/audit.py +320 -21
  18. {soliplex-0.81 → soliplex-0.82}/src/soliplex/cli/cli_util.py +141 -20
  19. soliplex-0.82/src/soliplex/cli/database.py +413 -0
  20. {soliplex-0.81 → soliplex-0.82}/src/soliplex/cli/main.py +30 -1
  21. {soliplex-0.81 → soliplex-0.82}/src/soliplex/cli/room_authz.py +119 -48
  22. {soliplex-0.81 → soliplex-0.82}/src/soliplex/cli/serve.py +6 -0
  23. {soliplex-0.81 → soliplex-0.82}/src/soliplex/config/installation.py +227 -36
  24. {soliplex-0.81 → soliplex-0.82}/src/soliplex/installation.py +185 -52
  25. {soliplex-0.81 → soliplex-0.82}/src/soliplex/main.py +8 -0
  26. {soliplex-0.81 → soliplex-0.82}/src/soliplex/models.py +15 -12
  27. {soliplex-0.81 → soliplex-0.82/src/soliplex.egg-info}/PKG-INFO +1 -1
  28. {soliplex-0.81 → soliplex-0.82}/src/soliplex.egg-info/SOURCES.txt +9 -0
  29. {soliplex-0.81 → soliplex-0.82}/src/soliplex.egg-info/entry_points.txt +1 -1
  30. {soliplex-0.81 → soliplex-0.82}/LICENSE +0 -0
  31. {soliplex-0.81 → soliplex-0.82}/MANIFEST.in +0 -0
  32. {soliplex-0.81 → soliplex-0.82}/README.md +0 -0
  33. {soliplex-0.81 → soliplex-0.82}/example/skills/bare-bones/SKILL.md +0 -0
  34. {soliplex-0.81 → soliplex-0.82}/example/skills/stanzas/SKILL.md +0 -0
  35. {soliplex-0.81 → soliplex-0.82}/setup.cfg +0 -0
  36. {soliplex-0.81 → soliplex-0.82}/skills/soliplex-docs/SKILL.md +0 -0
  37. {soliplex-0.81 → soliplex-0.82}/src/soliplex/agents.py +0 -0
  38. {soliplex-0.81 → soliplex-0.82}/src/soliplex/agui/parser.py +0 -0
  39. {soliplex-0.81 → soliplex-0.82}/src/soliplex/agui/util.py +0 -0
  40. {soliplex-0.81 → soliplex-0.82}/src/soliplex/authn.py +0 -0
  41. {soliplex-0.81 → soliplex-0.82}/src/soliplex/authz/__init__.py +0 -0
  42. {soliplex-0.81 → soliplex-0.82}/src/soliplex/authz/persistence.py +0 -0
  43. {soliplex-0.81 → soliplex-0.82}/src/soliplex/capabilities/filesystem.py +0 -0
  44. {soliplex-0.81 → soliplex-0.82}/src/soliplex/capabilities/rag_audit.py +0 -0
  45. {soliplex-0.81 → soliplex-0.82}/src/soliplex/cli/ollama.py +0 -0
  46. {soliplex-0.81 → soliplex-0.82}/src/soliplex/cli/types.py +0 -0
  47. {soliplex-0.81 → soliplex-0.82}/src/soliplex/completions.py +0 -0
  48. {soliplex-0.81 → soliplex-0.82}/src/soliplex/config/__init__.py +0 -0
  49. {soliplex-0.81 → soliplex-0.82}/src/soliplex/config/_utils.py +0 -0
  50. {soliplex-0.81 → soliplex-0.82}/src/soliplex/config/agents.py +0 -0
  51. {soliplex-0.81 → soliplex-0.82}/src/soliplex/config/agui.py +0 -0
  52. {soliplex-0.81 → soliplex-0.82}/src/soliplex/config/authsystem.py +0 -0
  53. {soliplex-0.81 → soliplex-0.82}/src/soliplex/config/completions.py +0 -0
  54. {soliplex-0.81 → soliplex-0.82}/src/soliplex/config/exceptions.py +0 -0
  55. {soliplex-0.81 → soliplex-0.82}/src/soliplex/config/interpolation.py +0 -0
  56. {soliplex-0.81 → soliplex-0.82}/src/soliplex/config/logfire.py +0 -0
  57. {soliplex-0.81 → soliplex-0.82}/src/soliplex/config/meta.py +0 -0
  58. {soliplex-0.81 → soliplex-0.82}/src/soliplex/config/middleware.py +0 -0
  59. {soliplex-0.81 → soliplex-0.82}/src/soliplex/config/quizzes.py +0 -0
  60. {soliplex-0.81 → soliplex-0.82}/src/soliplex/config/rag.py +0 -0
  61. {soliplex-0.81 → soliplex-0.82}/src/soliplex/config/rooms.py +0 -0
  62. {soliplex-0.81 → soliplex-0.82}/src/soliplex/config/routing.py +0 -0
  63. {soliplex-0.81 → soliplex-0.82}/src/soliplex/config/secrets.py +0 -0
  64. {soliplex-0.81 → soliplex-0.82}/src/soliplex/config/skills.py +0 -0
  65. {soliplex-0.81 → soliplex-0.82}/src/soliplex/config/tools.py +0 -0
  66. {soliplex-0.81 → soliplex-0.82}/src/soliplex/examples.py +0 -0
  67. {soliplex-0.81 → soliplex-0.82}/src/soliplex/log_ingest.py +0 -0
  68. {soliplex-0.81 → soliplex-0.82}/src/soliplex/loggers.py +0 -0
  69. {soliplex-0.81 → soliplex-0.82}/src/soliplex/mcp_auth.py +0 -0
  70. {soliplex-0.81 → soliplex-0.82}/src/soliplex/mcp_client.py +0 -0
  71. {soliplex-0.81 → soliplex-0.82}/src/soliplex/mcp_server.py +0 -0
  72. {soliplex-0.81 → soliplex-0.82}/src/soliplex/ollama.py +0 -0
  73. {soliplex-0.81 → soliplex-0.82}/src/soliplex/quizzes.py +0 -0
  74. {soliplex-0.81 → soliplex-0.82}/src/soliplex/rag_audit.py +0 -0
  75. {soliplex-0.81 → soliplex-0.82}/src/soliplex/sandbox_audit.py +0 -0
  76. {soliplex-0.81 → soliplex-0.82}/src/soliplex/secrets.py +0 -0
  77. {soliplex-0.81 → soliplex-0.82}/src/soliplex/skills/bwrap_sandbox/SKILL.md +0 -0
  78. {soliplex-0.81 → soliplex-0.82}/src/soliplex/skills/bwrap_sandbox/__init__.py +0 -0
  79. {soliplex-0.81 → soliplex-0.82}/src/soliplex/titles.py +0 -0
  80. {soliplex-0.81 → soliplex-0.82}/src/soliplex/tools/__init__.py +0 -0
  81. {soliplex-0.81 → soliplex-0.82}/src/soliplex/tools/agui_run_feedback.py +0 -0
  82. {soliplex-0.81 → soliplex-0.82}/src/soliplex/tools/file_uploads.py +0 -0
  83. {soliplex-0.81 → soliplex-0.82}/src/soliplex/tools/rag.py +0 -0
  84. {soliplex-0.81 → soliplex-0.82}/src/soliplex/tui/cli.py +0 -0
  85. {soliplex-0.81 → soliplex-0.82}/src/soliplex/tui/main.py +0 -0
  86. {soliplex-0.81 → soliplex-0.82}/src/soliplex/tui/rest_api.py +0 -0
  87. {soliplex-0.81 → soliplex-0.82}/src/soliplex/tui/serve.py +0 -0
  88. {soliplex-0.81 → soliplex-0.82}/src/soliplex/util.py +0 -0
  89. {soliplex-0.81 → soliplex-0.82}/src/soliplex/views/__init__.py +0 -0
  90. {soliplex-0.81 → soliplex-0.82}/src/soliplex/views/agui.py +0 -0
  91. {soliplex-0.81 → soliplex-0.82}/src/soliplex/views/authn.py +0 -0
  92. {soliplex-0.81 → soliplex-0.82}/src/soliplex/views/authz.py +0 -0
  93. {soliplex-0.81 → soliplex-0.82}/src/soliplex/views/completions.py +0 -0
  94. {soliplex-0.81 → soliplex-0.82}/src/soliplex/views/installation.py +0 -0
  95. {soliplex-0.81 → soliplex-0.82}/src/soliplex/views/log_ingest.py +0 -0
  96. {soliplex-0.81 → soliplex-0.82}/src/soliplex/views/quizzes.py +0 -0
  97. {soliplex-0.81 → soliplex-0.82}/src/soliplex/views/room_file_uploads.py +0 -0
  98. {soliplex-0.81 → soliplex-0.82}/src/soliplex/views/rooms.py +0 -0
  99. {soliplex-0.81 → soliplex-0.82}/src/soliplex/views/sandbox_workdirs.py +0 -0
  100. {soliplex-0.81 → soliplex-0.82}/src/soliplex/views/stats.py +0 -0
  101. {soliplex-0.81 → soliplex-0.82}/src/soliplex/views/streaming.py +0 -0
  102. {soliplex-0.81 → soliplex-0.82}/src/soliplex/views/thread_file_uploads.py +0 -0
  103. {soliplex-0.81 → soliplex-0.82}/src/soliplex.egg-info/dependency_links.txt +0 -0
  104. {soliplex-0.81 → soliplex-0.82}/src/soliplex.egg-info/requires.txt +0 -0
  105. {soliplex-0.81 → soliplex-0.82}/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.81
3
+ Version: 0.82
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.81"
7
+ version = "0.82"
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"
@@ -50,13 +50,13 @@ dependencies = [
50
50
  ]
51
51
 
52
52
  [project.scripts]
53
- soliplex-cli = "soliplex.cli.main:the_cli"
53
+ soliplex-cli = "soliplex.cli.main:main"
54
54
  soliplex-tui = "soliplex.tui.cli:the_cli"
55
55
  soliplex-tui-serve = "soliplex.tui.serve:the_cli"
56
56
 
57
57
  [dependency-groups]
58
58
  dev = [
59
- "anyio >= 4.12.1, < 4.13",
59
+ "anyio >= 4.14.2, < 4.15",
60
60
  "coverage >= 7.13.5, < 7.14",
61
61
  "pytest >= 9.0.2, < 9.1",
62
62
  "pytest-cov >= 7.0.0, < 7.1",
@@ -102,7 +102,15 @@ markers = [
102
102
  omit = [
103
103
  "scripts/lint_textio.py", # runs its own '--self-test' during lint
104
104
  "scripts/lint_interpolation.py", # ditto
105
+ "scripts/lint_alembic_chain.py", # ditto
106
+ # Run-once deployment repair (issue #1367), applied by hand against a
107
+ # live database and verified there; like a migration script, it is not
108
+ # exercised by the unit suite.
109
+ "scripts/bootstrap_alembic_version.py",
105
110
  "src/soliplex/tui/*", # the TUI is deliberately untested
111
+ # Generated revision scripts. The package's '__init__.py' and
112
+ # 'env.py' are hand-written, and are held to the 100% bar.
113
+ "src/soliplex/alembic_migrations/versions/*",
106
114
  ]
107
115
 
108
116
  [tool.coverage.report]
@@ -141,6 +149,7 @@ ignore = [
141
149
 
142
150
  [tool.ruff.lint.isort]
143
151
  force-single-line = true
152
+ known-third-party = ["alembic"]
144
153
 
145
154
  [tool.ruff.lint.flake8-pytest-style]
146
155
  parametrize-names-type = "csv"
@@ -171,7 +180,7 @@ rolling_prefix = "docs"
171
180
 
172
181
 
173
182
  [tool.alembic]
174
-
183
+ script_location = "%(here)s/src/soliplex/alembic_migrations"
175
184
 
176
185
  # template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
177
186
  # Uncomment the line below if you want the files to be prepended with date and time
@@ -104,10 +104,11 @@ RunUsageStats = collections.namedtuple(
104
104
  "tool_calls",
105
105
  "final_input_tokens",
106
106
  "resolved_model_name",
107
+ "final_output_tokens",
107
108
  ],
108
109
  # Rows written before these columns existed deserialize with them
109
- # unset, so both stay optional at the tuple boundary too.
110
- defaults=(None, None),
110
+ # unset, so they stay optional at the tuple boundary too.
111
+ defaults=(None, None, None),
111
112
  )
112
113
 
113
114
 
@@ -142,6 +143,21 @@ class RunUsage(abc.ABC):
142
143
  tokenizer from it in preference to the configured string.
143
144
  """
144
145
 
146
+ final_output_tokens: int | None
147
+ """Output tokens of the run's *last* model request.
148
+
149
+ That response is the reply the thread now ends with, and it is part of
150
+ the next request's input. 'final_input_tokens' alone therefore reads
151
+ one reply short of what the window holds; the sum of the two is the
152
+ thread's occupancy. None when the run produced no model response.
153
+
154
+ The provider's own count, as reported: on a reasoning model it includes
155
+ the reasoning, which providers generally do not carry into the next
156
+ request, so the sum reads high there by the last reply's reasoning --
157
+ never low -- until the next run measures. Ollama itemises no reasoning
158
+ count, so nothing more exact is available for it.
159
+ """
160
+
145
161
  @abc.abstractmethod
146
162
  def as_tuple(self) -> RunUsageStats:
147
163
  """Return values as a tuple."""
@@ -554,6 +554,7 @@ class ThreadStorage(agui.ThreadStorage):
554
554
  tool_calls: int,
555
555
  final_input_tokens: int | None = None,
556
556
  resolved_model_name: str | None = None,
557
+ final_output_tokens: int | None = None,
557
558
  ):
558
559
  """Save the run usage statistics"""
559
560
  async with self.session as session:
@@ -573,6 +574,7 @@ class ThreadStorage(agui.ThreadStorage):
573
574
  tool_calls=tool_calls,
574
575
  final_input_tokens=final_input_tokens,
575
576
  resolved_model_name=resolved_model_name,
577
+ final_output_tokens=final_output_tokens,
576
578
  )
577
579
  )
578
580
 
@@ -893,20 +895,22 @@ class ThreadStorage(agui.ThreadStorage):
893
895
 
894
896
 
895
897
  def _final_request_usage(result):
896
- """Usage and model name of the run's last model response.
898
+ """Input and output tokens, and model name, of the run's last response.
897
899
 
898
900
  'RunUsage.input_tokens' accumulates across every request a run makes, so
899
901
  it answers "what did this run cost", not "how full was the window". The
900
- last response's own 'RequestUsage' answers the second question, and its
902
+ last response's own 'RequestUsage' answers the second question: its
903
+ input is what the window held when the request went out, and its output
904
+ is the reply that joins that input in the *next* request. The response's
901
905
  'model_name' is what the provider actually served -- a better tokenizer
902
906
  key than the configured name, which may be an alias.
903
907
 
904
- Returns '(None, None)' for a run that never reached the model.
908
+ Returns '(None, None, None)' for a run that never reached the model.
905
909
  """
906
910
  all_messages = getattr(result, "all_messages", None)
907
911
 
908
912
  if all_messages is None: # pragma: NO COVER - defensive
909
- return None, None
913
+ return None, None, None
910
914
 
911
915
  for message in reversed(all_messages()):
912
916
  message_usage = getattr(message, "usage", None)
@@ -916,10 +920,11 @@ def _final_request_usage(result):
916
920
 
917
921
  return (
918
922
  getattr(message_usage, "input_tokens", None),
923
+ getattr(message_usage, "output_tokens", None),
919
924
  getattr(message, "model_name", None),
920
925
  )
921
926
 
922
- return None, None
927
+ return None, None, None
923
928
 
924
929
 
925
930
  async def capture_usage_after_stream(
@@ -935,7 +940,9 @@ async def capture_usage_after_stream(
935
940
  usage = getattr(result, "usage", None)
936
941
 
937
942
  if usage is not None:
938
- final_input_tokens, resolved_model_name = _final_request_usage(result)
943
+ final_input_tokens, final_output_tokens, resolved_model_name = (
944
+ _final_request_usage(result)
945
+ )
939
946
  async with sqla_asyncio.AsyncSession(bind=sqla_engine) as session:
940
947
  the_threads = ThreadStorage(session)
941
948
 
@@ -951,6 +958,7 @@ async def capture_usage_after_stream(
951
958
  tool_calls=usage.tool_calls,
952
959
  final_input_tokens=final_input_tokens,
953
960
  resolved_model_name=resolved_model_name,
961
+ final_output_tokens=final_output_tokens,
954
962
  )
955
963
 
956
964
 
@@ -429,6 +429,7 @@ class RunUsage(Base):
429
429
  'tool_calls', int, required
430
430
  'final_input_tokens', int, optional
431
431
  'resolved_model_name', str, optional
432
+ 'final_output_tokens', int, optional
432
433
  """
433
434
 
434
435
  __tablename__ = "run_usage"
@@ -453,6 +454,7 @@ class RunUsage(Base):
453
454
  # the model has no final request to measure.
454
455
  final_input_tokens: Mapped[int | None] = mapped_column(default=None)
455
456
  resolved_model_name: Mapped[str | None] = mapped_column(default=None)
457
+ final_output_tokens: Mapped[int | None] = mapped_column(default=None)
456
458
 
457
459
  def as_tuple(self) -> agui.RunUsageStats:
458
460
  return agui.RunUsageStats(
@@ -462,6 +464,7 @@ class RunUsage(Base):
462
464
  self.tool_calls,
463
465
  self.final_input_tokens,
464
466
  self.resolved_model_name,
467
+ self.final_output_tokens,
465
468
  )
466
469
 
467
470
 
@@ -532,7 +535,6 @@ class RunFeedbackReviewEntry(Base):
532
535
 
533
536
  def get_engine(
534
537
  engine_url=config_installation.SYNC_MEMORY_ENGINE_URL,
535
- init_schema=False,
536
538
  **engine_kwargs,
537
539
  ) -> sqlalchemy.Engine:
538
540
  engine = sqlalchemy.create_engine(
@@ -557,21 +559,15 @@ def get_engine(
557
559
  cursor_fk.execute("PRAGMA foreign_keys=ON")
558
560
  cursor_fk.close()
559
561
 
560
- if init_schema:
561
- with engine.connect() as connection:
562
- Base.metadata.create_all(connection)
563
-
564
562
  return engine
565
563
 
566
564
 
567
565
  def get_session(
568
566
  engine_url=config_installation.SYNC_MEMORY_ENGINE_URL,
569
- init_schema=False,
570
567
  **engine_kwargs,
571
568
  ) -> sqla_orm.Session:
572
569
  engine = get_engine(
573
570
  engine_url=engine_url,
574
- init_schema=init_schema,
575
571
  **engine_kwargs,
576
572
  )
577
573
  return sqla_orm.Session(bind=engine)
@@ -579,7 +575,6 @@ def get_session(
579
575
 
580
576
  async def get_async_engine(
581
577
  engine_url=config_installation.ASYNC_MEMORY_ENGINE_URL,
582
- init_schema=False,
583
578
  **engine_kwargs,
584
579
  ) -> sqla_asyncio.AsyncEngine:
585
580
  engine = sqla_asyncio.create_async_engine(
@@ -588,21 +583,15 @@ async def get_async_engine(
588
583
  **engine_kwargs,
589
584
  )
590
585
 
591
- if init_schema:
592
- async with engine.begin() as connection:
593
- await connection.run_sync(Base.metadata.create_all)
594
-
595
586
  return engine
596
587
 
597
588
 
598
589
  async def get_async_session(
599
590
  engine_url=config_installation.ASYNC_MEMORY_ENGINE_URL,
600
- init_schema=False,
601
591
  **engine_kwargs,
602
592
  ):
603
593
  engine = await get_async_engine(
604
594
  engine_url=engine_url,
605
- init_schema=init_schema,
606
595
  **engine_kwargs,
607
596
  )
608
597
  return sqla_asyncio.AsyncSession(bind=engine)