OpenRAG 0.6.0.dev17__py3-none-any.whl → 0.6.0.dev19__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/flows.py +76 -16
- app/routes/internal.py +20 -0
- {openrag-0.6.0.dev17.dist-info → openrag-0.6.0.dev19.dist-info}/METADATA +1 -1
- {openrag-0.6.0.dev17.dist-info → openrag-0.6.0.dev19.dist-info}/RECORD +10 -10
- services/flows_service.py +429 -239
- services/startup_orchestrator.py +3 -39
- {openrag-0.6.0.dev17.dist-info → openrag-0.6.0.dev19.dist-info}/WHEEL +0 -0
- {openrag-0.6.0.dev17.dist-info → openrag-0.6.0.dev19.dist-info}/entry_points.txt +0 -0
- {openrag-0.6.0.dev17.dist-info → openrag-0.6.0.dev19.dist-info}/licenses/LICENSE +0 -0
- {openrag-0.6.0.dev17.dist-info → openrag-0.6.0.dev19.dist-info}/top_level.txt +0 -0
api/flows.py
CHANGED
|
@@ -1,18 +1,26 @@
|
|
|
1
1
|
"""Reset Flow API endpoints"""
|
|
2
|
+
|
|
2
3
|
from typing import Literal
|
|
3
4
|
|
|
4
5
|
from fastapi import Depends
|
|
5
6
|
from fastapi.responses import JSONResponse
|
|
6
|
-
from
|
|
7
|
+
from pydantic import BaseModel
|
|
7
8
|
|
|
8
|
-
from dependencies import get_flows_service,
|
|
9
|
+
from dependencies import get_flows_service, require_permission
|
|
9
10
|
from session_manager import User
|
|
11
|
+
from utils.logging_config import get_logger
|
|
10
12
|
|
|
11
13
|
logger = get_logger(__name__)
|
|
12
14
|
|
|
13
15
|
FlowType = Literal["nudges", "retrieval", "ingest"]
|
|
14
16
|
|
|
15
17
|
|
|
18
|
+
def _is_oss_mode() -> bool:
|
|
19
|
+
from config.settings import IBM_AUTH_ENABLED, is_dev_ibm_cos_enabled
|
|
20
|
+
|
|
21
|
+
return not (IBM_AUTH_ENABLED or is_dev_ibm_cos_enabled())
|
|
22
|
+
|
|
23
|
+
|
|
16
24
|
async def reset_flow_endpoint(
|
|
17
25
|
flow_type: str,
|
|
18
26
|
flows_service=Depends(get_flows_service),
|
|
@@ -23,27 +31,19 @@ async def reset_flow_endpoint(
|
|
|
23
31
|
return JSONResponse(
|
|
24
32
|
{
|
|
25
33
|
"success": False,
|
|
26
|
-
"error": "Invalid flow type. Must be 'nudges', 'retrieval', or 'ingest'"
|
|
34
|
+
"error": "Invalid flow type. Must be 'nudges', 'retrieval', or 'ingest'",
|
|
27
35
|
},
|
|
28
|
-
status_code=400
|
|
36
|
+
status_code=400,
|
|
29
37
|
)
|
|
30
38
|
|
|
31
39
|
try:
|
|
32
40
|
result = await flows_service.reset_langflow_flow(flow_type)
|
|
33
41
|
|
|
34
42
|
if result.get("success"):
|
|
35
|
-
logger.info(
|
|
36
|
-
"Flow reset successful",
|
|
37
|
-
flow_type=flow_type,
|
|
38
|
-
flow_id=result.get("flow_id")
|
|
39
|
-
)
|
|
43
|
+
logger.info("Flow reset successful", flow_type=flow_type, flow_id=result.get("flow_id"))
|
|
40
44
|
return JSONResponse(result, status_code=200)
|
|
41
45
|
else:
|
|
42
|
-
logger.error(
|
|
43
|
-
"Flow reset failed",
|
|
44
|
-
flow_type=flow_type,
|
|
45
|
-
error=result.get("error")
|
|
46
|
-
)
|
|
46
|
+
logger.error("Flow reset failed", flow_type=flow_type, error=result.get("error"))
|
|
47
47
|
return JSONResponse(result, status_code=500)
|
|
48
48
|
|
|
49
49
|
except ValueError as e:
|
|
@@ -51,7 +51,67 @@ async def reset_flow_endpoint(
|
|
|
51
51
|
return JSONResponse({"success": False, "error": str(e)}, status_code=400)
|
|
52
52
|
except Exception as e:
|
|
53
53
|
logger.error("Unexpected error in flow reset", error=str(e))
|
|
54
|
+
return JSONResponse({"success": False, "error": "Internal server error"}, status_code=500)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class BulkUpdateFlowsRequest(BaseModel):
|
|
58
|
+
flow_types: list[str]
|
|
59
|
+
backup_custom: bool = True
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
async def get_flows_updates_endpoint(
|
|
63
|
+
flows_service=Depends(get_flows_service),
|
|
64
|
+
user: User = Depends(require_permission("flows:read")),
|
|
65
|
+
):
|
|
66
|
+
"""Get available updates for core flows"""
|
|
67
|
+
if not _is_oss_mode():
|
|
68
|
+
return JSONResponse({"success": True, "updates": []}, status_code=200)
|
|
69
|
+
try:
|
|
70
|
+
user_id = getattr(user, "db_user_id", None) or getattr(user, "user_id", None)
|
|
71
|
+
updates = await flows_service.get_flows_updates_available(user_id=user_id)
|
|
72
|
+
return JSONResponse({"success": True, "updates": updates}, status_code=200)
|
|
73
|
+
except Exception as e:
|
|
74
|
+
logger.error("Error getting flow updates", error=str(e))
|
|
75
|
+
return JSONResponse({"success": False, "error": "Internal server error"}, status_code=500)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
async def bulk_update_flows_endpoint(
|
|
79
|
+
request: BulkUpdateFlowsRequest,
|
|
80
|
+
flows_service=Depends(get_flows_service),
|
|
81
|
+
user: User = Depends(require_permission("flows:edit")),
|
|
82
|
+
):
|
|
83
|
+
"""Bulk update multiple flows and optionally backup custom flows"""
|
|
84
|
+
if not _is_oss_mode():
|
|
54
85
|
return JSONResponse(
|
|
55
|
-
{"success": False, "error":
|
|
56
|
-
status_code=
|
|
86
|
+
{"success": False, "error": "Flow updates are only available in OSS mode"},
|
|
87
|
+
status_code=403,
|
|
57
88
|
)
|
|
89
|
+
try:
|
|
90
|
+
results = await flows_service.bulk_update_flows(request.flow_types, request.backup_custom)
|
|
91
|
+
overall_success = all(r.get("success", False) for r in results)
|
|
92
|
+
return JSONResponse({"success": overall_success, "results": results}, status_code=200)
|
|
93
|
+
except Exception as e:
|
|
94
|
+
logger.error("Error bulk updating flows", error=str(e))
|
|
95
|
+
return JSONResponse({"success": False, "error": "Internal server error"}, status_code=500)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class DismissFlowsUpdateRequest(BaseModel):
|
|
99
|
+
flow_types: list[str] | None = None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
async def dismiss_flows_update_endpoint(
|
|
103
|
+
request: DismissFlowsUpdateRequest | None = None,
|
|
104
|
+
flows_service=Depends(get_flows_service),
|
|
105
|
+
user: User = Depends(require_permission("flows:read")),
|
|
106
|
+
):
|
|
107
|
+
"""Dismiss available flow update notifications ephemerally in backend memory"""
|
|
108
|
+
if not _is_oss_mode():
|
|
109
|
+
return JSONResponse({"success": True}, status_code=200)
|
|
110
|
+
try:
|
|
111
|
+
flow_types = request.flow_types if request else None
|
|
112
|
+
user_id = getattr(user, "db_user_id", None) or getattr(user, "user_id", None)
|
|
113
|
+
flows_service.dismiss_flows_updates(flow_types, user_id=user_id)
|
|
114
|
+
return JSONResponse({"success": True}, status_code=200)
|
|
115
|
+
except Exception as e:
|
|
116
|
+
logger.error("Error dismissing flow updates", error=str(e))
|
|
117
|
+
return JSONResponse({"success": False, "error": "Internal server error"}, status_code=500)
|
app/routes/internal.py
CHANGED
|
@@ -426,6 +426,26 @@ def register_internal_routes(app: FastAPI):
|
|
|
426
426
|
tags=["internal"],
|
|
427
427
|
)
|
|
428
428
|
|
|
429
|
+
# Flow updates endpoints
|
|
430
|
+
app.add_api_route(
|
|
431
|
+
"/settings/flows/updates-available",
|
|
432
|
+
flows.get_flows_updates_endpoint,
|
|
433
|
+
methods=["GET"],
|
|
434
|
+
tags=["internal"],
|
|
435
|
+
)
|
|
436
|
+
app.add_api_route(
|
|
437
|
+
"/settings/flows/update",
|
|
438
|
+
flows.bulk_update_flows_endpoint,
|
|
439
|
+
methods=["POST"],
|
|
440
|
+
tags=["internal"],
|
|
441
|
+
)
|
|
442
|
+
app.add_api_route(
|
|
443
|
+
"/settings/flows/dismiss-update",
|
|
444
|
+
flows.dismiss_flows_update_endpoint,
|
|
445
|
+
methods=["POST"],
|
|
446
|
+
tags=["internal"],
|
|
447
|
+
)
|
|
448
|
+
|
|
429
449
|
# Router upload ingest endpoint
|
|
430
450
|
app.add_api_route(
|
|
431
451
|
"/router/upload_ingest",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openrag
|
|
3
|
-
Version: 0.6.0.
|
|
3
|
+
Version: 0.6.0.dev19
|
|
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
|
|
@@ -13,7 +13,7 @@ api/connectors.py,sha256=XgNnPjBIB-FGlZ0krEPbXen017BFbS-1rDzpbpeMJg4,103927
|
|
|
13
13
|
api/docling.py,sha256=PKdLaunKpSB3PaIheogbbRSTPdpGQDV7jc1HuD3d2J8,2430
|
|
14
14
|
api/documents.py,sha256=R3IfnpGzaE73MJxE5kREJRCX6JmTEyFQGNjF3ib5fWk,11841
|
|
15
15
|
api/files.py,sha256=TMxR-NG-e9BLaZccN_bE3uP-CEbdZWThPfKTHvgG6E4,3626
|
|
16
|
-
api/flows.py,sha256=
|
|
16
|
+
api/flows.py,sha256=9dEUSEdoxo4tBBGG0IJVqFDj1SbO1pCrv5nCcliE70o,4592
|
|
17
17
|
api/health.py,sha256=pAEEVl9uVG4zcoR0ZqL_zPfDLz4ficHtALcpzOXjgDI,3102
|
|
18
18
|
api/ingest_preview.py,sha256=mffMLdBuzc--JT8gHvxfRDQcWoWRK5vVQg8uljLNn6o,2431
|
|
19
19
|
api/keys.py,sha256=cPocE-ocfbtSWf3YEvMTHZgrxUcLJIm_7Pex6UVggwY,3477
|
|
@@ -51,7 +51,7 @@ app/lifespan.py,sha256=3KJ5pSAg_rILTJrzojFVS9ULJE0D0SZL74-S7fA4UVc,18556
|
|
|
51
51
|
app/middleware.py,sha256=iMl8TP7XzS8UWB1ZKnjPhAPlbQHWTUXZ2e-Y_evSUNI,2334
|
|
52
52
|
app/router_app.py,sha256=pOxCVnsakUgBIxs42UeE94EJ5peCNRYTP7jk6Eo7on0,5873
|
|
53
53
|
app/routes/__init__.py,sha256=QESovwz429ht1sXMHL1ciQKZOgxCcvuYgLMjoDtxyDA,698
|
|
54
|
-
app/routes/internal.py,sha256=
|
|
54
|
+
app/routes/internal.py,sha256=DjDdYaDs1cPjwGsyUWxRvznMnLrqxmmJznupceJ714U,14619
|
|
55
55
|
app/routes/mcp.py,sha256=JF_JEy6ATnraACeQFk4KDIuaT9LkVSZqzvabHkL51xQ,1159
|
|
56
56
|
app/routes/public_v1.py,sha256=qEzuSIPwXR5FrqaZ0lqOjYqDxH2knqsJyW6L7t0mZak,3406
|
|
57
57
|
auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -128,7 +128,7 @@ models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
|
128
128
|
models/processors.py,sha256=F0LOVNOOrDS9zCfb-piYf3uRYawiPbmNGO2Da4g6G4I,70039
|
|
129
129
|
models/tasks.py,sha256=BByR2xDl4zqvbCbTGgBC3aChJ_uJoRpHy0TTJrvW5Dc,3152
|
|
130
130
|
models/url.py,sha256=KMleXhlwd3W59axcubMJxIEtHpyBQTvfxkycc-o0kOY,5601
|
|
131
|
-
openrag-0.6.0.
|
|
131
|
+
openrag-0.6.0.dev19.dist-info/licenses/LICENSE,sha256=jWcnPXkUxEinX60Yn-_zjsY9i84vz2QSb0fi53djOkg,11334
|
|
132
132
|
services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
133
133
|
services/api_key_service.py,sha256=m04jVSJyDqIl-NgNROKLIfy4QYArZxlvz_WkjqQK7TA,13269
|
|
134
134
|
services/auth_service.py,sha256=qxg5Mj1KvR23dBaZnwe2gjQia5DqvNNi7VQCkkKxq8k,23794
|
|
@@ -145,7 +145,7 @@ services/docling_service.py,sha256=C9nXzg4eRsmnjS0kv3SH1MliNqL87cPr6cgbhG8hjKs,1
|
|
|
145
145
|
services/document_index_writer.py,sha256=204buyb7bHBKciuci3MGF-Q8_bI8SiLMOfuje9BnsE0,12270
|
|
146
146
|
services/document_service.py,sha256=ObGyfDgCdQsJkFiFUoN34yqkjCJyy-6aSvxqAqjfXEw,7604
|
|
147
147
|
services/file_service.py,sha256=aBNwMUTsZqRAsg31IBRlbDMHNZkP9LsUJM6_IEgesVM,8701
|
|
148
|
-
services/flows_service.py,sha256=
|
|
148
|
+
services/flows_service.py,sha256=vdr-chLE7VkhxqqIWBzXb7vTA3SaJL4Qen0gfVaiS30,68434
|
|
149
149
|
services/group_acl_service.py,sha256=_Pt1vf8SAgxKuaqnnu4pJ_G2MzyV8czmNBXflkwIg6A,4170
|
|
150
150
|
services/ingest_preview_service.py,sha256=BfH054zIQ_osYnr8rSLs_CW-zDFDsYLuXXK26jIlwi0,6238
|
|
151
151
|
services/knowledge_filter_service.py,sha256=4SXHaFfMJzAZGMjAU8qpdh4H_X3cEnyJp0ijyL9WAiE,15495
|
|
@@ -159,7 +159,7 @@ services/onboarding_reset.py,sha256=LiIsmK5mnj4SbGAtma8Hsc6U-I3WjWdsoDeK_cPOtIo,
|
|
|
159
159
|
services/rbac_service.py,sha256=6lfthiLIRQEk-ahDeiDg_eNDbDyrXNHShfb4qxfirSM,4962
|
|
160
160
|
services/search_service.py,sha256=Qu9cAZTYChuy-L47N1z04HHGI43VIJFqemUdcAC-ZMg,30354
|
|
161
161
|
services/session_ownership_service.py,sha256=RHM2N7n9bETNqOpqXrgEK3F54XHJn2AkoMcXJoBzrEM,9868
|
|
162
|
-
services/startup_orchestrator.py,sha256=
|
|
162
|
+
services/startup_orchestrator.py,sha256=iLkoPHXyOIB4mFzTapF6viMntSSb-PEJkxRXhk6VY-0,7385
|
|
163
163
|
services/task_service.py,sha256=wnvmd9MEYl_BDWUqmEtjmN08Iik1ANaZrJYyp71lqNA,64551
|
|
164
164
|
services/user_service.py,sha256=KsqQN0qPSwxQsBUBkIxE60TDJNveaDk-5rhQE9bN2Fs,11993
|
|
165
165
|
services/workspace_config_service.py,sha256=Ij-fvy0zd3OSsML4ZwG4WG-QmOuxLWcCf9f3HvtD6iY,16544
|
|
@@ -240,8 +240,8 @@ utils/telemetry/__init__.py,sha256=xoStY8hoYWnVe0B_jNthSdzRb7AxCT0rhM8ba9Q0Obg,2
|
|
|
240
240
|
utils/telemetry/category.py,sha256=wei3V5WyUaiadYjEE5s0ys9PY0TjnebIvTykQU0P_2k,1114
|
|
241
241
|
utils/telemetry/client.py,sha256=RhjW1a27w7W6gHt9X7AxdRPdZGmCEQDPskkxj5nsuiQ,12500
|
|
242
242
|
utils/telemetry/message_id.py,sha256=05CvlKW_LaD00INxd3WV8uXBZ9f_5Cvy-jAyOPmpxTk,9600
|
|
243
|
-
openrag-0.6.0.
|
|
244
|
-
openrag-0.6.0.
|
|
245
|
-
openrag-0.6.0.
|
|
246
|
-
openrag-0.6.0.
|
|
247
|
-
openrag-0.6.0.
|
|
243
|
+
openrag-0.6.0.dev19.dist-info/METADATA,sha256=CF2Z-QYMINwAljaYaWpYsUO56NM-ILJqbMwPuzVT_YA,8728
|
|
244
|
+
openrag-0.6.0.dev19.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
245
|
+
openrag-0.6.0.dev19.dist-info/entry_points.txt,sha256=ArKNHB63I3K8vynNkogvZOLmmQFpIsHDL7MvreCmju4,45
|
|
246
|
+
openrag-0.6.0.dev19.dist-info/top_level.txt,sha256=TpFyXTBO-cpphRZHx7Vgwgzg4KmPXwNkkfOo93D5qOk,132
|
|
247
|
+
openrag-0.6.0.dev19.dist-info/RECORD,,
|
services/flows_service.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import json
|
|
3
3
|
import os
|
|
4
|
-
from datetime import datetime
|
|
4
|
+
from datetime import UTC, datetime
|
|
5
5
|
from typing import Any
|
|
6
6
|
|
|
7
7
|
from cachetools import LRUCache
|
|
@@ -25,6 +25,48 @@ logger = get_logger(__name__)
|
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
class FlowsService:
|
|
28
|
+
def __init__(self) -> None:
|
|
29
|
+
# Ephemeral dict of dismissed flow update notifications per user_id
|
|
30
|
+
self._dismissed_updates: dict[str, set[str]] = {}
|
|
31
|
+
# Cache for flow file mappings to avoid repeated filesystem scans
|
|
32
|
+
self._flow_file_cache: dict[str, str] = {}
|
|
33
|
+
# Semaphores to prevent race conditions when updating the same flow concurrently
|
|
34
|
+
self._flow_locks = LRUCache(maxsize=128)
|
|
35
|
+
self._lock_creation_lock = asyncio.Lock()
|
|
36
|
+
# Cache for enabled models to avoid redundant API calls
|
|
37
|
+
self._enabled_models_cache: dict[str, Any] | None = None
|
|
38
|
+
self._enabled_models_cache_time: datetime | None = None
|
|
39
|
+
self._enabled_models_cache_ttl = 60 # seconds
|
|
40
|
+
self._enabled_models_lock = asyncio.Lock()
|
|
41
|
+
# Cache for available custom flow updates
|
|
42
|
+
self._custom_updates_cache: list[dict[str, Any]] | None = None
|
|
43
|
+
|
|
44
|
+
def dismiss_flows_updates(
|
|
45
|
+
self, flow_types: list[str] | None = None, user_id: str | None = None
|
|
46
|
+
) -> None:
|
|
47
|
+
"""Dismiss flow update notifications ephemerally in backend memory per user."""
|
|
48
|
+
if not hasattr(self, "_dismissed_updates") or not isinstance(self._dismissed_updates, dict):
|
|
49
|
+
self._dismissed_updates = {}
|
|
50
|
+
target_user = user_id or "default"
|
|
51
|
+
if target_user not in self._dismissed_updates:
|
|
52
|
+
self._dismissed_updates[target_user] = set()
|
|
53
|
+
|
|
54
|
+
if flow_types:
|
|
55
|
+
self._dismissed_updates[target_user].update(flow_types)
|
|
56
|
+
else:
|
|
57
|
+
self._dismissed_updates[target_user].update(
|
|
58
|
+
["nudges", "retrieval", "ingest", "url_ingest"]
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
def clear_dismissed_update(self, flow_type: str, user_id: str | None = None):
|
|
62
|
+
if hasattr(self, "_dismissed_updates") and isinstance(self._dismissed_updates, dict):
|
|
63
|
+
if user_id:
|
|
64
|
+
if user_id in self._dismissed_updates:
|
|
65
|
+
self._dismissed_updates[user_id].discard(flow_type)
|
|
66
|
+
else:
|
|
67
|
+
for user_set in self._dismissed_updates.values():
|
|
68
|
+
user_set.discard(flow_type)
|
|
69
|
+
|
|
28
70
|
async def resolve_ollama_url(self, endpoint: str, force_refresh: bool = False) -> str:
|
|
29
71
|
"""Find the correct Ollama URL by probing candidates via Langflow's validate-provider API."""
|
|
30
72
|
from config.config_manager import config_manager
|
|
@@ -87,18 +129,6 @@ class FlowsService:
|
|
|
87
129
|
|
|
88
130
|
return resolved_url
|
|
89
131
|
|
|
90
|
-
def __init__(self):
|
|
91
|
-
# Cache for flow file mappings to avoid repeated filesystem scans
|
|
92
|
-
self._flow_file_cache = {}
|
|
93
|
-
# Semaphores to prevent race conditions when updating the same flow concurrently
|
|
94
|
-
self._flow_locks = LRUCache(maxsize=128)
|
|
95
|
-
self._lock_creation_lock = asyncio.Lock()
|
|
96
|
-
# Cache for enabled models to avoid redundant API calls
|
|
97
|
-
self._enabled_models_cache = None
|
|
98
|
-
self._enabled_models_cache_time = None
|
|
99
|
-
self._enabled_models_cache_ttl = 60 # seconds
|
|
100
|
-
self._enabled_models_lock = asyncio.Lock()
|
|
101
|
-
|
|
102
132
|
async def _get_flow_lock(self, flow_id: str) -> asyncio.Lock:
|
|
103
133
|
"""Get or create an asyncio.Lock for a specific flow ID"""
|
|
104
134
|
async with self._lock_creation_lock:
|
|
@@ -305,8 +335,12 @@ class FlowsService:
|
|
|
305
335
|
for result in results:
|
|
306
336
|
if not result:
|
|
307
337
|
continue
|
|
308
|
-
|
|
309
|
-
if
|
|
338
|
+
res_type = result.get("type")
|
|
339
|
+
if res_type in ("backed_up", "skipped", "failed"):
|
|
340
|
+
target_list = backup_results[res_type]
|
|
341
|
+
if isinstance(target_list, list):
|
|
342
|
+
target_list.append(result.get("data"))
|
|
343
|
+
if res_type == "failed":
|
|
310
344
|
backup_results["success"] = False
|
|
311
345
|
|
|
312
346
|
logger.info(
|
|
@@ -328,7 +362,7 @@ class FlowsService:
|
|
|
328
362
|
|
|
329
363
|
return backup_results
|
|
330
364
|
|
|
331
|
-
async def _backup_flow(self, flow_id: str, flow_type: str, flow_data: dict = None):
|
|
365
|
+
async def _backup_flow(self, flow_id: str, flow_type: str, flow_data: dict | None = None):
|
|
332
366
|
"""
|
|
333
367
|
Backup a single flow to the backup folder.
|
|
334
368
|
|
|
@@ -435,19 +469,7 @@ class FlowsService:
|
|
|
435
469
|
logger.warning(f"Flow with ID {flow_id} not found in flows directory")
|
|
436
470
|
return None
|
|
437
471
|
|
|
438
|
-
|
|
439
|
-
"""Reset a Langflow flow by uploading the corresponding JSON file
|
|
440
|
-
|
|
441
|
-
Args:
|
|
442
|
-
flow_type: Either 'nudges', 'retrieval', or 'ingest'
|
|
443
|
-
|
|
444
|
-
Returns:
|
|
445
|
-
dict: Success/error response
|
|
446
|
-
"""
|
|
447
|
-
if not LANGFLOW_URL:
|
|
448
|
-
raise ValueError("LANGFLOW_URL environment variable is required")
|
|
449
|
-
|
|
450
|
-
# Determine flow ID based on type
|
|
472
|
+
def _get_flow_id_by_type(self, flow_type: str) -> str:
|
|
451
473
|
if flow_type == "nudges":
|
|
452
474
|
flow_id = NUDGES_FLOW_ID
|
|
453
475
|
elif flow_type == "retrieval":
|
|
@@ -460,9 +482,64 @@ class FlowsService:
|
|
|
460
482
|
raise ValueError(
|
|
461
483
|
"flow_type must be either 'nudges', 'retrieval', 'ingest', or 'url_ingest'"
|
|
462
484
|
)
|
|
463
|
-
|
|
464
485
|
if not flow_id:
|
|
465
486
|
raise ValueError(f"Flow ID not configured for flow_type '{flow_type}'")
|
|
487
|
+
return flow_id
|
|
488
|
+
|
|
489
|
+
async def reset_langflow_flow(self, flow_type: str):
|
|
490
|
+
"""Reset a Langflow flow by uploading the corresponding JSON file
|
|
491
|
+
|
|
492
|
+
Args:
|
|
493
|
+
flow_type: Either 'nudges', 'retrieval', 'ingest', or 'url_ingest'
|
|
494
|
+
|
|
495
|
+
Returns:
|
|
496
|
+
dict: Success/error response
|
|
497
|
+
"""
|
|
498
|
+
flow_id = self._get_flow_id_by_type(flow_type)
|
|
499
|
+
flow_lock = await self._get_flow_lock(flow_id)
|
|
500
|
+
async with flow_lock:
|
|
501
|
+
result = await self._reset_langflow_flow_locked(flow_type, flow_id)
|
|
502
|
+
if result.get("success"):
|
|
503
|
+
self._custom_updates_cache = None
|
|
504
|
+
try:
|
|
505
|
+
config = get_openrag_config()
|
|
506
|
+
if config.edited:
|
|
507
|
+
from api.settings import reapply_all_settings
|
|
508
|
+
|
|
509
|
+
await reapply_all_settings()
|
|
510
|
+
except Exception as e:
|
|
511
|
+
logger.error(f"Error reapplying settings after flow reset: {e}")
|
|
512
|
+
return result
|
|
513
|
+
|
|
514
|
+
async def _unlock_flow(self, flow_id: str):
|
|
515
|
+
"""Unlock a flow before making changes."""
|
|
516
|
+
try:
|
|
517
|
+
response = await clients.langflow_request(
|
|
518
|
+
"PATCH", f"/api/v1/flows/{flow_id}", json={"locked": False}
|
|
519
|
+
)
|
|
520
|
+
if response.status_code not in (200, 404):
|
|
521
|
+
logger.warning(
|
|
522
|
+
f"Failed to unlock flow {flow_id}: HTTP {response.status_code} - {response.text}"
|
|
523
|
+
)
|
|
524
|
+
except Exception as e:
|
|
525
|
+
logger.warning(f"Error unlocking flow {flow_id}: {e}")
|
|
526
|
+
|
|
527
|
+
async def _lock_flow(self, flow_id: str):
|
|
528
|
+
"""Lock a flow after making changes."""
|
|
529
|
+
try:
|
|
530
|
+
response = await clients.langflow_request(
|
|
531
|
+
"PATCH", f"/api/v1/flows/{flow_id}", json={"locked": True}
|
|
532
|
+
)
|
|
533
|
+
if response.status_code not in (200, 404):
|
|
534
|
+
logger.warning(
|
|
535
|
+
f"Failed to lock flow {flow_id}: HTTP {response.status_code} - {response.text}"
|
|
536
|
+
)
|
|
537
|
+
except Exception as e:
|
|
538
|
+
logger.warning(f"Error locking flow {flow_id}: {e}")
|
|
539
|
+
|
|
540
|
+
async def _reset_langflow_flow_locked(self, flow_type: str, flow_id: str):
|
|
541
|
+
if not LANGFLOW_URL:
|
|
542
|
+
raise ValueError("LANGFLOW_URL environment variable is required")
|
|
466
543
|
|
|
467
544
|
# Dynamically find the flow file by ID
|
|
468
545
|
flow_path = self._find_flow_file_by_id(flow_id)
|
|
@@ -481,6 +558,8 @@ class FlowsService:
|
|
|
481
558
|
except FileNotFoundError as e:
|
|
482
559
|
raise ValueError(f"Flow file not found: {flow_path}") from e
|
|
483
560
|
|
|
561
|
+
await self._unlock_flow(flow_id)
|
|
562
|
+
|
|
484
563
|
# Make PATCH request to Langflow API to update the flow using shared client
|
|
485
564
|
try:
|
|
486
565
|
response = await clients.langflow_request(
|
|
@@ -488,73 +567,12 @@ class FlowsService:
|
|
|
488
567
|
)
|
|
489
568
|
|
|
490
569
|
if response.status_code == 200:
|
|
570
|
+
await self._lock_flow(flow_id)
|
|
491
571
|
logger.info(
|
|
492
572
|
f"Successfully reset {flow_type} flow",
|
|
493
573
|
flow_id=flow_id,
|
|
494
574
|
flow_file=os.path.basename(flow_path),
|
|
495
575
|
)
|
|
496
|
-
|
|
497
|
-
# Now update the flow with current configuration settings
|
|
498
|
-
try:
|
|
499
|
-
config = get_openrag_config()
|
|
500
|
-
|
|
501
|
-
# Check if configuration has been edited (onboarding completed)
|
|
502
|
-
if config.edited:
|
|
503
|
-
logger.info(
|
|
504
|
-
f"Updating {flow_type} flow with current configuration settings"
|
|
505
|
-
)
|
|
506
|
-
|
|
507
|
-
# Update current LLM provider
|
|
508
|
-
update_result = await self._update_provider_components(
|
|
509
|
-
{"name": flow_type, "flow_id": flow_id},
|
|
510
|
-
config.agent.llm_provider.lower(),
|
|
511
|
-
llm_model=config.agent.llm_model,
|
|
512
|
-
force_llm_update=True,
|
|
513
|
-
)
|
|
514
|
-
|
|
515
|
-
# Update all configured embedding providers
|
|
516
|
-
embedding_providers = []
|
|
517
|
-
if config.providers.openai.configured:
|
|
518
|
-
embedding_providers.append("openai")
|
|
519
|
-
if config.providers.watsonx.configured:
|
|
520
|
-
embedding_providers.append("watsonx")
|
|
521
|
-
if config.providers.ollama.configured:
|
|
522
|
-
embedding_providers.append("ollama")
|
|
523
|
-
|
|
524
|
-
current_emb_provider = config.knowledge.embedding_provider.lower()
|
|
525
|
-
for provider in embedding_providers:
|
|
526
|
-
model = (
|
|
527
|
-
config.knowledge.embedding_model
|
|
528
|
-
if provider == current_emb_provider
|
|
529
|
-
else None
|
|
530
|
-
)
|
|
531
|
-
await self._update_provider_components(
|
|
532
|
-
{"name": flow_type, "flow_id": flow_id},
|
|
533
|
-
provider,
|
|
534
|
-
embedding_model=model,
|
|
535
|
-
force_embedding_update=True,
|
|
536
|
-
)
|
|
537
|
-
|
|
538
|
-
if update_result.get("success"):
|
|
539
|
-
logger.info(
|
|
540
|
-
f"Successfully updated {flow_type} flow with current configuration"
|
|
541
|
-
)
|
|
542
|
-
else:
|
|
543
|
-
logger.warning(
|
|
544
|
-
f"Failed to update {flow_type} flow with current configuration: {update_result.get('error', 'Unknown error')}"
|
|
545
|
-
)
|
|
546
|
-
else:
|
|
547
|
-
logger.info(
|
|
548
|
-
f"Configuration not yet edited (onboarding not completed), skipping model updates for {flow_type} flow"
|
|
549
|
-
)
|
|
550
|
-
|
|
551
|
-
except Exception as e:
|
|
552
|
-
logger.error(
|
|
553
|
-
f"Error updating {flow_type} flow with current configuration",
|
|
554
|
-
error=str(e),
|
|
555
|
-
)
|
|
556
|
-
# Don't fail the entire reset operation if configuration update fails
|
|
557
|
-
|
|
558
576
|
return {
|
|
559
577
|
"success": True,
|
|
560
578
|
"message": f"Successfully reset {flow_type} flow",
|
|
@@ -626,7 +644,7 @@ class FlowsService:
|
|
|
626
644
|
return "OpenAI"
|
|
627
645
|
|
|
628
646
|
async def _update_flow_field(
|
|
629
|
-
self, flow_id: str, field_name: str, field_value: Any, node_display_name: str = None
|
|
647
|
+
self, flow_id: str, field_name: str, field_value: Any, node_display_name: str | None = None
|
|
630
648
|
):
|
|
631
649
|
"""
|
|
632
650
|
Generic helper function to update any field in any Langflow component.
|
|
@@ -683,6 +701,8 @@ class FlowsService:
|
|
|
683
701
|
identifier = node_display_name
|
|
684
702
|
raise Exception(f"{field_name} field not found in {identifier} component")
|
|
685
703
|
|
|
704
|
+
await self._unlock_flow(flow_id)
|
|
705
|
+
|
|
686
706
|
# Update the flow via PATCH request
|
|
687
707
|
patch_response = await clients.langflow_request(
|
|
688
708
|
"PATCH", f"/api/v1/flows/{flow_id}", json=flow_data
|
|
@@ -692,6 +712,7 @@ class FlowsService:
|
|
|
692
712
|
raise Exception(
|
|
693
713
|
f"Failed to update flow: HTTP {patch_response.status_code} - {patch_response.text}"
|
|
694
714
|
)
|
|
715
|
+
await self._lock_flow(flow_id)
|
|
695
716
|
|
|
696
717
|
async def update_chat_flow_model(self, model_name: str, provider: str):
|
|
697
718
|
"""Helper function to update the model in the chat flow"""
|
|
@@ -774,20 +795,34 @@ class FlowsService:
|
|
|
774
795
|
return True
|
|
775
796
|
return False
|
|
776
797
|
|
|
777
|
-
def _normalize_flow_structure(self, flow_data):
|
|
798
|
+
def _normalize_flow_structure(self, flow_data: dict[str, Any]) -> dict[str, Any]:
|
|
778
799
|
"""
|
|
779
800
|
Normalize flow structure for comparison by removing dynamic fields.
|
|
780
|
-
Keeps structural elements: nodes (types, display names,
|
|
781
|
-
Removes: IDs, timestamps, positions,
|
|
801
|
+
Keeps structural elements: nodes (types, display names, template definitions), edges (connections).
|
|
802
|
+
Removes: IDs, timestamps, positions, dynamic runtime field values, and dynamic options lists.
|
|
782
803
|
"""
|
|
783
|
-
normalized = {"data": {"nodes": [], "edges": []}}
|
|
804
|
+
normalized: dict[str, Any] = {"data": {"nodes": [], "edges": []}}
|
|
784
805
|
|
|
785
|
-
# Normalize nodes - keep structural info including
|
|
806
|
+
# Normalize nodes - keep structural info including template definitions
|
|
786
807
|
nodes = flow_data.get("data", {}).get("nodes", [])
|
|
787
808
|
for node in nodes:
|
|
788
809
|
node_data = node.get("data", {})
|
|
789
810
|
node_template = node_data.get("node", {})
|
|
790
811
|
|
|
812
|
+
template = node_template.get("template", {})
|
|
813
|
+
normalized_template: dict[str, Any] = {}
|
|
814
|
+
if isinstance(template, dict):
|
|
815
|
+
for field_name, field_def in template.items():
|
|
816
|
+
if isinstance(field_def, dict):
|
|
817
|
+
normalized_template[field_name] = {
|
|
818
|
+
"type": field_def.get("type"),
|
|
819
|
+
"display_name": field_def.get("display_name"),
|
|
820
|
+
"required": field_def.get("required"),
|
|
821
|
+
"input_types": field_def.get("input_types"),
|
|
822
|
+
}
|
|
823
|
+
else:
|
|
824
|
+
normalized_template[field_name] = {"value": str(field_def)}
|
|
825
|
+
|
|
791
826
|
normalized_node = {
|
|
792
827
|
"id": node.get("id"), # Keep ID for edge matching
|
|
793
828
|
"type": node.get("type"),
|
|
@@ -796,7 +831,7 @@ class FlowsService:
|
|
|
796
831
|
"display_name": node_template.get("display_name"),
|
|
797
832
|
"name": node_template.get("name"),
|
|
798
833
|
"base_classes": node_template.get("base_classes", []),
|
|
799
|
-
"template":
|
|
834
|
+
"template": normalized_template,
|
|
800
835
|
}
|
|
801
836
|
},
|
|
802
837
|
}
|
|
@@ -815,70 +850,51 @@ class FlowsService:
|
|
|
815
850
|
|
|
816
851
|
return normalized
|
|
817
852
|
|
|
818
|
-
async def
|
|
853
|
+
async def _check_flow_update(
|
|
854
|
+
self, flow_type: str, flow_id: str, flow_data: dict[str, Any] | None = None
|
|
855
|
+
) -> dict[str, Any] | None:
|
|
819
856
|
"""
|
|
820
|
-
|
|
821
|
-
Returns
|
|
857
|
+
Check if a flow has a newer version on disk compared to Langflow.
|
|
858
|
+
Returns flow update info dict if an update is available, None otherwise.
|
|
822
859
|
"""
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
)
|
|
830
|
-
return False
|
|
831
|
-
|
|
832
|
-
langflow_flow = response.json()
|
|
833
|
-
|
|
834
|
-
# Find and load the corresponding JSON file
|
|
835
|
-
flow_path = self._find_flow_file_by_id(flow_id)
|
|
836
|
-
if not flow_path:
|
|
837
|
-
logger.warning(f"Flow file not found for flow ID: {flow_id}")
|
|
838
|
-
return False
|
|
839
|
-
|
|
840
|
-
with open(flow_path) as f:
|
|
841
|
-
file_flow = json.load(f)
|
|
842
|
-
|
|
843
|
-
# Normalize both flows for comparison
|
|
844
|
-
normalized_langflow = self._normalize_flow_structure(langflow_flow)
|
|
845
|
-
normalized_file = self._normalize_flow_structure(file_flow)
|
|
846
|
-
|
|
847
|
-
# Compare entire normalized structures exactly
|
|
848
|
-
# Sort nodes and edges for consistent comparison
|
|
849
|
-
normalized_langflow["data"]["nodes"] = sorted(
|
|
850
|
-
normalized_langflow["data"]["nodes"],
|
|
851
|
-
key=lambda x: (x.get("id", ""), x.get("type", "")),
|
|
852
|
-
)
|
|
853
|
-
normalized_file["data"]["nodes"] = sorted(
|
|
854
|
-
normalized_file["data"]["nodes"], key=lambda x: (x.get("id", ""), x.get("type", ""))
|
|
855
|
-
)
|
|
856
|
-
|
|
857
|
-
normalized_langflow["data"]["edges"] = sorted(
|
|
858
|
-
normalized_langflow["data"]["edges"],
|
|
859
|
-
key=lambda x: (
|
|
860
|
-
x.get("source", ""),
|
|
861
|
-
x.get("target", ""),
|
|
862
|
-
x.get("sourceHandle", ""),
|
|
863
|
-
x.get("targetHandle", ""),
|
|
864
|
-
),
|
|
865
|
-
)
|
|
866
|
-
normalized_file["data"]["edges"] = sorted(
|
|
867
|
-
normalized_file["data"]["edges"],
|
|
868
|
-
key=lambda x: (
|
|
869
|
-
x.get("source", ""),
|
|
870
|
-
x.get("target", ""),
|
|
871
|
-
x.get("sourceHandle", ""),
|
|
872
|
-
x.get("targetHandle", ""),
|
|
873
|
-
),
|
|
874
|
-
)
|
|
860
|
+
if not flow_id:
|
|
861
|
+
return None
|
|
862
|
+
flow_path = self._find_flow_file_by_id(flow_id)
|
|
863
|
+
if not flow_path or not os.path.exists(flow_path):
|
|
864
|
+
return None
|
|
865
|
+
local_mtime = os.path.getmtime(flow_path)
|
|
875
866
|
|
|
876
|
-
|
|
877
|
-
|
|
867
|
+
if flow_data is None:
|
|
868
|
+
try:
|
|
869
|
+
response = await clients.langflow_request("GET", f"/api/v1/flows/{flow_id}")
|
|
870
|
+
if response.status_code != 200:
|
|
871
|
+
return None
|
|
872
|
+
flow_data = response.json()
|
|
873
|
+
except Exception as e:
|
|
874
|
+
logger.error(f"Failed to fetch flow {flow_id} for update check: {e}")
|
|
875
|
+
return None
|
|
878
876
|
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
877
|
+
langflow_updated_at_str = flow_data.get("updated_at")
|
|
878
|
+
langflow_mtime: float = 0.0
|
|
879
|
+
if langflow_updated_at_str:
|
|
880
|
+
if langflow_updated_at_str.endswith("Z"):
|
|
881
|
+
langflow_updated_at_str = langflow_updated_at_str[:-1] + "+00:00"
|
|
882
|
+
try:
|
|
883
|
+
dt = datetime.fromisoformat(langflow_updated_at_str)
|
|
884
|
+
if dt.tzinfo is None:
|
|
885
|
+
dt = dt.replace(tzinfo=UTC)
|
|
886
|
+
langflow_mtime = dt.timestamp()
|
|
887
|
+
except ValueError:
|
|
888
|
+
pass
|
|
889
|
+
|
|
890
|
+
if local_mtime > langflow_mtime + 1.0:
|
|
891
|
+
is_locked = flow_data.get("locked", False)
|
|
892
|
+
return {
|
|
893
|
+
"flow_type": flow_type,
|
|
894
|
+
"flow_id": flow_id,
|
|
895
|
+
"is_custom": not is_locked,
|
|
896
|
+
}
|
|
897
|
+
return None
|
|
882
898
|
|
|
883
899
|
async def ensure_flows_exist(self) -> set[str]:
|
|
884
900
|
"""
|
|
@@ -889,9 +905,6 @@ class FlowsService:
|
|
|
889
905
|
patches or overwrites an existing flow, preserving any edits the user
|
|
890
906
|
has made in the Langflow UI.
|
|
891
907
|
|
|
892
|
-
This replaces the LANGFLOW_LOAD_FLOWS_PATH mechanism, which performed a
|
|
893
|
-
blind upsert on every container start and discarded user edits.
|
|
894
|
-
|
|
895
908
|
Returns the set of flow type names that were actually created.
|
|
896
909
|
"""
|
|
897
910
|
flow_configs = [
|
|
@@ -951,47 +964,31 @@ class FlowsService:
|
|
|
951
964
|
tasks = [ensure_single_flow_exists(ft, fi) for ft, fi in flow_configs]
|
|
952
965
|
results = await asyncio.gather(*tasks)
|
|
953
966
|
created_flow_types = {r for r in results if r is not None}
|
|
954
|
-
return created_flow_types
|
|
955
967
|
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
("nudges", NUDGES_FLOW_ID),
|
|
965
|
-
("retrieval", LANGFLOW_CHAT_FLOW_ID),
|
|
966
|
-
("ingest", LANGFLOW_INGEST_FLOW_ID),
|
|
967
|
-
("url_ingest", LANGFLOW_URL_INGEST_FLOW_ID),
|
|
968
|
-
]
|
|
968
|
+
if created_flow_types:
|
|
969
|
+
try:
|
|
970
|
+
config = get_openrag_config()
|
|
971
|
+
if config.edited:
|
|
972
|
+
logger.info(
|
|
973
|
+
f"Newly created flows detected ({', '.join(created_flow_types)}), reapplying settings."
|
|
974
|
+
)
|
|
975
|
+
from api.settings import reapply_all_settings
|
|
969
976
|
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
is_reset = await self._compare_flow_with_file(flow_id)
|
|
975
|
-
if is_reset:
|
|
976
|
-
logger.info(f"Flow {flow_type} (ID: {flow_id}) appears to have been reset")
|
|
977
|
-
return flow_type
|
|
978
|
-
else:
|
|
979
|
-
logger.info(f"Flow {flow_type} (ID: {flow_id}) does not match reset state")
|
|
980
|
-
return None
|
|
977
|
+
await reapply_all_settings()
|
|
978
|
+
logger.info("Successfully reapplied settings for newly created flows")
|
|
979
|
+
except Exception as e:
|
|
980
|
+
logger.error(f"Failed to reapply settings for newly created flows: {e}")
|
|
981
981
|
|
|
982
|
-
|
|
983
|
-
results = await asyncio.gather(*tasks)
|
|
984
|
-
reset_flows = [r for r in results if r is not None]
|
|
985
|
-
return reset_flows
|
|
982
|
+
return created_flow_types
|
|
986
983
|
|
|
987
984
|
async def change_langflow_model_value(
|
|
988
985
|
self,
|
|
989
986
|
provider: str,
|
|
990
|
-
embedding_model: str = None,
|
|
991
|
-
llm_model: str = None,
|
|
987
|
+
embedding_model: str | None = None,
|
|
988
|
+
llm_model: str | None = None,
|
|
992
989
|
force_embedding_update: bool = False,
|
|
993
990
|
force_llm_update: bool = False,
|
|
994
|
-
flow_configs: list = None,
|
|
991
|
+
flow_configs: list | None = None,
|
|
995
992
|
):
|
|
996
993
|
"""
|
|
997
994
|
Change dropdown values for provider-specific components across flows
|
|
@@ -1052,35 +1049,55 @@ class FlowsService:
|
|
|
1052
1049
|
|
|
1053
1050
|
async def _update_provider_components(
|
|
1054
1051
|
self,
|
|
1055
|
-
config,
|
|
1056
|
-
|
|
1057
|
-
embedding_model: str = None,
|
|
1058
|
-
llm_model: str = None,
|
|
1052
|
+
config: dict,
|
|
1053
|
+
provider_name: str,
|
|
1054
|
+
embedding_model: str | None = None,
|
|
1055
|
+
llm_model: str | None = None,
|
|
1059
1056
|
force_embedding_update: bool = False,
|
|
1060
1057
|
force_llm_update: bool = False,
|
|
1061
1058
|
):
|
|
1062
1059
|
"""Update provider components and their dropdown values in a flow"""
|
|
1063
|
-
flow_name = config["name"]
|
|
1064
1060
|
flow_id = config["flow_id"]
|
|
1065
|
-
# Use a flow-specific lock to prevent race conditions during the GET-Modify-PATCH cycle
|
|
1066
1061
|
flow_lock = await self._get_flow_lock(flow_id)
|
|
1067
1062
|
async with flow_lock:
|
|
1068
|
-
|
|
1069
|
-
|
|
1063
|
+
return await self._update_provider_components_locked(
|
|
1064
|
+
config,
|
|
1065
|
+
provider_name,
|
|
1066
|
+
embedding_model=embedding_model,
|
|
1067
|
+
llm_model=llm_model,
|
|
1068
|
+
force_embedding_update=force_embedding_update,
|
|
1069
|
+
force_llm_update=force_llm_update,
|
|
1070
|
+
)
|
|
1070
1071
|
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1072
|
+
async def _update_provider_components_locked(
|
|
1073
|
+
self,
|
|
1074
|
+
config: dict,
|
|
1075
|
+
provider_name: str,
|
|
1076
|
+
embedding_model: str | None = None,
|
|
1077
|
+
llm_model: str | None = None,
|
|
1078
|
+
force_embedding_update: bool = False,
|
|
1079
|
+
force_llm_update: bool = False,
|
|
1080
|
+
):
|
|
1081
|
+
"""Internal helper to update provider components when flow_lock is already held"""
|
|
1082
|
+
flow_name = config["name"]
|
|
1083
|
+
flow_id = config["flow_id"]
|
|
1084
|
+
# Get flow data from Langflow API instead of file
|
|
1085
|
+
response = await clients.langflow_request("GET", f"/api/v1/flows/{flow_id}")
|
|
1075
1086
|
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1087
|
+
if response.status_code != 200:
|
|
1088
|
+
raise Exception(
|
|
1089
|
+
f"Failed to get flow from Langflow: HTTP {response.status_code} - {response.text}"
|
|
1090
|
+
)
|
|
1079
1091
|
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1092
|
+
flow_data = response.json()
|
|
1093
|
+
provider = provider_name
|
|
1094
|
+
updates_made = []
|
|
1095
|
+
node_tasks: list[Any] = []
|
|
1096
|
+
|
|
1097
|
+
async def wrap_node_update(node, p, m, label):
|
|
1098
|
+
if await self._update_component_fields(node, p, m):
|
|
1099
|
+
return label
|
|
1100
|
+
return None
|
|
1084
1101
|
|
|
1085
1102
|
# Update embedding component
|
|
1086
1103
|
if not get_openrag_config().knowledge.disable_ingest_with_langflow and (
|
|
@@ -1205,39 +1222,41 @@ class FlowsService:
|
|
|
1205
1222
|
)
|
|
1206
1223
|
)
|
|
1207
1224
|
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
# If no updates were made, return skip message
|
|
1214
|
-
if not updates_made:
|
|
1215
|
-
return {
|
|
1216
|
-
"flow": flow_name,
|
|
1217
|
-
"success": True,
|
|
1218
|
-
"message": f"No compatible components found in {flow_name} flow (skipped)",
|
|
1219
|
-
"flow_id": flow_id,
|
|
1220
|
-
}
|
|
1221
|
-
|
|
1222
|
-
logger.info(f"Updated {', '.join(updates_made)} in {flow_name} flow")
|
|
1223
|
-
|
|
1224
|
-
# PATCH the updated flow
|
|
1225
|
-
response = await clients.langflow_request(
|
|
1226
|
-
"PATCH", f"/api/v1/flows/{flow_id}", json=flow_data
|
|
1227
|
-
)
|
|
1228
|
-
|
|
1229
|
-
if response.status_code != 200:
|
|
1230
|
-
raise Exception(
|
|
1231
|
-
f"Failed to update flow: HTTP {response.status_code} - {response.text}"
|
|
1232
|
-
)
|
|
1225
|
+
# Execute all node updates simultaneously
|
|
1226
|
+
if node_tasks:
|
|
1227
|
+
node_results = await asyncio.gather(*node_tasks)
|
|
1228
|
+
updates_made.extend([r for r in node_results if r])
|
|
1233
1229
|
|
|
1230
|
+
# If no updates were made, return skip message
|
|
1231
|
+
if not updates_made:
|
|
1234
1232
|
return {
|
|
1235
1233
|
"flow": flow_name,
|
|
1236
1234
|
"success": True,
|
|
1237
|
-
"message": f"
|
|
1235
|
+
"message": f"No compatible components found in {flow_name} flow (skipped)",
|
|
1238
1236
|
"flow_id": flow_id,
|
|
1239
1237
|
}
|
|
1240
1238
|
|
|
1239
|
+
logger.info(f"Updated {', '.join(updates_made)} in {flow_name} flow")
|
|
1240
|
+
|
|
1241
|
+
await self._unlock_flow(flow_id)
|
|
1242
|
+
|
|
1243
|
+
# PATCH the updated flow
|
|
1244
|
+
response = await clients.langflow_request(
|
|
1245
|
+
"PATCH", f"/api/v1/flows/{flow_id}", json=flow_data
|
|
1246
|
+
)
|
|
1247
|
+
|
|
1248
|
+
if response.status_code != 200:
|
|
1249
|
+
raise Exception(f"Failed to update flow: HTTP {response.status_code} - {response.text}")
|
|
1250
|
+
|
|
1251
|
+
await self._lock_flow(flow_id)
|
|
1252
|
+
|
|
1253
|
+
return {
|
|
1254
|
+
"flow": flow_name,
|
|
1255
|
+
"success": True,
|
|
1256
|
+
"message": f"Successfully updated {', '.join(updates_made)}",
|
|
1257
|
+
"flow_id": flow_id,
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1241
1260
|
async def _update_component_langflow(self, template, model: Any):
|
|
1242
1261
|
# Call custom_component/update endpoint to get updated template
|
|
1243
1262
|
# Only call if code field exists (custom components should have code)
|
|
@@ -1445,3 +1464,174 @@ class FlowsService:
|
|
|
1445
1464
|
elif provider == "anthropic":
|
|
1446
1465
|
return (None, "Anthropic")
|
|
1447
1466
|
return (None, None)
|
|
1467
|
+
|
|
1468
|
+
async def get_flows_updates_available(self, user_id: str | None = None) -> list[dict[str, Any]]:
|
|
1469
|
+
"""
|
|
1470
|
+
Check for available flow updates.
|
|
1471
|
+
Uses cached update results from startup/previous checks if available.
|
|
1472
|
+
Surfaces all available updates regardless of locked status.
|
|
1473
|
+
"""
|
|
1474
|
+
target_user = user_id or "default"
|
|
1475
|
+
if not hasattr(self, "_custom_updates_cache") or self._custom_updates_cache is None:
|
|
1476
|
+
flow_configs = [
|
|
1477
|
+
("nudges", NUDGES_FLOW_ID),
|
|
1478
|
+
("retrieval", LANGFLOW_CHAT_FLOW_ID),
|
|
1479
|
+
("ingest", LANGFLOW_INGEST_FLOW_ID),
|
|
1480
|
+
("url_ingest", LANGFLOW_URL_INGEST_FLOW_ID),
|
|
1481
|
+
]
|
|
1482
|
+
cache = []
|
|
1483
|
+
for flow_type, flow_id in flow_configs:
|
|
1484
|
+
update_info = await self._check_flow_update(flow_type, flow_id)
|
|
1485
|
+
if update_info:
|
|
1486
|
+
cache.append(update_info)
|
|
1487
|
+
self._custom_updates_cache = cache
|
|
1488
|
+
|
|
1489
|
+
updates = []
|
|
1490
|
+
for item in self._custom_updates_cache:
|
|
1491
|
+
is_dismissed = False
|
|
1492
|
+
if hasattr(self, "_dismissed_updates") and isinstance(self._dismissed_updates, dict):
|
|
1493
|
+
is_dismissed = item["flow_type"] in self._dismissed_updates.get(target_user, set())
|
|
1494
|
+
updates.append(
|
|
1495
|
+
{
|
|
1496
|
+
**item,
|
|
1497
|
+
"dismissed": is_dismissed,
|
|
1498
|
+
}
|
|
1499
|
+
)
|
|
1500
|
+
|
|
1501
|
+
return updates
|
|
1502
|
+
|
|
1503
|
+
async def bulk_update_flows(self, flow_types: list[str], backup_custom: bool = True):
|
|
1504
|
+
"""
|
|
1505
|
+
Bulk updates multiple flows.
|
|
1506
|
+
If backup_custom is True, flows are backed up in Langflow before resetting.
|
|
1507
|
+
Local file backups are always saved via _backup_flow prior to resetting.
|
|
1508
|
+
Returns the backup flow details if any.
|
|
1509
|
+
"""
|
|
1510
|
+
results: list[dict[str, Any]] = []
|
|
1511
|
+
flow_configs_dict = {
|
|
1512
|
+
"nudges": NUDGES_FLOW_ID,
|
|
1513
|
+
"retrieval": LANGFLOW_CHAT_FLOW_ID,
|
|
1514
|
+
"ingest": LANGFLOW_INGEST_FLOW_ID,
|
|
1515
|
+
"url_ingest": LANGFLOW_URL_INGEST_FLOW_ID,
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
for flow_type in flow_types:
|
|
1519
|
+
flow_id = flow_configs_dict.get(flow_type)
|
|
1520
|
+
if not flow_id:
|
|
1521
|
+
results.append(
|
|
1522
|
+
{"flow_type": flow_type, "success": False, "error": "Invalid flow_type"}
|
|
1523
|
+
)
|
|
1524
|
+
continue
|
|
1525
|
+
|
|
1526
|
+
flow_lock = await self._get_flow_lock(flow_id)
|
|
1527
|
+
async with flow_lock:
|
|
1528
|
+
backup_path = None
|
|
1529
|
+
backup_flow_id = None
|
|
1530
|
+
flow_check_ok = False
|
|
1531
|
+
|
|
1532
|
+
try:
|
|
1533
|
+
response = await clients.langflow_request("GET", f"/api/v1/flows/{flow_id}")
|
|
1534
|
+
if response.status_code == 200:
|
|
1535
|
+
flow_check_ok = True
|
|
1536
|
+
flow_data = response.json()
|
|
1537
|
+
|
|
1538
|
+
# Always attempt local file backup for safety
|
|
1539
|
+
backup_path = await self._backup_flow(flow_id, flow_type, flow_data)
|
|
1540
|
+
|
|
1541
|
+
if backup_custom:
|
|
1542
|
+
logger.info(f"Backing up flow {flow_type} in Langflow before update.")
|
|
1543
|
+
# Create a backup flow directly in Langflow
|
|
1544
|
+
backup_payload = dict(flow_data)
|
|
1545
|
+
backup_payload.pop("id", None)
|
|
1546
|
+
flow_name = flow_data.get("name", flow_type.capitalize())
|
|
1547
|
+
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M")
|
|
1548
|
+
backup_payload["name"] = f"Backup - {flow_name} ({timestamp})"
|
|
1549
|
+
backup_payload["description"] = (
|
|
1550
|
+
f"Backup of flow '{flow_name}' created before flow update on {timestamp}. "
|
|
1551
|
+
"Use this flow to reference or redo your modifications."
|
|
1552
|
+
)
|
|
1553
|
+
backup_payload["locked"] = False
|
|
1554
|
+
|
|
1555
|
+
lf_create_res = await clients.langflow_request(
|
|
1556
|
+
"POST", "/api/v1/flows/", json=backup_payload
|
|
1557
|
+
)
|
|
1558
|
+
if lf_create_res.status_code in (200, 201):
|
|
1559
|
+
created_flow = lf_create_res.json()
|
|
1560
|
+
backup_flow_id = created_flow.get("id")
|
|
1561
|
+
logger.info(
|
|
1562
|
+
f"Created backup flow in Langflow for {flow_type}: '{backup_payload['name']}' (ID: {backup_flow_id})"
|
|
1563
|
+
)
|
|
1564
|
+
else:
|
|
1565
|
+
logger.warning(
|
|
1566
|
+
f"Failed to create backup flow in Langflow for {flow_type}: HTTP {lf_create_res.status_code}"
|
|
1567
|
+
)
|
|
1568
|
+
except Exception as e:
|
|
1569
|
+
logger.error(f"Failed to check/backup flow {flow_id} before update: {e}")
|
|
1570
|
+
|
|
1571
|
+
if not flow_check_ok:
|
|
1572
|
+
logger.error(
|
|
1573
|
+
f"Pre-flight check failed for flow {flow_type}; aborting update to protect flow modifications."
|
|
1574
|
+
)
|
|
1575
|
+
results.append(
|
|
1576
|
+
{
|
|
1577
|
+
"flow_type": flow_type,
|
|
1578
|
+
"success": False,
|
|
1579
|
+
"error": "Pre-flight check failed; update aborted to avoid losing changes",
|
|
1580
|
+
"backup_path": None,
|
|
1581
|
+
"backup_flow_id": None,
|
|
1582
|
+
}
|
|
1583
|
+
)
|
|
1584
|
+
continue
|
|
1585
|
+
|
|
1586
|
+
# If backup_custom is enabled, abort update if BOTH backups failed
|
|
1587
|
+
if backup_custom and backup_path is None and backup_flow_id is None:
|
|
1588
|
+
logger.error(
|
|
1589
|
+
f"Backup failed for flow {flow_type}; aborting update to protect flow modifications."
|
|
1590
|
+
)
|
|
1591
|
+
results.append(
|
|
1592
|
+
{
|
|
1593
|
+
"flow_type": flow_type,
|
|
1594
|
+
"success": False,
|
|
1595
|
+
"error": "Backup failed; update aborted to avoid losing changes",
|
|
1596
|
+
"backup_path": None,
|
|
1597
|
+
"backup_flow_id": None,
|
|
1598
|
+
}
|
|
1599
|
+
)
|
|
1600
|
+
continue
|
|
1601
|
+
|
|
1602
|
+
try:
|
|
1603
|
+
reset_res = await self._reset_langflow_flow_locked(flow_type, flow_id)
|
|
1604
|
+
if reset_res.get("success"):
|
|
1605
|
+
self.clear_dismissed_update(flow_type)
|
|
1606
|
+
results.append(
|
|
1607
|
+
{
|
|
1608
|
+
"flow_type": flow_type,
|
|
1609
|
+
"success": reset_res.get("success", False),
|
|
1610
|
+
"error": reset_res.get("error"),
|
|
1611
|
+
"backup_path": backup_path,
|
|
1612
|
+
"backup_flow_id": backup_flow_id,
|
|
1613
|
+
}
|
|
1614
|
+
)
|
|
1615
|
+
except Exception as e:
|
|
1616
|
+
results.append(
|
|
1617
|
+
{
|
|
1618
|
+
"flow_type": flow_type,
|
|
1619
|
+
"success": False,
|
|
1620
|
+
"error": str(e),
|
|
1621
|
+
"backup_path": backup_path,
|
|
1622
|
+
"backup_flow_id": backup_flow_id,
|
|
1623
|
+
}
|
|
1624
|
+
)
|
|
1625
|
+
|
|
1626
|
+
if results and any(r.get("success") for r in results):
|
|
1627
|
+
self._custom_updates_cache = None
|
|
1628
|
+
try:
|
|
1629
|
+
config = get_openrag_config()
|
|
1630
|
+
if config.edited:
|
|
1631
|
+
from api.settings import reapply_all_settings
|
|
1632
|
+
|
|
1633
|
+
await reapply_all_settings()
|
|
1634
|
+
except Exception as e:
|
|
1635
|
+
logger.error(f"Error reapplying settings after bulk update: {e}")
|
|
1636
|
+
|
|
1637
|
+
return results
|
services/startup_orchestrator.py
CHANGED
|
@@ -170,13 +170,11 @@ async def startup_tasks(services):
|
|
|
170
170
|
# Update MCP server URLs (patch localhost and convert to streamable HTTP)
|
|
171
171
|
await _update_mcp_server_urls(services["langflow_mcp_service"])
|
|
172
172
|
|
|
173
|
-
# Ensure all configured flows exist in Langflow (create-only, never overwrites).
|
|
174
|
-
#
|
|
175
|
-
# every container start and discarded any user edits made in the Langflow UI.
|
|
176
|
-
newly_created: set[str] = set()
|
|
173
|
+
# Ensure all configured flows exist in Langflow (create-only, never overwrites existing).
|
|
174
|
+
# If a flow does not exist, it is created and active configuration settings are reapplied.
|
|
177
175
|
try:
|
|
178
176
|
flows_service = services["flows_service"]
|
|
179
|
-
|
|
177
|
+
await flows_service.ensure_flows_exist()
|
|
180
178
|
except Exception as e:
|
|
181
179
|
logger.error(
|
|
182
180
|
"Failed to ensure Langflow flows exist at startup — "
|
|
@@ -184,38 +182,4 @@ async def startup_tasks(services):
|
|
|
184
182
|
error=str(e),
|
|
185
183
|
)
|
|
186
184
|
|
|
187
|
-
# Check if flows were reset and reapply settings if config is edited
|
|
188
|
-
try:
|
|
189
|
-
config = get_openrag_config()
|
|
190
|
-
if config.edited:
|
|
191
|
-
logger.info("Checking if Langflow flows were reset")
|
|
192
|
-
flows_service = services["flows_service"]
|
|
193
|
-
reset_flows = await flows_service.check_flows_reset()
|
|
194
|
-
# Exclude flows that were just seeded — they match the JSON by design,
|
|
195
|
-
# not because they were externally reset.
|
|
196
|
-
reset_flows = [f for f in reset_flows if f not in newly_created]
|
|
197
|
-
|
|
198
|
-
if reset_flows:
|
|
199
|
-
logger.info(
|
|
200
|
-
f"Detected reset flows: {', '.join(reset_flows)}. Reapplying all settings."
|
|
201
|
-
)
|
|
202
|
-
await TelemetryClient.send_event(
|
|
203
|
-
Category.FLOW_OPERATIONS, MessageId.ORB_FLOW_RESET_DETECTED
|
|
204
|
-
)
|
|
205
|
-
from api.settings import reapply_all_settings
|
|
206
185
|
|
|
207
|
-
await reapply_all_settings(session_manager=services["session_manager"])
|
|
208
|
-
logger.info("Successfully reapplied settings after detecting flow resets")
|
|
209
|
-
await TelemetryClient.send_event(
|
|
210
|
-
Category.FLOW_OPERATIONS, MessageId.ORB_FLOW_SETTINGS_REAPPLIED
|
|
211
|
-
)
|
|
212
|
-
else:
|
|
213
|
-
logger.info("No flows detected as reset, skipping settings reapplication")
|
|
214
|
-
else:
|
|
215
|
-
logger.debug("Configuration not yet edited, skipping flow reset check")
|
|
216
|
-
except Exception as e:
|
|
217
|
-
logger.error(f"Failed to check flows reset or reapply settings: {str(e)}")
|
|
218
|
-
await TelemetryClient.send_event(
|
|
219
|
-
Category.FLOW_OPERATIONS, MessageId.ORB_FLOW_RESET_CHECK_FAIL
|
|
220
|
-
)
|
|
221
|
-
# Don't fail startup if this check fails
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|