soliplex 0.78.1__tar.gz → 0.78.2__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.
- {soliplex-0.78.1/src/soliplex.egg-info → soliplex-0.78.2}/PKG-INFO +1 -1
- {soliplex-0.78.1 → soliplex-0.78.2}/pyproject.toml +1 -1
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/config/rag.py +5 -2
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/config/skills.py +1 -1
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/config/tools.py +4 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/models.py +1 -1
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/skills/bwrap_sandbox/__init__.py +117 -67
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/views/agui.py +72 -10
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/views/room_file_uploads.py +8 -2
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/views/sandbox_workdirs.py +89 -15
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/views/thread_file_uploads.py +26 -24
- {soliplex-0.78.1 → soliplex-0.78.2/src/soliplex.egg-info}/PKG-INFO +1 -1
- {soliplex-0.78.1 → soliplex-0.78.2}/LICENSE +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/MANIFEST.in +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/README.md +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/example/skills/bare-bones/SKILL.md +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/example/skills/stanzas/SKILL.md +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/setup.cfg +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/skills/soliplex-docs/SKILL.md +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/agents.py +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/agui/__init__.py +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/agui/parser.py +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/agui/persistence.py +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/agui/schema.py +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/agui/util.py +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/authn.py +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/authz/__init__.py +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/authz/persistence.py +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/authz/schema.py +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/capabilities/filesystem.py +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/capabilities/rag_audit.py +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/cli/admin_users.py +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/cli/ask.py +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/cli/audit.py +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/cli/cli_util.py +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/cli/main.py +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/cli/ollama.py +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/cli/room_authz.py +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/cli/serve.py +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/cli/types.py +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/completions.py +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/config/__init__.py +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/config/_utils.py +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/config/agents.py +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/config/agui.py +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/config/authsystem.py +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/config/completions.py +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/config/exceptions.py +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/config/installation.py +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/config/logfire.py +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/config/meta.py +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/config/middleware.py +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/config/quizzes.py +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/config/rooms.py +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/config/routing.py +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/config/secrets.py +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/examples.py +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/installation.py +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/log_ingest.py +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/loggers.py +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/main.py +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/mcp_auth.py +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/mcp_client.py +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/mcp_server.py +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/ollama.py +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/quizzes.py +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/rag_audit.py +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/sandbox_audit.py +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/secrets.py +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/skills/bwrap_sandbox/SKILL.md +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/titles.py +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/tools/__init__.py +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/tools/agui_run_feedback.py +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/tools/file_uploads.py +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/tools/rag.py +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/tui/cli.py +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/tui/main.py +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/tui/rest_api.py +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/tui/serve.py +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/util.py +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/views/__init__.py +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/views/authn.py +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/views/authz.py +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/views/completions.py +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/views/installation.py +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/views/log_ingest.py +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/views/quizzes.py +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/views/rooms.py +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/views/stats.py +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex/views/streaming.py +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex.egg-info/SOURCES.txt +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex.egg-info/dependency_links.txt +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex.egg-info/entry_points.txt +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/src/soliplex.egg-info/requires.txt +0 -0
- {soliplex-0.78.1 → soliplex-0.78.2}/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.78.
|
|
3
|
+
Version: 0.78.2
|
|
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.78.
|
|
7
|
+
version = "0.78.2"
|
|
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"
|
|
@@ -177,10 +177,13 @@ class _RAGDatabaseBase:
|
|
|
177
177
|
try:
|
|
178
178
|
rag_lancedb_path = self.rag_lancedb_path
|
|
179
179
|
except RagDbFileNotFound as exc:
|
|
180
|
-
|
|
180
|
+
missing = exc.rag_db_filename
|
|
181
|
+
db_name = f"MISSING: {missing.stem}"
|
|
182
|
+
else:
|
|
183
|
+
db_name = rag_lancedb_path.stem
|
|
181
184
|
|
|
182
185
|
return {
|
|
183
|
-
"
|
|
186
|
+
"database_names": [db_name],
|
|
184
187
|
}
|
|
185
188
|
|
|
186
189
|
|
|
@@ -177,7 +177,7 @@ class FilesystemSkillConfig:
|
|
|
177
177
|
|
|
178
178
|
@property
|
|
179
179
|
def extra_parameters(self) -> dict[str, pathlib.Path]:
|
|
180
|
-
return {
|
|
180
|
+
return {}
|
|
181
181
|
|
|
182
182
|
def with_defer_loading(self, defer_loading: bool) -> FilesystemSkillConfig:
|
|
183
183
|
"""A copy whose capability loads as a room asked.
|
|
@@ -315,6 +315,10 @@ class ToolConfig:
|
|
|
315
315
|
def get_extra_parameters(self) -> dict:
|
|
316
316
|
return {}
|
|
317
317
|
|
|
318
|
+
@property
|
|
319
|
+
def extra_parameters(self) -> dict:
|
|
320
|
+
return self.get_extra_parameters()
|
|
321
|
+
|
|
318
322
|
|
|
319
323
|
SDTC_TOOL_NAME = "soliplex.tools.rag.search_documents"
|
|
320
324
|
_, SDTC_TOOL_KIND = SDTC_TOOL_NAME.rsplit(".", 1)
|
|
@@ -111,7 +111,7 @@ class Tool(pydantic.BaseModel):
|
|
|
111
111
|
tool_requires=tool_config.tool_requires,
|
|
112
112
|
allow_mcp=tool_config.allow_mcp,
|
|
113
113
|
agui_feature_names=list(tool_config.agui_feature_names),
|
|
114
|
-
extra_parameters=tool_config.
|
|
114
|
+
extra_parameters=tool_config.extra_parameters,
|
|
115
115
|
)
|
|
116
116
|
|
|
117
117
|
|
|
@@ -419,63 +419,102 @@ async def skill_run_python(
|
|
|
419
419
|
return str(output)
|
|
420
420
|
|
|
421
421
|
|
|
422
|
+
class InvalidSubdir(ValueError):
|
|
423
|
+
def __init__(self, path, expected_parent):
|
|
424
|
+
self.path = path
|
|
425
|
+
self.expected_parent = expected_parent
|
|
426
|
+
super().__init__(f"{path} not a subdir of {expected_parent}")
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def _check_is_subdir(path: pathlib.Path, expected_parent: pathlib.Path):
|
|
430
|
+
ep_resolved = expected_parent.resolve()
|
|
431
|
+
|
|
432
|
+
try:
|
|
433
|
+
resolved = path.resolve()
|
|
434
|
+
except ValueError as exc:
|
|
435
|
+
raise InvalidSubdir(path, ep_resolved) from exc
|
|
436
|
+
else:
|
|
437
|
+
if resolved.parent == ep_resolved:
|
|
438
|
+
return
|
|
439
|
+
|
|
440
|
+
raise InvalidSubdir(path, ep_resolved)
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def _check_subdirs(base: pathlib.Path, paths: list[str]) -> pathlib.Path:
|
|
444
|
+
current = base
|
|
445
|
+
for path in paths:
|
|
446
|
+
on_deck = current / path
|
|
447
|
+
_check_is_subdir(on_deck, current)
|
|
448
|
+
current = on_deck
|
|
449
|
+
return current.resolve()
|
|
450
|
+
|
|
451
|
+
|
|
422
452
|
def get_workdir(
|
|
423
453
|
workdirs_path: pathlib.Path | None,
|
|
424
|
-
room_id: str,
|
|
425
|
-
thread_id: str,
|
|
426
|
-
run_id: str,
|
|
454
|
+
room_id: str | None,
|
|
455
|
+
thread_id: str | None,
|
|
456
|
+
run_id: str | None,
|
|
427
457
|
):
|
|
428
|
-
if
|
|
429
|
-
|
|
458
|
+
if (
|
|
459
|
+
workdirs_path is not None
|
|
460
|
+
and room_id is not None
|
|
461
|
+
and thread_id is not None
|
|
462
|
+
and run_id is not None
|
|
463
|
+
):
|
|
464
|
+
workdir = _check_subdirs(
|
|
465
|
+
workdirs_path,
|
|
466
|
+
[room_id, str(thread_id), str(run_id)],
|
|
467
|
+
)
|
|
430
468
|
workdir.mkdir(parents=True, exist_ok=True)
|
|
469
|
+
return workdir
|
|
431
470
|
else:
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
return workdir
|
|
471
|
+
return None
|
|
435
472
|
|
|
436
473
|
|
|
437
|
-
def
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
room_id: str,
|
|
441
|
-
thread_id: str,
|
|
474
|
+
def _get_upload_volume(
|
|
475
|
+
upload_path: pathlib.Path | None,
|
|
476
|
+
volume_id: str | None,
|
|
442
477
|
):
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
result["room"] = bs_models.VolumeInfo(
|
|
449
|
-
host_path=room_dir,
|
|
478
|
+
if upload_path is not None and volume_id is not None:
|
|
479
|
+
volume_dir = _check_subdirs(upload_path, [str(volume_id)])
|
|
480
|
+
if volume_dir.exists():
|
|
481
|
+
return bs_models.VolumeInfo(
|
|
482
|
+
host_path=volume_dir,
|
|
450
483
|
writable=False,
|
|
451
484
|
)
|
|
452
485
|
else:
|
|
453
|
-
|
|
486
|
+
return bs_models.VolumeInfo(
|
|
454
487
|
host_path=None,
|
|
455
488
|
writable=False,
|
|
456
489
|
)
|
|
457
490
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
491
|
+
|
|
492
|
+
def get_extra_volumes(
|
|
493
|
+
rooms_upload_path: pathlib.Path | None,
|
|
494
|
+
threads_upload_path: pathlib.Path | None,
|
|
495
|
+
room_id: str | None,
|
|
496
|
+
thread_id: str | None,
|
|
497
|
+
):
|
|
498
|
+
result = {}
|
|
499
|
+
|
|
500
|
+
room_volume = _get_upload_volume(rooms_upload_path, room_id)
|
|
501
|
+
|
|
502
|
+
if room_volume is not None:
|
|
503
|
+
result["room"] = room_volume
|
|
504
|
+
|
|
505
|
+
thread_volume = _get_upload_volume(threads_upload_path, str(thread_id))
|
|
506
|
+
|
|
507
|
+
if thread_volume is not None:
|
|
508
|
+
result["thread"] = thread_volume
|
|
470
509
|
|
|
471
510
|
return result
|
|
472
511
|
|
|
473
512
|
|
|
474
513
|
def write_transcript(
|
|
475
514
|
transcripts_path: pathlib.Path | None,
|
|
476
|
-
room_id: str,
|
|
477
|
-
thread_id: str,
|
|
478
|
-
run_id: str,
|
|
515
|
+
room_id: str | None,
|
|
516
|
+
thread_id: str | None,
|
|
517
|
+
run_id: str | None,
|
|
479
518
|
*,
|
|
480
519
|
content: str,
|
|
481
520
|
suffix: str,
|
|
@@ -488,16 +527,24 @@ def write_transcript(
|
|
|
488
527
|
with the saved transcript. Returns 'None' (writing nothing) when no
|
|
489
528
|
'transcripts_path' is configured.
|
|
490
529
|
"""
|
|
491
|
-
if
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
530
|
+
if (
|
|
531
|
+
transcripts_path is not None
|
|
532
|
+
and room_id is not None
|
|
533
|
+
and thread_id is not None
|
|
534
|
+
and run_id is not None
|
|
535
|
+
):
|
|
536
|
+
run_dir = _check_subdirs(
|
|
537
|
+
transcripts_path,
|
|
538
|
+
[room_id, str(thread_id), str(run_id)],
|
|
539
|
+
)
|
|
540
|
+
run_dir.mkdir(parents=True, exist_ok=True)
|
|
541
|
+
target = run_dir / f"{uuid.uuid4()}{suffix}"
|
|
542
|
+
target.write_text(content, encoding="utf-8")
|
|
543
|
+
target.chmod(0o600)
|
|
499
544
|
|
|
500
|
-
|
|
545
|
+
return str(target)
|
|
546
|
+
else:
|
|
547
|
+
return None
|
|
501
548
|
|
|
502
549
|
|
|
503
550
|
def create_sandbox_toolset(
|
|
@@ -581,19 +628,22 @@ def create_sandbox_toolset(
|
|
|
581
628
|
|
|
582
629
|
else:
|
|
583
630
|
deps = ctx.deps
|
|
584
|
-
room_id = deps.room_id
|
|
585
|
-
thread_id = deps.thread_id
|
|
631
|
+
room_id = deps.room_id
|
|
632
|
+
thread_id = deps.thread_id
|
|
586
633
|
|
|
587
634
|
return await skill_list_volume_files(
|
|
588
635
|
volume=volume,
|
|
589
636
|
room_upload_path=(
|
|
590
|
-
rooms_upload_path
|
|
591
|
-
if rooms_upload_path is not None
|
|
637
|
+
_check_subdirs(rooms_upload_path, [room_id])
|
|
638
|
+
if (rooms_upload_path is not None and room_id is not None)
|
|
592
639
|
else None
|
|
593
640
|
),
|
|
594
641
|
thread_upload_path=(
|
|
595
|
-
threads_upload_path
|
|
596
|
-
if
|
|
642
|
+
_check_subdirs(threads_upload_path, [thread_id])
|
|
643
|
+
if (
|
|
644
|
+
threads_upload_path is not None
|
|
645
|
+
and thread_id is not None
|
|
646
|
+
)
|
|
597
647
|
else None
|
|
598
648
|
),
|
|
599
649
|
)
|
|
@@ -614,16 +664,16 @@ def create_sandbox_toolset(
|
|
|
614
664
|
deps = ctx.deps
|
|
615
665
|
workdir = get_workdir(
|
|
616
666
|
workdirs_path,
|
|
617
|
-
deps.room_id
|
|
618
|
-
deps.thread_id
|
|
619
|
-
deps.run_id
|
|
667
|
+
deps.room_id,
|
|
668
|
+
deps.thread_id,
|
|
669
|
+
deps.run_id,
|
|
620
670
|
)
|
|
621
671
|
|
|
622
672
|
extra_volumes = get_extra_volumes(
|
|
623
673
|
rooms_upload_path,
|
|
624
674
|
threads_upload_path,
|
|
625
|
-
deps.room_id
|
|
626
|
-
deps.thread_id
|
|
675
|
+
deps.room_id,
|
|
676
|
+
deps.thread_id,
|
|
627
677
|
)
|
|
628
678
|
|
|
629
679
|
with sandbox_audit.audit_sandbox_exec(
|
|
@@ -634,9 +684,9 @@ def create_sandbox_toolset(
|
|
|
634
684
|
) as access:
|
|
635
685
|
ref = write_transcript(
|
|
636
686
|
transcripts_path,
|
|
637
|
-
deps.room_id
|
|
638
|
-
deps.thread_id
|
|
639
|
-
deps.run_id
|
|
687
|
+
deps.room_id,
|
|
688
|
+
deps.thread_id,
|
|
689
|
+
deps.run_id,
|
|
640
690
|
content=(
|
|
641
691
|
command
|
|
642
692
|
if isinstance(command, str)
|
|
@@ -672,16 +722,16 @@ def create_sandbox_toolset(
|
|
|
672
722
|
deps = ctx.deps
|
|
673
723
|
workdir = get_workdir(
|
|
674
724
|
workdirs_path,
|
|
675
|
-
deps.room_id
|
|
676
|
-
deps.thread_id
|
|
677
|
-
deps.run_id
|
|
725
|
+
deps.room_id,
|
|
726
|
+
deps.thread_id,
|
|
727
|
+
deps.run_id,
|
|
678
728
|
)
|
|
679
729
|
|
|
680
730
|
extra_volumes = get_extra_volumes(
|
|
681
731
|
rooms_upload_path,
|
|
682
732
|
threads_upload_path,
|
|
683
|
-
deps.room_id
|
|
684
|
-
deps.thread_id
|
|
733
|
+
deps.room_id,
|
|
734
|
+
deps.thread_id,
|
|
685
735
|
)
|
|
686
736
|
|
|
687
737
|
with sandbox_audit.audit_sandbox_exec(
|
|
@@ -692,9 +742,9 @@ def create_sandbox_toolset(
|
|
|
692
742
|
) as access:
|
|
693
743
|
ref = write_transcript(
|
|
694
744
|
transcripts_path,
|
|
695
|
-
deps.room_id
|
|
696
|
-
deps.thread_id
|
|
697
|
-
deps.run_id
|
|
745
|
+
deps.room_id,
|
|
746
|
+
deps.thread_id,
|
|
747
|
+
deps.run_id,
|
|
698
748
|
content=script,
|
|
699
749
|
suffix=".py",
|
|
700
750
|
)
|
|
@@ -99,6 +99,61 @@ async def _check_user_room_agent(
|
|
|
99
99
|
return user_profile, agent
|
|
100
100
|
|
|
101
101
|
|
|
102
|
+
async def _check_thread_ownership(
|
|
103
|
+
*,
|
|
104
|
+
room_id: str,
|
|
105
|
+
thread_id: str,
|
|
106
|
+
run_id: str | None = None,
|
|
107
|
+
the_installation: installation.Installation,
|
|
108
|
+
the_threads: agui.ThreadStorage,
|
|
109
|
+
the_room_authz: authz.RoomAuthorizationPolicy,
|
|
110
|
+
the_user_claims: authn.UserClaims,
|
|
111
|
+
the_logger: loggers.LogWrapper,
|
|
112
|
+
) -> config_rooms.RoomConfig:
|
|
113
|
+
"""Raise if user is not the owner of the thread
|
|
114
|
+
|
|
115
|
+
If 'run_id' is passed, also check that it is a valid member of the thread.
|
|
116
|
+
"""
|
|
117
|
+
user_name = the_user_claims.get("preferred_username", "<unknown>")
|
|
118
|
+
|
|
119
|
+
_room_config = await _check_user_in_room( # raises
|
|
120
|
+
room_id=room_id,
|
|
121
|
+
the_installation=the_installation,
|
|
122
|
+
the_room_authz=the_room_authz,
|
|
123
|
+
the_user_claims=the_user_claims,
|
|
124
|
+
the_logger=the_logger,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
try: # verify user's ownership of the thread
|
|
128
|
+
await the_threads.get_thread(
|
|
129
|
+
user_name=user_name,
|
|
130
|
+
room_id=room_id,
|
|
131
|
+
thread_id=thread_id,
|
|
132
|
+
)
|
|
133
|
+
except agui.AGUI_Exception:
|
|
134
|
+
raise fastapi.HTTPException(
|
|
135
|
+
status_code=404,
|
|
136
|
+
detail="No such thread",
|
|
137
|
+
) from None
|
|
138
|
+
|
|
139
|
+
if run_id is not None:
|
|
140
|
+
try:
|
|
141
|
+
await the_threads.get_run(
|
|
142
|
+
user_name=user_name,
|
|
143
|
+
room_id=room_id,
|
|
144
|
+
thread_id=thread_id,
|
|
145
|
+
run_id=run_id,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
except agui.AGUI_Exception:
|
|
149
|
+
raise fastapi.HTTPException(
|
|
150
|
+
status_code=404,
|
|
151
|
+
detail="No such run",
|
|
152
|
+
) from None
|
|
153
|
+
|
|
154
|
+
return _room_config
|
|
155
|
+
|
|
156
|
+
|
|
102
157
|
@util.logfire_span("GET /v1/rooms/{room_id}/agui")
|
|
103
158
|
@router.get("/v1/rooms/{room_id}/agui")
|
|
104
159
|
async def get_room_agui(
|
|
@@ -732,16 +787,12 @@ async def post_room_agui_thread_id_run_id(
|
|
|
732
787
|
the_logger=the_logger,
|
|
733
788
|
)
|
|
734
789
|
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
the_room_authz=the_room_authz,
|
|
742
|
-
the_logger=the_logger,
|
|
743
|
-
)
|
|
744
|
-
),
|
|
790
|
+
# This adapter is only used to encode the stream from the database:
|
|
791
|
+
# it doesn't need an 'agent' or a 'run_input' for that job.
|
|
792
|
+
agui_adapter = ai_ag_ui.AGUIAdapter(
|
|
793
|
+
agent=None,
|
|
794
|
+
run_input=None,
|
|
795
|
+
accept=request.headers.get("accept"),
|
|
745
796
|
)
|
|
746
797
|
|
|
747
798
|
db_event_stream = streaming_views.stream_from_db(
|
|
@@ -787,6 +838,17 @@ async def post_room_agui_thread_id_run_id(
|
|
|
787
838
|
agent=agent,
|
|
788
839
|
)
|
|
789
840
|
|
|
841
|
+
# Ensure IDs in request body match the checked URL path IDs.
|
|
842
|
+
# The values must match the ones minted / serialized earlier
|
|
843
|
+
# by us *exactly*: client must return them verbatim. See #1325.
|
|
844
|
+
run_input = agui_adapter.run_input
|
|
845
|
+
|
|
846
|
+
if thread_id != run_input.thread_id or run_id != run_input.run_id:
|
|
847
|
+
raise fastapi.HTTPException(
|
|
848
|
+
status_code=400,
|
|
849
|
+
detail="Invalid run input",
|
|
850
|
+
)
|
|
851
|
+
|
|
790
852
|
try:
|
|
791
853
|
async with sqla_asyncio.AsyncSession(
|
|
792
854
|
bind=request.state.threads_engine,
|
|
@@ -110,7 +110,13 @@ async def get_uploads_room_filename(
|
|
|
110
110
|
the_user_claims: authn_package.UserClaims = depend_the_user_claims,
|
|
111
111
|
the_logger: loggers.LogWrapper = depend_the_logger,
|
|
112
112
|
) -> str: # file path, converted to file response by FastAPI
|
|
113
|
-
"""Download a file from the room uploads directory
|
|
113
|
+
"""Download a file from the room uploads directory
|
|
114
|
+
|
|
115
|
+
Unlike sandbox workdir files, which are created by a possibly-
|
|
116
|
+
hallucinating or mischievous agent, files in the room-uploads tree
|
|
117
|
+
are under purview of the admin / operator: symlinks to share e.g.
|
|
118
|
+
large datasets between rooms may be intentional and appropriate.
|
|
119
|
+
"""
|
|
114
120
|
the_logger.debug(loggers.UPLOADS_GET_ROOM_FILE)
|
|
115
121
|
|
|
116
122
|
_room_config = await soliplex_views_agui._check_user_in_room(
|
|
@@ -134,7 +140,7 @@ async def get_uploads_room_filename(
|
|
|
134
140
|
if not room_dir.is_dir():
|
|
135
141
|
raise fastapi.HTTPException(
|
|
136
142
|
status_code=404,
|
|
137
|
-
detail="No uploads in room: {room_id}",
|
|
143
|
+
detail=f"No uploads in room: {room_id}",
|
|
138
144
|
)
|
|
139
145
|
|
|
140
146
|
file_path = room_dir / filename
|
|
@@ -1,10 +1,14 @@
|
|
|
1
|
+
import os
|
|
1
2
|
import pathlib
|
|
3
|
+
import stat
|
|
4
|
+
import urllib.parse as url_parse
|
|
2
5
|
|
|
3
6
|
import fastapi
|
|
4
7
|
import pydantic
|
|
5
8
|
from fastapi import responses
|
|
6
9
|
|
|
7
|
-
from soliplex import
|
|
10
|
+
from soliplex import agui
|
|
11
|
+
from soliplex import authn
|
|
8
12
|
from soliplex import authz
|
|
9
13
|
from soliplex import installation
|
|
10
14
|
from soliplex import loggers
|
|
@@ -16,11 +20,20 @@ from soliplex.views import util as soliplex_views_util
|
|
|
16
20
|
router = fastapi.APIRouter(tags=["uploads"])
|
|
17
21
|
|
|
18
22
|
depend_the_installation = installation.depend_the_installation
|
|
23
|
+
depend_the_threads = agui.depend_the_threads
|
|
19
24
|
depend_the_room_authz = views.depend_the_room_authz_policy
|
|
20
25
|
depend_the_user_claims = views.depend_the_user_claims
|
|
21
26
|
depend_the_logger = views.depend_the_logger
|
|
22
27
|
|
|
23
28
|
|
|
29
|
+
CHUNKSIZE = 2**16 # 64k
|
|
30
|
+
|
|
31
|
+
# DO NOT SIMPLIFY HERE:
|
|
32
|
+
# This is the RFC 5987 extended-parameter syntax for a
|
|
33
|
+
# 'Content-Disposition' header value using 'attachment'.
|
|
34
|
+
ATTACHMENT_PREFIX = "attachment; filename*=UTF-8''"
|
|
35
|
+
|
|
36
|
+
|
|
24
37
|
@soliplex_views_util.logfire_span(
|
|
25
38
|
"GET /v1/workdirs/{room_id}/thread/{thread_id}/{run_id}",
|
|
26
39
|
)
|
|
@@ -31,8 +44,9 @@ async def get_workdirs_room_thread_run(
|
|
|
31
44
|
thread_id: pydantic.UUID4,
|
|
32
45
|
run_id: pydantic.UUID4,
|
|
33
46
|
the_installation: installation.Installation = depend_the_installation,
|
|
47
|
+
the_threads: agui.ThreadStorage = depend_the_threads,
|
|
34
48
|
the_room_authz: authz.RoomAuthorizationPolicy = depend_the_room_authz,
|
|
35
|
-
the_user_claims:
|
|
49
|
+
the_user_claims: authn.UserClaims = depend_the_user_claims,
|
|
36
50
|
the_logger: loggers.LogWrapper = depend_the_logger,
|
|
37
51
|
) -> models.RunWorkdirFiles:
|
|
38
52
|
"""Return a list of files uploaded to the thread"""
|
|
@@ -41,9 +55,12 @@ async def get_workdirs_room_thread_run(
|
|
|
41
55
|
|
|
42
56
|
the_logger.debug(loggers.WORKDIRS_GET_ROOM_THREAD_RUN)
|
|
43
57
|
|
|
44
|
-
|
|
58
|
+
await soliplex_views_agui._check_thread_ownership(
|
|
45
59
|
room_id=room_id,
|
|
60
|
+
thread_id=thread_id,
|
|
61
|
+
run_id=run_id,
|
|
46
62
|
the_installation=the_installation,
|
|
63
|
+
the_threads=the_threads,
|
|
47
64
|
the_room_authz=the_room_authz,
|
|
48
65
|
the_user_claims=the_user_claims,
|
|
49
66
|
the_logger=the_logger,
|
|
@@ -62,7 +79,7 @@ async def get_workdirs_room_thread_run(
|
|
|
62
79
|
|
|
63
80
|
if run_dir.is_dir():
|
|
64
81
|
for file_or_sub in run_dir.glob("*"):
|
|
65
|
-
if file_or_sub.is_file():
|
|
82
|
+
if not file_or_sub.is_symlink() and file_or_sub.is_file():
|
|
66
83
|
filename = file_or_sub.name
|
|
67
84
|
filename_urls[filename] = request.url_for(
|
|
68
85
|
# View function name, not the route path.
|
|
@@ -87,13 +104,48 @@ async def get_workdirs_room_thread_run(
|
|
|
87
104
|
)
|
|
88
105
|
|
|
89
106
|
|
|
107
|
+
def _open_no_symlinks(file_path: pathlib.Path):
|
|
108
|
+
"""Return a streaming generator for 'file_path'
|
|
109
|
+
|
|
110
|
+
First ensure that it is a regular file (no symlinks, etc.)
|
|
111
|
+
"""
|
|
112
|
+
not_found = f"No workdir file: {file_path.name}"
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
fd = os.open(
|
|
116
|
+
file_path,
|
|
117
|
+
os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK | os.O_CLOEXEC,
|
|
118
|
+
)
|
|
119
|
+
except OSError: # ELOOP for a symlink, ENOENT, EACCES
|
|
120
|
+
raise fastapi.HTTPException(
|
|
121
|
+
status_code=404,
|
|
122
|
+
detail=not_found,
|
|
123
|
+
) from None
|
|
124
|
+
|
|
125
|
+
st = os.fstat(fd)
|
|
126
|
+
if not stat.S_ISREG(st.st_mode): # directory, FIFO, device, socket
|
|
127
|
+
os.close(fd)
|
|
128
|
+
raise fastapi.HTTPException(status_code=404, detail=not_found)
|
|
129
|
+
|
|
130
|
+
def _stream(): # now owns the fd
|
|
131
|
+
with os.fdopen(fd, "rb") as fh:
|
|
132
|
+
while chunk := fh.read(CHUNKSIZE):
|
|
133
|
+
yield chunk
|
|
134
|
+
|
|
135
|
+
return _stream, st.st_size
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _disposition(file_path: pathlib.Path) -> str:
|
|
139
|
+
attachment_name = url_parse.quote(file_path.name, safe="")
|
|
140
|
+
return f"{ATTACHMENT_PREFIX}{attachment_name}"
|
|
141
|
+
|
|
142
|
+
|
|
90
143
|
@soliplex_views_util.logfire_span(
|
|
91
144
|
"GET "
|
|
92
145
|
"/v1/workdirs/{room_id}/thread/{thread_id}/run/{run_id}/file/{filename}"
|
|
93
146
|
)
|
|
94
147
|
@router.get(
|
|
95
148
|
"/v1/workdirs/{room_id}/thread/{thread_id}/run/{run_id}/file/{filename}",
|
|
96
|
-
response_class=responses.FileResponse,
|
|
97
149
|
)
|
|
98
150
|
async def get_workdirs_room_thread_run_filename(
|
|
99
151
|
room_id: str,
|
|
@@ -101,19 +153,28 @@ async def get_workdirs_room_thread_run_filename(
|
|
|
101
153
|
run_id: pydantic.UUID4,
|
|
102
154
|
filename: str,
|
|
103
155
|
the_installation: installation.Installation = depend_the_installation,
|
|
156
|
+
the_threads: agui.ThreadStorage = depend_the_threads,
|
|
104
157
|
the_room_authz: authz.RoomAuthorizationPolicy = depend_the_room_authz,
|
|
105
|
-
the_user_claims:
|
|
158
|
+
the_user_claims: authn.UserClaims = depend_the_user_claims,
|
|
106
159
|
the_logger: loggers.LogWrapper = depend_the_logger,
|
|
107
|
-
) ->
|
|
108
|
-
"""Return a
|
|
160
|
+
) -> responses.StreamingResponse:
|
|
161
|
+
"""Return a file uploaded to the thread.
|
|
162
|
+
|
|
163
|
+
In order to prevent an agent-created symlink from exposing a file
|
|
164
|
+
not in the workdir, this view returns a streaming response, rather
|
|
165
|
+
than using `fastapi`s 'FileResponse-from-filename' affordance.
|
|
166
|
+
"""
|
|
109
167
|
thread_id = str(thread_id)
|
|
110
168
|
run_id = str(run_id)
|
|
111
169
|
|
|
112
170
|
the_logger.debug(loggers.WORKDIRS_GET_ROOM_THREAD_RUN_FILE)
|
|
113
171
|
|
|
114
|
-
|
|
172
|
+
await soliplex_views_agui._check_thread_ownership(
|
|
115
173
|
room_id=room_id,
|
|
174
|
+
thread_id=thread_id,
|
|
175
|
+
run_id=run_id,
|
|
116
176
|
the_installation=the_installation,
|
|
177
|
+
the_threads=the_threads,
|
|
117
178
|
the_room_authz=the_room_authz,
|
|
118
179
|
the_user_claims=the_user_claims,
|
|
119
180
|
the_logger=the_logger,
|
|
@@ -130,10 +191,23 @@ async def get_workdirs_room_thread_run_filename(
|
|
|
130
191
|
run_dir = pathlib.Path(workdirs_path) / room_id / thread_id / run_id
|
|
131
192
|
|
|
132
193
|
file_path = run_dir / filename
|
|
133
|
-
if not file_path.is_file():
|
|
134
|
-
raise fastapi.HTTPException(
|
|
135
|
-
status_code=404,
|
|
136
|
-
detail=f"No workdir file: {filename}",
|
|
137
|
-
)
|
|
138
194
|
|
|
139
|
-
|
|
195
|
+
streamer, size = _open_no_symlinks(file_path)
|
|
196
|
+
|
|
197
|
+
stream = streamer()
|
|
198
|
+
|
|
199
|
+
# Declare one fixed, inert type rather than guessing from the
|
|
200
|
+
# filename as 'FileResponse' did: the name is chosen by
|
|
201
|
+
# model-authored code, so a guess would hand the browser a
|
|
202
|
+
# content type derived from untrusted input. Together with the
|
|
203
|
+
# 'attachment' disposition and 'nosniff', this makes the browser
|
|
204
|
+
# download the file and never interpret it.
|
|
205
|
+
return responses.StreamingResponse(
|
|
206
|
+
stream,
|
|
207
|
+
media_type="application/octet-stream",
|
|
208
|
+
headers={
|
|
209
|
+
"content-length": str(size),
|
|
210
|
+
"content-disposition": _disposition(file_path),
|
|
211
|
+
"x-content-type-options": "nosniff",
|
|
212
|
+
},
|
|
213
|
+
)
|
|
@@ -5,7 +5,7 @@ import pydantic
|
|
|
5
5
|
from fastapi import responses
|
|
6
6
|
|
|
7
7
|
from soliplex import agui
|
|
8
|
-
from soliplex import authn
|
|
8
|
+
from soliplex import authn
|
|
9
9
|
from soliplex import authz
|
|
10
10
|
from soliplex import installation
|
|
11
11
|
from soliplex import loggers
|
|
@@ -32,18 +32,21 @@ async def get_uploads_room_id_thread_thread_id(
|
|
|
32
32
|
room_id: str,
|
|
33
33
|
thread_id: pydantic.UUID4,
|
|
34
34
|
the_installation: installation.Installation = depend_the_installation,
|
|
35
|
+
the_threads: agui.ThreadStorage = depend_the_threads,
|
|
35
36
|
the_room_authz: authz.RoomAuthorizationPolicy = depend_the_room_authz,
|
|
36
|
-
the_user_claims:
|
|
37
|
+
the_user_claims: authn.UserClaims = depend_the_user_claims,
|
|
37
38
|
the_logger: loggers.LogWrapper = depend_the_logger,
|
|
38
|
-
) -> models.
|
|
39
|
+
) -> models.ThreadUploads:
|
|
39
40
|
"""Return a list of files uploaded to the thread"""
|
|
40
41
|
thread_id = str(thread_id)
|
|
41
42
|
|
|
42
43
|
the_logger.debug(loggers.UPLOADS_GET_ROOM_THREAD)
|
|
43
44
|
|
|
44
|
-
|
|
45
|
+
await soliplex_views_agui._check_thread_ownership(
|
|
45
46
|
room_id=room_id,
|
|
47
|
+
thread_id=thread_id,
|
|
46
48
|
the_installation=the_installation,
|
|
49
|
+
the_threads=the_threads,
|
|
47
50
|
the_room_authz=the_room_authz,
|
|
48
51
|
the_user_claims=the_user_claims,
|
|
49
52
|
the_logger=the_logger,
|
|
@@ -97,18 +100,28 @@ async def get_uploads_room_id_thread_thread_id_filename(
|
|
|
97
100
|
thread_id: pydantic.UUID4,
|
|
98
101
|
filename: str,
|
|
99
102
|
the_installation: installation.Installation = depend_the_installation,
|
|
103
|
+
the_threads: agui.ThreadStorage = depend_the_threads,
|
|
100
104
|
the_room_authz: authz.RoomAuthorizationPolicy = depend_the_room_authz,
|
|
101
|
-
the_user_claims:
|
|
105
|
+
the_user_claims: authn.UserClaims = depend_the_user_claims,
|
|
102
106
|
the_logger: loggers.LogWrapper = depend_the_logger,
|
|
103
107
|
) -> str: # file path, converted to file response by FastAPI
|
|
104
|
-
"""Download a file from the
|
|
108
|
+
"""Download a file from the thread uploads directory
|
|
109
|
+
|
|
110
|
+
Unlike sandbox workdir files, which are created by a possibly-
|
|
111
|
+
hallucinating or mischievous agent, files in the thread-uploads tree
|
|
112
|
+
are durable. The operator may choose to add a symlink into a particular
|
|
113
|
+
thread's directory to share a large dataset, without requiring that
|
|
114
|
+
the user download it from somewhere and then re-upload it here.
|
|
115
|
+
"""
|
|
105
116
|
thread_id = str(thread_id)
|
|
106
117
|
|
|
107
118
|
the_logger.debug(loggers.UPLOADS_GET_ROOM_THREAD_FILE)
|
|
108
119
|
|
|
109
|
-
|
|
120
|
+
await soliplex_views_agui._check_thread_ownership(
|
|
110
121
|
room_id=room_id,
|
|
122
|
+
thread_id=thread_id,
|
|
111
123
|
the_installation=the_installation,
|
|
124
|
+
the_threads=the_threads,
|
|
112
125
|
the_room_authz=the_room_authz,
|
|
113
126
|
the_user_claims=the_user_claims,
|
|
114
127
|
the_logger=the_logger,
|
|
@@ -127,7 +140,7 @@ async def get_uploads_room_id_thread_thread_id_filename(
|
|
|
127
140
|
if not thread_dir.is_dir():
|
|
128
141
|
raise fastapi.HTTPException(
|
|
129
142
|
status_code=404,
|
|
130
|
-
detail="No uploads in thread: {thread_id}",
|
|
143
|
+
detail=f"No uploads in thread: {thread_id}",
|
|
131
144
|
)
|
|
132
145
|
|
|
133
146
|
file_path = thread_dir / filename
|
|
@@ -152,7 +165,7 @@ async def post_uploads_room_id_thread_thread_id(
|
|
|
152
165
|
the_installation: installation.Installation = depend_the_installation,
|
|
153
166
|
the_threads: agui.ThreadStorage = depend_the_threads,
|
|
154
167
|
the_room_authz: authz.RoomAuthorizationPolicy = depend_the_room_authz,
|
|
155
|
-
the_user_claims:
|
|
168
|
+
the_user_claims: authn.UserClaims = depend_the_user_claims,
|
|
156
169
|
the_logger: loggers.LogWrapper = depend_the_logger,
|
|
157
170
|
) -> fastapi.Response:
|
|
158
171
|
"""Upload a file for a thread within the given room
|
|
@@ -164,16 +177,17 @@ async def post_uploads_room_id_thread_thread_id(
|
|
|
164
177
|
|
|
165
178
|
the_logger.debug(loggers.UPLOADS_POST_ROOM_THREAD)
|
|
166
179
|
|
|
167
|
-
|
|
168
|
-
_room_config = await soliplex_views_agui._check_user_in_room(
|
|
180
|
+
room_config = await soliplex_views_agui._check_thread_ownership(
|
|
169
181
|
room_id=room_id,
|
|
182
|
+
thread_id=thread_id,
|
|
170
183
|
the_installation=the_installation,
|
|
184
|
+
the_threads=the_threads,
|
|
171
185
|
the_room_authz=the_room_authz,
|
|
172
186
|
the_user_claims=the_user_claims,
|
|
173
187
|
the_logger=the_logger,
|
|
174
188
|
)
|
|
175
189
|
|
|
176
|
-
if not
|
|
190
|
+
if not room_config.has_sandbox:
|
|
177
191
|
raise fastapi.HTTPException(
|
|
178
192
|
status_code=405,
|
|
179
193
|
detail="Sandbox not configured",
|
|
@@ -189,18 +203,6 @@ async def post_uploads_room_id_thread_thread_id(
|
|
|
189
203
|
headers={"Allow": "GET"},
|
|
190
204
|
)
|
|
191
205
|
|
|
192
|
-
try:
|
|
193
|
-
await the_threads.get_thread(
|
|
194
|
-
user_name=user_name,
|
|
195
|
-
room_id=room_id,
|
|
196
|
-
thread_id=thread_id,
|
|
197
|
-
)
|
|
198
|
-
except agui.AGUI_Exception as exc:
|
|
199
|
-
raise fastapi.HTTPException(
|
|
200
|
-
status_code=exc.status_code,
|
|
201
|
-
detail=exc.args,
|
|
202
|
-
) from None
|
|
203
|
-
|
|
204
206
|
thread_dir = pathlib.Path(uploads_path) / thread_id
|
|
205
207
|
thread_dir.mkdir(parents=True, exist_ok=True)
|
|
206
208
|
stripped_filename = pathlib.Path(upload_file.filename).name
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: soliplex
|
|
3
|
-
Version: 0.78.
|
|
3
|
+
Version: 0.78.2
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|