OpenRAG 0.5.0.dev30__py3-none-any.whl → 0.5.0.dev32__py3-none-any.whl
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.
- api/settings.py +4 -0
- connectors/google_drive/connector.py +144 -148
- connectors/langflow_connector_service.py +5 -1
- connectors/service.py +20 -7
- models/processors.py +15 -7
- {openrag-0.5.0.dev30.dist-info → openrag-0.5.0.dev32.dist-info}/METADATA +1 -1
- {openrag-0.5.0.dev30.dist-info → openrag-0.5.0.dev32.dist-info}/RECORD +13 -13
- services/auth_service.py +1 -3
- tui/screens/monitor.py +85 -93
- {openrag-0.5.0.dev30.dist-info → openrag-0.5.0.dev32.dist-info}/WHEEL +0 -0
- {openrag-0.5.0.dev30.dist-info → openrag-0.5.0.dev32.dist-info}/entry_points.txt +0 -0
- {openrag-0.5.0.dev30.dist-info → openrag-0.5.0.dev32.dist-info}/licenses/LICENSE +0 -0
- {openrag-0.5.0.dev30.dist-info → openrag-0.5.0.dev32.dist-info}/top_level.txt +0 -0
api/settings.py
CHANGED
|
@@ -1940,6 +1940,10 @@ async def rollback_onboarding(
|
|
|
1940
1940
|
current_config.knowledge.embedding_model = ""
|
|
1941
1941
|
current_config.onboarding.openrag_docs_ingested_version = None
|
|
1942
1942
|
current_config.onboarding.openrag_docs_remote_signature = None
|
|
1943
|
+
current_config.onboarding.assistant_message = None
|
|
1944
|
+
current_config.onboarding.selected_nudge = None
|
|
1945
|
+
current_config.onboarding.card_steps = None
|
|
1946
|
+
current_config.onboarding.upload_steps = None
|
|
1943
1947
|
|
|
1944
1948
|
embedding_only = body.embedding_only if body else False
|
|
1945
1949
|
|
|
@@ -3,9 +3,10 @@ import io
|
|
|
3
3
|
import os
|
|
4
4
|
import time
|
|
5
5
|
from collections import deque
|
|
6
|
+
from collections.abc import Iterable
|
|
6
7
|
from dataclasses import dataclass
|
|
7
8
|
from pathlib import Path
|
|
8
|
-
from typing import Any
|
|
9
|
+
from typing import Any
|
|
9
10
|
|
|
10
11
|
from googleapiclient.errors import HttpError
|
|
11
12
|
from googleapiclient.http import MediaIoBaseDownload
|
|
@@ -17,6 +18,7 @@ from .oauth import GoogleDriveOAuth
|
|
|
17
18
|
|
|
18
19
|
logger = get_logger(__name__)
|
|
19
20
|
|
|
21
|
+
|
|
20
22
|
# -------------------------
|
|
21
23
|
# Config model
|
|
22
24
|
# -------------------------
|
|
@@ -27,26 +29,26 @@ class GoogleDriveConfig:
|
|
|
27
29
|
token_file: str
|
|
28
30
|
|
|
29
31
|
# Selective sync
|
|
30
|
-
file_ids:
|
|
31
|
-
folder_ids:
|
|
32
|
+
file_ids: list[str] | None = None
|
|
33
|
+
folder_ids: list[str] | None = None
|
|
32
34
|
recursive: bool = True
|
|
33
35
|
|
|
34
36
|
# Shared Drives control
|
|
35
|
-
drive_id:
|
|
36
|
-
corpora:
|
|
37
|
+
drive_id: str | None = None # when set, we use corpora='drive'
|
|
38
|
+
corpora: str | None = None # 'user' | 'drive' | 'domain'; auto-picked if None
|
|
37
39
|
|
|
38
40
|
# Optional filtering
|
|
39
|
-
include_mime_types:
|
|
40
|
-
exclude_mime_types:
|
|
41
|
+
include_mime_types: list[str] | None = None
|
|
42
|
+
exclude_mime_types: list[str] | None = None
|
|
41
43
|
|
|
42
44
|
# Export overrides for Google-native types
|
|
43
|
-
export_format_overrides:
|
|
45
|
+
export_format_overrides: dict[str, str] | None = None # mime -> export-mime
|
|
44
46
|
|
|
45
47
|
# Changes API state persistence (store these in your DB/kv if needed)
|
|
46
|
-
changes_page_token:
|
|
48
|
+
changes_page_token: str | None = None
|
|
47
49
|
|
|
48
50
|
# Optional: resource_id for webhook cleanup
|
|
49
|
-
resource_id:
|
|
51
|
+
resource_id: str | None = None
|
|
50
52
|
|
|
51
53
|
|
|
52
54
|
# -------------------------
|
|
@@ -89,7 +91,7 @@ class GoogleDriveConnector(BaseConnector):
|
|
|
89
91
|
# Otherwise, implement your custom logic here.
|
|
90
92
|
logger.debug(f"Emitting document: {doc.id} ({doc.filename})")
|
|
91
93
|
|
|
92
|
-
def __init__(self, config:
|
|
94
|
+
def __init__(self, config: dict[str, Any]) -> None:
|
|
93
95
|
# Read from config OR env (backend env, not NEXT_PUBLIC_*):
|
|
94
96
|
env_client_id = os.getenv(self.CLIENT_ID_ENV_VAR)
|
|
95
97
|
env_client_secret = os.getenv(self.CLIENT_SECRET_ENV_VAR)
|
|
@@ -99,6 +101,7 @@ class GoogleDriveConnector(BaseConnector):
|
|
|
99
101
|
|
|
100
102
|
# Token file default - use data directory for persistence
|
|
101
103
|
from config.paths import get_data_file
|
|
104
|
+
|
|
102
105
|
token_file = config.get("token_file") or get_data_file("google_drive_token.json")
|
|
103
106
|
Path(token_file).parent.mkdir(parents=True, exist_ok=True)
|
|
104
107
|
|
|
@@ -114,9 +117,7 @@ class GoogleDriveConnector(BaseConnector):
|
|
|
114
117
|
)
|
|
115
118
|
|
|
116
119
|
# Normalize incoming IDs from any of the supported alias keys
|
|
117
|
-
def _first_present_list(
|
|
118
|
-
cfg: Dict[str, Any], keys: Iterable[str]
|
|
119
|
-
) -> Optional[List[str]]:
|
|
120
|
+
def _first_present_list(cfg: dict[str, Any], keys: Iterable[str]) -> list[str] | None:
|
|
120
121
|
for k in keys:
|
|
121
122
|
v = cfg.get(k)
|
|
122
123
|
if v: # accept non-empty list
|
|
@@ -153,11 +154,15 @@ class GoogleDriveConnector(BaseConnector):
|
|
|
153
154
|
# Drive client is built in authenticate()
|
|
154
155
|
from google.oauth2.credentials import Credentials
|
|
155
156
|
|
|
156
|
-
self.creds:
|
|
157
|
+
self.creds: Credentials | None = None
|
|
157
158
|
self.service: Any = None
|
|
158
159
|
|
|
159
160
|
# cache of resolved shortcutId -> target file metadata
|
|
160
|
-
self._shortcut_cache:
|
|
161
|
+
self._shortcut_cache: dict[str, dict[str, Any]] = {}
|
|
162
|
+
|
|
163
|
+
import threading
|
|
164
|
+
|
|
165
|
+
self._lock = threading.Lock()
|
|
161
166
|
|
|
162
167
|
# Authentication state
|
|
163
168
|
self._authenticated: bool = False
|
|
@@ -170,20 +175,20 @@ class GoogleDriveConnector(BaseConnector):
|
|
|
170
175
|
self._shortcut_cache.clear()
|
|
171
176
|
|
|
172
177
|
@property
|
|
173
|
-
def _drives_get_flags(self) ->
|
|
178
|
+
def _drives_get_flags(self) -> dict[str, Any]:
|
|
174
179
|
"""
|
|
175
180
|
Flags valid for GET-like calls (files.get, changes.getStartPageToken).
|
|
176
181
|
"""
|
|
177
182
|
return {"supportsAllDrives": True}
|
|
178
183
|
|
|
179
184
|
@property
|
|
180
|
-
def _drives_list_flags(self) ->
|
|
185
|
+
def _drives_list_flags(self) -> dict[str, Any]:
|
|
181
186
|
"""
|
|
182
187
|
Flags valid for LIST-like calls (files.list, changes.list).
|
|
183
188
|
"""
|
|
184
189
|
return {"supportsAllDrives": True, "includeItemsFromAllDrives": True}
|
|
185
190
|
|
|
186
|
-
def _pick_corpora_args(self) ->
|
|
191
|
+
def _pick_corpora_args(self) -> dict[str, Any]:
|
|
187
192
|
"""
|
|
188
193
|
Decide corpora/driveId based on config.
|
|
189
194
|
|
|
@@ -197,7 +202,7 @@ class GoogleDriveConnector(BaseConnector):
|
|
|
197
202
|
# Default to allDrives so Picker selections from Shared Drives work without explicit drive_id
|
|
198
203
|
return {"corpora": "allDrives"}
|
|
199
204
|
|
|
200
|
-
def _resolve_shortcut(self, file_obj:
|
|
205
|
+
def _resolve_shortcut(self, file_obj: dict[str, Any]) -> dict[str, Any]:
|
|
201
206
|
"""
|
|
202
207
|
If a file is a shortcut, fetch and return the real target metadata.
|
|
203
208
|
"""
|
|
@@ -234,7 +239,7 @@ class GoogleDriveConnector(BaseConnector):
|
|
|
234
239
|
# shortcut target not accessible
|
|
235
240
|
return file_obj
|
|
236
241
|
|
|
237
|
-
def _list_children(self, folder_id: str) ->
|
|
242
|
+
def _list_children(self, folder_id: str) -> list[dict[str, Any]]:
|
|
238
243
|
"""
|
|
239
244
|
List immediate children of a folder.
|
|
240
245
|
"""
|
|
@@ -245,7 +250,7 @@ class GoogleDriveConnector(BaseConnector):
|
|
|
245
250
|
|
|
246
251
|
query = f"'{folder_id}' in parents and trashed = false"
|
|
247
252
|
page_token = None
|
|
248
|
-
results:
|
|
253
|
+
results: list[dict[str, Any]] = []
|
|
249
254
|
|
|
250
255
|
while True:
|
|
251
256
|
resp = (
|
|
@@ -272,13 +277,13 @@ class GoogleDriveConnector(BaseConnector):
|
|
|
272
277
|
|
|
273
278
|
return results
|
|
274
279
|
|
|
275
|
-
def _bfs_expand_folders(self, folder_ids: Iterable[str]) ->
|
|
280
|
+
def _bfs_expand_folders(self, folder_ids: Iterable[str]) -> list[dict[str, Any]]:
|
|
276
281
|
"""
|
|
277
282
|
Breadth-first traversal to expand folders to all descendant files (if recursive),
|
|
278
283
|
or just immediate children (if not recursive). Folders themselves are returned
|
|
279
284
|
as items too, but filtered later.
|
|
280
285
|
"""
|
|
281
|
-
out:
|
|
286
|
+
out: list[dict[str, Any]] = []
|
|
282
287
|
queue = deque(folder_ids)
|
|
283
288
|
|
|
284
289
|
while queue:
|
|
@@ -295,7 +300,7 @@ class GoogleDriveConnector(BaseConnector):
|
|
|
295
300
|
|
|
296
301
|
return out
|
|
297
302
|
|
|
298
|
-
def _get_file_meta_by_id(self, file_id: str) ->
|
|
303
|
+
def _get_file_meta_by_id(self, file_id: str) -> dict[str, Any] | None:
|
|
299
304
|
"""
|
|
300
305
|
Fetch metadata for a file by ID (resolving shortcuts).
|
|
301
306
|
"""
|
|
@@ -304,30 +309,31 @@ class GoogleDriveConnector(BaseConnector):
|
|
|
304
309
|
"Google Drive service is not initialized. Please authenticate first."
|
|
305
310
|
)
|
|
306
311
|
try:
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
312
|
+
with self._lock:
|
|
313
|
+
meta = (
|
|
314
|
+
self.service.files()
|
|
315
|
+
.get(
|
|
316
|
+
fileId=file_id,
|
|
317
|
+
fields=(
|
|
318
|
+
"id, name, mimeType, modifiedTime, createdTime, size, "
|
|
319
|
+
"webViewLink, parents, shortcutDetails, driveId"
|
|
320
|
+
),
|
|
321
|
+
**self._drives_get_flags,
|
|
322
|
+
)
|
|
323
|
+
.execute()
|
|
316
324
|
)
|
|
317
|
-
.execute()
|
|
318
|
-
)
|
|
319
325
|
return self._resolve_shortcut(meta)
|
|
320
326
|
except HttpError:
|
|
321
327
|
return None
|
|
322
328
|
|
|
323
|
-
def _filter_by_mime(self, items: Iterable[
|
|
329
|
+
def _filter_by_mime(self, items: Iterable[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
324
330
|
"""
|
|
325
331
|
Apply include/exclude mime filters if configured.
|
|
326
332
|
"""
|
|
327
333
|
include = set(self.cfg.include_mime_types or [])
|
|
328
334
|
exclude = set(self.cfg.exclude_mime_types or [])
|
|
329
335
|
|
|
330
|
-
def keep(m:
|
|
336
|
+
def keep(m: dict[str, Any]) -> bool:
|
|
331
337
|
mt = m.get("mimeType")
|
|
332
338
|
if exclude and mt in exclude:
|
|
333
339
|
return False
|
|
@@ -337,7 +343,7 @@ class GoogleDriveConnector(BaseConnector):
|
|
|
337
343
|
|
|
338
344
|
return [m for m in items if keep(m)]
|
|
339
345
|
|
|
340
|
-
def _iter_selected_items(self) ->
|
|
346
|
+
def _iter_selected_items(self) -> list[dict[str, Any]]:
|
|
341
347
|
"""
|
|
342
348
|
Return a de-duplicated list of file metadata for the selected scope:
|
|
343
349
|
- explicit file_ids (automatically expands folders to their contents)
|
|
@@ -351,16 +357,21 @@ class GoogleDriveConnector(BaseConnector):
|
|
|
351
357
|
)
|
|
352
358
|
self._clear_shortcut_cache()
|
|
353
359
|
|
|
354
|
-
seen:
|
|
355
|
-
items:
|
|
356
|
-
folders_to_expand:
|
|
360
|
+
seen: set[str] = set()
|
|
361
|
+
items: list[dict[str, Any]] = []
|
|
362
|
+
folders_to_expand: list[str] = []
|
|
357
363
|
|
|
358
364
|
if self.cfg.file_ids:
|
|
359
|
-
logger.debug(
|
|
365
|
+
logger.debug(
|
|
366
|
+
"[GoogleDrive] _iter_selected_items: processing %d file_id(s)",
|
|
367
|
+
len(self.cfg.file_ids),
|
|
368
|
+
)
|
|
360
369
|
for fid in self.cfg.file_ids:
|
|
361
370
|
meta = self._get_file_meta_by_id(fid)
|
|
362
371
|
if not meta:
|
|
363
|
-
logger.debug(
|
|
372
|
+
logger.debug(
|
|
373
|
+
"[GoogleDrive] _iter_selected_items: no metadata for file_id=%s", fid
|
|
374
|
+
)
|
|
364
375
|
continue
|
|
365
376
|
|
|
366
377
|
if meta.get("mimeType") == "application/vnd.google-apps.folder":
|
|
@@ -378,7 +389,9 @@ class GoogleDriveConnector(BaseConnector):
|
|
|
378
389
|
folders_to_expand.extend(self.cfg.folder_ids)
|
|
379
390
|
|
|
380
391
|
if folders_to_expand:
|
|
381
|
-
logger.debug(
|
|
392
|
+
logger.debug(
|
|
393
|
+
"[GoogleDrive] _iter_selected_items: expanding %d folder(s)", len(folders_to_expand)
|
|
394
|
+
)
|
|
382
395
|
folder_children = self._bfs_expand_folders(folders_to_expand)
|
|
383
396
|
for meta in folder_children:
|
|
384
397
|
meta = self._resolve_shortcut(meta)
|
|
@@ -394,11 +407,7 @@ class GoogleDriveConnector(BaseConnector):
|
|
|
394
407
|
return []
|
|
395
408
|
|
|
396
409
|
items = self._filter_by_mime(items)
|
|
397
|
-
items = [
|
|
398
|
-
m
|
|
399
|
-
for m in items
|
|
400
|
-
if m.get("mimeType") != "application/vnd.google-apps.folder"
|
|
401
|
-
]
|
|
410
|
+
items = [m for m in items if m.get("mimeType") != "application/vnd.google-apps.folder"]
|
|
402
411
|
|
|
403
412
|
if not items and (self.cfg.file_ids or self.cfg.folder_ids):
|
|
404
413
|
logger.warning(
|
|
@@ -414,7 +423,7 @@ class GoogleDriveConnector(BaseConnector):
|
|
|
414
423
|
# -------------------------
|
|
415
424
|
# Download logic
|
|
416
425
|
# -------------------------
|
|
417
|
-
def _pick_export_mime(self, source_mime: str) ->
|
|
426
|
+
def _pick_export_mime(self, source_mime: str) -> str | None:
|
|
418
427
|
"""
|
|
419
428
|
Choose export mime for Google-native docs if needed.
|
|
420
429
|
"""
|
|
@@ -437,7 +446,7 @@ class GoogleDriveConnector(BaseConnector):
|
|
|
437
446
|
# Return None for non-Google-native or unsupported types
|
|
438
447
|
return overrides.get(source_mime)
|
|
439
448
|
|
|
440
|
-
def _download_file_bytes(self, file_meta:
|
|
449
|
+
def _download_file_bytes(self, file_meta: dict[str, Any]) -> bytes:
|
|
441
450
|
"""
|
|
442
451
|
Download bytes for a given file (exporting if Google-native).
|
|
443
452
|
Raises ValueError if the item is a folder (folders cannot be downloaded).
|
|
@@ -478,57 +487,56 @@ class GoogleDriveConnector(BaseConnector):
|
|
|
478
487
|
"application/vnd.google-apps.drawing", # Google Drawings
|
|
479
488
|
}
|
|
480
489
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
490
|
+
with self._lock:
|
|
491
|
+
if mime_type in exportable_types:
|
|
492
|
+
# This is an exportable Google Workspace file - must use export_media
|
|
493
|
+
export_mime = self._pick_export_mime(mime_type)
|
|
494
|
+
if not export_mime:
|
|
495
|
+
# Default fallback for unsupported Google native types
|
|
496
|
+
export_mime = "application/pdf"
|
|
487
497
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
)
|
|
493
|
-
# NOTE: export_media does not accept supportsAllDrives/includeItemsFromAllDrives
|
|
494
|
-
request = self.service.files().export_media(
|
|
495
|
-
fileId=file_id, mimeType=export_mime
|
|
496
|
-
)
|
|
497
|
-
else:
|
|
498
|
-
# This is a regular uploaded file (PDF, image, video, etc.) - use get_media
|
|
499
|
-
# Also handles non-exportable Google Apps files (Forms, Sites, Maps, etc.)
|
|
500
|
-
logger.debug("[GoogleDrive] _download_file_bytes: using get_media (%s)", mime_type)
|
|
501
|
-
# Binary download (get_media also doesn't accept the Drive flags)
|
|
502
|
-
request = self.service.files().get_media(fileId=file_id)
|
|
503
|
-
|
|
504
|
-
# Download the file with error handling for misclassified Google Docs
|
|
505
|
-
fh = io.BytesIO()
|
|
506
|
-
downloader = MediaIoBaseDownload(fh, request, chunksize=1024 * 1024)
|
|
507
|
-
done = False
|
|
508
|
-
|
|
509
|
-
try:
|
|
510
|
-
while not done:
|
|
511
|
-
status, done = downloader.next_chunk()
|
|
512
|
-
# Optional: you can log progress via status.progress()
|
|
513
|
-
except HttpError as e:
|
|
514
|
-
# If download fails with "fileNotDownloadable", it's a Docs Editor file
|
|
515
|
-
# that wasn't properly detected. Retry with export_media.
|
|
516
|
-
if "fileNotDownloadable" in str(e) and mime_type not in exportable_types:
|
|
517
|
-
logger.warning(
|
|
518
|
-
f"Download failed for {file_id} ({mime_type}) with fileNotDownloadable error. "
|
|
519
|
-
f"Retrying with export_media (file might be a Google Doc)"
|
|
520
|
-
)
|
|
521
|
-
export_mime = "application/pdf"
|
|
522
|
-
request = self.service.files().export_media(
|
|
523
|
-
fileId=file_id, mimeType=export_mime
|
|
498
|
+
logger.debug(
|
|
499
|
+
"[GoogleDrive] _download_file_bytes: using export_media (%s -> %s)",
|
|
500
|
+
mime_type,
|
|
501
|
+
export_mime,
|
|
524
502
|
)
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
503
|
+
# NOTE: export_media does not accept supportsAllDrives/includeItemsFromAllDrives
|
|
504
|
+
request = self.service.files().export_media(fileId=file_id, mimeType=export_mime)
|
|
505
|
+
else:
|
|
506
|
+
# This is a regular uploaded file (PDF, image, video, etc.) - use get_media
|
|
507
|
+
# Also handles non-exportable Google Apps files (Forms, Sites, Maps, etc.)
|
|
508
|
+
logger.debug("[GoogleDrive] _download_file_bytes: using get_media (%s)", mime_type)
|
|
509
|
+
# Binary download (get_media also doesn't accept the Drive flags)
|
|
510
|
+
request = self.service.files().get_media(fileId=file_id)
|
|
511
|
+
|
|
512
|
+
# Download the file with error handling for misclassified Google Docs
|
|
513
|
+
fh = io.BytesIO()
|
|
514
|
+
downloader = MediaIoBaseDownload(fh, request, chunksize=1024 * 1024)
|
|
515
|
+
done = False
|
|
516
|
+
|
|
517
|
+
try:
|
|
528
518
|
while not done:
|
|
529
519
|
status, done = downloader.next_chunk()
|
|
530
|
-
|
|
531
|
-
|
|
520
|
+
# Optional: you can log progress via status.progress()
|
|
521
|
+
except HttpError as e:
|
|
522
|
+
# If download fails with "fileNotDownloadable", it's a Docs Editor file
|
|
523
|
+
# that wasn't properly detected. Retry with export_media.
|
|
524
|
+
if "fileNotDownloadable" in str(e) and mime_type not in exportable_types:
|
|
525
|
+
logger.warning(
|
|
526
|
+
f"Download failed for {file_id} ({mime_type}) with fileNotDownloadable error. "
|
|
527
|
+
f"Retrying with export_media (file might be a Google Doc)"
|
|
528
|
+
)
|
|
529
|
+
export_mime = "application/pdf"
|
|
530
|
+
request = self.service.files().export_media(
|
|
531
|
+
fileId=file_id, mimeType=export_mime
|
|
532
|
+
)
|
|
533
|
+
fh = io.BytesIO()
|
|
534
|
+
downloader = MediaIoBaseDownload(fh, request, chunksize=1024 * 1024)
|
|
535
|
+
done = False
|
|
536
|
+
while not done:
|
|
537
|
+
status, done = downloader.next_chunk()
|
|
538
|
+
else:
|
|
539
|
+
raise
|
|
532
540
|
|
|
533
541
|
data = fh.getvalue()
|
|
534
542
|
logger.debug("[GoogleDrive] _download_file_bytes: done, %d bytes", len(data))
|
|
@@ -572,10 +580,10 @@ class GoogleDriveConnector(BaseConnector):
|
|
|
572
580
|
|
|
573
581
|
async def list_files(
|
|
574
582
|
self,
|
|
575
|
-
page_token:
|
|
576
|
-
max_files:
|
|
583
|
+
page_token: str | None = None,
|
|
584
|
+
max_files: int | None = None,
|
|
577
585
|
**kwargs,
|
|
578
|
-
) ->
|
|
586
|
+
) -> dict[str, Any]:
|
|
579
587
|
"""
|
|
580
588
|
List files in the currently selected scope (file_ids/folder_ids/recursive).
|
|
581
589
|
Returns a dict with 'files' and 'next_page_token'.
|
|
@@ -589,7 +597,9 @@ class GoogleDriveConnector(BaseConnector):
|
|
|
589
597
|
"Google Drive service is not initialized. Please authenticate first."
|
|
590
598
|
)
|
|
591
599
|
|
|
592
|
-
logger.debug(
|
|
600
|
+
logger.debug(
|
|
601
|
+
"[GoogleDrive] list_files: entry (page_token=%s, max_files=%s)", page_token, max_files
|
|
602
|
+
)
|
|
593
603
|
|
|
594
604
|
try:
|
|
595
605
|
items = await asyncio.to_thread(self._iter_selected_items)
|
|
@@ -609,7 +619,7 @@ class GoogleDriveConnector(BaseConnector):
|
|
|
609
619
|
logger.error("[GoogleDrive] list_files failed: %s", e, exc_info=True)
|
|
610
620
|
raise
|
|
611
621
|
|
|
612
|
-
def _extract_google_drive_acl(self, file_meta:
|
|
622
|
+
def _extract_google_drive_acl(self, file_meta: dict) -> DocumentACL:
|
|
613
623
|
"""
|
|
614
624
|
Extract ACL from Google Drive file metadata.
|
|
615
625
|
|
|
@@ -624,10 +634,15 @@ class GoogleDriveConnector(BaseConnector):
|
|
|
624
634
|
"""
|
|
625
635
|
try:
|
|
626
636
|
# Fetch permissions (requires additional API call)
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
637
|
+
with self._lock:
|
|
638
|
+
permissions_list = (
|
|
639
|
+
self.service.permissions()
|
|
640
|
+
.list(
|
|
641
|
+
fileId=file_meta["id"],
|
|
642
|
+
fields="permissions(emailAddress,role,type,deleted,displayName)",
|
|
643
|
+
)
|
|
644
|
+
.execute()
|
|
645
|
+
)
|
|
631
646
|
|
|
632
647
|
allowed_users = []
|
|
633
648
|
allowed_groups = []
|
|
@@ -730,12 +745,12 @@ class GoogleDriveConnector(BaseConnector):
|
|
|
730
745
|
metadata={
|
|
731
746
|
"parents": meta.get("parents"),
|
|
732
747
|
"driveId": meta.get("driveId"),
|
|
733
|
-
"size": int(meta.get("size", 0))
|
|
734
|
-
if str(meta.get("size", "")).isdigit()
|
|
735
|
-
else None,
|
|
748
|
+
"size": int(meta.get("size", 0)) if str(meta.get("size", "")).isdigit() else None,
|
|
736
749
|
},
|
|
737
750
|
)
|
|
738
|
-
logger.debug(
|
|
751
|
+
logger.debug(
|
|
752
|
+
"[GoogleDrive] get_file_content: done for file_id=%s (%d bytes)", file_id, len(blob)
|
|
753
|
+
)
|
|
739
754
|
return doc
|
|
740
755
|
|
|
741
756
|
async def setup_subscription(self) -> str:
|
|
@@ -752,9 +767,7 @@ class GoogleDriveConnector(BaseConnector):
|
|
|
752
767
|
# 1) Ensure we are authenticated and have a live Drive service
|
|
753
768
|
ok = await self.authenticate()
|
|
754
769
|
if not ok:
|
|
755
|
-
raise RuntimeError(
|
|
756
|
-
"GoogleDriveConnector.setup_subscription: not authenticated"
|
|
757
|
-
)
|
|
770
|
+
raise RuntimeError("GoogleDriveConnector.setup_subscription: not authenticated")
|
|
758
771
|
|
|
759
772
|
# 2) Resolve webhook address (no param in ABC, so pull from config/env)
|
|
760
773
|
webhook_address = getattr(self.cfg, "webhook_address", None) or os.getenv(
|
|
@@ -810,9 +823,7 @@ class GoogleDriveConnector(BaseConnector):
|
|
|
810
823
|
}
|
|
811
824
|
|
|
812
825
|
if not isinstance(channel_id, str) or not channel_id:
|
|
813
|
-
raise RuntimeError(
|
|
814
|
-
f"Drive watch returned invalid channel id: {channel_id!r}"
|
|
815
|
-
)
|
|
826
|
+
raise RuntimeError(f"Drive watch returned invalid channel id: {channel_id!r}")
|
|
816
827
|
|
|
817
828
|
return channel_id
|
|
818
829
|
|
|
@@ -850,13 +861,13 @@ class GoogleDriveConnector(BaseConnector):
|
|
|
850
861
|
|
|
851
862
|
# Single-channel memory
|
|
852
863
|
if getattr(self, "_active_channel", None):
|
|
853
|
-
ch =
|
|
864
|
+
ch = self._active_channel
|
|
854
865
|
if isinstance(ch, dict) and ch.get("channel_id") == subscription_id:
|
|
855
866
|
resource_id = ch.get("resource_id")
|
|
856
867
|
|
|
857
868
|
# Multi-channel memory
|
|
858
869
|
if resource_id is None and hasattr(self, "_subscriptions"):
|
|
859
|
-
subs =
|
|
870
|
+
subs = self._subscriptions
|
|
860
871
|
if isinstance(subs, dict):
|
|
861
872
|
entry = subs.get(subscription_id)
|
|
862
873
|
if isinstance(entry, dict):
|
|
@@ -888,9 +899,7 @@ class GoogleDriveConnector(BaseConnector):
|
|
|
888
899
|
):
|
|
889
900
|
self._active_channel = {}
|
|
890
901
|
|
|
891
|
-
if hasattr(self, "_subscriptions") and isinstance(
|
|
892
|
-
self._subscriptions, dict
|
|
893
|
-
):
|
|
902
|
+
if hasattr(self, "_subscriptions") and isinstance(self._subscriptions, dict):
|
|
894
903
|
self._subscriptions.pop(subscription_id, None)
|
|
895
904
|
|
|
896
905
|
return True
|
|
@@ -902,7 +911,7 @@ class GoogleDriveConnector(BaseConnector):
|
|
|
902
911
|
pass
|
|
903
912
|
return False
|
|
904
913
|
|
|
905
|
-
async def handle_webhook(self, payload:
|
|
914
|
+
async def handle_webhook(self, payload: dict[str, Any]) -> list[str]:
|
|
906
915
|
"""
|
|
907
916
|
Process a Google Drive Changes webhook.
|
|
908
917
|
Drive push notifications do NOT include the changed files themselves; they merely tell us
|
|
@@ -915,7 +924,7 @@ class GoogleDriveConnector(BaseConnector):
|
|
|
915
924
|
Returns:
|
|
916
925
|
List[str]: unique list of affected file IDs (filtered to our selected scope).
|
|
917
926
|
"""
|
|
918
|
-
affected:
|
|
927
|
+
affected: list[str] = []
|
|
919
928
|
try:
|
|
920
929
|
# 1) Ensure we're authenticated / service ready
|
|
921
930
|
ok = await self.authenticate()
|
|
@@ -941,9 +950,7 @@ class GoogleDriveConnector(BaseConnector):
|
|
|
941
950
|
except Exception as e:
|
|
942
951
|
selected_ids = set()
|
|
943
952
|
try:
|
|
944
|
-
logger.error(
|
|
945
|
-
f"handle_webhook: scope build failed, proceeding unfiltered: {e}"
|
|
946
|
-
)
|
|
953
|
+
logger.error(f"handle_webhook: scope build failed, proceeding unfiltered: {e}")
|
|
947
954
|
except Exception:
|
|
948
955
|
pass
|
|
949
956
|
|
|
@@ -980,11 +987,7 @@ class GoogleDriveConnector(BaseConnector):
|
|
|
980
987
|
# Filter to our selected scope if we have one; otherwise accept all
|
|
981
988
|
if selected_ids and (rid not in selected_ids):
|
|
982
989
|
# Shortcut target might be in scope even if the shortcut isn't
|
|
983
|
-
tgt = (
|
|
984
|
-
fobj.get("shortcutDetails", {}).get("targetId")
|
|
985
|
-
if fobj
|
|
986
|
-
else None
|
|
987
|
-
)
|
|
990
|
+
tgt = fobj.get("shortcutDetails", {}).get("targetId") if fobj else None
|
|
988
991
|
if not (tgt and tgt in selected_ids):
|
|
989
992
|
continue
|
|
990
993
|
|
|
@@ -1007,7 +1010,7 @@ class GoogleDriveConnector(BaseConnector):
|
|
|
1007
1010
|
|
|
1008
1011
|
# Deduplicate while preserving order
|
|
1009
1012
|
seen = set()
|
|
1010
|
-
deduped:
|
|
1013
|
+
deduped: list[str] = []
|
|
1011
1014
|
for x in affected:
|
|
1012
1015
|
if x not in seen:
|
|
1013
1016
|
seen.add(x)
|
|
@@ -1033,9 +1036,7 @@ class GoogleDriveConnector(BaseConnector):
|
|
|
1033
1036
|
blob = self._download_file_bytes(meta)
|
|
1034
1037
|
except HttpError as e:
|
|
1035
1038
|
# Skip/record failures
|
|
1036
|
-
logger.error(
|
|
1037
|
-
f"Failed to download {meta.get('name')} ({meta.get('id')}): {e}"
|
|
1038
|
-
)
|
|
1039
|
+
logger.error(f"Failed to download {meta.get('name')} ({meta.get('id')}): {e}")
|
|
1039
1040
|
continue
|
|
1040
1041
|
|
|
1041
1042
|
from datetime import datetime
|
|
@@ -1081,12 +1082,10 @@ class GoogleDriveConnector(BaseConnector):
|
|
|
1081
1082
|
# -------------------------
|
|
1082
1083
|
def get_start_page_token(self) -> str:
|
|
1083
1084
|
# getStartPageToken accepts supportsAllDrives (not includeItemsFromAllDrives)
|
|
1084
|
-
resp = (
|
|
1085
|
-
self.service.changes().getStartPageToken(**self._drives_get_flags).execute()
|
|
1086
|
-
)
|
|
1085
|
+
resp = self.service.changes().getStartPageToken(**self._drives_get_flags).execute()
|
|
1087
1086
|
return resp["startPageToken"]
|
|
1088
1087
|
|
|
1089
|
-
def poll_changes_and_sync(self) ->
|
|
1088
|
+
def poll_changes_and_sync(self) -> str | None:
|
|
1090
1089
|
"""
|
|
1091
1090
|
Incrementally process changes since the last page token in cfg.changes_page_token.
|
|
1092
1091
|
|
|
@@ -1122,10 +1121,7 @@ class GoogleDriveConnector(BaseConnector):
|
|
|
1122
1121
|
# Match scope
|
|
1123
1122
|
if fid not in selected_ids:
|
|
1124
1123
|
# also consider shortcut target
|
|
1125
|
-
if (
|
|
1126
|
-
file_obj.get("mimeType")
|
|
1127
|
-
== "application/vnd.google-apps.shortcut"
|
|
1128
|
-
):
|
|
1124
|
+
if file_obj.get("mimeType") == "application/vnd.google-apps.shortcut":
|
|
1129
1125
|
tgt = file_obj.get("shortcutDetails", {}).get("targetId")
|
|
1130
1126
|
if tgt and tgt in selected_ids:
|
|
1131
1127
|
pass
|
|
@@ -1189,8 +1185,8 @@ class GoogleDriveConnector(BaseConnector):
|
|
|
1189
1185
|
# Optional: webhook stubs
|
|
1190
1186
|
# -------------------------
|
|
1191
1187
|
def build_watch_body(
|
|
1192
|
-
self, webhook_address: str, channel_id:
|
|
1193
|
-
) ->
|
|
1188
|
+
self, webhook_address: str, channel_id: str | None = None
|
|
1189
|
+
) -> dict[str, Any]:
|
|
1194
1190
|
"""
|
|
1195
1191
|
Prepare the request body for changes.watch if you use webhooks.
|
|
1196
1192
|
"""
|
|
@@ -1200,7 +1196,7 @@ class GoogleDriveConnector(BaseConnector):
|
|
|
1200
1196
|
"address": webhook_address,
|
|
1201
1197
|
}
|
|
1202
1198
|
|
|
1203
|
-
def start_watch(self, webhook_address: str) ->
|
|
1199
|
+
def start_watch(self, webhook_address: str) -> dict[str, Any]:
|
|
1204
1200
|
"""
|
|
1205
1201
|
Start a webhook watch on changes using the current page token.
|
|
1206
1202
|
Persist the returned resourceId/expiration on your side.
|
|
@@ -53,9 +53,13 @@ class LangflowConnectorService:
|
|
|
53
53
|
filename=document.filename,
|
|
54
54
|
)
|
|
55
55
|
|
|
56
|
+
import os
|
|
57
|
+
|
|
56
58
|
from utils.file_utils import auto_cleanup_tempfile
|
|
57
59
|
|
|
58
|
-
suffix =
|
|
60
|
+
suffix = os.path.splitext(document.filename)[1]
|
|
61
|
+
if not suffix:
|
|
62
|
+
suffix = get_file_extension(document.mimetype)
|
|
59
63
|
|
|
60
64
|
# Create temporary file from document content
|
|
61
65
|
with auto_cleanup_tempfile(suffix=suffix) as tmp_path:
|
connectors/service.py
CHANGED
|
@@ -52,11 +52,15 @@ class ConnectorService:
|
|
|
52
52
|
"""Process a document from a connector using existing processing pipeline"""
|
|
53
53
|
|
|
54
54
|
# Create temporary file from document content
|
|
55
|
+
import os
|
|
56
|
+
|
|
55
57
|
from utils.file_utils import auto_cleanup_tempfile
|
|
56
58
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
suffix = os.path.splitext(document.filename)[1]
|
|
60
|
+
if not suffix:
|
|
61
|
+
suffix = get_file_extension(document.mimetype)
|
|
62
|
+
|
|
63
|
+
with auto_cleanup_tempfile(suffix=suffix) as tmp_path:
|
|
60
64
|
# Write document content to temp file
|
|
61
65
|
with open(tmp_path, "wb") as f:
|
|
62
66
|
f.write(document.content)
|
|
@@ -363,6 +367,7 @@ class ConnectorService:
|
|
|
363
367
|
original_folder_ids = getattr(connector.cfg, "folder_ids", None)
|
|
364
368
|
|
|
365
369
|
expanded_file_ids = file_ids # Default to original IDs
|
|
370
|
+
expanded_files_info = []
|
|
366
371
|
|
|
367
372
|
try:
|
|
368
373
|
# Set the file_ids we want to sync in the connector's config
|
|
@@ -373,7 +378,12 @@ class ConnectorService:
|
|
|
373
378
|
# Get the expanded list of file IDs (folders will be expanded to their contents)
|
|
374
379
|
# This uses the connector's list_files() which calls _iter_selected_items()
|
|
375
380
|
result = await connector.list_files()
|
|
376
|
-
|
|
381
|
+
expanded_files = result.get("files", [])
|
|
382
|
+
expanded_file_ids = [f["id"] for f in expanded_files]
|
|
383
|
+
|
|
384
|
+
# Save the expanded files info so we can set correct names in the task UI
|
|
385
|
+
for f in expanded_files:
|
|
386
|
+
expanded_files_info.append(f)
|
|
377
387
|
|
|
378
388
|
if not expanded_file_ids:
|
|
379
389
|
logger.warning(
|
|
@@ -423,12 +433,15 @@ class ConnectorService:
|
|
|
423
433
|
|
|
424
434
|
# Create custom task using TaskService
|
|
425
435
|
original_filenames = {}
|
|
426
|
-
|
|
436
|
+
|
|
437
|
+
# Combine file_infos and expanded_files_info
|
|
438
|
+
all_infos = (file_infos or []) + expanded_files_info
|
|
439
|
+
if all_infos:
|
|
427
440
|
original_filenames = {
|
|
428
441
|
f["id"]: clean_connector_filename(
|
|
429
|
-
f["name"], f.get("mimeType") or f.get("mimetype")
|
|
442
|
+
f["name"], f.get("mimeType") or f.get("mimetype", "")
|
|
430
443
|
)
|
|
431
|
-
for f in
|
|
444
|
+
for f in all_infos
|
|
432
445
|
if "id" in f and "name" in f
|
|
433
446
|
}
|
|
434
447
|
|
models/processors.py
CHANGED
|
@@ -257,15 +257,16 @@ class TaskProcessor:
|
|
|
257
257
|
file_hash=file_hash,
|
|
258
258
|
)
|
|
259
259
|
slim_doc = process_text_file(file_path)
|
|
260
|
-
# Override filename with original_filename if provided
|
|
261
|
-
if original_filename:
|
|
262
|
-
slim_doc["filename"] = original_filename
|
|
263
260
|
else:
|
|
264
261
|
from utils.docling_client import convert_file
|
|
265
262
|
|
|
266
263
|
full_doc = await convert_file(file_path, httpx_client=clients.docling_http_client)
|
|
267
264
|
slim_doc = extract_relevant(full_doc)
|
|
268
265
|
|
|
266
|
+
# Override filename with original_filename if provided
|
|
267
|
+
if original_filename:
|
|
268
|
+
slim_doc["filename"] = original_filename
|
|
269
|
+
|
|
269
270
|
if chunk_size is not None:
|
|
270
271
|
try:
|
|
271
272
|
cs = int(chunk_size)
|
|
@@ -527,10 +528,13 @@ class ConnectorFileProcessor(TaskProcessor):
|
|
|
527
528
|
if not self.user_id:
|
|
528
529
|
raise ValueError("user_id not provided to ConnectorFileProcessor")
|
|
529
530
|
|
|
530
|
-
# Create temporary file from document content
|
|
531
531
|
from utils.file_utils import auto_cleanup_tempfile
|
|
532
|
+
# Create temporary file from document content import os
|
|
533
|
+
|
|
534
|
+
suffix = os.path.splitext(document.filename)[1]
|
|
535
|
+
if not suffix:
|
|
536
|
+
suffix = get_file_extension(document.mimetype)
|
|
532
537
|
|
|
533
|
-
suffix = get_file_extension(document.mimetype)
|
|
534
538
|
with auto_cleanup_tempfile(suffix=suffix) as tmp_path:
|
|
535
539
|
# Write content to temp file
|
|
536
540
|
with open(tmp_path, 'wb') as f:
|
|
@@ -654,7 +658,10 @@ class LangflowConnectorFileProcessor(TaskProcessor):
|
|
|
654
658
|
# Create temporary file and compute hash to check for duplicates
|
|
655
659
|
from utils.file_utils import auto_cleanup_tempfile
|
|
656
660
|
|
|
657
|
-
suffix =
|
|
661
|
+
suffix = os.path.splitext(document.filename)[1]
|
|
662
|
+
if not suffix:
|
|
663
|
+
suffix = get_file_extension(document.mimetype)
|
|
664
|
+
|
|
658
665
|
with auto_cleanup_tempfile(suffix=suffix) as tmp_path:
|
|
659
666
|
# Write content to temp file
|
|
660
667
|
with open(tmp_path, 'wb') as f:
|
|
@@ -740,7 +747,8 @@ class S3FileProcessor(TaskProcessor):
|
|
|
740
747
|
from utils.hash_utils import hash_id
|
|
741
748
|
|
|
742
749
|
try:
|
|
743
|
-
|
|
750
|
+
suffix = os.path.splitext(item)[1]
|
|
751
|
+
with auto_cleanup_tempfile(suffix=suffix) as tmp_path:
|
|
744
752
|
# Download object to temporary file
|
|
745
753
|
with open(tmp_path, 'wb') as tmp_file:
|
|
746
754
|
self.s3_client.download_fileobj(self.bucket, item, tmp_file)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openrag
|
|
3
|
-
Version: 0.5.0.
|
|
3
|
+
Version: 0.5.0.dev32
|
|
4
4
|
Summary: OpenRAG is a comprehensive Retrieval-Augmented Generation platform that enables intelligent document search and AI-powered conversations.
|
|
5
5
|
Classifier: Development Status :: 4 - Beta
|
|
6
6
|
Classifier: Environment :: Console
|
|
@@ -22,7 +22,7 @@ api/provider_health.py,sha256=u4gNGhEAqsQY1ml6xWzWWAOfLHDrSVYPkw1NAmBCMsI,9690
|
|
|
22
22
|
api/provider_validation.py,sha256=ik00rpXjL6pPCyJt3gEdeNhyoYlhIPGz_Kigmhf0zLw,31430
|
|
23
23
|
api/router.py,sha256=nOusNdc-eOMRZo6a5eqNb91oOoleYNomHTGcKuPsMmU,6229
|
|
24
24
|
api/search.py,sha256=z6M7PaO1oIdZtp_yU0eGkz0dTmDJItc7O4ULc67VIHk,1982
|
|
25
|
-
api/settings.py,sha256=
|
|
25
|
+
api/settings.py,sha256=UNvTkZSI1hHOKYzmNe1yPFBby-Mn22R0fDyeCwy1lyM,92701
|
|
26
26
|
api/tasks.py,sha256=j3tAfpE_X4UxhiGPrgea5jkxILF6HZK9Hoy3QTFGG0g,1524
|
|
27
27
|
api/upload.py,sha256=vh8pxlf7LnTIlRH8gsJhogTbFq93-SQ-1UNIQTtaer4,7288
|
|
28
28
|
api/v1/__init__.py,sha256=A7dLY63RcJv6ii-Xh7WgREcwXixjzOXz2w5C0LGWTCc,190
|
|
@@ -43,8 +43,8 @@ config/settings.py,sha256=zhvJ3TAmdZwoWk55BPX4OuAAJiDTHlJg4_MoSWPhZMI,39298
|
|
|
43
43
|
connectors/__init__.py,sha256=6ijOhF7jG-zJ3bIxQ4HIHZHqpT127cNvCDaaqdN_Pm8,390
|
|
44
44
|
connectors/base.py,sha256=s9gBEIxl8_nZVtCeMKz7LrlOV-H0rCxvN_cFH8B9DMg,4532
|
|
45
45
|
connectors/connection_manager.py,sha256=LrfiuoBB-hqNGjXhH-5x4uL6msU-OVOp7--4V302a14,28187
|
|
46
|
-
connectors/langflow_connector_service.py,sha256=
|
|
47
|
-
connectors/service.py,sha256=
|
|
46
|
+
connectors/langflow_connector_service.py,sha256=HjVgTmlpZCWsoR2msEuCouw3snX0HBHNv6Szm4A0Iik,17251
|
|
47
|
+
connectors/service.py,sha256=_qyRDskMiNfPa2OyOan9gAmOgB5JjcLTidzzg-rhLWs,18229
|
|
48
48
|
connectors/aws_s3/__init__.py,sha256=ZRXSKmYvYK7Kr7Rzcd0_YIMpqfAm5qjm06zF-YfPPhA,369
|
|
49
49
|
connectors/aws_s3/api.py,sha256=CDpsdOZaOM7hT93Je4O_JD7GxJjPN0-_qgQPPSPiy40,6861
|
|
50
50
|
connectors/aws_s3/auth.py,sha256=qvrjxNM5MvgK-I5F5Njl8B7pSJTrGIZ0wuZImFP9UVg,3060
|
|
@@ -52,7 +52,7 @@ connectors/aws_s3/connector.py,sha256=QZPFt0PycyIMp72ut2yxXrEYl45_N5MUloZXrw_xvf
|
|
|
52
52
|
connectors/aws_s3/models.py,sha256=olSwBxzvVKqn6j3M5tENq7ksLQIuTfhGPGIEKmqxbR4,398
|
|
53
53
|
connectors/aws_s3/support.py,sha256=vwsy6Q9EAssb2FeeT9NVwlJ9K_NWTulGh8HK4hxwmrg,1458
|
|
54
54
|
connectors/google_drive/__init__.py,sha256=y3HqyvfVvNV8PYMgFF_MeM7djjWxkyDw3hJivLfhGXc,136
|
|
55
|
-
connectors/google_drive/connector.py,sha256=
|
|
55
|
+
connectors/google_drive/connector.py,sha256=UCSpGxOtCGRtTtiF4p8cZLkWAEOvdymzGhRYLYztaMY,47609
|
|
56
56
|
connectors/google_drive/oauth.py,sha256=7zRf5zuFNbrREGI516BJuOzuMpGEaqhAtyzENFyPp-U,8179
|
|
57
57
|
connectors/ibm_cos/__init__.py,sha256=gp6L4nW7EIHsEYIlbZQUDE9o90JO-0CEojTUtEv5-Po,369
|
|
58
58
|
connectors/ibm_cos/api.py,sha256=vsCGoJnEfUMWqrWAdw0znpJQUcex7Lwx1ODWZeuY7m8,8440
|
|
@@ -70,13 +70,13 @@ connectors/sharepoint/utils.py,sha256=OBNEbXb8uAfR-iR-ML0aOojGfGj03vJnKm9DbYf2hW
|
|
|
70
70
|
mcp_http/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
71
71
|
mcp_http/server.py,sha256=JutP94fQRCizeCN69kKkdYDFE71L3DqLTCcb397T8Eo,7738
|
|
72
72
|
models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
73
|
-
models/processors.py,sha256=
|
|
73
|
+
models/processors.py,sha256=Vc6a1HIHpIe9t3Jx7HJ_lCRrSmWtzw63C5bzJpxTz8s,35793
|
|
74
74
|
models/tasks.py,sha256=9iINXQZxA9-hcYzG8xC_baBfLa2w6gcRPJdQHsx_Kms,1643
|
|
75
75
|
models/url.py,sha256=92rsrLLyIqUErYipYz5QwUNTdHwJ2jBRV7PIVpncTyo,5764
|
|
76
|
-
openrag-0.5.0.
|
|
76
|
+
openrag-0.5.0.dev32.dist-info/licenses/LICENSE,sha256=jWcnPXkUxEinX60Yn-_zjsY9i84vz2QSb0fi53djOkg,11334
|
|
77
77
|
services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
78
78
|
services/api_key_service.py,sha256=NMz2ohJo0jcuHjDN3tHuzhCTVo6lsOqAkUhozzmWSv0,11720
|
|
79
|
-
services/auth_service.py,sha256=
|
|
79
|
+
services/auth_service.py,sha256=nAKTvMyX-YUtiNfKF06Dq4E4iwHPBhfKR6nliqGpOSs,22090
|
|
80
80
|
services/chat_service.py,sha256=Pg34Wg1krrRcZeVR0dGnZ_PU6mPB-CW4W39-2JwQAjQ,28366
|
|
81
81
|
services/conversation_persistence_service.py,sha256=zfNMcgbD7GbQ_W8tB5jV_NDa_78l6fAmaIiiICg1YH8,6092
|
|
82
82
|
services/document_service.py,sha256=-Lq5fYLnLz4mCsU4aS3tKXVTiDZ3P1jzyuNsxH-QdGw,7397
|
|
@@ -117,7 +117,7 @@ tui/screens/__init__.py,sha256=ZzB_lbAnSTwDqiG0ppafOE-sAiOUVaNy4V6x5AJvqyM,27
|
|
|
117
117
|
tui/screens/config.py,sha256=_XcUisNNYR4-IVOhfXIZ9lCY1bPe8eaZY9JMh5nRi0g,27975
|
|
118
118
|
tui/screens/diagnostics.py,sha256=ZMDD9KSY0GoXo67ft6j7x4RGWzI_Hy83_a3FovnstrM,24864
|
|
119
119
|
tui/screens/logs.py,sha256=3fM6fAqjdFXVI_laiF5yl7lH8n0bSmWcdozMAWW3SLM,11853
|
|
120
|
-
tui/screens/monitor.py,sha256=
|
|
120
|
+
tui/screens/monitor.py,sha256=kTDaaDjoJT5Lr4kGYwNeVV9jsOMLLYfapZnDcJjjbcE,46900
|
|
121
121
|
tui/screens/welcome.py,sha256=YYotbQOVbiLZjnyvcZ5OJqYNrh0i_bI22T9WdXqMjHQ,28452
|
|
122
122
|
tui/utils/__init__.py,sha256=MIui8sXgp8IUvEkaCNuI5kRME8XfwCnGsapBIldaY_s,29
|
|
123
123
|
tui/utils/clipboard.py,sha256=tUoesmliVEVgrWarsNkEGWj3fHc2L_IsKEMIhwrgG9c,1901
|
|
@@ -159,8 +159,8 @@ utils/telemetry/__init__.py,sha256=xoStY8hoYWnVe0B_jNthSdzRb7AxCT0rhM8ba9Q0Obg,2
|
|
|
159
159
|
utils/telemetry/category.py,sha256=wei3V5WyUaiadYjEE5s0ys9PY0TjnebIvTykQU0P_2k,1114
|
|
160
160
|
utils/telemetry/client.py,sha256=RhjW1a27w7W6gHt9X7AxdRPdZGmCEQDPskkxj5nsuiQ,12500
|
|
161
161
|
utils/telemetry/message_id.py,sha256=IAu0wBVNOGPcSlQ0bAzvqIYr1Ac_n8wSTSxXzq_tiV0,9634
|
|
162
|
-
openrag-0.5.0.
|
|
163
|
-
openrag-0.5.0.
|
|
164
|
-
openrag-0.5.0.
|
|
165
|
-
openrag-0.5.0.
|
|
166
|
-
openrag-0.5.0.
|
|
162
|
+
openrag-0.5.0.dev32.dist-info/METADATA,sha256=UfANP1bsEv8iz1SeZw2O9gH1qIOiUWpP_1YB8xttsE4,7819
|
|
163
|
+
openrag-0.5.0.dev32.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
164
|
+
openrag-0.5.0.dev32.dist-info/entry_points.txt,sha256=ArKNHB63I3K8vynNkogvZOLmmQFpIsHDL7MvreCmju4,45
|
|
165
|
+
openrag-0.5.0.dev32.dist-info/top_level.txt,sha256=5vfLBIkzuwBJrizhCSZiXV7NTzTRIMlNLsyzYDEeCF8,125
|
|
166
|
+
openrag-0.5.0.dev32.dist-info/RECORD,,
|
services/auth_service.py
CHANGED
|
@@ -324,9 +324,7 @@ class AuthService:
|
|
|
324
324
|
|
|
325
325
|
# Add expiry if provided
|
|
326
326
|
if token_data.get("expires_in"):
|
|
327
|
-
expiry = datetime.
|
|
328
|
-
seconds=int(token_data["expires_in"])
|
|
329
|
-
)
|
|
327
|
+
expiry = datetime.utcnow() + timedelta(seconds=int(token_data["expires_in"]))
|
|
330
328
|
token_file_data["expiry"] = expiry.isoformat()
|
|
331
329
|
|
|
332
330
|
# Save tokens to file
|
tui/screens/monitor.py
CHANGED
|
@@ -3,29 +3,34 @@
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import re
|
|
5
5
|
import shutil
|
|
6
|
+
from collections.abc import AsyncIterator
|
|
6
7
|
from pathlib import Path
|
|
7
|
-
from typing import
|
|
8
|
-
|
|
9
|
-
# Define button variant type
|
|
10
|
-
ButtonVariant = Literal["default", "primary", "success", "warning", "error"]
|
|
8
|
+
from typing import Any, Literal, cast
|
|
11
9
|
|
|
10
|
+
from rich.text import Text
|
|
12
11
|
from textual.app import ComposeResult
|
|
13
|
-
from textual.containers import
|
|
12
|
+
from textual.containers import Horizontal, ScrollableContainer
|
|
13
|
+
from textual.coordinate import Coordinate
|
|
14
14
|
from textual.screen import Screen
|
|
15
|
-
from textual.widgets import
|
|
16
|
-
|
|
17
|
-
from
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
15
|
+
from textual.widgets import Button, DataTable, Footer, Static
|
|
16
|
+
|
|
17
|
+
from ..managers.container_manager import (
|
|
18
|
+
ContainerManager,
|
|
19
|
+
ServiceInfo,
|
|
20
|
+
ServiceStatus,
|
|
21
|
+
format_port_conflict_message,
|
|
22
|
+
)
|
|
21
23
|
from ..managers.docling_manager import DoclingManager
|
|
22
24
|
from ..utils.platform import RuntimeType
|
|
23
25
|
from ..widgets.command_modal import CommandOutputModal
|
|
24
|
-
from ..widgets.
|
|
26
|
+
from ..widgets.diagnostics_notification import notify_with_diagnostics
|
|
25
27
|
from ..widgets.factory_reset_warning_modal import FactoryResetWarningModal
|
|
26
|
-
from ..widgets.
|
|
28
|
+
from ..widgets.flow_backup_warning_modal import FlowBackupWarningModal
|
|
27
29
|
from ..widgets.upgrade_instructions_modal import UpgradeInstructionsModal
|
|
28
|
-
from ..widgets.
|
|
30
|
+
from ..widgets.version_mismatch_warning_modal import VersionMismatchWarningModal
|
|
31
|
+
|
|
32
|
+
# Define button variant type
|
|
33
|
+
ButtonVariant = Literal["default", "primary", "success", "warning", "error"]
|
|
29
34
|
|
|
30
35
|
|
|
31
36
|
class MonitorScreen(Screen):
|
|
@@ -55,6 +60,8 @@ class MonitorScreen(Screen):
|
|
|
55
60
|
self.refresh_timer = None
|
|
56
61
|
self.operation_in_progress = False
|
|
57
62
|
self._follow_task = None
|
|
63
|
+
self._follow_service: str | None = None
|
|
64
|
+
self._logs_buffer: list[str] = []
|
|
58
65
|
|
|
59
66
|
# Track which table was last selected for mutual exclusion
|
|
60
67
|
self._last_selected_table = None
|
|
@@ -63,10 +70,9 @@ class MonitorScreen(Screen):
|
|
|
63
70
|
def container_manager(self) -> ContainerManager:
|
|
64
71
|
"""Get the shared container manager from the app."""
|
|
65
72
|
if self._container_manager is None:
|
|
66
|
-
self._container_manager = self.app.container_manager
|
|
73
|
+
self._container_manager = cast(Any, self.app).container_manager
|
|
67
74
|
return self._container_manager
|
|
68
75
|
|
|
69
|
-
|
|
70
76
|
def compose(self) -> ComposeResult:
|
|
71
77
|
"""Create the monitoring screen layout."""
|
|
72
78
|
# Just show the services content directly (no header, no tabs)
|
|
@@ -98,9 +104,7 @@ class MonitorScreen(Screen):
|
|
|
98
104
|
yield Horizontal(id="services-controls", classes="button-row")
|
|
99
105
|
# Create services table with image + digest info
|
|
100
106
|
self.services_table = DataTable(id="services-table")
|
|
101
|
-
self.services_table.add_columns(
|
|
102
|
-
"Service", "Status", "Health", "Ports", "Image", "Digest"
|
|
103
|
-
)
|
|
107
|
+
self.services_table.add_columns("Service", "Status", "Health", "Ports", "Image", "Digest")
|
|
104
108
|
yield self.services_table
|
|
105
109
|
yield Static(" ")
|
|
106
110
|
|
|
@@ -118,12 +122,8 @@ class MonitorScreen(Screen):
|
|
|
118
122
|
status_text = Text()
|
|
119
123
|
|
|
120
124
|
if not self.container_manager.is_available():
|
|
121
|
-
status_text.append(
|
|
122
|
-
|
|
123
|
-
)
|
|
124
|
-
status_text.append(
|
|
125
|
-
"Please install Docker or Podman to continue.\n", style="dim"
|
|
126
|
-
)
|
|
125
|
+
status_text.append("WARNING: No container runtime available\n", style="bold red")
|
|
126
|
+
status_text.append("Please install Docker or Podman to continue.\n", style="dim")
|
|
127
127
|
return status_text
|
|
128
128
|
|
|
129
129
|
runtime_info = self.container_manager.get_runtime_info()
|
|
@@ -150,7 +150,7 @@ class MonitorScreen(Screen):
|
|
|
150
150
|
"""Initialize the screen when mounted."""
|
|
151
151
|
await self._refresh_services()
|
|
152
152
|
# Set up auto-refresh every 5 seconds
|
|
153
|
-
self.refresh_timer = self.set_interval(5.0, self.
|
|
153
|
+
self.refresh_timer = self.set_interval(5.0, self._auto_refresh_services)
|
|
154
154
|
|
|
155
155
|
self._focus_services_table()
|
|
156
156
|
|
|
@@ -186,9 +186,9 @@ class MonitorScreen(Screen):
|
|
|
186
186
|
images_set.add(img)
|
|
187
187
|
# Ensure compose-declared images are also shown (e.g., langflow when stopped)
|
|
188
188
|
try:
|
|
189
|
-
for
|
|
190
|
-
|
|
191
|
-
): # best-effort, no YAML dep
|
|
189
|
+
for (
|
|
190
|
+
img
|
|
191
|
+
) in await self.container_manager._parse_compose_images(): # best-effort, no YAML dep
|
|
192
192
|
if img:
|
|
193
193
|
images_set.add(img)
|
|
194
194
|
except Exception:
|
|
@@ -205,7 +205,7 @@ class MonitorScreen(Screen):
|
|
|
205
205
|
self.images_table.clear()
|
|
206
206
|
|
|
207
207
|
# Add container service rows
|
|
208
|
-
for
|
|
208
|
+
for _service_name, service_info in services.items():
|
|
209
209
|
status_style = self._get_status_style(service_info.status)
|
|
210
210
|
|
|
211
211
|
self.services_table.add_row(
|
|
@@ -238,9 +238,7 @@ class MonitorScreen(Screen):
|
|
|
238
238
|
if (docling_running or docling_starting)
|
|
239
239
|
else "N/A"
|
|
240
240
|
)
|
|
241
|
-
docling_pid = (
|
|
242
|
-
str(docling_status.get("pid")) if docling_status.get("pid") else "N/A"
|
|
243
|
-
)
|
|
241
|
+
docling_pid = str(docling_status.get("pid")) if docling_status.get("pid") else "N/A"
|
|
244
242
|
|
|
245
243
|
if self.docling_table:
|
|
246
244
|
self.docling_table.add_row(
|
|
@@ -275,7 +273,7 @@ class MonitorScreen(Screen):
|
|
|
275
273
|
}
|
|
276
274
|
return status_styles.get(status, "white")
|
|
277
275
|
|
|
278
|
-
async def
|
|
276
|
+
async def _auto_refresh_services(self) -> None:
|
|
279
277
|
"""Auto-refresh services if not in operation."""
|
|
280
278
|
if not self.operation_in_progress:
|
|
281
279
|
await self._refresh_services()
|
|
@@ -283,7 +281,6 @@ class MonitorScreen(Screen):
|
|
|
283
281
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
284
282
|
"""Handle button presses."""
|
|
285
283
|
button_id = event.button.id or ""
|
|
286
|
-
button_label = event.button.label or ""
|
|
287
284
|
|
|
288
285
|
# Use button ID prefixes to determine action, ignoring any random suffix
|
|
289
286
|
if button_id.startswith("start-btn"):
|
|
@@ -328,7 +325,7 @@ class MonitorScreen(Screen):
|
|
|
328
325
|
self.run_worker(self._show_logs(service_name))
|
|
329
326
|
self._start_follow(service_name)
|
|
330
327
|
|
|
331
|
-
async def _start_services(self, cpu_mode:
|
|
328
|
+
async def _start_services(self, cpu_mode: bool | None = None) -> None:
|
|
332
329
|
"""Start services with progress updates."""
|
|
333
330
|
self.operation_in_progress = True
|
|
334
331
|
try:
|
|
@@ -365,6 +362,7 @@ class MonitorScreen(Screen):
|
|
|
365
362
|
# This ensures docker compose reads the correct version
|
|
366
363
|
try:
|
|
367
364
|
from ..managers.env_manager import EnvManager
|
|
365
|
+
|
|
368
366
|
env_manager = EnvManager()
|
|
369
367
|
env_manager.ensure_openrag_version()
|
|
370
368
|
# Small delay to ensure .env file is written and flushed
|
|
@@ -433,7 +431,7 @@ class MonitorScreen(Screen):
|
|
|
433
431
|
# Show "this is the latest version" toast
|
|
434
432
|
self.notify(
|
|
435
433
|
f"You are running the latest version ({current_version}).",
|
|
436
|
-
severity="
|
|
434
|
+
severity="information",
|
|
437
435
|
timeout=5,
|
|
438
436
|
)
|
|
439
437
|
else:
|
|
@@ -442,9 +440,7 @@ class MonitorScreen(Screen):
|
|
|
442
440
|
UpgradeInstructionsModal(current_version, latest_version)
|
|
443
441
|
)
|
|
444
442
|
except Exception as e:
|
|
445
|
-
self.notify(
|
|
446
|
-
f"Error checking version: {str(e)}", severity="error", timeout=10
|
|
447
|
-
)
|
|
443
|
+
self.notify(f"Error checking version: {str(e)}", severity="error", timeout=10)
|
|
448
444
|
finally:
|
|
449
445
|
self.operation_in_progress = False
|
|
450
446
|
|
|
@@ -453,9 +449,7 @@ class MonitorScreen(Screen):
|
|
|
453
449
|
self.operation_in_progress = True
|
|
454
450
|
try:
|
|
455
451
|
# Show factory reset warning modal first
|
|
456
|
-
should_continue = await self.app.push_screen_wait(
|
|
457
|
-
FactoryResetWarningModal()
|
|
458
|
-
)
|
|
452
|
+
should_continue = await self.app.push_screen_wait(FactoryResetWarningModal())
|
|
459
453
|
if not should_continue:
|
|
460
454
|
self.notify("Factory reset cancelled", severity="information")
|
|
461
455
|
return
|
|
@@ -475,6 +469,7 @@ class MonitorScreen(Screen):
|
|
|
475
469
|
try:
|
|
476
470
|
# Get paths from env config
|
|
477
471
|
from ..managers.env_manager import EnvManager
|
|
472
|
+
|
|
478
473
|
env_manager = EnvManager()
|
|
479
474
|
env_manager.load_existing_env()
|
|
480
475
|
|
|
@@ -487,7 +482,9 @@ class MonitorScreen(Screen):
|
|
|
487
482
|
|
|
488
483
|
if config_path.exists():
|
|
489
484
|
# Use container to handle files owned by container user
|
|
490
|
-
success, msg = await self.container_manager.clear_directory_with_container(
|
|
485
|
+
success, msg = await self.container_manager.clear_directory_with_container(
|
|
486
|
+
config_path
|
|
487
|
+
)
|
|
491
488
|
if not success:
|
|
492
489
|
# Fallback to regular rmtree if container method fails
|
|
493
490
|
shutil.rmtree(config_path)
|
|
@@ -497,18 +494,32 @@ class MonitorScreen(Screen):
|
|
|
497
494
|
# Also delete legacy TUI config folder if it exists (~/.openrag/tui/config/)
|
|
498
495
|
tui_config_path = expand_path(env_manager.config.openrag_tui_config_path_legacy)
|
|
499
496
|
if tui_config_path.exists():
|
|
500
|
-
success, msg = await self.container_manager.clear_directory_with_container(
|
|
497
|
+
success, msg = await self.container_manager.clear_directory_with_container(
|
|
498
|
+
tui_config_path
|
|
499
|
+
)
|
|
501
500
|
if not success:
|
|
502
501
|
# Fallback to regular rmtree if container method fails
|
|
503
502
|
shutil.rmtree(tui_config_path)
|
|
504
503
|
# Recreate empty config directory
|
|
505
504
|
tui_config_path.mkdir(parents=True, exist_ok=True)
|
|
506
505
|
|
|
506
|
+
# Clear backend data directory (database, session ownership, conversations, oauth tokens)
|
|
507
|
+
data_path = expand_path(env_manager.config.openrag_data_path)
|
|
508
|
+
if data_path.exists():
|
|
509
|
+
success, msg = await self.container_manager.clear_directory_with_container(
|
|
510
|
+
data_path
|
|
511
|
+
)
|
|
512
|
+
if not success:
|
|
513
|
+
shutil.rmtree(data_path)
|
|
514
|
+
data_path.mkdir(parents=True, exist_ok=True)
|
|
515
|
+
|
|
507
516
|
# Delete flow backups only if user chose to (and they actually exist)
|
|
508
517
|
if self._check_flow_backups():
|
|
509
518
|
if delete_backups:
|
|
510
519
|
# Use container to handle files owned by container user
|
|
511
|
-
success, msg = await self.container_manager.clear_directory_with_container(
|
|
520
|
+
success, msg = await self.container_manager.clear_directory_with_container(
|
|
521
|
+
flows_backup_path
|
|
522
|
+
)
|
|
512
523
|
if not success:
|
|
513
524
|
# Fallback to regular rmtree if container method fails
|
|
514
525
|
shutil.rmtree(flows_backup_path)
|
|
@@ -516,8 +527,10 @@ class MonitorScreen(Screen):
|
|
|
516
527
|
flows_backup_path.mkdir(parents=True, exist_ok=True)
|
|
517
528
|
self.notify("Flow backups deleted", severity="information")
|
|
518
529
|
else:
|
|
519
|
-
self.notify(
|
|
520
|
-
|
|
530
|
+
self.notify(
|
|
531
|
+
f"Flow backups preserved in {flows_backup_path}", severity="information"
|
|
532
|
+
)
|
|
533
|
+
|
|
521
534
|
except Exception as e:
|
|
522
535
|
self.notify(
|
|
523
536
|
f"Error clearing config: {str(e)}",
|
|
@@ -548,12 +561,14 @@ class MonitorScreen(Screen):
|
|
|
548
561
|
|
|
549
562
|
# Get data paths from env config
|
|
550
563
|
from ..managers.env_manager import EnvManager
|
|
564
|
+
|
|
551
565
|
env_manager = EnvManager()
|
|
552
566
|
env_manager.load_existing_env()
|
|
553
567
|
|
|
554
568
|
# Delete langflow-data directory (mirrors Makefile factory-reset behaviour)
|
|
555
569
|
yield False, "Clearing Langflow data..."
|
|
556
570
|
from tui.main import _resolve_langflow_data_path
|
|
571
|
+
|
|
557
572
|
langflow_data_path = _resolve_langflow_data_path(Path.home() / ".openrag").resolve()
|
|
558
573
|
home = Path.home().resolve()
|
|
559
574
|
if not str(langflow_data_path).startswith(str(home) + "/"):
|
|
@@ -575,13 +590,13 @@ class MonitorScreen(Screen):
|
|
|
575
590
|
try:
|
|
576
591
|
# Show prune options modal
|
|
577
592
|
from tui.widgets.prune_options_modal import PruneOptionsModal
|
|
578
|
-
|
|
593
|
+
|
|
579
594
|
prune_choice = await self.app.push_screen_wait(PruneOptionsModal())
|
|
580
|
-
|
|
595
|
+
|
|
581
596
|
if prune_choice == "cancel":
|
|
582
597
|
self.notify("Prune cancelled", severity="information")
|
|
583
598
|
return
|
|
584
|
-
|
|
599
|
+
|
|
585
600
|
# Choose the appropriate pruning method based on user choice
|
|
586
601
|
if prune_choice == "all":
|
|
587
602
|
# Stop services and prune all images
|
|
@@ -591,7 +606,7 @@ class MonitorScreen(Screen):
|
|
|
591
606
|
# Prune only unused images (default)
|
|
592
607
|
command_generator = self.container_manager.prune_old_images()
|
|
593
608
|
modal_title = "Pruning Unused Images"
|
|
594
|
-
|
|
609
|
+
|
|
595
610
|
# Show command output in modal dialog
|
|
596
611
|
modal = CommandOutputModal(
|
|
597
612
|
modal_title,
|
|
@@ -605,12 +620,15 @@ class MonitorScreen(Screen):
|
|
|
605
620
|
def _check_flow_backups(self) -> bool:
|
|
606
621
|
"""Check if there are any flow backups in flows/backup directory."""
|
|
607
622
|
from pathlib import Path
|
|
623
|
+
|
|
608
624
|
from ..managers.env_manager import EnvManager
|
|
609
625
|
|
|
610
626
|
# Get flows path from env config
|
|
611
627
|
env_manager = EnvManager()
|
|
612
628
|
env_manager.load_existing_env()
|
|
613
|
-
flows_path = Path(
|
|
629
|
+
flows_path = Path(
|
|
630
|
+
env_manager.config.openrag_flows_path.replace("$HOME", str(Path.home()))
|
|
631
|
+
).expanduser()
|
|
614
632
|
backup_dir = flows_path / "backup"
|
|
615
633
|
if not backup_dir.exists():
|
|
616
634
|
return False
|
|
@@ -651,9 +669,7 @@ class MonitorScreen(Screen):
|
|
|
651
669
|
if success:
|
|
652
670
|
self.notify(message, severity="information")
|
|
653
671
|
else:
|
|
654
|
-
self.notify(
|
|
655
|
-
f"Failed to start docling serve: {message}", severity="error"
|
|
656
|
-
)
|
|
672
|
+
self.notify(f"Failed to start docling serve: {message}", severity="error")
|
|
657
673
|
# Refresh again to show final status (running or stopped)
|
|
658
674
|
await self._refresh_services()
|
|
659
675
|
except Exception as e:
|
|
@@ -671,9 +687,7 @@ class MonitorScreen(Screen):
|
|
|
671
687
|
if success:
|
|
672
688
|
self.notify(message, severity="information")
|
|
673
689
|
else:
|
|
674
|
-
self.notify(
|
|
675
|
-
f"Failed to stop docling serve: {message}", severity="error"
|
|
676
|
-
)
|
|
690
|
+
self.notify(f"Failed to stop docling serve: {message}", severity="error")
|
|
677
691
|
# Refresh the services table to show updated status
|
|
678
692
|
await self._refresh_services()
|
|
679
693
|
except Exception as e:
|
|
@@ -689,9 +703,7 @@ class MonitorScreen(Screen):
|
|
|
689
703
|
if success:
|
|
690
704
|
self.notify(message, severity="information")
|
|
691
705
|
else:
|
|
692
|
-
self.notify(
|
|
693
|
-
f"Failed to restart docling serve: {message}", severity="error"
|
|
694
|
-
)
|
|
706
|
+
self.notify(f"Failed to restart docling serve: {message}", severity="error")
|
|
695
707
|
# Refresh the services table to show updated status
|
|
696
708
|
await self._refresh_services()
|
|
697
709
|
except Exception as e:
|
|
@@ -780,9 +792,7 @@ class MonitorScreen(Screen):
|
|
|
780
792
|
except Exception:
|
|
781
793
|
pass
|
|
782
794
|
except Exception as e:
|
|
783
|
-
notify_with_diagnostics(
|
|
784
|
-
self.app, f"Error following logs: {e}", severity="error"
|
|
785
|
-
)
|
|
795
|
+
notify_with_diagnostics(self.app, f"Error following logs: {e}", severity="error")
|
|
786
796
|
|
|
787
797
|
def action_refresh(self) -> None:
|
|
788
798
|
"""Refresh services manually."""
|
|
@@ -861,9 +871,7 @@ class MonitorScreen(Screen):
|
|
|
861
871
|
self._update_mode_row()
|
|
862
872
|
self.action_refresh()
|
|
863
873
|
except Exception as e:
|
|
864
|
-
notify_with_diagnostics(
|
|
865
|
-
self.app, f"Failed to toggle mode: {e}", severity="error"
|
|
866
|
-
)
|
|
874
|
+
notify_with_diagnostics(self.app, f"Failed to toggle mode: {e}", severity="error")
|
|
867
875
|
|
|
868
876
|
def _update_controls(self, services: list[ServiceInfo]) -> None:
|
|
869
877
|
"""Update control buttons based on running state."""
|
|
@@ -886,31 +894,19 @@ class MonitorScreen(Screen):
|
|
|
886
894
|
# Add appropriate buttons based on service state
|
|
887
895
|
if any_running:
|
|
888
896
|
# When services are running, show stop and restart
|
|
889
|
-
controls.mount(
|
|
890
|
-
|
|
891
|
-
)
|
|
892
|
-
controls.mount(
|
|
893
|
-
Button("Restart", variant="primary", id=f"restart-btn{suffix}")
|
|
894
|
-
)
|
|
897
|
+
controls.mount(Button("Stop Services", variant="error", id=f"stop-btn{suffix}"))
|
|
898
|
+
controls.mount(Button("Restart", variant="primary", id=f"restart-btn{suffix}"))
|
|
895
899
|
else:
|
|
896
900
|
# When services are not running, show start
|
|
897
|
-
controls.mount(
|
|
898
|
-
Button("Start Services", variant="success", id=f"start-btn{suffix}")
|
|
899
|
-
)
|
|
901
|
+
controls.mount(Button("Start Services", variant="success", id=f"start-btn{suffix}"))
|
|
900
902
|
|
|
901
903
|
# Always show upgrade, prune, and reset buttons
|
|
902
|
-
controls.mount(
|
|
903
|
-
|
|
904
|
-
)
|
|
905
|
-
controls.mount(
|
|
906
|
-
Button("Prune Images", variant="default", id=f"prune-btn{suffix}")
|
|
907
|
-
)
|
|
904
|
+
controls.mount(Button("Upgrade", variant="warning", id=f"upgrade-btn{suffix}"))
|
|
905
|
+
controls.mount(Button("Prune Images", variant="default", id=f"prune-btn{suffix}"))
|
|
908
906
|
controls.mount(Button("Factory Reset", variant="error", id=f"reset-btn{suffix}"))
|
|
909
907
|
|
|
910
908
|
except Exception as e:
|
|
911
|
-
notify_with_diagnostics(
|
|
912
|
-
self.app, f"Error updating controls: {e}", severity="error"
|
|
913
|
-
)
|
|
909
|
+
notify_with_diagnostics(self.app, f"Error updating controls: {e}", severity="error")
|
|
914
910
|
|
|
915
911
|
# Update docling controls separately
|
|
916
912
|
self._update_docling_controls()
|
|
@@ -940,9 +936,7 @@ class MonitorScreen(Screen):
|
|
|
940
936
|
Button("Stop", variant="error", id=f"docling-stop-btn{suffix}")
|
|
941
937
|
)
|
|
942
938
|
docling_controls.mount(
|
|
943
|
-
Button(
|
|
944
|
-
"Restart", variant="primary", id=f"docling-restart-btn{suffix}"
|
|
945
|
-
)
|
|
939
|
+
Button("Restart", variant="primary", id=f"docling-restart-btn{suffix}")
|
|
946
940
|
)
|
|
947
941
|
elif docling_starting:
|
|
948
942
|
# Show disabled button or no button when starting
|
|
@@ -1102,7 +1096,7 @@ class MonitorScreen(Screen):
|
|
|
1102
1096
|
return
|
|
1103
1097
|
|
|
1104
1098
|
try:
|
|
1105
|
-
table.cursor_coordinate = (row, 0)
|
|
1099
|
+
table.cursor_coordinate = Coordinate(row, 0)
|
|
1106
1100
|
except Exception:
|
|
1107
1101
|
move_cursor = getattr(table, "move_cursor", None)
|
|
1108
1102
|
if callable(move_cursor):
|
|
@@ -1111,9 +1105,7 @@ class MonitorScreen(Screen):
|
|
|
1111
1105
|
except Exception:
|
|
1112
1106
|
pass
|
|
1113
1107
|
|
|
1114
|
-
def _focus_services_table(
|
|
1115
|
-
self, row: str | None = None, set_last: bool = True
|
|
1116
|
-
) -> None:
|
|
1108
|
+
def _focus_services_table(self, row: str | None = None, set_last: bool = True) -> None:
|
|
1117
1109
|
"""Focus the services table and update selection."""
|
|
1118
1110
|
if not self.services_table:
|
|
1119
1111
|
return
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|