From d92cb994a938c946a3e8162fd21bd9743d7f883d Mon Sep 17 00:00:00 2001 From: takatost Date: Fri, 1 Nov 2024 00:53:38 -0700 Subject: [PATCH 01/48] fix voice list --- api/core/plugin/manager/model.py | 1 + 1 file changed, 1 insertion(+) diff --git a/api/core/plugin/manager/model.py b/api/core/plugin/manager/model.py index 2081dcc298..4188148812 100644 --- a/api/core/plugin/manager/model.py +++ b/api/core/plugin/manager/model.py @@ -437,6 +437,7 @@ class PluginModelManager(BasePluginManager): voices = [] for voice in resp.voices: voices.append({"name": voice.name, "value": voice.value}) + return voices return [] From c100f24f7d99338560e71674c27762d3992733c1 Mon Sep 17 00:00:00 2001 From: takatost Date: Fri, 1 Nov 2024 19:20:26 -0700 Subject: [PATCH 02/48] compatible model daemon request exception --- .../model_providers/__base/ai_model.py | 33 ++++++++++++++----- api/core/plugin/manager/base.py | 15 +++++++-- api/core/rag/datasource/retrieval_service.py | 2 +- 3 files changed, 38 insertions(+), 12 deletions(-) diff --git a/api/core/model_runtime/model_providers/__base/ai_model.py b/api/core/model_runtime/model_providers/__base/ai_model.py index bdbafc8ded..a044a948aa 100644 --- a/api/core/model_runtime/model_providers/__base/ai_model.py +++ b/api/core/model_runtime/model_providers/__base/ai_model.py @@ -10,8 +10,15 @@ from core.model_runtime.entities.model_entities import ( PriceInfo, PriceType, ) -from core.model_runtime.errors.invoke import InvokeAuthorizationError, InvokeError -from core.plugin.entities.plugin_daemon import PluginModelProviderEntity +from core.model_runtime.errors.invoke import ( + InvokeAuthorizationError, + InvokeBadRequestError, + InvokeConnectionError, + InvokeError, + InvokeRateLimitError, + InvokeServerUnavailableError, +) +from core.plugin.entities.plugin_daemon import PluginDaemonInnerError, PluginModelProviderEntity from core.plugin.manager.model import PluginModelManager @@ -31,7 +38,7 @@ class AIModel(BaseModel): model_config = ConfigDict(protected_namespaces=()) @property - def _invoke_error_mapping(self) -> dict[type[InvokeError], list[type[Exception]]]: + def _invoke_error_mapping(self) -> dict[type[Exception], list[type[Exception]]]: """ Map model invoke error to unified error The key is the error type thrown to the caller @@ -40,9 +47,17 @@ class AIModel(BaseModel): :return: Invoke error mapping """ - raise NotImplementedError + return { + InvokeConnectionError: [InvokeConnectionError], + InvokeServerUnavailableError: [InvokeServerUnavailableError], + InvokeRateLimitError: [InvokeRateLimitError], + InvokeAuthorizationError: [InvokeAuthorizationError], + InvokeBadRequestError: [InvokeBadRequestError], + PluginDaemonInnerError: [PluginDaemonInnerError], + ValueError: [ValueError], + } - def _transform_invoke_error(self, error: Exception) -> InvokeError: + def _transform_invoke_error(self, error: Exception) -> Exception: """ Transform invoke error to unified error @@ -52,13 +67,15 @@ class AIModel(BaseModel): for invoke_error, model_errors in self._invoke_error_mapping.items(): if isinstance(error, tuple(model_errors)): if invoke_error == InvokeAuthorizationError: - return invoke_error( + return InvokeAuthorizationError( description=( f"[{self.provider_name}] Incorrect model credentials provided, please check and try again." ) ) - - return invoke_error(description=f"[{self.provider_name}] {invoke_error.description}, {str(error)}") + elif isinstance(invoke_error, InvokeError): + return invoke_error(description=f"[{self.provider_name}] {invoke_error.description}, {str(error)}") + else: + return error return InvokeError(description=f"[{self.provider_name}] Error: {str(error)}") diff --git a/api/core/plugin/manager/base.py b/api/core/plugin/manager/base.py index b25282cde2..6654c05e2d 100644 --- a/api/core/plugin/manager/base.py +++ b/api/core/plugin/manager/base.py @@ -53,7 +53,7 @@ class BasePluginManager: ) except requests.exceptions.ConnectionError as e: logger.exception(f"Request to Plugin Daemon Service failed: {e}") - raise ValueError("Request to Plugin Daemon Service failed") + raise PluginDaemonInnerError(code=-500, message="Request to Plugin Daemon Service failed") return response @@ -157,8 +157,17 @@ class BasePluginManager: Make a stream request to the plugin daemon inner API and yield the response as a model. """ for line in self._stream_request(method, path, params, headers, data, files): - line_data = json.loads(line) - rep = PluginDaemonBasicResponse[type](**line_data) + line_data = None + try: + line_data = json.loads(line) + rep = PluginDaemonBasicResponse[type](**line_data) + except Exception as e: + # TODO modify this when line_data has code and message + if line_data and "error" in line_data: + raise ValueError(line_data["error"]) + else: + raise ValueError(line) + if rep.code != 0: if rep.code == -500: try: diff --git a/api/core/rag/datasource/retrieval_service.py b/api/core/rag/datasource/retrieval_service.py index 57af05861c..277513f3f2 100644 --- a/api/core/rag/datasource/retrieval_service.py +++ b/api/core/rag/datasource/retrieval_service.py @@ -103,7 +103,7 @@ class RetrievalService: if exceptions: exception_message = ";\n".join(exceptions) - raise Exception(exception_message) + raise ValueError(exception_message) if retrieval_method == RetrievalMethod.HYBRID_SEARCH.value: data_post_processor = DataPostProcessor( From 007b561e32c65ea398dadda69b58b0f555b05d4d Mon Sep 17 00:00:00 2001 From: Lawrence Li Date: Fri, 1 Nov 2024 17:23:30 +0800 Subject: [PATCH 03/48] feat: add gpustack model provider (#10158) --- .../gpustack/_assets/icon_l_en.png | Bin 0 -> 283620 bytes .../gpustack/_assets/icon_l_en.svg | 15 ++ .../gpustack/_assets/icon_s_en.png | Bin 0 -> 57988 bytes .../gpustack/_assets/icon_s_en.svg | 11 ++ .../model_providers/gpustack/gpustack.py | 10 ++ .../model_providers/gpustack/gpustack.yaml | 120 +++++++++++++ .../model_providers/gpustack/llm/__init__.py | 0 .../model_providers/gpustack/llm/llm.py | 45 +++++ .../gpustack/rerank/__init__.py | 0 .../model_providers/gpustack/rerank/rerank.py | 146 ++++++++++++++++ .../gpustack/text_embedding/__init__.py | 0 .../gpustack/text_embedding/text_embedding.py | 35 ++++ api/tests/integration_tests/.env.example | 4 + .../model_runtime/gpustack/__init__.py | 0 .../model_runtime/gpustack/test_embedding.py | 49 ++++++ .../model_runtime/gpustack/test_llm.py | 162 ++++++++++++++++++ .../model_runtime/gpustack/test_rerank.py | 107 ++++++++++++ 17 files changed, 704 insertions(+) create mode 100644 api/core/model_runtime/model_providers/gpustack/_assets/icon_l_en.png create mode 100644 api/core/model_runtime/model_providers/gpustack/_assets/icon_l_en.svg create mode 100644 api/core/model_runtime/model_providers/gpustack/_assets/icon_s_en.png create mode 100644 api/core/model_runtime/model_providers/gpustack/_assets/icon_s_en.svg create mode 100644 api/core/model_runtime/model_providers/gpustack/gpustack.py create mode 100644 api/core/model_runtime/model_providers/gpustack/gpustack.yaml create mode 100644 api/core/model_runtime/model_providers/gpustack/llm/__init__.py create mode 100644 api/core/model_runtime/model_providers/gpustack/llm/llm.py create mode 100644 api/core/model_runtime/model_providers/gpustack/rerank/__init__.py create mode 100644 api/core/model_runtime/model_providers/gpustack/rerank/rerank.py create mode 100644 api/core/model_runtime/model_providers/gpustack/text_embedding/__init__.py create mode 100644 api/core/model_runtime/model_providers/gpustack/text_embedding/text_embedding.py create mode 100644 api/tests/integration_tests/model_runtime/gpustack/__init__.py create mode 100644 api/tests/integration_tests/model_runtime/gpustack/test_embedding.py create mode 100644 api/tests/integration_tests/model_runtime/gpustack/test_llm.py create mode 100644 api/tests/integration_tests/model_runtime/gpustack/test_rerank.py diff --git a/api/core/model_runtime/model_providers/gpustack/_assets/icon_l_en.png b/api/core/model_runtime/model_providers/gpustack/_assets/icon_l_en.png new file mode 100644 index 0000000000000000000000000000000000000000..dfe8e78049c4dec13e8516565780f2ec9e239f91 GIT binary patch literal 283620 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

<3pdY}>J6xUIJV2DT)eP6Scl1PG_} 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 z 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 + + + + + + + + + + + + + + diff --git a/api/core/model_runtime/model_providers/gpustack/_assets/icon_s_en.png b/api/core/model_runtime/model_providers/gpustack/_assets/icon_s_en.png new file mode 100644 index 0000000000000000000000000000000000000000..b154821db91ab02007d7c48700dea34d57871ec6 GIT binary patch literal 57988 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

edBnRuu(@m|j_~Qk1q_=Uy zGo1j^nDI22$rnC(MxrdyYtGaON4NR2pKAZI=7{mw?Qfp~kvn!l(%QSLQ}<|1l(Mid z3)`N=uE!qeb(e0)uF*h)FS>iZBi)Df;c{`M#hcC2d&Z{SJJWdFVz8O;p(rPf%in|N z@q9tTD%lb|uIBiG@|{0DAJe%uwO)Kz_DGq>+ygsKs#Pnz@$?8fWa%{K*LhZb-xb$o ze=p-V+bQ8r^)VaL=64|w-wV6#%A~_&5XnS|fre5Np|ijfBMHzc=`>1I3~;phW+52I zfZ)8i?lCSO5G~!0H01AZGCh(RX_W9v^23m?fY9xDEIK6&NX>0Pgd zGkyZFz2)qjRE5uz$2FPOmXBR=#g;di&OWYVPFI$_1&%!(iS+OK^OX4~;gYuXj|YNMiI=R#@P52uIN`Y<3H zE=W@2NEFE`__JmqoCT+%`&eseUU^?tCCl)ODc~mu&Kv&O?*3_#)h=7!^1TgN^#ELl z_)o~&pdCSm?=@EOFu7XSZL?>0oj!d!zFUAY`}q8TqmS$S3ofKy0w+Bc4t_NB8;>C2 zF$1^q;?%iv?X0J5K7acB{@+K|KV7#!)|B^eoH+Hv8_!<1)20=$$EOh9yD^~4r)F9o zU#D2Fs4a*&XpTr+jd@aGGKl0xkj52tjxSg#DRE2(22d-(5Qm474jTBkB*WUq>N_h8 z6Qv|Sl2uu+&^HoiH#y|V1lCi&WRZD#sI#@>McX@BK`Tvx^$uN*-8ugf62`m2CzwNM zJN5-*r$uam^=x{co`b+pqO}h^ds=`wv_NcVg z?u2vj@iBR&C7W^4KeOe7V!93Dxku!CcV=CmKrp-(Q?1bPwF^l2a&{&AR$i%n070+| z4a{Irkl}>^LDMLhqR!F{d9~%_YyPn9uNZAZt?fI!Ag@emZ+{kk{_XA9Jv|2(n_&m2 zz&Uf4Y%5q%BT1NjFye4I2?JU*#79GI+N^}_^9(eVnC(F=D$%8aH zjF)tBS#herg&fNBomDY6%HkZQzv)`+s(dWw$(nkEE!PMD;LAnr_>wRlHA8*{Pc9U?l0$wNLwhV#Ei8iQ({;CD&y4UVe4U>$_6zC`h_U9?2l8=2P*+hfiZ?rDR38 z1qH@*ssU_z3ytD69QV*X@A&2X`Hzq1(N8^N?Vi&<(0=CB&Fx6ec&J{M{sjSzZ+Bu& zSY!nxgI4py{Q0RF{w=B4+!erl!GCekeo7v66byu&9EW4VC0vKmGa&>I$`BsLB_nWw zBl*I`3*kU8TJ$+y93?p%iqk-}O+JCt0rVw3LI+B+al_YT0N?{^aLBDyv-dw%$%<-F z?H#_rV9Udn<=QN12E|0~9PmV1(rd7W*KjR)1YfV0B3}O#Fzm*r0O{vQS__|^^1}x# z(+TlZ(a&RkUG9XhIC1aXZl@S5<89DibyeFnS6tP?UmJb_64Kw9oWgM}04#v&rwm{S zlyF|w7GT-jM@MzMc^l#$v*ClzT(jH$AJ};6v=40fRo2>iH|F|DOm&i7{KhQM@p+hz zasZo~5(S}fu1Ff1KN10HL`QmBMSKjmyBz_O-GJ4Nk1XgBh_)xk;Nzo^0eY7gd@O0X zd`2*Ifq2O&=p~bSOSO{zfDl&@YfFFXMjBqTV(s+xxd6ACezbnZm|^|u4K?elaJoBu z!4EY27E0QqUua0QP<1=-P5Fjz3y6s?+?cH#-I4ey-3@Uq3(RkZaXvDlE6v3PooB)) z{sl5`kz>dU(mXie(x+APZ{sn?dj+pX>&5qHE8zG)q4OhYwB~tDw^uaEXQ233UDonz zSoGBqeCGU^DGkis2L2T^?6XV9W*8udcq&1A;JVfa1}c9z=%${&Vc-4Vx9Ob+{Kw|M zcGlYNhh@*pv&!?Ke;f7XMRfjxG42 zC@b2QWYC99lZ8RaN8c}O7m3m3($=CwMLxC;PEY1euO#I-Me+(-)ZNz)yCm09>qj2a z$z*VLh2E}*=1@u3t!p3iMv8eX3$%TFl(}Q`bC66v;J!k;>L^X%p-NXeZgG2d>p*98 za!dY4@Wu1?;MBEr#KP45@iDxAYL}G$9SitTIKeLAgm6;)>QDBG5u4Mw1G&C+{GRz? zl}diC92R${cV^kdOIKzO4|E=l(j9et$JwCykUSLzWzfmV>98#BxPs9*3NXOw_;PmP z&sz4IwyTA|onNNCGapf{Zak`1sm@1`zYy9SRp_ta#H2JY*BKF6oaVC4Icbi8XY*&^ z7Kt3=;_3YAiHS78FX)X{K~8$NSGPWs1Zij|BtR8_WJ~f#3H?L9y-}DxI#rL;bjl#D z4!pQ7;zFG$g%hFv?2^vctk5awwaat8s0VFh^~$cidhEu-ce;lx9?POBJ(ddeSN4*% z*_p84n_%0A!oHxzNAuRlYV3}M`#;`S)nA@H@wmIUi~Th$Z6}u( z4m1{y@TpBB^BYeZmA`mv^jm{p{6Kacb+_Mn%SXeX{(Wn>!voKz(|PHfa#wl<4*GdG zI)j3J?o8z93;^~yf532~^C82d+m>9Xzhm7~4ti(%iw=1Arq3Pt|JpC9v~FHgt5hyV zu$~Qljtc!$E9gvD>)V_-87{TW7ZdTe0<<)D2Dj*lK)VoC;|7m}Lb%z_afru*Nq~}~ zU({%j9}8rJE#-cM`(dZ*(z4(Y$x$=~Ka(H3C(|)_gPwTY9R_~LL_c&H>av*SjjDOiv_D{VM!Yfn4ZD)`Zh+M5sb7F~F6~*zv`WJ71K>>}tJLsloyxIDOjDFNsV6m~t_(nLchlc?2tF6!Mgj@_ zH4b_uHjMjRjVHNVLRK<@9PMs`CS=N9OggTMd_gSTC+fNtdU9vQcL3^4nQXF%9P<~l+z|@Zdfb=ecgkns zlf*@s%xLbA@NxBT_uQCX$cNuGGPy`tG)ko(2`hNx)`;g~hxIuG!$B6n^u-J@Ry(iS zS^3a~>$Cd3KVo6fI7V} z$L$snl-n~n(f`-$FBq|vC$<{Jq*K#~ab0O#M_2p!YPB-HR_Pj_S1aQXQsZH+@pw1i z6-MJzGNwVFX$YF#G)E6$1)_o*t+HO_cDG;$b+2PuB^x6@3L#yzK~qoC^)aGpZICcL zppB*^LpZKOIq?Kt*tnoC(}w(Rx^S*Q;+Q8d)f5zR*j57MT!8P%STfnysa|P}SRq$@ zLGOL?)~*ilS|vMm{h?#dC;J_xP|YVqnTtEtu`alPoLaa>nAo=#wP$Z_WOAou`5&>6 zx`u^Q{cY~B%Dj1fYwWo64#+(V?x5>iP=k)Y7T@>##OPgcee1Tq@86ocU+0VPypo@r zW2QUyUQwCWXwog~)K`1f3HehiS?9a)`NiAdpyMS|0pPmb(w2JFUt`k)XdY*pa&+KP%I2L`=%i-l+Ptds>&6o2a zU)V(Rk{mJvJu4je(22f=m-@WL-FK9Q{<14^O=rWQQu8;uE4Q zY0v(EiM-9%maFy_{3O?h8kw9vhBV)3giN@+zO`^o_9gi6{@87O4Zu!+mmZ7jJD=LH zCco)~J^Q}CwdBs~1qiIa%W*1c(tZ1Py{utHwn4x<*=HT!`K>H=pRWS^k82w-|f`(_fHf# z?mCdBF3a_&UX%}c##^?v2^IDS6Y3H4&eMI(d6EvL=ln%y^ps8+vK4X3hdd4E+#NAl zX-a%~qLjs(vhQF4K31B~*(bH;uPycaY{naApGeynyl`!{YT@eYX}G~|CYWJRi;KVk|u?wN|;9WCiLv|zSQFAuWK5A~~&ugjkCg|~I$QU6x z)U1;J1bLU5vqCdEEerMV0OJfI05r~EP=EoB8*1^Q7%{@$UU7ELKs0%3+b&9+$wL8C zoZ7JOIvAeG*DVRu&jXk`7Y8Xgro-E=KjnseX~#&_ZpP$b6ytUJ`lpD1bDlmXKH>>_ z{2Lu<$pF2Qys|w-=esAuBLR}W&_CphM_re)e9qtI2PUppod;Sj-{w-nbvm;HgnCjf z=0*MO_$HFod(hh<3#a#lDvLH{XCczhqwQI6P3zn9Tk=z^w~bd4Y4x{(*N@@FtFoIG zJzRMkVX*G#pSqN&tErV%-eyn!xqfy ziC5<12~+s0^l!0F{7HHG$XdLdfakGa*Vk-u_xc|({tObpM;f|@)fZpt)45me9dNd@ z;CZ{2-o4ksAwM~;rSgUH^{wOf@50V1w?`?b{0`v!6FxQ#pN5Wv6Rw1EOYlzn|6Q@H z^{>6jK5cV1=vMNj=hez(9i8qHPk;AOS=QAVMw%Vi~)>*qgDEh{6R#{xS!dduUV>H9*KIJvvDkI}9tmmUHfQULtlwzfCUXA}sL#dsSHIj@ zt?>HRSKv6E3@kVke>LZmYr68AkDJ{2lHi7CSp5WnjVFi$QpqO4p^rtf+8x0(27!xD zBGVfymCA+39Nzh-*)#LWK{snwzSGf1cfD)F#;)4{`vFe-}ip+f3l>4qKn$} ztIKlw&;h)|R^?=Ab~GJrEnHvF**#O>FCLU*@{42#kPM|7P1*4CauD^8j0E<9V z?KYteiaubRzmi>4Pt=}0cfb7ES+(|gIPt-9uu66U+W2WPekx_o9mz)ksRid|z_8dm=3a2hzbX-s zMq_AHvK!#|pBd9q{o%reEuBWt-`6a^?sw@TVDnHp;>DYf!Z)<9gaii4$pLSu=A?|C zubPtPjTcKw7}r1gm>Y}1(IBZGoFfB2Y{Z7y2{q@BzKxMu`WmflgYlkqL0=jp111hn z9_j)V2`A)R@^bwp+w^3ja_EwQmoUw&6RY% zyIyyWawK!6?eTWALmPOW`=Z-I(FQFx?$ zF&y-{S1zf{yz1hX?=M`4)8ip1*ZjP7Da?0|bj;F0Ghe#tz((0>ZnQB7?YfW8mvdx9 zY0T!X5!it87}zeisEN*`A)XvAGqLIM001G%NklmmEOd?g9d zk~j5mdhj+`=(|kGcYO)d8WGQ=7(Kwn7@Cq#dho8x;v50=!;+lKC%F&V{DDhd$%#8E z|5KarfKdkjP?N5(wcFF=r!OK|;nU*BPqO07HGMXo$mPP$44bX=8v3B-nWvYXQc0iL zV$NJ(UwC`I=@uA64q!sL)BU;+ODNndY47^mIYWlKk7sIG#L zAWR3aup=wW0lFYX->D~>;OY487vUc^U*OPBf}iU^I|J%)r%%~`uAMpVbm*ZW4nJNB zr}d;Lf9=YCisgAPE@)^?QJ;Z}@ZBDuj~~bM!R^0}BdYi=Bp+K0pQ3{+c+`SQdgsJ4 z#h9EPcnYv^NIGL}_u*a4>{sFZ(C`I~$kI7S1drF8r?La;*hOjQ$a!LJHtzEVjDg8O zx1O zvd~OVnt~@Sd=c+j=hDyI5zalOn!o7el<$K_%X}D%jmMJEd2q8*o_=}q=={4Ujm@9G zJq*;@RXah>M&)!54W#-zsXLqLTnat~30IH~Y){BmpFgg4S+$zI^}sR<;hs1lO@Q7zRH-o@jQZ->JaDD|ZI=cQ5aP&QaaKyeWEP|nbS^BN4M<10|v z%b{16Iy#;QDgWIcNw}p#-*=;JE$B$aFpB_0lR-!u@DH`Zkn{B9VFAy8#t7k-q=io$ z5j#D4vWsj3P8uf+f5g-27m+yT$w~=`II+_XUci@L?7K|ukVq{YGz$4j9i|_^(vkE^ zG)8awQxBuFxeyls@K+yQC28s_{bdb8Wha{#x&{%7WCT6+1fB4*?EXrgKYz`PaaV}o zas2Y6OIQnV-ZVeYUsJ6l++~iR9p_Uh=}cVcSV$%fE;t>C&YRM)UAUNi($4t{E?S)} z3bbw2^KmD8N7gw5d68e!nE|Jsc~7Nw2-uH6*g+n77|6Xs`5Y14%>9!WPui$79a*fbKqK zfnBnntOpz=kEqGa3*ju#h51%%4!acz#MhuKUt@xUPtZ$0zPQi8{+2+jUtd3kGy)P_eIJLrD+HiAiEhj13nFW>;9PamOl@er=zL;vL+t)0)^bjU7uiuQ5- z@}x^xT(5;G+jL4Lzne*ei*EtJ1;(;-vCTy=R(C**jK@3gZ%*DhpYhk#S+~WXwRgVL zm};8d)zyWhgzpemvKintvorCU5}xKnmN>$h#1(RiK;d{LdjJ@FhVz-zntTQrOVC9v z`eHZ05UlliC%R!GHXKiz{?dHR@}Fkw`omRvGq~|bgV?UaLak`a zP8!w@BWx!7fv&I__!R9}H`TPW(HAs@Ou(aUHU|LOIDNOB6~^fbh0erbDbY(l=Mrmx zVWVM#$kYYn1fmW<6HkK3OXEN!z8_oDpBfZ)Dz&AVk|2R(>^4a0&#fIDFI{`su8(5m z2~nPu3G3-C`MfUNY^g+m1r7(|VzbSpQ85Ont;IF}PJskLYbgr*u)n%O{)AduPq;+@yy>>I?Pou*z zFeEV?QMKqn=DZ05$o1b?$y$f~C&PJX`}EheUj(UqXyJ%7W;4I*_%j@Rm$LrAlx;`> zxB`^couCw{Ie(3BhuUo@2AH0qa<4*Obf>46vMSWk}5 zL)5Um=0#p8oC|Bx#k|CWeKt(TLcaS)K?4-6rTK~RA(eRW4XG|~3!4GYWLi>3``mS_ z#=I|`G&Hxhko}HOo|Fj-r*~h_lQZ2x>dC3}4$gN(tWY?9Tv>$@t|>o2=GyQ8;-U%C|MoriaR27&P{@pAccLX%_; z?8(Xkh|X*}<1XzlT)8m&XK%cR8ivrh0D;Pc#k=!)TDa|mMI{uVv=xd6W!ZjA5=Ej= zA^@g<9>|oZ`2$Rw7i&bG#3h`##4{RO+qh6q9Z1K@=t*O`7`^6XXyRFx1F+O#EgG)3 zx6NA!ce&CV z<2Y+Os^^bs>v}z8%n^TgesgYmCb;-Pr;lU>7v9i1s%6Xj%K9U805~13 z9lvg=wS1kk$1e3b^B3eT3m0tTom_Kqb5SOfP0R-iLNgY9&5bx7v#z9}-(?jwVM=mQlLkmGN4y|Oe*jA_@z$Jf#wW}#S~p{C-_ItajK^tt(j_dsDf0S~ ztu&n_+{_of2#8ov!s(1oBm6AgRjFp5PkGn%8&k`&D^^xk1^Qs><;%0p_yRMYoXTgr ziXucVzi+dDr9=yv(Jty{i3`rhfMZoV>A^V!g5mj~|Y_~P`p zZu(ty!-&5v`+o1=SLCy*BLOhkGMPwwpNGxIgHy2j8flE{)NQu{B-jE_6-zdC8XF-F zhD*0n`1(;9qvv=W;^|I_PaXZK7E+m3&8&kMv9~&fCU}FGYlWvX}1kG5pCc z;%B;uKi6Bj8IJUY9BB}{dhBvXj%zn_96ATB zP&k@g!(nJMD1-wV)}qPoTC_6z@}hfMFS_EMY)Fz-=(==i_Gq=WatieNCiKE-B^{Sy zDIaa>8ts&BEG4h^ANh>U+vmY+3vhWFKSvg}gI=fM@eP0I7`^$1gWuY5V$7vn<5OAa zZTi!MN&vDa;eIe76=P@+j-q+{onDj)m(4RbbXPsQ!*3)}aggZ>|$ zA)A9at4mk>PreDpJwW?WhWVuM{t;I;Pwi-XTFV=)^n+sd^ohtF3oI|Qo~Ok0+f>{ zx&Hi97Ui&xQn2Uzrq3G>b(%b{&&x{t1nV{00e_AC)&}sSjy6gc_t0;7(<%9+U^)5u z4>qT^vW{jsyIth2#>%#r?j7Fed#LFVNvEktwA3|{b68=NZ1kN%oxpWa1r+Ig$6(%f zEd3&zyV#NNi@6)G^85&ES3R=q-4J*DKBjbM5?#Ec^kYm$>)9w+QNkmzpMT@|{DRJ~ zXvmGFPXUzvm!PiKZQv5-6^uTZg5cS=zhB?t4S@^3c5rr>N3uWX*GI`Z911ZOYooFfF>5?swX}VbNbsDX&_d4xHjY?gc zYjxG|*XW{f4`j--AE_R?=nJ+@vCuCSv7t@ z^RjX0HP7JlSY$h)>CUiHI5j^V9r|SpfFNHEgFKq%p0TLu;i_!Af_Y>u1E9;P9-uDTo-{hOF+7TJ)yj z_Hz4f9nj~`(x|@WWJMl8dv`Y9t?dI zor=RO3xFUTgN`<*zOnNDxIyIy$2BD*PNXyO;AXK{$bAt3__}P3!baQ_c<$nfg~1(d z2i9yjwdyYhtMgM$uwwLi$|bXcmY2-VpH*KvKK4NQ(Sp_4Z)G*XY~{uy{p9n#m;4NN zob>12m*C(|!Ydy{uFE`bf_Bqc^tS%e+o2CKaMG%G+vMYVe!Y5PdpJFTYSG!HoDeiT#oRP42w8|k6RdC3KTE}^csAUQ6>6@8 z{%y{VvvGgmMZ7-qiH_1;EY?2-+4{PU1(^$c2AcT^ZVi8Uh(7i-xW#XNWFi?T7dK?c zZU=xZF4;W4_VjO67V`#BI!|`mRBvaxwzK(^ZLk~8%-FrzNejugCC%UJ{rpr&q z=qIUs7kA>mOpM}sSoUkq&c_Y}*I+{j1wNP*q;`LVuE`yY|!N!>{ymB<^LFfcF3rwEjdT3 zyYMRqE=QvlBEY!H1sVu{VK2anqJ`_WYbQxExJ2Q;L81$y5I!)WH7QH^c(Ik5DoM0fVswCKZd z|Dx^j%~)e*G&=c%0H#goMp(LBJ!u;{CWCTH{OfSw(ZGsMT>f^rap{}!9rb6#=b)2M z3~~{cjt?q6fY4n>hH>&xQf}ThdUW%nW5#sO#Q3V`>enFfC&2y#3^;6Kiw(tj{QOsk zob0&7*NqTl_`@C)g=IWYi8bVdzgyQY|Jl}HC7m_F?_YCb;M3I#c7`j1R^Ve`!Fp$Z z8u$iY1|9++(=R>?e%4VY#o~|CYv)w95-Mo3*)JNL8I{pe{%Da7{EipY1}aV_KUP6` zl23kY;zyR3f-I5^sqX4A+vN4DeY8VmMuWN?@WX9;H})BwffanxD$%uBs__8VtJ-o_ zP`7Lgg1rM0o?JgCS82Wpo$@Ah^D})%_P@@!HeD4&SJ08~TKP)-o9!`a*mVc5!8PhL2tIx71}FPRtA3iFj%b3|<4+gfTAU7G zc}!x$aDHKL-IPs_g$LV`IN%%nE@6Q05<)ctTtV&f2w?#WgBSmO@UzSZx9Xw5z@K!e zp$zd*j^&Z7sjKo6L`YA00u9(fS{pr)mj@saos*5cYoqE(Z-7Mmolen{4x6(@cHywK z{oXdw(Gu~PUEt^P*#8#gN_XuZd0OW$6GZ#EVWP{jbOJmud2$kO+El(84xb75z~nfd zq;Z{|ztqSd!1gpcE|$*Fd7N>xtGht&as*F#t<#c7WxARsiAw$OWz zLtsFn|9IKv{AXZ;a)FTJWxHjk2Se)jdCHPp*yHk&2VT?&a8iDP8Oy>gE z?YTE54fTBs+hKDf0<)Y8>&{sFFX2(|Tn)J6;B2(@UJp)bn-$EE?!nphNCrQFhnHUl z5Dr{YIuK~yr&@09qo1&Wml)&0N}$0;04xkZSnq&J0OQKZgH1rDJTLK3=K+aAX|rgm zvTDy+EpSnnx8K_2bdo>DoMsf7Ff|eO*a&$w^t77 z8~PRao?{~e5pz+Cj|Hrxhds@WEMx5>lA zqG6fkQ-0F0&gqmtnU@_XN2=9nz~*?w2LkSSPQ-%)9a}l|w6NdRd6~3dx=`-zp!=PS zRTX?oV9)5rOER#6Puh{_)+~K&9tbP=B-aZo7Uw4hl^jM|cpDHw<%7Wo5j#2a?)bPK zN?rYv{A`R?=32lYgu>tlLuWKkXm8W;;Kmnx0Ajtu`NztS1rHIdXo1>-tOF0W)xd}u z=Gj(k+=E_$k8)1b^Vrv=fQ~wW3gt5I?zVZKY0o;x&toO8AII{!(<=b=;K#kQLz>A; zS&pZgcQSoBbxEF$uAFwh@3|H=*Y6qIX9O<)o`KzLPL|r?0k}0wUBLq@7KiU65VrYW zZ3PzY!udc+gE|Vv51&wca`@*G!}Wy&P3p#_5p(((CyJo=(4jl5)UfG9ABud03=Bs0XYTRtHZA?t-I*j z((S`0CTAQ=8~D|mhK9OHkgW%15#YVbVdcMv%q(Elhoh(ScQ@nx#0tNkNLyCFeP6GQ zN9Xbs9AQ-fSP0{S+*9bXjy;m?^?)K>b}S&GA6Vu9jDFIlp6u()U*3l)A82f(b{KvX zr64F9=x?*d+oQeL??z~#F#WR0@p%3)c&!puChQs4*tBbW-=|=+-WMlNmX6N@bZwTp zhzFvu@=q{jp8_Wh7C#a|M1;v61W{4IWdL?Qp1!{)h)P?AO)kwFI=S>G7CuLA5MP<8 zRO%)`>aQGVoQA-Dv1!(UbB@eqHF(_W3mz;mgLUkqE3$LFdpD9w0YpL0I==zX2tWb_ zWKfXTn@Q)AV)?O&$R}372Ru;f~;jVSirwX4B;T_>9@%(_2r3J8**! zYqBR>EFgn3%VG4=z*&3om7gOgL>+AtSVVE$KEUD}78Cik#~*@&0D=I(Jb{IPfS7)keMR!g=7!R=GS;5Q+E%=?|MOz6{M^=` z)4FJK>5uRn{j4AtT!$vqr-5sM-Hf1nHi_a#-aIdUF~Bl3t($ql&4t03TKXA4;u8#F z=vtodRri@y_Z_;-@u0sxI4H@5{HhOwcI!L$)}IXy#eQqKhdb_n*GIE4kEQ3Z#N-1X zwra$Ky#X|8Ib>12&l`BC+yMzmC%F;(m zD8uxdOw>EDCY@;UUKf-v#3=U%J5TR3KWGYzsL4_%dVsFaQaASiUv7?br5i9mGZO(c zBn>d0T?Sc_Y{n`JkCvLtbJji4duwfT2j5a0gZfth5WN||22rD=z)2VCc`3+wFMb!Cyz)^T{u;R- zR+8MD-BVzb@;qM7efX{2 z^)L8${s!K1@N)>v{{l~rqa7>|_W?!?ze8~F;GsDj_wXcR7`NyjXl$;(<8Y&~!?{y{ z6#}rBpo6lHJhCzxj&KAp?4@q=#|E6V&+4q5IKY37l4LLWQ7v8vV4`Q9$=PADK`R={ z02dnR+b*6s*aZ$eiV*yC=ii# zk(E)l11G0XAcbPug0u^==2!CJ)4TfhUKorDODfI&d~6_H5pD8{89vFOcaSTF! z76SOs5Qx0AOThpE&u>;Qz#aE_0F-|PIP~>FTniw|*YFuQP)@S&>|Dd=79Higg#GUZ zUA2Xd@XwIGN-pEB{yZ)avtg5V$;eQK4*gx~<%oQ8zF51Z$G7dX`d;HeS{xD?zga zl&{8hyrO$==y)GUwb59_W!ocVcBB?)B`TbvpnV7D9LnKo!P<>5y6dR)TI%U$41l% z9S2sjXRg=XoF-VRfEwyM^Sn4~F{( z{Y?Siwj&g{0g?jU0^S3ajZQ$tjKIN~=K-V?l?S5ah=8Ssz;sCmD)LjXz|Vmm=dGLt zR3QK(2@Y)3#e-^jI?|#Hf)-Ya#f#bN0ZjZx zZhiVX#&`t#*&1CDuxda-BN~AQfr`2uI0$H51@Z(GOj6@P(l%1mv?LH4)lI=(~P!PJ#E9ftg4%MAOoy! za-urZOkCb!NnQghfrTGIrJw=r%HxkObMyigR)r9&EkA#RX5z5{(r6PMVGA4cQS#pAJF?GFogAH^ zYquf;tgd~2PKb4H-`jf$7MWXWli*P4fywXHO8`z9T@k2AgGvFH@^`B&`;&Q~lRc0D zRwp~SyNJge(X9X>e7T3CEP@j(SKb}hK0aV~<@9I+aBIoA2kRf{F73?h?A{*80IPeS zzYeqhop06sGKnfv0YI1lw@X3)?p^DV74GZVlkrioCQh0s9__W94l^j8Gd zEM*>O=>dH7^}L}6uE*`aNvIfw5!LTvzKUPMeh;Aaa*z)n-}v|Sd^j^pnFmg&2mT+k WV5hA3PnA6Y0000 + + + + + + + + + + diff --git a/api/core/model_runtime/model_providers/gpustack/gpustack.py b/api/core/model_runtime/model_providers/gpustack/gpustack.py new file mode 100644 index 0000000000..321100167e --- /dev/null +++ b/api/core/model_runtime/model_providers/gpustack/gpustack.py @@ -0,0 +1,10 @@ +import logging + +from core.model_runtime.model_providers.__base.model_provider import ModelProvider + +logger = logging.getLogger(__name__) + + +class GPUStackProvider(ModelProvider): + def validate_provider_credentials(self, credentials: dict) -> None: + pass diff --git a/api/core/model_runtime/model_providers/gpustack/gpustack.yaml b/api/core/model_runtime/model_providers/gpustack/gpustack.yaml new file mode 100644 index 0000000000..ee4a3c159a --- /dev/null +++ b/api/core/model_runtime/model_providers/gpustack/gpustack.yaml @@ -0,0 +1,120 @@ +provider: gpustack +label: + en_US: GPUStack +icon_small: + en_US: icon_s_en.png +icon_large: + en_US: icon_l_en.png +supported_model_types: + - llm + - text-embedding + - rerank +configurate_methods: + - customizable-model +model_credential_schema: + model: + label: + en_US: Model Name + zh_Hans: 模型名称 + placeholder: + en_US: Enter your model name + zh_Hans: 输入模型名称 + credential_form_schemas: + - variable: endpoint_url + label: + zh_Hans: 服务器地址 + en_US: Server URL + type: text-input + required: true + placeholder: + zh_Hans: 输入 GPUStack 的服务器地址,如 http://192.168.1.100 + en_US: Enter the GPUStack server URL, e.g. http://192.168.1.100 + - variable: api_key + label: + en_US: API Key + type: secret-input + required: true + placeholder: + zh_Hans: 输入您的 API Key + en_US: Enter your API Key + - variable: mode + show_on: + - variable: __model_type + value: llm + label: + en_US: Completion mode + type: select + required: false + default: chat + placeholder: + zh_Hans: 选择补全类型 + en_US: Select completion type + options: + - value: completion + label: + en_US: Completion + zh_Hans: 补全 + - value: chat + label: + en_US: Chat + zh_Hans: 对话 + - variable: context_size + label: + zh_Hans: 模型上下文长度 + en_US: Model context size + required: true + type: text-input + default: "8192" + placeholder: + zh_Hans: 输入您的模型上下文长度 + en_US: Enter your Model context size + - variable: max_tokens_to_sample + label: + zh_Hans: 最大 token 上限 + en_US: Upper bound for max tokens + show_on: + - variable: __model_type + value: llm + default: "8192" + type: text-input + - variable: function_calling_type + show_on: + - variable: __model_type + value: llm + label: + en_US: Function calling + type: select + required: false + default: no_call + options: + - value: function_call + label: + en_US: Function Call + zh_Hans: Function Call + - value: tool_call + label: + en_US: Tool Call + zh_Hans: Tool Call + - value: no_call + label: + en_US: Not Support + zh_Hans: 不支持 + - variable: vision_support + show_on: + - variable: __model_type + value: llm + label: + zh_Hans: Vision 支持 + en_US: Vision Support + type: select + required: false + default: no_support + options: + - value: support + label: + en_US: Support + zh_Hans: 支持 + - value: no_support + label: + en_US: Not Support + zh_Hans: 不支持 diff --git a/api/core/model_runtime/model_providers/gpustack/llm/__init__.py b/api/core/model_runtime/model_providers/gpustack/llm/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/core/model_runtime/model_providers/gpustack/llm/llm.py b/api/core/model_runtime/model_providers/gpustack/llm/llm.py new file mode 100644 index 0000000000..ce6780b6a7 --- /dev/null +++ b/api/core/model_runtime/model_providers/gpustack/llm/llm.py @@ -0,0 +1,45 @@ +from collections.abc import Generator + +from yarl import URL + +from core.model_runtime.entities.llm_entities import LLMResult +from core.model_runtime.entities.message_entities import ( + PromptMessage, + PromptMessageTool, +) +from core.model_runtime.model_providers.openai_api_compatible.llm.llm import ( + OAIAPICompatLargeLanguageModel, +) + + +class GPUStackLanguageModel(OAIAPICompatLargeLanguageModel): + def _invoke( + self, + model: str, + credentials: dict, + prompt_messages: list[PromptMessage], + model_parameters: dict, + tools: list[PromptMessageTool] | None = None, + stop: list[str] | None = None, + stream: bool = True, + user: str | None = None, + ) -> LLMResult | Generator: + return super()._invoke( + model, + credentials, + prompt_messages, + model_parameters, + tools, + stop, + stream, + user, + ) + + def validate_credentials(self, model: str, credentials: dict) -> None: + self._add_custom_parameters(credentials) + super().validate_credentials(model, credentials) + + @staticmethod + def _add_custom_parameters(credentials: dict) -> None: + credentials["endpoint_url"] = str(URL(credentials["endpoint_url"]) / "v1-openai") + credentials["mode"] = "chat" diff --git a/api/core/model_runtime/model_providers/gpustack/rerank/__init__.py b/api/core/model_runtime/model_providers/gpustack/rerank/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/core/model_runtime/model_providers/gpustack/rerank/rerank.py b/api/core/model_runtime/model_providers/gpustack/rerank/rerank.py new file mode 100644 index 0000000000..5ea7532564 --- /dev/null +++ b/api/core/model_runtime/model_providers/gpustack/rerank/rerank.py @@ -0,0 +1,146 @@ +from json import dumps +from typing import Optional + +import httpx +from requests import post +from yarl import URL + +from core.model_runtime.entities.common_entities import I18nObject +from core.model_runtime.entities.model_entities import ( + AIModelEntity, + FetchFrom, + ModelPropertyKey, + ModelType, +) +from core.model_runtime.entities.rerank_entities import RerankDocument, RerankResult +from core.model_runtime.errors.invoke import ( + InvokeAuthorizationError, + InvokeBadRequestError, + InvokeConnectionError, + InvokeError, + InvokeRateLimitError, + InvokeServerUnavailableError, +) +from core.model_runtime.errors.validate import CredentialsValidateFailedError +from core.model_runtime.model_providers.__base.rerank_model import RerankModel + + +class GPUStackRerankModel(RerankModel): + """ + Model class for GPUStack rerank model. + """ + + def _invoke( + self, + model: str, + credentials: dict, + query: str, + docs: list[str], + score_threshold: Optional[float] = None, + top_n: Optional[int] = None, + user: Optional[str] = None, + ) -> RerankResult: + """ + Invoke rerank model + + :param model: model name + :param credentials: model credentials + :param query: search query + :param docs: docs for reranking + :param score_threshold: score threshold + :param top_n: top n documents to return + :param user: unique user id + :return: rerank result + """ + if len(docs) == 0: + return RerankResult(model=model, docs=[]) + + endpoint_url = credentials["endpoint_url"] + headers = { + "Authorization": f"Bearer {credentials.get('api_key')}", + "Content-Type": "application/json", + } + + data = {"model": model, "query": query, "documents": docs, "top_n": top_n} + + try: + response = post( + str(URL(endpoint_url) / "v1" / "rerank"), + headers=headers, + data=dumps(data), + timeout=10, + ) + response.raise_for_status() + results = response.json() + + rerank_documents = [] + for result in results["results"]: + index = result["index"] + if "document" in result: + text = result["document"]["text"] + else: + text = docs[index] + + rerank_document = RerankDocument( + index=index, + text=text, + score=result["relevance_score"], + ) + + if score_threshold is None or result["relevance_score"] >= score_threshold: + rerank_documents.append(rerank_document) + + return RerankResult(model=model, docs=rerank_documents) + except httpx.HTTPStatusError as e: + raise InvokeServerUnavailableError(str(e)) + + def validate_credentials(self, model: str, credentials: dict) -> None: + """ + Validate model credentials + + :param model: model name + :param credentials: model credentials + :return: + """ + try: + self._invoke( + model=model, + credentials=credentials, + query="What is the capital of the United States?", + docs=[ + "Carson City is the capital city of the American state of Nevada. At the 2010 United States " + "Census, Carson City had a population of 55,274.", + "The Commonwealth of the Northern Mariana Islands is a group of islands in the Pacific Ocean that " + "are a political division controlled by the United States. Its capital is Saipan.", + ], + score_threshold=0.8, + ) + except Exception as ex: + raise CredentialsValidateFailedError(str(ex)) + + @property + def _invoke_error_mapping(self) -> dict[type[InvokeError], list[type[Exception]]]: + """ + Map model invoke error to unified error + """ + return { + InvokeConnectionError: [httpx.ConnectError], + InvokeServerUnavailableError: [httpx.RemoteProtocolError], + InvokeRateLimitError: [], + InvokeAuthorizationError: [httpx.HTTPStatusError], + InvokeBadRequestError: [httpx.RequestError], + } + + def get_customizable_model_schema(self, model: str, credentials: dict) -> AIModelEntity: + """ + generate custom model entities from credentials + """ + entity = AIModelEntity( + model=model, + label=I18nObject(en_US=model), + model_type=ModelType.RERANK, + fetch_from=FetchFrom.CUSTOMIZABLE_MODEL, + model_properties={ModelPropertyKey.CONTEXT_SIZE: int(credentials.get("context_size"))}, + ) + + return entity diff --git a/api/core/model_runtime/model_providers/gpustack/text_embedding/__init__.py b/api/core/model_runtime/model_providers/gpustack/text_embedding/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/core/model_runtime/model_providers/gpustack/text_embedding/text_embedding.py b/api/core/model_runtime/model_providers/gpustack/text_embedding/text_embedding.py new file mode 100644 index 0000000000..eb324491a2 --- /dev/null +++ b/api/core/model_runtime/model_providers/gpustack/text_embedding/text_embedding.py @@ -0,0 +1,35 @@ +from typing import Optional + +from yarl import URL + +from core.entities.embedding_type import EmbeddingInputType +from core.model_runtime.entities.text_embedding_entities import ( + TextEmbeddingResult, +) +from core.model_runtime.model_providers.openai_api_compatible.text_embedding.text_embedding import ( + OAICompatEmbeddingModel, +) + + +class GPUStackTextEmbeddingModel(OAICompatEmbeddingModel): + """ + Model class for GPUStack text embedding model. + """ + + def _invoke( + self, + model: str, + credentials: dict, + texts: list[str], + user: Optional[str] = None, + input_type: EmbeddingInputType = EmbeddingInputType.DOCUMENT, + ) -> TextEmbeddingResult: + return super()._invoke(model, credentials, texts, user, input_type) + + def validate_credentials(self, model: str, credentials: dict) -> None: + self._add_custom_parameters(credentials) + super().validate_credentials(model, credentials) + + @staticmethod + def _add_custom_parameters(credentials: dict) -> None: + credentials["endpoint_url"] = str(URL(credentials["endpoint_url"]) / "v1-openai") diff --git a/api/tests/integration_tests/.env.example b/api/tests/integration_tests/.env.example index fa4a2eb36c..baa100531f 100644 --- a/api/tests/integration_tests/.env.example +++ b/api/tests/integration_tests/.env.example @@ -96,5 +96,9 @@ VESSL_AI_MODEL_NAME= VESSL_AI_API_KEY= VESSL_AI_ENDPOINT_URL= +# GPUStack Credentials +GPUSTACK_SERVER_URL= +GPUSTACK_API_KEY= + # Gitee AI Credentials GITEE_AI_API_KEY= diff --git a/api/tests/integration_tests/model_runtime/gpustack/__init__.py b/api/tests/integration_tests/model_runtime/gpustack/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/tests/integration_tests/model_runtime/gpustack/test_embedding.py b/api/tests/integration_tests/model_runtime/gpustack/test_embedding.py new file mode 100644 index 0000000000..f56ad0dadc --- /dev/null +++ b/api/tests/integration_tests/model_runtime/gpustack/test_embedding.py @@ -0,0 +1,49 @@ +import os + +import pytest + +from core.model_runtime.entities.text_embedding_entities import TextEmbeddingResult +from core.model_runtime.errors.validate import CredentialsValidateFailedError +from core.model_runtime.model_providers.gpustack.text_embedding.text_embedding import ( + GPUStackTextEmbeddingModel, +) + + +def test_validate_credentials(): + model = GPUStackTextEmbeddingModel() + + with pytest.raises(CredentialsValidateFailedError): + model.validate_credentials( + model="bge-m3", + credentials={ + "endpoint_url": "invalid_url", + "api_key": "invalid_api_key", + }, + ) + + model.validate_credentials( + model="bge-m3", + credentials={ + "endpoint_url": os.environ.get("GPUSTACK_SERVER_URL"), + "api_key": os.environ.get("GPUSTACK_API_KEY"), + }, + ) + + +def test_invoke_model(): + model = GPUStackTextEmbeddingModel() + + result = model.invoke( + model="bge-m3", + credentials={ + "endpoint_url": os.environ.get("GPUSTACK_SERVER_URL"), + "api_key": os.environ.get("GPUSTACK_API_KEY"), + "context_size": 8192, + }, + texts=["hello", "world"], + user="abc-123", + ) + + assert isinstance(result, TextEmbeddingResult) + assert len(result.embeddings) == 2 + assert result.usage.total_tokens == 7 diff --git a/api/tests/integration_tests/model_runtime/gpustack/test_llm.py b/api/tests/integration_tests/model_runtime/gpustack/test_llm.py new file mode 100644 index 0000000000..326b7b16f0 --- /dev/null +++ b/api/tests/integration_tests/model_runtime/gpustack/test_llm.py @@ -0,0 +1,162 @@ +import os +from collections.abc import Generator + +import pytest + +from core.model_runtime.entities.llm_entities import ( + LLMResult, + LLMResultChunk, + LLMResultChunkDelta, +) +from core.model_runtime.entities.message_entities import ( + AssistantPromptMessage, + PromptMessageTool, + SystemPromptMessage, + UserPromptMessage, +) +from core.model_runtime.errors.validate import CredentialsValidateFailedError +from core.model_runtime.model_providers.gpustack.llm.llm import GPUStackLanguageModel + + +def test_validate_credentials_for_chat_model(): + model = GPUStackLanguageModel() + + with pytest.raises(CredentialsValidateFailedError): + model.validate_credentials( + model="llama-3.2-1b-instruct", + credentials={ + "endpoint_url": "invalid_url", + "api_key": "invalid_api_key", + "mode": "chat", + }, + ) + + model.validate_credentials( + model="llama-3.2-1b-instruct", + credentials={ + "endpoint_url": os.environ.get("GPUSTACK_SERVER_URL"), + "api_key": os.environ.get("GPUSTACK_API_KEY"), + "mode": "chat", + }, + ) + + +def test_invoke_completion_model(): + model = GPUStackLanguageModel() + + response = model.invoke( + model="llama-3.2-1b-instruct", + credentials={ + "endpoint_url": os.environ.get("GPUSTACK_SERVER_URL"), + "api_key": os.environ.get("GPUSTACK_API_KEY"), + "mode": "completion", + }, + prompt_messages=[UserPromptMessage(content="ping")], + model_parameters={"temperature": 0.7, "top_p": 1.0, "max_tokens": 10}, + stop=[], + user="abc-123", + stream=False, + ) + + assert isinstance(response, LLMResult) + assert len(response.message.content) > 0 + assert response.usage.total_tokens > 0 + + +def test_invoke_chat_model(): + model = GPUStackLanguageModel() + + response = model.invoke( + model="llama-3.2-1b-instruct", + credentials={ + "endpoint_url": os.environ.get("GPUSTACK_SERVER_URL"), + "api_key": os.environ.get("GPUSTACK_API_KEY"), + "mode": "chat", + }, + prompt_messages=[UserPromptMessage(content="ping")], + model_parameters={"temperature": 0.7, "top_p": 1.0, "max_tokens": 10}, + stop=[], + user="abc-123", + stream=False, + ) + + assert isinstance(response, LLMResult) + assert len(response.message.content) > 0 + assert response.usage.total_tokens > 0 + + +def test_invoke_stream_chat_model(): + model = GPUStackLanguageModel() + + response = model.invoke( + model="llama-3.2-1b-instruct", + credentials={ + "endpoint_url": os.environ.get("GPUSTACK_SERVER_URL"), + "api_key": os.environ.get("GPUSTACK_API_KEY"), + "mode": "chat", + }, + prompt_messages=[UserPromptMessage(content="Hello World!")], + model_parameters={"temperature": 0.7, "top_p": 1.0, "max_tokens": 10}, + stop=["you"], + stream=True, + user="abc-123", + ) + + assert isinstance(response, Generator) + for chunk in response: + assert isinstance(chunk, LLMResultChunk) + assert isinstance(chunk.delta, LLMResultChunkDelta) + assert isinstance(chunk.delta.message, AssistantPromptMessage) + assert len(chunk.delta.message.content) > 0 if chunk.delta.finish_reason is None else True + + +def test_get_num_tokens(): + model = GPUStackLanguageModel() + + num_tokens = model.get_num_tokens( + model="????", + credentials={ + "endpoint_url": os.environ.get("GPUSTACK_SERVER_URL"), + "api_key": os.environ.get("GPUSTACK_API_KEY"), + "mode": "chat", + }, + prompt_messages=[ + SystemPromptMessage( + content="You are a helpful AI assistant.", + ), + UserPromptMessage(content="Hello World!"), + ], + tools=[ + PromptMessageTool( + name="get_current_weather", + description="Get the current weather in a given location", + parameters={ + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "The city and state e.g. San Francisco, CA", + }, + "unit": {"type": "string", "enum": ["c", "f"]}, + }, + "required": ["location"], + }, + ) + ], + ) + + assert isinstance(num_tokens, int) + assert num_tokens == 80 + + num_tokens = model.get_num_tokens( + model="????", + credentials={ + "endpoint_url": os.environ.get("GPUSTACK_SERVER_URL"), + "api_key": os.environ.get("GPUSTACK_API_KEY"), + "mode": "chat", + }, + prompt_messages=[UserPromptMessage(content="Hello World!")], + ) + + assert isinstance(num_tokens, int) + assert num_tokens == 10 diff --git a/api/tests/integration_tests/model_runtime/gpustack/test_rerank.py b/api/tests/integration_tests/model_runtime/gpustack/test_rerank.py new file mode 100644 index 0000000000..f5c2d2d21c --- /dev/null +++ b/api/tests/integration_tests/model_runtime/gpustack/test_rerank.py @@ -0,0 +1,107 @@ +import os + +import pytest + +from core.model_runtime.entities.rerank_entities import RerankDocument, RerankResult +from core.model_runtime.errors.validate import CredentialsValidateFailedError +from core.model_runtime.model_providers.gpustack.rerank.rerank import ( + GPUStackRerankModel, +) + + +def test_validate_credentials_for_rerank_model(): + model = GPUStackRerankModel() + + with pytest.raises(CredentialsValidateFailedError): + model.validate_credentials( + model="bge-reranker-v2-m3", + credentials={ + "endpoint_url": "invalid_url", + "api_key": "invalid_api_key", + }, + ) + + model.validate_credentials( + model="bge-reranker-v2-m3", + credentials={ + "endpoint_url": os.environ.get("GPUSTACK_SERVER_URL"), + "api_key": os.environ.get("GPUSTACK_API_KEY"), + }, + ) + + +def test_invoke_rerank_model(): + model = GPUStackRerankModel() + + response = model.invoke( + model="bge-reranker-v2-m3", + credentials={ + "endpoint_url": os.environ.get("GPUSTACK_SERVER_URL"), + "api_key": os.environ.get("GPUSTACK_API_KEY"), + }, + query="Organic skincare products for sensitive skin", + docs=[ + "Eco-friendly kitchenware for modern homes", + "Biodegradable cleaning supplies for eco-conscious consumers", + "Organic cotton baby clothes for sensitive skin", + "Natural organic skincare range for sensitive skin", + "Tech gadgets for smart homes: 2024 edition", + "Sustainable gardening tools and compost solutions", + "Sensitive skin-friendly facial cleansers and toners", + "Organic food wraps and storage solutions", + "Yoga mats made from recycled materials", + ], + top_n=3, + score_threshold=-0.75, + user="abc-123", + ) + + assert isinstance(response, RerankResult) + assert len(response.docs) == 3 + + +def test__invoke(): + model = GPUStackRerankModel() + + # Test case 1: Empty docs + result = model._invoke( + model="bge-reranker-v2-m3", + credentials={ + "endpoint_url": os.environ.get("GPUSTACK_SERVER_URL"), + "api_key": os.environ.get("GPUSTACK_API_KEY"), + }, + query="Organic skincare products for sensitive skin", + docs=[], + top_n=3, + score_threshold=0.75, + user="abc-123", + ) + assert isinstance(result, RerankResult) + assert len(result.docs) == 0 + + # Test case 2: Expected docs + result = model._invoke( + model="bge-reranker-v2-m3", + credentials={ + "endpoint_url": os.environ.get("GPUSTACK_SERVER_URL"), + "api_key": os.environ.get("GPUSTACK_API_KEY"), + }, + query="Organic skincare products for sensitive skin", + docs=[ + "Eco-friendly kitchenware for modern homes", + "Biodegradable cleaning supplies for eco-conscious consumers", + "Organic cotton baby clothes for sensitive skin", + "Natural organic skincare range for sensitive skin", + "Tech gadgets for smart homes: 2024 edition", + "Sustainable gardening tools and compost solutions", + "Sensitive skin-friendly facial cleansers and toners", + "Organic food wraps and storage solutions", + "Yoga mats made from recycled materials", + ], + top_n=3, + score_threshold=-0.75, + user="abc-123", + ) + assert isinstance(result, RerankResult) + assert len(result.docs) == 3 + assert all(isinstance(doc, RerankDocument) for doc in result.docs) From d1c480a7d86c8a5eb8f99bbfac26743824817a2e Mon Sep 17 00:00:00 2001 From: jiangbo721 <365065261@qq.com> Date: Fri, 1 Nov 2024 17:25:31 +0800 Subject: [PATCH 04/48] fix: Cannot find declaration to go to CLEAN_DAY_SETTING (#10157) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 刘江波 --- api/schedule/clean_embedding_cache_task.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/schedule/clean_embedding_cache_task.py b/api/schedule/clean_embedding_cache_task.py index 67d0706828..9efe120b7a 100644 --- a/api/schedule/clean_embedding_cache_task.py +++ b/api/schedule/clean_embedding_cache_task.py @@ -14,7 +14,7 @@ from models.dataset import Embedding @app.celery.task(queue="dataset") def clean_embedding_cache_task(): click.echo(click.style("Start clean embedding cache.", fg="green")) - clean_days = int(dify_config.CLEAN_DAY_SETTING) + clean_days = int(dify_config.PLAN_SANDBOX_CLEAN_DAY_SETTING) start_at = time.perf_counter() thirty_days_ago = datetime.datetime.now() - datetime.timedelta(days=clean_days) while True: From 0d5c0b4fe4efcbbbe3f96d1a3670b8500346bb53 Mon Sep 17 00:00:00 2001 From: -LAN- Date: Fri, 1 Nov 2024 18:58:54 +0800 Subject: [PATCH 05/48] fix(workflow model): ensure consistent timestamp updating (#10172) --- api/models/workflow.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/api/models/workflow.py b/api/models/workflow.py index bc4434ae5a..1ad94d163b 100644 --- a/api/models/workflow.py +++ b/api/models/workflow.py @@ -1,6 +1,6 @@ import json from collections.abc import Mapping, Sequence -from datetime import datetime +from datetime import datetime, timezone from enum import Enum from typing import TYPE_CHECKING, Any, Optional, Union @@ -111,7 +111,9 @@ class Workflow(Base): db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)") ) updated_by: Mapped[Optional[str]] = mapped_column(StringUUID) - updated_at: Mapped[datetime] = mapped_column(db.DateTime, nullable=False) + updated_at: Mapped[datetime] = mapped_column( + sa.DateTime, nullable=False, default=datetime.now(tz=timezone.utc), server_onupdate=func.current_timestamp() + ) _environment_variables: Mapped[str] = mapped_column( "environment_variables", db.Text, nullable=False, server_default="{}" ) From ac1f93e3d5715c97501f0db55529dab611c0c9e8 Mon Sep 17 00:00:00 2001 From: Cling_o3 <45124798+ProseGuys@users.noreply.github.com> Date: Fri, 1 Nov 2024 18:59:15 +0800 Subject: [PATCH 06/48] [fix] fix the bug that modify document name not effective (#10154) --- api/services/dataset_service.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/api/services/dataset_service.py b/api/services/dataset_service.py index ac05cbc4f5..50da547fd8 100644 --- a/api/services/dataset_service.py +++ b/api/services/dataset_service.py @@ -986,9 +986,6 @@ class DocumentService: raise NotFound("Document not found") if document.display_status != "available": raise ValueError("Document is not available") - # update document name - if document_data.get("name"): - document.name = document_data["name"] # save process rule if document_data.get("process_rule"): process_rule = document_data["process_rule"] @@ -1065,6 +1062,10 @@ class DocumentService: document.data_source_type = document_data["data_source"]["type"] document.data_source_info = json.dumps(data_source_info) document.name = file_name + + # update document name + if document_data.get("name"): + document.name = document_data["name"] # update document to be waiting document.indexing_status = "waiting" document.completed_at = None From 81a77d06239fb9756a5882428d224a7c5cedb209 Mon Sep 17 00:00:00 2001 From: -LAN- Date: Fri, 1 Nov 2024 23:19:11 +0800 Subject: [PATCH 07/48] feat(document_extractor): integrate unstructured API for PPTX extraction (#10180) --- api/core/workflow/nodes/document_extractor/node.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/api/core/workflow/nodes/document_extractor/node.py b/api/core/workflow/nodes/document_extractor/node.py index c2f51ad1e5..aacee94095 100644 --- a/api/core/workflow/nodes/document_extractor/node.py +++ b/api/core/workflow/nodes/document_extractor/node.py @@ -6,12 +6,14 @@ import docx import pandas as pd import pypdfium2 import yaml +from unstructured.partition.api import partition_via_api from unstructured.partition.email import partition_email from unstructured.partition.epub import partition_epub from unstructured.partition.msg import partition_msg from unstructured.partition.ppt import partition_ppt from unstructured.partition.pptx import partition_pptx +from configs import dify_config from core.file import File, FileTransferMethod, file_manager from core.helper import ssrf_proxy from core.variables import ArrayFileSegment @@ -263,7 +265,14 @@ def _extract_text_from_ppt(file_content: bytes) -> str: def _extract_text_from_pptx(file_content: bytes) -> str: try: with io.BytesIO(file_content) as file: - elements = partition_pptx(file=file) + if dify_config.UNSTRUCTURED_API_URL and dify_config.UNSTRUCTURED_API_KEY: + elements = partition_via_api( + file=file, + api_url=dify_config.UNSTRUCTURED_API_URL, + api_key=dify_config.UNSTRUCTURED_API_KEY, + ) + else: + elements = partition_pptx(file=file) return "\n".join([getattr(element, "text", "") for element in elements]) except Exception as e: raise TextExtractionError(f"Failed to extract text from PPTX: {str(e)}") from e From 987e1b9ced7baa9fb9ee7cf74db80114c9b0f3bd Mon Sep 17 00:00:00 2001 From: -LAN- Date: Sat, 2 Nov 2024 17:03:00 +0800 Subject: [PATCH 08/48] fix(api): replace current_user with end_user in file upload (#10194) --- api/controllers/web/remote_files.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/api/controllers/web/remote_files.py b/api/controllers/web/remote_files.py index cb529340af..0b8a586d0c 100644 --- a/api/controllers/web/remote_files.py +++ b/api/controllers/web/remote_files.py @@ -1,6 +1,5 @@ import urllib.parse -from flask_login import current_user from flask_restful import marshal_with, reqparse from controllers.common import helpers @@ -27,7 +26,7 @@ class RemoteFileInfoApi(WebApiResource): class RemoteFileUploadApi(WebApiResource): @marshal_with(file_fields_with_signed_url) - def post(self): + def post(self, app_model, end_user): # Add app_model and end_user parameters parser = reqparse.RequestParser() parser.add_argument("url", type=str, required=True, help="URL is required") args = parser.parse_args() @@ -51,7 +50,7 @@ class RemoteFileUploadApi(WebApiResource): filename=file_info.filename, content=content, mimetype=file_info.mimetype, - user=current_user, + user=end_user, # Use end_user instead of current_user source_url=url, ) except Exception as e: From 8cd386f2c178ba300e47125d3a6e1c7bfe9cfc8b Mon Sep 17 00:00:00 2001 From: zxhlyh Date: Sat, 2 Nov 2024 17:03:14 +0800 Subject: [PATCH 09/48] fix: webapp upload file (#10195) --- web/app/components/base/file-uploader/hooks.ts | 2 +- web/service/common.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/web/app/components/base/file-uploader/hooks.ts b/web/app/components/base/file-uploader/hooks.ts index a78c414913..088160691b 100644 --- a/web/app/components/base/file-uploader/hooks.ts +++ b/web/app/components/base/file-uploader/hooks.ts @@ -216,7 +216,7 @@ export const useFile = (fileConfig: FileUpload) => { handleAddFile(uploadingFile) startProgressTimer(uploadingFile.id) - uploadRemoteFileInfo(url).then((res) => { + uploadRemoteFileInfo(url, !!params.token).then((res) => { const newFile = { ...uploadingFile, type: res.mime_type, diff --git a/web/service/common.ts b/web/service/common.ts index 4ea2d9fd27..81b96aa97c 100644 --- a/web/service/common.ts +++ b/web/service/common.ts @@ -324,8 +324,8 @@ export const verifyForgotPasswordToken: Fetcher = ({ url, body }) => post(url, { body }) -export const uploadRemoteFileInfo = (url: string) => { - return post<{ id: string; name: string; size: number; mime_type: string; url: string }>('/remote-files/upload', { body: { url } }) +export const uploadRemoteFileInfo = (url: string, isPublic?: boolean) => { + return post<{ id: string; name: string; size: number; mime_type: string; url: string }>('/remote-files/upload', { body: { url } }, { isPublicAPI: isPublic }) } export const sendEMailLoginCode = (email: string, language = 'en-US') => From e7d947379f69c94f5baab7e8bcaf68fd38adcfce Mon Sep 17 00:00:00 2001 From: Kota-Yamaguchi <50980947+Kota-Yamaguchi@users.noreply.github.com> Date: Sat, 2 Nov 2024 20:45:07 +0900 Subject: [PATCH 10/48] chore : code generator preview hint (#10188) --- .../config/code-generator/get-code-generator-res.tsx | 10 ++++++++++ web/i18n/en-US/app-debug.ts | 2 ++ web/i18n/ja-JP/app-debug.ts | 2 ++ web/i18n/zh-Hans/app-debug.ts | 2 ++ 4 files changed, 16 insertions(+) diff --git a/web/app/components/app/configuration/config/code-generator/get-code-generator-res.tsx b/web/app/components/app/configuration/config/code-generator/get-code-generator-res.tsx index b63e3e2693..85c522ca0f 100644 --- a/web/app/components/app/configuration/config/code-generator/get-code-generator-res.tsx +++ b/web/app/components/app/configuration/config/code-generator/get-code-generator-res.tsx @@ -105,6 +105,15 @@ export const GetCodeGeneratorResModal: FC = (

{t('appDebug.codegen.loading')}
) + const renderNoData = ( +
+ +
+
{t('appDebug.codegen.noDataLine1')}
+
{t('appDebug.codegen.noDataLine2')}
+
+
+ ) return ( = ( {isLoading && renderLoading} + {!isLoading && !res && renderNoData} {(!isLoading && res) && (
{t('appDebug.codegen.resTitle')}
diff --git a/web/i18n/en-US/app-debug.ts b/web/i18n/en-US/app-debug.ts index b2144262f6..e17afc38bf 100644 --- a/web/i18n/en-US/app-debug.ts +++ b/web/i18n/en-US/app-debug.ts @@ -224,6 +224,8 @@ const translation = { description: 'The Code Generator uses configured models to generate high-quality code based on your instructions. Please provide clear and detailed instructions.', instruction: 'Instructions', instructionPlaceholder: 'Enter detailed description of the code you want to generate.', + noDataLine1: 'Describe your use case on the left,', + noDataLine2: 'the code preview will show here.', generate: 'Generate', generatedCodeTitle: 'Generated Code', loading: 'Generating code...', diff --git a/web/i18n/ja-JP/app-debug.ts b/web/i18n/ja-JP/app-debug.ts index 620d9b2f55..05e81a2ae2 100644 --- a/web/i18n/ja-JP/app-debug.ts +++ b/web/i18n/ja-JP/app-debug.ts @@ -224,6 +224,8 @@ const translation = { description: 'コードジェネレーターは、設定されたモデルを使用して指示に基づいて高品質なコードを生成します。明確で詳細な指示を提供してください。', instruction: '指示', instructionPlaceholder: '生成したいコードの詳細な説明を入力してください。', + noDataLine1: '左側に使用例を記入してください,', + noDataLine2: 'コードのプレビューがこちらに表示されます。', generate: '生成', generatedCodeTitle: '生成されたコード', loading: 'コードを生成中...', diff --git a/web/i18n/zh-Hans/app-debug.ts b/web/i18n/zh-Hans/app-debug.ts index 3e801bcf62..9e21945755 100644 --- a/web/i18n/zh-Hans/app-debug.ts +++ b/web/i18n/zh-Hans/app-debug.ts @@ -224,6 +224,8 @@ const translation = { description: '代码生成器使用配置的模型根据您的指令生成高质量的代码。请提供清晰详细的说明。', instruction: '指令', instructionPlaceholder: '请输入您想要生成的代码的详细描述。', + noDataLine1: '在左侧描述您的用例,', + noDataLine2: '代码预览将在此处显示。', generate: '生成', generatedCodeTitle: '生成的代码', loading: '正在生成代码...', From aa9fd76072521088312e5ae0e4a0d2cd58fc72e9 Mon Sep 17 00:00:00 2001 From: Kota-Yamaguchi <50980947+Kota-Yamaguchi@users.noreply.github.com> Date: Sat, 2 Nov 2024 20:46:28 +0900 Subject: [PATCH 11/48] Feat : add LLM model indicator in prompt generator (#10187) --- .../config/automatic/get-automatic-res.tsx | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/web/app/components/app/configuration/config/automatic/get-automatic-res.tsx b/web/app/components/app/configuration/config/automatic/get-automatic-res.tsx index 05339c7216..0a20f4b376 100644 --- a/web/app/components/app/configuration/config/automatic/get-automatic-res.tsx +++ b/web/app/components/app/configuration/config/automatic/get-automatic-res.tsx @@ -33,6 +33,10 @@ import { LoveMessage } from '@/app/components/base/icons/src/vender/features' // type import type { AutomaticRes } from '@/service/debug' import { Generator } from '@/app/components/base/icons/src/vender/other' +import ModelIcon from '@/app/components/header/account-setting/model-provider-page/model-icon' +import ModelName from '@/app/components/header/account-setting/model-provider-page/model-name' +import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' +import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks' export type IGetAutomaticResProps = { mode: AppType @@ -68,7 +72,10 @@ const GetAutomaticRes: FC = ({ onFinished, }) => { const { t } = useTranslation() - + const { + currentProvider, + currentModel, + } = useModelListAndDefaultModelAndCurrentProviderAndModel(ModelTypeEnum.textGeneration) const tryList = [ { icon: RiTerminalBoxLine, @@ -191,6 +198,19 @@ const GetAutomaticRes: FC = ({
{t('appDebug.generate.title')}
{t('appDebug.generate.description')}
+
+ + +
{t('appDebug.generate.tryIt')}
From 7c458595946fb4e9a9f987cc63ed269d0e69ea01 Mon Sep 17 00:00:00 2001 From: -LAN- Date: Sun, 3 Nov 2024 11:55:07 +0800 Subject: [PATCH 12/48] fix(document_extractor): update base exception class (#10208) --- api/core/workflow/nodes/document_extractor/exc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/core/workflow/nodes/document_extractor/exc.py b/api/core/workflow/nodes/document_extractor/exc.py index c9d4bb8ef6..5caf00ebc5 100644 --- a/api/core/workflow/nodes/document_extractor/exc.py +++ b/api/core/workflow/nodes/document_extractor/exc.py @@ -1,4 +1,4 @@ -class DocumentExtractorError(Exception): +class DocumentExtractorError(ValueError): """Base exception for errors related to the DocumentExtractorNode.""" From b01e7d778ec50fb51ab808143338a01674c1382e Mon Sep 17 00:00:00 2001 From: -LAN- Date: Sun, 3 Nov 2024 11:55:19 +0800 Subject: [PATCH 13/48] chore(list_operator): refine exception handling for error specificity (#10206) --- api/core/workflow/nodes/list_operator/exc.py | 16 ++ api/core/workflow/nodes/list_operator/node.py | 153 +++++++++++------- 2 files changed, 112 insertions(+), 57 deletions(-) create mode 100644 api/core/workflow/nodes/list_operator/exc.py diff --git a/api/core/workflow/nodes/list_operator/exc.py b/api/core/workflow/nodes/list_operator/exc.py new file mode 100644 index 0000000000..f88aa0be29 --- /dev/null +++ b/api/core/workflow/nodes/list_operator/exc.py @@ -0,0 +1,16 @@ +class ListOperatorError(ValueError): + """Base class for all ListOperator errors.""" + + pass + + +class InvalidFilterValueError(ListOperatorError): + pass + + +class InvalidKeyError(ListOperatorError): + pass + + +class InvalidConditionError(ListOperatorError): + pass diff --git a/api/core/workflow/nodes/list_operator/node.py b/api/core/workflow/nodes/list_operator/node.py index d7e4c64313..6053a15d96 100644 --- a/api/core/workflow/nodes/list_operator/node.py +++ b/api/core/workflow/nodes/list_operator/node.py @@ -1,5 +1,5 @@ from collections.abc import Callable, Sequence -from typing import Literal +from typing import Literal, Union from core.file import File from core.variables import ArrayFileSegment, ArrayNumberSegment, ArrayStringSegment @@ -9,6 +9,7 @@ from core.workflow.nodes.enums import NodeType from models.workflow import WorkflowNodeExecutionStatus from .entities import ListOperatorNodeData +from .exc import InvalidConditionError, InvalidFilterValueError, InvalidKeyError, ListOperatorError class ListOperatorNode(BaseNode[ListOperatorNodeData]): @@ -26,7 +27,17 @@ class ListOperatorNode(BaseNode[ListOperatorNodeData]): return NodeRunResult( status=WorkflowNodeExecutionStatus.FAILED, error=error_message, inputs=inputs, outputs=outputs ) - if variable.value and not isinstance(variable, ArrayFileSegment | ArrayNumberSegment | ArrayStringSegment): + if not variable.value: + inputs = {"variable": []} + process_data = {"variable": []} + outputs = {"result": [], "first_record": None, "last_record": None} + return NodeRunResult( + status=WorkflowNodeExecutionStatus.SUCCEEDED, + inputs=inputs, + process_data=process_data, + outputs=outputs, + ) + if not isinstance(variable, ArrayFileSegment | ArrayNumberSegment | ArrayStringSegment): error_message = ( f"Variable {self.node_data.variable} is not an ArrayFileSegment, ArrayNumberSegment " "or ArrayStringSegment" @@ -36,70 +47,98 @@ class ListOperatorNode(BaseNode[ListOperatorNodeData]): ) if isinstance(variable, ArrayFileSegment): + inputs = {"variable": [item.to_dict() for item in variable.value]} process_data["variable"] = [item.to_dict() for item in variable.value] else: + inputs = {"variable": variable.value} process_data["variable"] = variable.value - # Filter - if self.node_data.filter_by.enabled: - for condition in self.node_data.filter_by.conditions: - if isinstance(variable, ArrayStringSegment): - if not isinstance(condition.value, str): - raise ValueError(f"Invalid filter value: {condition.value}") - value = self.graph_runtime_state.variable_pool.convert_template(condition.value).text - filter_func = _get_string_filter_func(condition=condition.comparison_operator, value=value) - result = list(filter(filter_func, variable.value)) - variable = variable.model_copy(update={"value": result}) - elif isinstance(variable, ArrayNumberSegment): - if not isinstance(condition.value, str): - raise ValueError(f"Invalid filter value: {condition.value}") - value = self.graph_runtime_state.variable_pool.convert_template(condition.value).text - filter_func = _get_number_filter_func(condition=condition.comparison_operator, value=float(value)) - result = list(filter(filter_func, variable.value)) - variable = variable.model_copy(update={"value": result}) - elif isinstance(variable, ArrayFileSegment): - if isinstance(condition.value, str): - value = self.graph_runtime_state.variable_pool.convert_template(condition.value).text - else: - value = condition.value - filter_func = _get_file_filter_func( - key=condition.key, - condition=condition.comparison_operator, - value=value, - ) - result = list(filter(filter_func, variable.value)) - variable = variable.model_copy(update={"value": result}) + try: + # Filter + if self.node_data.filter_by.enabled: + variable = self._apply_filter(variable) - # Order - if self.node_data.order_by.enabled: + # Order + if self.node_data.order_by.enabled: + variable = self._apply_order(variable) + + # Slice + if self.node_data.limit.enabled: + variable = self._apply_slice(variable) + + outputs = { + "result": variable.value, + "first_record": variable.value[0] if variable.value else None, + "last_record": variable.value[-1] if variable.value else None, + } + return NodeRunResult( + status=WorkflowNodeExecutionStatus.SUCCEEDED, + inputs=inputs, + process_data=process_data, + outputs=outputs, + ) + except ListOperatorError as e: + return NodeRunResult( + status=WorkflowNodeExecutionStatus.FAILED, + error=str(e), + inputs=inputs, + process_data=process_data, + outputs=outputs, + ) + + def _apply_filter( + self, variable: Union[ArrayFileSegment, ArrayNumberSegment, ArrayStringSegment] + ) -> Union[ArrayFileSegment, ArrayNumberSegment, ArrayStringSegment]: + for condition in self.node_data.filter_by.conditions: if isinstance(variable, ArrayStringSegment): - result = _order_string(order=self.node_data.order_by.value, array=variable.value) + if not isinstance(condition.value, str): + raise InvalidFilterValueError(f"Invalid filter value: {condition.value}") + value = self.graph_runtime_state.variable_pool.convert_template(condition.value).text + filter_func = _get_string_filter_func(condition=condition.comparison_operator, value=value) + result = list(filter(filter_func, variable.value)) variable = variable.model_copy(update={"value": result}) elif isinstance(variable, ArrayNumberSegment): - result = _order_number(order=self.node_data.order_by.value, array=variable.value) + if not isinstance(condition.value, str): + raise InvalidFilterValueError(f"Invalid filter value: {condition.value}") + value = self.graph_runtime_state.variable_pool.convert_template(condition.value).text + filter_func = _get_number_filter_func(condition=condition.comparison_operator, value=float(value)) + result = list(filter(filter_func, variable.value)) variable = variable.model_copy(update={"value": result}) elif isinstance(variable, ArrayFileSegment): - result = _order_file( - order=self.node_data.order_by.value, order_by=self.node_data.order_by.key, array=variable.value + if isinstance(condition.value, str): + value = self.graph_runtime_state.variable_pool.convert_template(condition.value).text + else: + value = condition.value + filter_func = _get_file_filter_func( + key=condition.key, + condition=condition.comparison_operator, + value=value, ) + result = list(filter(filter_func, variable.value)) variable = variable.model_copy(update={"value": result}) + return variable - # Slice - if self.node_data.limit.enabled: - result = variable.value[: self.node_data.limit.size] + def _apply_order( + self, variable: Union[ArrayFileSegment, ArrayNumberSegment, ArrayStringSegment] + ) -> Union[ArrayFileSegment, ArrayNumberSegment, ArrayStringSegment]: + if isinstance(variable, ArrayStringSegment): + result = _order_string(order=self.node_data.order_by.value, array=variable.value) variable = variable.model_copy(update={"value": result}) + elif isinstance(variable, ArrayNumberSegment): + result = _order_number(order=self.node_data.order_by.value, array=variable.value) + variable = variable.model_copy(update={"value": result}) + elif isinstance(variable, ArrayFileSegment): + result = _order_file( + order=self.node_data.order_by.value, order_by=self.node_data.order_by.key, array=variable.value + ) + variable = variable.model_copy(update={"value": result}) + return variable - outputs = { - "result": variable.value, - "first_record": variable.value[0] if variable.value else None, - "last_record": variable.value[-1] if variable.value else None, - } - return NodeRunResult( - status=WorkflowNodeExecutionStatus.SUCCEEDED, - inputs=inputs, - process_data=process_data, - outputs=outputs, - ) + def _apply_slice( + self, variable: Union[ArrayFileSegment, ArrayNumberSegment, ArrayStringSegment] + ) -> Union[ArrayFileSegment, ArrayNumberSegment, ArrayStringSegment]: + result = variable.value[: self.node_data.limit.size] + return variable.model_copy(update={"value": result}) def _get_file_extract_number_func(*, key: str) -> Callable[[File], int]: @@ -107,7 +146,7 @@ def _get_file_extract_number_func(*, key: str) -> Callable[[File], int]: case "size": return lambda x: x.size case _: - raise ValueError(f"Invalid key: {key}") + raise InvalidKeyError(f"Invalid key: {key}") def _get_file_extract_string_func(*, key: str) -> Callable[[File], str]: @@ -125,7 +164,7 @@ def _get_file_extract_string_func(*, key: str) -> Callable[[File], str]: case "url": return lambda x: x.remote_url or "" case _: - raise ValueError(f"Invalid key: {key}") + raise InvalidKeyError(f"Invalid key: {key}") def _get_string_filter_func(*, condition: str, value: str) -> Callable[[str], bool]: @@ -151,7 +190,7 @@ def _get_string_filter_func(*, condition: str, value: str) -> Callable[[str], bo case "not empty": return lambda x: x != "" case _: - raise ValueError(f"Invalid condition: {condition}") + raise InvalidConditionError(f"Invalid condition: {condition}") def _get_sequence_filter_func(*, condition: str, value: Sequence[str]) -> Callable[[str], bool]: @@ -161,7 +200,7 @@ def _get_sequence_filter_func(*, condition: str, value: Sequence[str]) -> Callab case "not in": return lambda x: not _in(value)(x) case _: - raise ValueError(f"Invalid condition: {condition}") + raise InvalidConditionError(f"Invalid condition: {condition}") def _get_number_filter_func(*, condition: str, value: int | float) -> Callable[[int | float], bool]: @@ -179,7 +218,7 @@ def _get_number_filter_func(*, condition: str, value: int | float) -> Callable[[ case "≥": return _ge(value) case _: - raise ValueError(f"Invalid condition: {condition}") + raise InvalidConditionError(f"Invalid condition: {condition}") def _get_file_filter_func(*, key: str, condition: str, value: str | Sequence[str]) -> Callable[[File], bool]: @@ -193,7 +232,7 @@ def _get_file_filter_func(*, key: str, condition: str, value: str | Sequence[str extract_func = _get_file_extract_number_func(key=key) return lambda x: _get_number_filter_func(condition=condition, value=float(value))(extract_func(x)) else: - raise ValueError(f"Invalid key: {key}") + raise InvalidKeyError(f"Invalid key: {key}") def _contains(value: str): From 3ea3df7189a57686125896f247cf3e4bb6b7c0ea Mon Sep 17 00:00:00 2001 From: -LAN- Date: Sun, 3 Nov 2024 11:55:46 +0800 Subject: [PATCH 14/48] refactor(validation): improve input validation logic (#10175) --- api/core/app/apps/base_app_generator.py | 45 ++++++++++++++----------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/api/core/app/apps/base_app_generator.py b/api/core/app/apps/base_app_generator.py index 993f8e904d..2ce7d5d96f 100644 --- a/api/core/app/apps/base_app_generator.py +++ b/api/core/app/apps/base_app_generator.py @@ -77,6 +77,7 @@ class BaseAppGenerator: def _validate_input(self, *, inputs: Mapping[str, Any], var: "VariableEntity"): user_input_value = inputs.get(var.variable) + if not user_input_value: if var.required: raise ValueError(f"{var.variable} is required in input form") @@ -89,6 +90,7 @@ class BaseAppGenerator: VariableEntityType.PARAGRAPH, } and not isinstance(user_input_value, str): raise ValueError(f"(type '{var.type}') {var.variable} in input form must be a string") + if var.type == VariableEntityType.NUMBER and isinstance(user_input_value, str): # may raise ValueError if user_input_value is not a valid number try: @@ -98,25 +100,30 @@ class BaseAppGenerator: return int(user_input_value) except ValueError: raise ValueError(f"{var.variable} in input form must be a valid number") - if var.type == VariableEntityType.SELECT: - options = var.options - if user_input_value not in options: - raise ValueError(f"{var.variable} in input form must be one of the following: {options}") - elif var.type in {VariableEntityType.TEXT_INPUT, VariableEntityType.PARAGRAPH}: - if var.max_length and len(user_input_value) > var.max_length: - raise ValueError(f"{var.variable} in input form must be less than {var.max_length} characters") - elif var.type == VariableEntityType.FILE: - if not isinstance(user_input_value, dict) and not isinstance(user_input_value, File): - raise ValueError(f"{var.variable} in input form must be a file") - elif var.type == VariableEntityType.FILE_LIST: - if not ( - isinstance(user_input_value, list) - and ( - all(isinstance(item, dict) for item in user_input_value) - or all(isinstance(item, File) for item in user_input_value) - ) - ): - raise ValueError(f"{var.variable} in input form must be a list of files") + + match var.type: + case VariableEntityType.SELECT: + if user_input_value not in var.options: + raise ValueError(f"{var.variable} in input form must be one of the following: {var.options}") + case VariableEntityType.TEXT_INPUT | VariableEntityType.PARAGRAPH: + if var.max_length and len(user_input_value) > var.max_length: + raise ValueError(f"{var.variable} in input form must be less than {var.max_length} characters") + case VariableEntityType.FILE: + if not isinstance(user_input_value, dict) and not isinstance(user_input_value, File): + raise ValueError(f"{var.variable} in input form must be a file") + case VariableEntityType.FILE_LIST: + # if number of files exceeds the limit, raise ValueError + if not ( + isinstance(user_input_value, list) + and ( + all(isinstance(item, dict) for item in user_input_value) + or all(isinstance(item, File) for item in user_input_value) + ) + ): + raise ValueError(f"{var.variable} in input form must be a list of files") + + if var.max_length and len(user_input_value) > var.max_length: + raise ValueError(f"{var.variable} in input form must be less than {var.max_length} files") return user_input_value From e79c3e453177361ecb4c68769b90677080869ebc Mon Sep 17 00:00:00 2001 From: crazywoola <100913391+crazywoola@users.noreply.github.com> Date: Sun, 3 Nov 2024 12:53:49 +0800 Subject: [PATCH 15/48] Fix/10199 application error a client side exception has occurred see the browser console for more information (#10211) --- .../nodes/_base/hooks/use-one-step-run.ts | 31 +++++++++---------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/web/app/components/workflow/nodes/_base/hooks/use-one-step-run.ts b/web/app/components/workflow/nodes/_base/hooks/use-one-step-run.ts index 59ebb72b72..c500f0c8cf 100644 --- a/web/app/components/workflow/nodes/_base/hooks/use-one-step-run.ts +++ b/web/app/components/workflow/nodes/_base/hooks/use-one-step-run.ts @@ -105,32 +105,29 @@ const useOneStepRun = ({ const availableNodesIncludeParent = getBeforeNodesInSameBranchIncludeParent(id) const allOutputVars = toNodeOutputVars(availableNodes, isChatMode, undefined, undefined, conversationVariables) const getVar = (valueSelector: ValueSelector): Var | undefined => { - let res: Var | undefined const isSystem = valueSelector[0] === 'sys' - const targetVar = isSystem ? allOutputVars.find(item => !!item.isStartNode) : allOutputVars.find(v => v.nodeId === valueSelector[0]) + const targetVar = allOutputVars.find(item => isSystem ? !!item.isStartNode : item.nodeId === valueSelector[0]) if (!targetVar) return undefined + if (isSystem) return targetVar.vars.find(item => item.variable.split('.')[1] === valueSelector[1]) let curr: any = targetVar.vars - if (!curr) - return + for (let i = 1; i < valueSelector.length; i++) { + const key = valueSelector[i] + const isLast = i === valueSelector.length - 1 - valueSelector.slice(1).forEach((key, i) => { - const isLast = i === valueSelector.length - 2 - // conversation variable is start with 'conversation.' - curr = curr?.find((v: any) => v.variable.replace('conversation.', '') === key) - if (isLast) { - res = curr - } - else { - if (curr?.type === VarType.object || curr?.type === VarType.file) - curr = curr.children - } - }) + if (Array.isArray(curr)) + curr = curr.find((v: any) => v.variable.replace('conversation.', '') === key) - return res + if (isLast) + return curr + else if (curr?.type === VarType.object || curr?.type === VarType.file) + curr = curr.children + } + + return undefined } const checkValid = checkValidFns[data.type] From fe1c0ac602909b2dfea59cc923bc5a9ebef4ee84 Mon Sep 17 00:00:00 2001 From: Jiang <65766008+AlwaysBluer@users.noreply.github.com> Date: Mon, 4 Nov 2024 09:10:26 +0800 Subject: [PATCH 16/48] Add Lindorm as a VDB choice (#10202) Co-authored-by: jiangzhijie --- api/.env.example | 9 +- api/configs/middleware/__init__.py | 2 + api/configs/middleware/vdb/lindorm_config.py | 23 + api/controllers/console/datasets/datasets.py | 5 +- .../rag/datasource/vdb/lindorm/__init__.py | 0 .../datasource/vdb/lindorm/lindorm_vector.py | 498 ++++++++++++++++++ api/core/rag/datasource/vdb/vector_factory.py | 4 + api/core/rag/datasource/vdb/vector_type.py | 1 + .../integration_tests/vdb/lindorm/__init__.py | 0 .../vdb/lindorm/test_lindorm.py | 35 ++ docker/.env.example | 8 +- docker/docker-compose.yaml | 3 + 12 files changed, 584 insertions(+), 4 deletions(-) create mode 100644 api/configs/middleware/vdb/lindorm_config.py create mode 100644 api/core/rag/datasource/vdb/lindorm/__init__.py create mode 100644 api/core/rag/datasource/vdb/lindorm/lindorm_vector.py create mode 100644 api/tests/integration_tests/vdb/lindorm/__init__.py create mode 100644 api/tests/integration_tests/vdb/lindorm/test_lindorm.py diff --git a/api/.env.example b/api/.env.example index d13f4a13ad..fb785db174 100644 --- a/api/.env.example +++ b/api/.env.example @@ -120,7 +120,8 @@ SUPABASE_URL=your-server-url WEB_API_CORS_ALLOW_ORIGINS=http://127.0.0.1:3000,* CONSOLE_CORS_ALLOW_ORIGINS=http://127.0.0.1:3000,* -# Vector database configuration, support: weaviate, qdrant, milvus, myscale, relyt, pgvecto_rs, pgvector, pgvector, chroma, opensearch, tidb_vector, couchbase, vikingdb, upstash + +# Vector database configuration, support: weaviate, qdrant, milvus, myscale, relyt, pgvecto_rs, pgvector, pgvector, chroma, opensearch, tidb_vector, couchbase, vikingdb, upstash, lindorm VECTOR_STORE=weaviate # Weaviate configuration @@ -263,6 +264,11 @@ VIKINGDB_SCHEMA=http VIKINGDB_CONNECTION_TIMEOUT=30 VIKINGDB_SOCKET_TIMEOUT=30 +# Lindorm configuration +LINDORM_URL=http://ld-*******************-proxy-search-pub.lindorm.aliyuncs.com:30070 +LINDORM_USERNAME=admin +LINDORM_PASSWORD=admin + # OceanBase Vector configuration OCEANBASE_VECTOR_HOST=127.0.0.1 OCEANBASE_VECTOR_PORT=2881 @@ -271,6 +277,7 @@ OCEANBASE_VECTOR_PASSWORD= OCEANBASE_VECTOR_DATABASE=test OCEANBASE_MEMORY_LIMIT=6G + # Upload configuration UPLOAD_FILE_SIZE_LIMIT=15 UPLOAD_FILE_BATCH_LIMIT=5 diff --git a/api/configs/middleware/__init__.py b/api/configs/middleware/__init__.py index 4be761747d..57cc805ebf 100644 --- a/api/configs/middleware/__init__.py +++ b/api/configs/middleware/__init__.py @@ -20,6 +20,7 @@ from configs.middleware.vdb.baidu_vector_config import BaiduVectorDBConfig from configs.middleware.vdb.chroma_config import ChromaConfig from configs.middleware.vdb.couchbase_config import CouchbaseConfig from configs.middleware.vdb.elasticsearch_config import ElasticsearchConfig +from configs.middleware.vdb.lindorm_config import LindormConfig from configs.middleware.vdb.milvus_config import MilvusConfig from configs.middleware.vdb.myscale_config import MyScaleConfig from configs.middleware.vdb.oceanbase_config import OceanBaseVectorConfig @@ -259,6 +260,7 @@ class MiddlewareConfig( VikingDBConfig, UpstashConfig, TidbOnQdrantConfig, + LindormConfig, OceanBaseVectorConfig, BaiduVectorDBConfig, ): diff --git a/api/configs/middleware/vdb/lindorm_config.py b/api/configs/middleware/vdb/lindorm_config.py new file mode 100644 index 0000000000..0f6c652806 --- /dev/null +++ b/api/configs/middleware/vdb/lindorm_config.py @@ -0,0 +1,23 @@ +from typing import Optional + +from pydantic import Field +from pydantic_settings import BaseSettings + + +class LindormConfig(BaseSettings): + """ + Lindorm configs + """ + + LINDORM_URL: Optional[str] = Field( + description="Lindorm url", + default=None, + ) + LINDORM_USERNAME: Optional[str] = Field( + description="Lindorm user", + default=None, + ) + LINDORM_PASSWORD: Optional[str] = Field( + description="Lindorm password", + default=None, + ) diff --git a/api/controllers/console/datasets/datasets.py b/api/controllers/console/datasets/datasets.py index 07ef0ce3e5..82163a32ee 100644 --- a/api/controllers/console/datasets/datasets.py +++ b/api/controllers/console/datasets/datasets.py @@ -456,7 +456,7 @@ class DatasetIndexingEstimateApi(Resource): ) except LLMBadRequestError: raise ProviderNotInitializeError( - "No Embedding Model available. Please configure a valid provider in the Settings -> Model Provider." + "No Embedding Model available. Please configure a valid provider " "in the Settings -> Model Provider." ) except ProviderTokenNotInitError as ex: raise ProviderNotInitializeError(ex.description) @@ -620,6 +620,7 @@ class DatasetRetrievalSettingApi(Resource): case ( VectorType.MILVUS | VectorType.RELYT + | VectorType.PGVECTOR | VectorType.TIDB_VECTOR | VectorType.CHROMA | VectorType.TENCENT @@ -640,6 +641,7 @@ class DatasetRetrievalSettingApi(Resource): | VectorType.ELASTICSEARCH | VectorType.PGVECTOR | VectorType.TIDB_ON_QDRANT + | VectorType.LINDORM | VectorType.COUCHBASE ): return { @@ -682,6 +684,7 @@ class DatasetRetrievalSettingMockApi(Resource): | VectorType.ELASTICSEARCH | VectorType.COUCHBASE | VectorType.PGVECTOR + | VectorType.LINDORM ): return { "retrieval_method": [ diff --git a/api/core/rag/datasource/vdb/lindorm/__init__.py b/api/core/rag/datasource/vdb/lindorm/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/core/rag/datasource/vdb/lindorm/lindorm_vector.py b/api/core/rag/datasource/vdb/lindorm/lindorm_vector.py new file mode 100644 index 0000000000..abd8261a69 --- /dev/null +++ b/api/core/rag/datasource/vdb/lindorm/lindorm_vector.py @@ -0,0 +1,498 @@ +import copy +import json +import logging +from collections.abc import Iterable +from typing import Any, Optional + +from opensearchpy import OpenSearch +from opensearchpy.helpers import bulk +from pydantic import BaseModel, model_validator +from tenacity import retry, stop_after_attempt, wait_fixed + +from configs import dify_config +from core.rag.datasource.vdb.field import Field +from core.rag.datasource.vdb.vector_base import BaseVector +from core.rag.datasource.vdb.vector_factory import AbstractVectorFactory +from core.rag.datasource.vdb.vector_type import VectorType +from core.rag.embedding.embedding_base import Embeddings +from core.rag.models.document import Document +from extensions.ext_redis import redis_client +from models.dataset import Dataset + +logger = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s") +logging.getLogger("lindorm").setLevel(logging.WARN) + + +class LindormVectorStoreConfig(BaseModel): + hosts: str + username: Optional[str] = None + password: Optional[str] = None + + @model_validator(mode="before") + @classmethod + def validate_config(cls, values: dict) -> dict: + if not values["hosts"]: + raise ValueError("config URL is required") + if not values["username"]: + raise ValueError("config USERNAME is required") + if not values["password"]: + raise ValueError("config PASSWORD is required") + return values + + def to_opensearch_params(self) -> dict[str, Any]: + params = { + "hosts": self.hosts, + } + if self.username and self.password: + params["http_auth"] = (self.username, self.password) + return params + + +class LindormVectorStore(BaseVector): + def __init__(self, collection_name: str, config: LindormVectorStoreConfig, **kwargs): + super().__init__(collection_name.lower()) + self._client_config = config + self._client = OpenSearch(**config.to_opensearch_params()) + self.kwargs = kwargs + + def get_type(self) -> str: + return VectorType.LINDORM + + def create(self, texts: list[Document], embeddings: list[list[float]], **kwargs): + self.create_collection(len(embeddings[0]), **kwargs) + self.add_texts(texts, embeddings) + + def refresh(self): + self._client.indices.refresh(index=self._collection_name) + + def __filter_existed_ids( + self, + texts: list[str], + metadatas: list[dict], + ids: list[str], + bulk_size: int = 1024, + ) -> tuple[Iterable[str], Optional[list[dict]], Optional[list[str]]]: + @retry(stop=stop_after_attempt(3), wait=wait_fixed(60)) + def __fetch_existing_ids(batch_ids: list[str]) -> set[str]: + try: + existing_docs = self._client.mget(index=self._collection_name, body={"ids": batch_ids}, _source=False) + return {doc["_id"] for doc in existing_docs["docs"] if doc["found"]} + except Exception as e: + logger.error(f"Error fetching batch {batch_ids}: {e}") + return set() + + @retry(stop=stop_after_attempt(3), wait=wait_fixed(60)) + def __fetch_existing_routing_ids(batch_ids: list[str], route_ids: list[str]) -> set[str]: + try: + existing_docs = self._client.mget( + body={ + "docs": [ + {"_index": self._collection_name, "_id": id, "routing": routing} + for id, routing in zip(batch_ids, route_ids) + ] + }, + _source=False, + ) + return {doc["_id"] for doc in existing_docs["docs"] if doc["found"]} + except Exception as e: + logger.error(f"Error fetching batch {batch_ids}: {e}") + return set() + + if ids is None: + return texts, metadatas, ids + + if len(texts) != len(ids): + raise RuntimeError(f"texts {len(texts)} != {ids}") + + filtered_texts = [] + filtered_metadatas = [] + filtered_ids = [] + + def batch(iterable, n): + length = len(iterable) + for idx in range(0, length, n): + yield iterable[idx : min(idx + n, length)] + + for ids_batch, texts_batch, metadatas_batch in zip( + batch(ids, bulk_size), + batch(texts, bulk_size), + batch(metadatas, bulk_size) if metadatas is not None else batch([None] * len(ids), bulk_size), + ): + existing_ids_set = __fetch_existing_ids(ids_batch) + for text, metadata, doc_id in zip(texts_batch, metadatas_batch, ids_batch): + if doc_id not in existing_ids_set: + filtered_texts.append(text) + filtered_ids.append(doc_id) + if metadatas is not None: + filtered_metadatas.append(metadata) + + return filtered_texts, metadatas if metadatas is None else filtered_metadatas, filtered_ids + + def add_texts(self, documents: list[Document], embeddings: list[list[float]], **kwargs): + actions = [] + uuids = self._get_uuids(documents) + for i in range(len(documents)): + action = { + "_op_type": "index", + "_index": self._collection_name.lower(), + "_id": uuids[i], + "_source": { + Field.CONTENT_KEY.value: documents[i].page_content, + Field.VECTOR.value: embeddings[i], # Make sure you pass an array here + Field.METADATA_KEY.value: documents[i].metadata, + }, + } + actions.append(action) + bulk(self._client, actions) + self.refresh() + + def get_ids_by_metadata_field(self, key: str, value: str): + query = {"query": {"term": {f"{Field.METADATA_KEY.value}.{key}.keyword": value}}} + response = self._client.search(index=self._collection_name, body=query) + if response["hits"]["hits"]: + return [hit["_id"] for hit in response["hits"]["hits"]] + else: + return None + + def delete_by_metadata_field(self, key: str, value: str): + query_str = {"query": {"match": {f"metadata.{key}": f"{value}"}}} + results = self._client.search(index=self._collection_name, body=query_str) + ids = [hit["_id"] for hit in results["hits"]["hits"]] + if ids: + self.delete_by_ids(ids) + + def delete_by_ids(self, ids: list[str]) -> None: + for id in ids: + if self._client.exists(index=self._collection_name, id=id): + self._client.delete(index=self._collection_name, id=id) + else: + logger.warning(f"DELETE BY ID: ID {id} does not exist in the index.") + + def delete(self) -> None: + try: + if self._client.indices.exists(index=self._collection_name): + self._client.indices.delete(index=self._collection_name, params={"timeout": 60}) + logger.info("Delete index success") + else: + logger.warning(f"Index '{self._collection_name}' does not exist. No deletion performed.") + except Exception as e: + logger.error(f"Error occurred while deleting the index: {e}") + raise e + + def text_exists(self, id: str) -> bool: + try: + self._client.get(index=self._collection_name, id=id) + return True + except: + return False + + def search_by_vector(self, query_vector: list[float], **kwargs: Any) -> list[Document]: + # Make sure query_vector is a list + if not isinstance(query_vector, list): + raise ValueError("query_vector should be a list of floats") + + # Check whether query_vector is a floating-point number list + if not all(isinstance(x, float) for x in query_vector): + raise ValueError("All elements in query_vector should be floats") + + top_k = kwargs.get("top_k", 10) + query = default_vector_search_query(query_vector=query_vector, k=top_k, **kwargs) + try: + response = self._client.search(index=self._collection_name, body=query) + except Exception as e: + logger.error(f"Error executing search: {e}") + raise + + docs_and_scores = [] + for hit in response["hits"]["hits"]: + docs_and_scores.append( + ( + Document( + page_content=hit["_source"][Field.CONTENT_KEY.value], + vector=hit["_source"][Field.VECTOR.value], + metadata=hit["_source"][Field.METADATA_KEY.value], + ), + hit["_score"], + ) + ) + docs = [] + for doc, score in docs_and_scores: + score_threshold = kwargs.get("score_threshold", 0.0) or 0.0 + if score > score_threshold: + doc.metadata["score"] = score + docs.append(doc) + + return docs + + def search_by_full_text(self, query: str, **kwargs: Any) -> list[Document]: + must = kwargs.get("must") + must_not = kwargs.get("must_not") + should = kwargs.get("should") + minimum_should_match = kwargs.get("minimum_should_match", 0) + top_k = kwargs.get("top_k", 10) + filters = kwargs.get("filter") + routing = kwargs.get("routing") + full_text_query = default_text_search_query( + query_text=query, + k=top_k, + text_field=Field.CONTENT_KEY.value, + must=must, + must_not=must_not, + should=should, + minimum_should_match=minimum_should_match, + filters=filters, + routing=routing, + ) + response = self._client.search(index=self._collection_name, body=full_text_query) + docs = [] + for hit in response["hits"]["hits"]: + docs.append( + Document( + page_content=hit["_source"][Field.CONTENT_KEY.value], + vector=hit["_source"][Field.VECTOR.value], + metadata=hit["_source"][Field.METADATA_KEY.value], + ) + ) + + return docs + + def create_collection(self, dimension: int, **kwargs): + lock_name = f"vector_indexing_lock_{self._collection_name}" + with redis_client.lock(lock_name, timeout=20): + collection_exist_cache_key = f"vector_indexing_{self._collection_name}" + if redis_client.get(collection_exist_cache_key): + logger.info(f"Collection {self._collection_name} already exists.") + return + if self._client.indices.exists(index=self._collection_name): + logger.info("{self._collection_name.lower()} already exists.") + return + if len(self.kwargs) == 0 and len(kwargs) != 0: + self.kwargs = copy.deepcopy(kwargs) + vector_field = kwargs.pop("vector_field", Field.VECTOR.value) + shards = kwargs.pop("shards", 2) + + engine = kwargs.pop("engine", "lvector") + method_name = kwargs.pop("method_name", "hnsw") + data_type = kwargs.pop("data_type", "float") + space_type = kwargs.pop("space_type", "cosinesimil") + + hnsw_m = kwargs.pop("hnsw_m", 24) + hnsw_ef_construction = kwargs.pop("hnsw_ef_construction", 500) + ivfpq_m = kwargs.pop("ivfpq_m", dimension) + nlist = kwargs.pop("nlist", 1000) + centroids_use_hnsw = kwargs.pop("centroids_use_hnsw", True if nlist >= 5000 else False) + centroids_hnsw_m = kwargs.pop("centroids_hnsw_m", 24) + centroids_hnsw_ef_construct = kwargs.pop("centroids_hnsw_ef_construct", 500) + centroids_hnsw_ef_search = kwargs.pop("centroids_hnsw_ef_search", 100) + mapping = default_text_mapping( + dimension, + method_name, + shards=shards, + engine=engine, + data_type=data_type, + space_type=space_type, + vector_field=vector_field, + hnsw_m=hnsw_m, + hnsw_ef_construction=hnsw_ef_construction, + nlist=nlist, + ivfpq_m=ivfpq_m, + centroids_use_hnsw=centroids_use_hnsw, + centroids_hnsw_m=centroids_hnsw_m, + centroids_hnsw_ef_construct=centroids_hnsw_ef_construct, + centroids_hnsw_ef_search=centroids_hnsw_ef_search, + **kwargs, + ) + self._client.indices.create(index=self._collection_name.lower(), body=mapping) + redis_client.set(collection_exist_cache_key, 1, ex=3600) + # logger.info(f"create index success: {self._collection_name}") + + +def default_text_mapping(dimension: int, method_name: str, **kwargs: Any) -> dict: + routing_field = kwargs.get("routing_field") + excludes_from_source = kwargs.get("excludes_from_source") + analyzer = kwargs.get("analyzer", "ik_max_word") + text_field = kwargs.get("text_field", Field.CONTENT_KEY.value) + engine = kwargs["engine"] + shard = kwargs["shards"] + space_type = kwargs["space_type"] + data_type = kwargs["data_type"] + vector_field = kwargs.get("vector_field", Field.VECTOR.value) + + if method_name == "ivfpq": + ivfpq_m = kwargs["ivfpq_m"] + nlist = kwargs["nlist"] + centroids_use_hnsw = True if nlist > 10000 else False + centroids_hnsw_m = 24 + centroids_hnsw_ef_construct = 500 + centroids_hnsw_ef_search = 100 + parameters = { + "m": ivfpq_m, + "nlist": nlist, + "centroids_use_hnsw": centroids_use_hnsw, + "centroids_hnsw_m": centroids_hnsw_m, + "centroids_hnsw_ef_construct": centroids_hnsw_ef_construct, + "centroids_hnsw_ef_search": centroids_hnsw_ef_search, + } + elif method_name == "hnsw": + neighbor = kwargs["hnsw_m"] + ef_construction = kwargs["hnsw_ef_construction"] + parameters = {"m": neighbor, "ef_construction": ef_construction} + elif method_name == "flat": + parameters = {} + else: + raise RuntimeError(f"unexpected method_name: {method_name}") + + mapping = { + "settings": {"index": {"number_of_shards": shard, "knn": True}}, + "mappings": { + "properties": { + vector_field: { + "type": "knn_vector", + "dimension": dimension, + "data_type": data_type, + "method": { + "engine": engine, + "name": method_name, + "space_type": space_type, + "parameters": parameters, + }, + }, + text_field: {"type": "text", "analyzer": analyzer}, + } + }, + } + + if excludes_from_source: + mapping["mappings"]["_source"] = {"excludes": excludes_from_source} # e.g. {"excludes": ["vector_field"]} + + if method_name == "ivfpq" and routing_field is not None: + mapping["settings"]["index"]["knn_routing"] = True + mapping["settings"]["index"]["knn.offline.construction"] = True + + if method_name == "flat" and routing_field is not None: + mapping["settings"]["index"]["knn_routing"] = True + + return mapping + + +def default_text_search_query( + query_text: str, + k: int = 4, + text_field: str = Field.CONTENT_KEY.value, + must: Optional[list[dict]] = None, + must_not: Optional[list[dict]] = None, + should: Optional[list[dict]] = None, + minimum_should_match: int = 0, + filters: Optional[list[dict]] = None, + routing: Optional[str] = None, + **kwargs, +) -> dict: + if routing is not None: + routing_field = kwargs.get("routing_field", "routing_field") + query_clause = { + "bool": { + "must": [{"match": {text_field: query_text}}, {"term": {f"metadata.{routing_field}.keyword": routing}}] + } + } + else: + query_clause = {"match": {text_field: query_text}} + # build the simplest search_query when only query_text is specified + if not must and not must_not and not should and not filters: + search_query = {"size": k, "query": query_clause} + return search_query + + # build complex search_query when either of must/must_not/should/filter is specified + if must: + if not isinstance(must, list): + raise RuntimeError(f"unexpected [must] clause with {type(filters)}") + if query_clause not in must: + must.append(query_clause) + else: + must = [query_clause] + + boolean_query = {"must": must} + + if must_not: + if not isinstance(must_not, list): + raise RuntimeError(f"unexpected [must_not] clause with {type(filters)}") + boolean_query["must_not"] = must_not + + if should: + if not isinstance(should, list): + raise RuntimeError(f"unexpected [should] clause with {type(filters)}") + boolean_query["should"] = should + if minimum_should_match != 0: + boolean_query["minimum_should_match"] = minimum_should_match + + if filters: + if not isinstance(filters, list): + raise RuntimeError(f"unexpected [filter] clause with {type(filters)}") + boolean_query["filter"] = filters + + search_query = {"size": k, "query": {"bool": boolean_query}} + return search_query + + +def default_vector_search_query( + query_vector: list[float], + k: int = 4, + min_score: str = "0.0", + ef_search: Optional[str] = None, # only for hnsw + nprobe: Optional[str] = None, # "2000" + reorder_factor: Optional[str] = None, # "20" + client_refactor: Optional[str] = None, # "true" + vector_field: str = Field.VECTOR.value, + filters: Optional[list[dict]] = None, + filter_type: Optional[str] = None, + **kwargs, +) -> dict: + if filters is not None: + filter_type = "post_filter" if filter_type is None else filter_type + if not isinstance(filter, list): + raise RuntimeError(f"unexpected filter with {type(filters)}") + final_ext = {"lvector": {}} + if min_score != "0.0": + final_ext["lvector"]["min_score"] = min_score + if ef_search: + final_ext["lvector"]["ef_search"] = ef_search + if nprobe: + final_ext["lvector"]["nprobe"] = nprobe + if reorder_factor: + final_ext["lvector"]["reorder_factor"] = reorder_factor + if client_refactor: + final_ext["lvector"]["client_refactor"] = client_refactor + + search_query = { + "size": k, + "_source": True, # force return '_source' + "query": {"knn": {vector_field: {"vector": query_vector, "k": k}}}, + } + + if filters is not None: + # when using filter, transform filter from List[Dict] to Dict as valid format + filters = {"bool": {"must": filters}} if len(filters) > 1 else filters[0] + search_query["query"]["knn"][vector_field]["filter"] = filters # filter should be Dict + if filter_type: + final_ext["lvector"]["filter_type"] = filter_type + + if final_ext != {"lvector": {}}: + search_query["ext"] = final_ext + return search_query + + +class LindormVectorStoreFactory(AbstractVectorFactory): + def init_vector(self, dataset: Dataset, attributes: list, embeddings: Embeddings) -> LindormVectorStore: + if dataset.index_struct_dict: + class_prefix: str = dataset.index_struct_dict["vector_store"]["class_prefix"] + collection_name = class_prefix + else: + dataset_id = dataset.id + collection_name = Dataset.gen_collection_name_by_id(dataset_id) + dataset.index_struct = json.dumps(self.gen_index_struct_dict(VectorType.LINDORM, collection_name)) + lindorm_config = LindormVectorStoreConfig( + hosts=dify_config.LINDORM_URL, + username=dify_config.LINDORM_USERNAME, + password=dify_config.LINDORM_PASSWORD, + ) + return LindormVectorStore(collection_name, lindorm_config) diff --git a/api/core/rag/datasource/vdb/vector_factory.py b/api/core/rag/datasource/vdb/vector_factory.py index c8cb007ae8..6d2e04fc02 100644 --- a/api/core/rag/datasource/vdb/vector_factory.py +++ b/api/core/rag/datasource/vdb/vector_factory.py @@ -134,6 +134,10 @@ class Vector: from core.rag.datasource.vdb.tidb_on_qdrant.tidb_on_qdrant_vector import TidbOnQdrantVectorFactory return TidbOnQdrantVectorFactory + case VectorType.LINDORM: + from core.rag.datasource.vdb.lindorm.lindorm_vector import LindormVectorStoreFactory + + return LindormVectorStoreFactory case VectorType.OCEANBASE: from core.rag.datasource.vdb.oceanbase.oceanbase_vector import OceanBaseVectorFactory diff --git a/api/core/rag/datasource/vdb/vector_type.py b/api/core/rag/datasource/vdb/vector_type.py index e3b37ece88..8e53e3ae84 100644 --- a/api/core/rag/datasource/vdb/vector_type.py +++ b/api/core/rag/datasource/vdb/vector_type.py @@ -16,6 +16,7 @@ class VectorType(str, Enum): TENCENT = "tencent" ORACLE = "oracle" ELASTICSEARCH = "elasticsearch" + LINDORM = "lindorm" COUCHBASE = "couchbase" BAIDU = "baidu" VIKINGDB = "vikingdb" diff --git a/api/tests/integration_tests/vdb/lindorm/__init__.py b/api/tests/integration_tests/vdb/lindorm/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/tests/integration_tests/vdb/lindorm/test_lindorm.py b/api/tests/integration_tests/vdb/lindorm/test_lindorm.py new file mode 100644 index 0000000000..f8f43ba6ef --- /dev/null +++ b/api/tests/integration_tests/vdb/lindorm/test_lindorm.py @@ -0,0 +1,35 @@ +import environs + +from core.rag.datasource.vdb.lindorm.lindorm_vector import LindormVectorStore, LindormVectorStoreConfig +from tests.integration_tests.vdb.test_vector_store import AbstractVectorTest, setup_mock_redis + +env = environs.Env() + + +class Config: + SEARCH_ENDPOINT = env.str("SEARCH_ENDPOINT", "http://ld-*************-proxy-search-pub.lindorm.aliyuncs.com:30070") + SEARCH_USERNAME = env.str("SEARCH_USERNAME", "ADMIN") + SEARCH_PWD = env.str("SEARCH_PWD", "PWD") + + +class TestLindormVectorStore(AbstractVectorTest): + def __init__(self): + super().__init__() + self.vector = LindormVectorStore( + collection_name=self.collection_name, + config=LindormVectorStoreConfig( + hosts=Config.SEARCH_ENDPOINT, + username=Config.SEARCH_USERNAME, + password=Config.SEARCH_PWD, + ), + ) + + def get_ids_by_metadata_field(self): + ids = self.vector.get_ids_by_metadata_field(key="doc_id", value=self.example_doc_id) + assert ids is not None + assert len(ids) == 1 + assert ids[0] == self.example_doc_id + + +def test_lindorm_vector(setup_mock_redis): + TestLindormVectorStore().run_all_tests() diff --git a/docker/.env.example b/docker/.env.example index 34b2136302..5b82d62d7b 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -222,7 +222,6 @@ REDIS_PORT=6379 REDIS_USERNAME= REDIS_PASSWORD=difyai123456 REDIS_USE_SSL=false -REDIS_DB=0 # Whether to use Redis Sentinel mode. # If set to true, the application will automatically discover and connect to the master node through Sentinel. @@ -531,6 +530,12 @@ VIKINGDB_SCHEMA=http VIKINGDB_CONNECTION_TIMEOUT=30 VIKINGDB_SOCKET_TIMEOUT=30 + +# Lindorm configuration, only available when VECTOR_STORE is `lindorm` +LINDORM_URL=http://ld-***************-proxy-search-pub.lindorm.aliyuncs.com:30070 +LINDORM_USERNAME=username +LINDORM_PASSWORD=password + # OceanBase Vector configuration, only available when VECTOR_STORE is `oceanbase` OCEANBASE_VECTOR_HOST=oceanbase-vector OCEANBASE_VECTOR_PORT=2881 @@ -645,7 +650,6 @@ MAIL_DEFAULT_SEND_FROM= # API-Key for the Resend email provider, used when MAIL_TYPE is `resend`. RESEND_API_KEY=your-resend-api-key -RESEND_API_URL=https://api.resend.com # SMTP server configuration, used when MAIL_TYPE is `smtp` SMTP_SERVER= diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 112e9a2702..12cdf25e70 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -167,6 +167,9 @@ x-shared-env: &shared-api-worker-env ELASTICSEARCH_PORT: ${ELASTICSEARCH_PORT:-9200} ELASTICSEARCH_USERNAME: ${ELASTICSEARCH_USERNAME:-elastic} ELASTICSEARCH_PASSWORD: ${ELASTICSEARCH_PASSWORD:-elastic} + LINDORM_URL: ${LINDORM_URL:-http://lindorm:30070} + LINDORM_USERNAME: ${LINDORM_USERNAME:-lindorm} + LINDORM_PASSWORD: ${LINDORM_USERNAME:-lindorm } KIBANA_PORT: ${KIBANA_PORT:-5601} # AnalyticDB configuration ANALYTICDB_KEY_ID: ${ANALYTICDB_KEY_ID:-} From c8a5fee622d8e45b536a5bb72fc5f393ece7a0ae Mon Sep 17 00:00:00 2001 From: Hanqing Zhao Date: Mon, 4 Nov 2024 09:11:15 +0800 Subject: [PATCH 17/48] Modify translation (#10213) --- web/i18n/ja-JP/app.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/i18n/ja-JP/app.ts b/web/i18n/ja-JP/app.ts index 76c7d1c4f4..48a35c61af 100644 --- a/web/i18n/ja-JP/app.ts +++ b/web/i18n/ja-JP/app.ts @@ -39,10 +39,10 @@ const translation = { workflowWarning: '現在ベータ版です', chatbotType: 'チャットボットのオーケストレーション方法', basic: '基本', - basicTip: '初心者向け。後で Chatflow に切り替えることができます', + basicTip: '初心者向け。後で「チャットフロー」に切り替えることができます', basicFor: '初心者向け', basicDescription: '基本オーケストレートは、組み込みのプロンプトを変更する機能がなく、簡単な設定を使用してチャットボット アプリをオーケストレートします。初心者向けです。', - advanced: 'Chatflow', + advanced: 'チャットフロー', advancedFor: '上級ユーザー向け', advancedDescription: 'ワークフロー オーケストレートは、ワークフロー形式でチャットボットをオーケストレートし、組み込みのプロンプトを編集する機能を含む高度なカスタマイズを提供します。経験豊富なユーザー向けです。', captionName: 'アプリのアイコンと名前', From f38abaaa6a12c1726d5ead351118925cf21e3d75 Mon Sep 17 00:00:00 2001 From: Jyong <76649700+JohnJyong@users.noreply.github.com> Date: Mon, 4 Nov 2024 15:22:07 +0800 Subject: [PATCH 18/48] fix the ssrf of docx file extractor external images (#10237) --- api/core/rag/extractor/word_extractor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/api/core/rag/extractor/word_extractor.py b/api/core/rag/extractor/word_extractor.py index ae3c25125c..d4434ea28f 100644 --- a/api/core/rag/extractor/word_extractor.py +++ b/api/core/rag/extractor/word_extractor.py @@ -14,6 +14,7 @@ import requests from docx import Document as DocxDocument from configs import dify_config +from core.helper import ssrf_proxy from core.rag.extractor.extractor_base import BaseExtractor from core.rag.models.document import Document from extensions.ext_database import db @@ -86,7 +87,7 @@ class WordExtractor(BaseExtractor): image_count += 1 if rel.is_external: url = rel.reltype - response = requests.get(url, stream=True) + response = ssrf_proxy.get(url, stream=True) if response.status_code == 200: image_ext = mimetypes.guess_extension(response.headers["Content-Type"]) file_uuid = str(uuid.uuid4()) From 405b704f02ef41885440de7e4a6073f7077a5b17 Mon Sep 17 00:00:00 2001 From: -LAN- Date: Mon, 4 Nov 2024 15:22:31 +0800 Subject: [PATCH 19/48] chore(llm_node): remove unnecessary type ignore for context assignment (#10216) --- api/core/workflow/nodes/llm/node.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/core/workflow/nodes/llm/node.py b/api/core/workflow/nodes/llm/node.py index b4728e6abf..bb9290ddc2 100644 --- a/api/core/workflow/nodes/llm/node.py +++ b/api/core/workflow/nodes/llm/node.py @@ -103,7 +103,7 @@ class LLMNode(BaseNode[LLMNodeData]): yield event if context: - node_inputs["#context#"] = context # type: ignore + node_inputs["#context#"] = context # fetch model config model_instance, model_config = self._fetch_model_config(self.node_data.model) From 5735761920d4b23effb1cae0d412ca427cbc6c4b Mon Sep 17 00:00:00 2001 From: -LAN- Date: Mon, 4 Nov 2024 15:22:41 +0800 Subject: [PATCH 20/48] refactor(workflow): introduce specific exceptions for code validation (#10218) --- api/core/workflow/nodes/code/code_node.py | 46 +++++++++++++---------- api/core/workflow/nodes/code/exc.py | 16 ++++++++ 2 files changed, 42 insertions(+), 20 deletions(-) create mode 100644 api/core/workflow/nodes/code/exc.py diff --git a/api/core/workflow/nodes/code/code_node.py b/api/core/workflow/nodes/code/code_node.py index 9d7d9027c3..de70af58dd 100644 --- a/api/core/workflow/nodes/code/code_node.py +++ b/api/core/workflow/nodes/code/code_node.py @@ -12,6 +12,12 @@ from core.workflow.nodes.code.entities import CodeNodeData from core.workflow.nodes.enums import NodeType from models.workflow import WorkflowNodeExecutionStatus +from .exc import ( + CodeNodeError, + DepthLimitError, + OutputValidationError, +) + class CodeNode(BaseNode[CodeNodeData]): _node_data_cls = CodeNodeData @@ -60,7 +66,7 @@ class CodeNode(BaseNode[CodeNodeData]): # Transform result result = self._transform_result(result, self.node_data.outputs) - except (CodeExecutionError, ValueError) as e: + except (CodeExecutionError, CodeNodeError) as e: return NodeRunResult(status=WorkflowNodeExecutionStatus.FAILED, inputs=variables, error=str(e)) return NodeRunResult(status=WorkflowNodeExecutionStatus.SUCCEEDED, inputs=variables, outputs=result) @@ -76,10 +82,10 @@ class CodeNode(BaseNode[CodeNodeData]): if value is None: return None else: - raise ValueError(f"Output variable `{variable}` must be a string") + raise OutputValidationError(f"Output variable `{variable}` must be a string") if len(value) > dify_config.CODE_MAX_STRING_LENGTH: - raise ValueError( + raise OutputValidationError( f"The length of output variable `{variable}` must be" f" less than {dify_config.CODE_MAX_STRING_LENGTH} characters" ) @@ -97,10 +103,10 @@ class CodeNode(BaseNode[CodeNodeData]): if value is None: return None else: - raise ValueError(f"Output variable `{variable}` must be a number") + raise OutputValidationError(f"Output variable `{variable}` must be a number") if value > dify_config.CODE_MAX_NUMBER or value < dify_config.CODE_MIN_NUMBER: - raise ValueError( + raise OutputValidationError( f"Output variable `{variable}` is out of range," f" it must be between {dify_config.CODE_MIN_NUMBER} and {dify_config.CODE_MAX_NUMBER}." ) @@ -108,7 +114,7 @@ class CodeNode(BaseNode[CodeNodeData]): if isinstance(value, float): # raise error if precision is too high if len(str(value).split(".")[1]) > dify_config.CODE_MAX_PRECISION: - raise ValueError( + raise OutputValidationError( f"Output variable `{variable}` has too high precision," f" it must be less than {dify_config.CODE_MAX_PRECISION} digits." ) @@ -125,7 +131,7 @@ class CodeNode(BaseNode[CodeNodeData]): :return: """ if depth > dify_config.CODE_MAX_DEPTH: - raise ValueError(f"Depth limit ${dify_config.CODE_MAX_DEPTH} reached, object too deep.") + raise DepthLimitError(f"Depth limit ${dify_config.CODE_MAX_DEPTH} reached, object too deep.") transformed_result = {} if output_schema is None: @@ -177,14 +183,14 @@ class CodeNode(BaseNode[CodeNodeData]): depth=depth + 1, ) else: - raise ValueError( + raise OutputValidationError( f"Output {prefix}.{output_name} is not a valid array." f" make sure all elements are of the same type." ) elif output_value is None: pass else: - raise ValueError(f"Output {prefix}.{output_name} is not a valid type.") + raise OutputValidationError(f"Output {prefix}.{output_name} is not a valid type.") return result @@ -192,7 +198,7 @@ class CodeNode(BaseNode[CodeNodeData]): for output_name, output_config in output_schema.items(): dot = "." if prefix else "" if output_name not in result: - raise ValueError(f"Output {prefix}{dot}{output_name} is missing.") + raise OutputValidationError(f"Output {prefix}{dot}{output_name} is missing.") if output_config.type == "object": # check if output is object @@ -200,7 +206,7 @@ class CodeNode(BaseNode[CodeNodeData]): if isinstance(result.get(output_name), type(None)): transformed_result[output_name] = None else: - raise ValueError( + raise OutputValidationError( f"Output {prefix}{dot}{output_name} is not an object," f" got {type(result.get(output_name))} instead." ) @@ -228,13 +234,13 @@ class CodeNode(BaseNode[CodeNodeData]): if isinstance(result[output_name], type(None)): transformed_result[output_name] = None else: - raise ValueError( + raise OutputValidationError( f"Output {prefix}{dot}{output_name} is not an array," f" got {type(result.get(output_name))} instead." ) else: if len(result[output_name]) > dify_config.CODE_MAX_NUMBER_ARRAY_LENGTH: - raise ValueError( + raise OutputValidationError( f"The length of output variable `{prefix}{dot}{output_name}` must be" f" less than {dify_config.CODE_MAX_NUMBER_ARRAY_LENGTH} elements." ) @@ -249,13 +255,13 @@ class CodeNode(BaseNode[CodeNodeData]): if isinstance(result[output_name], type(None)): transformed_result[output_name] = None else: - raise ValueError( + raise OutputValidationError( f"Output {prefix}{dot}{output_name} is not an array," f" got {type(result.get(output_name))} instead." ) else: if len(result[output_name]) > dify_config.CODE_MAX_STRING_ARRAY_LENGTH: - raise ValueError( + raise OutputValidationError( f"The length of output variable `{prefix}{dot}{output_name}` must be" f" less than {dify_config.CODE_MAX_STRING_ARRAY_LENGTH} elements." ) @@ -270,13 +276,13 @@ class CodeNode(BaseNode[CodeNodeData]): if isinstance(result[output_name], type(None)): transformed_result[output_name] = None else: - raise ValueError( + raise OutputValidationError( f"Output {prefix}{dot}{output_name} is not an array," f" got {type(result.get(output_name))} instead." ) else: if len(result[output_name]) > dify_config.CODE_MAX_OBJECT_ARRAY_LENGTH: - raise ValueError( + raise OutputValidationError( f"The length of output variable `{prefix}{dot}{output_name}` must be" f" less than {dify_config.CODE_MAX_OBJECT_ARRAY_LENGTH} elements." ) @@ -286,7 +292,7 @@ class CodeNode(BaseNode[CodeNodeData]): if value is None: pass else: - raise ValueError( + raise OutputValidationError( f"Output {prefix}{dot}{output_name}[{i}] is not an object," f" got {type(value)} instead at index {i}." ) @@ -303,13 +309,13 @@ class CodeNode(BaseNode[CodeNodeData]): for i, value in enumerate(result[output_name]) ] else: - raise ValueError(f"Output type {output_config.type} is not supported.") + raise OutputValidationError(f"Output type {output_config.type} is not supported.") parameters_validated[output_name] = True # check if all output parameters are validated if len(parameters_validated) != len(result): - raise ValueError("Not all output parameters are validated.") + raise CodeNodeError("Not all output parameters are validated.") return transformed_result diff --git a/api/core/workflow/nodes/code/exc.py b/api/core/workflow/nodes/code/exc.py new file mode 100644 index 0000000000..d6334fd554 --- /dev/null +++ b/api/core/workflow/nodes/code/exc.py @@ -0,0 +1,16 @@ +class CodeNodeError(ValueError): + """Base class for code node errors.""" + + pass + + +class OutputValidationError(CodeNodeError): + """Raised when there is an output validation error.""" + + pass + + +class DepthLimitError(CodeNodeError): + """Raised when the depth limit is reached.""" + + pass From bd674d27be91537279b3079526a094f8388056bd Mon Sep 17 00:00:00 2001 From: -LAN- Date: Mon, 4 Nov 2024 15:22:50 +0800 Subject: [PATCH 21/48] refactor(http_request): add custom exception handling for HTTP request nodes (#10219) --- api/core/workflow/nodes/http_request/exc.py | 18 +++++++++++++++++ .../workflow/nodes/http_request/executor.py | 20 ++++++++++++------- api/core/workflow/nodes/http_request/node.py | 3 ++- 3 files changed, 33 insertions(+), 8 deletions(-) create mode 100644 api/core/workflow/nodes/http_request/exc.py diff --git a/api/core/workflow/nodes/http_request/exc.py b/api/core/workflow/nodes/http_request/exc.py new file mode 100644 index 0000000000..7a5ab7dbc1 --- /dev/null +++ b/api/core/workflow/nodes/http_request/exc.py @@ -0,0 +1,18 @@ +class HttpRequestNodeError(ValueError): + """Custom error for HTTP request node.""" + + +class AuthorizationConfigError(HttpRequestNodeError): + """Raised when authorization config is missing or invalid.""" + + +class FileFetchError(HttpRequestNodeError): + """Raised when a file cannot be fetched.""" + + +class InvalidHttpMethodError(HttpRequestNodeError): + """Raised when an invalid HTTP method is used.""" + + +class ResponseSizeError(HttpRequestNodeError): + """Raised when the response size exceeds the allowed threshold.""" diff --git a/api/core/workflow/nodes/http_request/executor.py b/api/core/workflow/nodes/http_request/executor.py index 6872478299..6204fc2644 100644 --- a/api/core/workflow/nodes/http_request/executor.py +++ b/api/core/workflow/nodes/http_request/executor.py @@ -18,6 +18,12 @@ from .entities import ( HttpRequestNodeTimeout, Response, ) +from .exc import ( + AuthorizationConfigError, + FileFetchError, + InvalidHttpMethodError, + ResponseSizeError, +) BODY_TYPE_TO_CONTENT_TYPE = { "json": "application/json", @@ -51,7 +57,7 @@ class Executor: # If authorization API key is present, convert the API key using the variable pool if node_data.authorization.type == "api-key": if node_data.authorization.config is None: - raise ValueError("authorization config is required") + raise AuthorizationConfigError("authorization config is required") node_data.authorization.config.api_key = variable_pool.convert_template( node_data.authorization.config.api_key ).text @@ -116,7 +122,7 @@ class Executor: file_selector = data[0].file file_variable = self.variable_pool.get_file(file_selector) if file_variable is None: - raise ValueError(f"cannot fetch file with selector {file_selector}") + raise FileFetchError(f"cannot fetch file with selector {file_selector}") file = file_variable.value self.content = file_manager.download(file) case "x-www-form-urlencoded": @@ -155,12 +161,12 @@ class Executor: headers = deepcopy(self.headers) or {} if self.auth.type == "api-key": if self.auth.config is None: - raise ValueError("self.authorization config is required") + raise AuthorizationConfigError("self.authorization config is required") if authorization.config is None: - raise ValueError("authorization config is required") + raise AuthorizationConfigError("authorization config is required") if self.auth.config.api_key is None: - raise ValueError("api_key is required") + raise AuthorizationConfigError("api_key is required") if not authorization.config.header: authorization.config.header = "Authorization" @@ -183,7 +189,7 @@ class Executor: else dify_config.HTTP_REQUEST_NODE_MAX_TEXT_SIZE ) if executor_response.size > threshold_size: - raise ValueError( + raise ResponseSizeError( f'{"File" if executor_response.is_file else "Text"} size is too large,' f' max size is {threshold_size / 1024 / 1024:.2f} MB,' f' but current size is {executor_response.readable_size}.' @@ -196,7 +202,7 @@ class Executor: do http request depending on api bundle """ if self.method not in {"get", "head", "post", "put", "delete", "patch"}: - raise ValueError(f"Invalid http method {self.method}") + raise InvalidHttpMethodError(f"Invalid http method {self.method}") request_args = { "url": self.url, diff --git a/api/core/workflow/nodes/http_request/node.py b/api/core/workflow/nodes/http_request/node.py index a037bee665..61c661e587 100644 --- a/api/core/workflow/nodes/http_request/node.py +++ b/api/core/workflow/nodes/http_request/node.py @@ -20,6 +20,7 @@ from .entities import ( HttpRequestNodeTimeout, Response, ) +from .exc import HttpRequestNodeError HTTP_REQUEST_DEFAULT_TIMEOUT = HttpRequestNodeTimeout( connect=dify_config.HTTP_REQUEST_MAX_CONNECT_TIMEOUT, @@ -77,7 +78,7 @@ class HttpRequestNode(BaseNode[HttpRequestNodeData]): "request": http_executor.to_log(), }, ) - except Exception as e: + except HttpRequestNodeError as e: logger.warning(f"http request node {self.node_id} failed to run: {e}") return NodeRunResult( status=WorkflowNodeExecutionStatus.FAILED, From 7e42de1e7bb150cc35b9f54b774f1eac10ef40f1 Mon Sep 17 00:00:00 2001 From: -LAN- Date: Mon, 4 Nov 2024 15:22:58 +0800 Subject: [PATCH 22/48] refactor(workflow): introduce specific error handling for LLM nodes (#10221) --- api/core/workflow/nodes/llm/exc.py | 26 +++++++++++++++++++++++ api/core/workflow/nodes/llm/node.py | 33 ++++++++++++++++++----------- 2 files changed, 47 insertions(+), 12 deletions(-) create mode 100644 api/core/workflow/nodes/llm/exc.py diff --git a/api/core/workflow/nodes/llm/exc.py b/api/core/workflow/nodes/llm/exc.py new file mode 100644 index 0000000000..f858be2515 --- /dev/null +++ b/api/core/workflow/nodes/llm/exc.py @@ -0,0 +1,26 @@ +class LLMNodeError(ValueError): + """Base class for LLM Node errors.""" + + +class VariableNotFoundError(LLMNodeError): + """Raised when a required variable is not found.""" + + +class InvalidContextStructureError(LLMNodeError): + """Raised when the context structure is invalid.""" + + +class InvalidVariableTypeError(LLMNodeError): + """Raised when the variable type is invalid.""" + + +class ModelNotExistError(LLMNodeError): + """Raised when the specified model does not exist.""" + + +class LLMModeRequiredError(LLMNodeError): + """Raised when LLM mode is required but not provided.""" + + +class NoPromptFoundError(LLMNodeError): + """Raised when no prompt is found in the LLM configuration.""" diff --git a/api/core/workflow/nodes/llm/node.py b/api/core/workflow/nodes/llm/node.py index bb9290ddc2..47b0e25d9c 100644 --- a/api/core/workflow/nodes/llm/node.py +++ b/api/core/workflow/nodes/llm/node.py @@ -56,6 +56,15 @@ from .entities import ( LLMNodeData, ModelConfig, ) +from .exc import ( + InvalidContextStructureError, + InvalidVariableTypeError, + LLMModeRequiredError, + LLMNodeError, + ModelNotExistError, + NoPromptFoundError, + VariableNotFoundError, +) if TYPE_CHECKING: from core.file.models import File @@ -115,7 +124,7 @@ class LLMNode(BaseNode[LLMNodeData]): if self.node_data.memory: query = self.graph_runtime_state.variable_pool.get((SYSTEM_VARIABLE_NODE_ID, SystemVariableKey.QUERY)) if not query: - raise ValueError("Query not found") + raise VariableNotFoundError("Query not found") query = query.text else: query = None @@ -161,7 +170,7 @@ class LLMNode(BaseNode[LLMNodeData]): usage = event.usage finish_reason = event.finish_reason break - except Exception as e: + except LLMNodeError as e: yield RunCompletedEvent( run_result=NodeRunResult( status=WorkflowNodeExecutionStatus.FAILED, @@ -275,7 +284,7 @@ class LLMNode(BaseNode[LLMNodeData]): variable_name = variable_selector.variable variable = self.graph_runtime_state.variable_pool.get(variable_selector.value_selector) if variable is None: - raise ValueError(f"Variable {variable_selector.variable} not found") + raise VariableNotFoundError(f"Variable {variable_selector.variable} not found") def parse_dict(input_dict: Mapping[str, Any]) -> str: """ @@ -325,7 +334,7 @@ class LLMNode(BaseNode[LLMNodeData]): for variable_selector in variable_selectors: variable = self.graph_runtime_state.variable_pool.get(variable_selector.value_selector) if variable is None: - raise ValueError(f"Variable {variable_selector.variable} not found") + raise VariableNotFoundError(f"Variable {variable_selector.variable} not found") if isinstance(variable, NoneSegment): inputs[variable_selector.variable] = "" inputs[variable_selector.variable] = variable.to_object() @@ -338,7 +347,7 @@ class LLMNode(BaseNode[LLMNodeData]): for variable_selector in query_variable_selectors: variable = self.graph_runtime_state.variable_pool.get(variable_selector.value_selector) if variable is None: - raise ValueError(f"Variable {variable_selector.variable} not found") + raise VariableNotFoundError(f"Variable {variable_selector.variable} not found") if isinstance(variable, NoneSegment): continue inputs[variable_selector.variable] = variable.to_object() @@ -355,7 +364,7 @@ class LLMNode(BaseNode[LLMNodeData]): return variable.value elif isinstance(variable, NoneSegment | ArrayAnySegment): return [] - raise ValueError(f"Invalid variable type: {type(variable)}") + raise InvalidVariableTypeError(f"Invalid variable type: {type(variable)}") def _fetch_context(self, node_data: LLMNodeData): if not node_data.context.enabled: @@ -376,7 +385,7 @@ class LLMNode(BaseNode[LLMNodeData]): context_str += item + "\n" else: if "content" not in item: - raise ValueError(f"Invalid context structure: {item}") + raise InvalidContextStructureError(f"Invalid context structure: {item}") context_str += item["content"] + "\n" @@ -441,7 +450,7 @@ class LLMNode(BaseNode[LLMNodeData]): ) if provider_model is None: - raise ValueError(f"Model {model_name} not exist.") + raise ModelNotExistError(f"Model {model_name} not exist.") if provider_model.status == ModelStatus.NO_CONFIGURE: raise ProviderTokenNotInitError(f"Model {model_name} credentials is not initialized.") @@ -460,12 +469,12 @@ class LLMNode(BaseNode[LLMNodeData]): # get model mode model_mode = node_data_model.mode if not model_mode: - raise ValueError("LLM mode is required.") + raise LLMModeRequiredError("LLM mode is required.") model_schema = model_type_instance.get_model_schema(model_name, model_credentials) if not model_schema: - raise ValueError(f"Model {model_name} not exist.") + raise ModelNotExistError(f"Model {model_name} not exist.") return model_instance, ModelConfigWithCredentialsEntity( provider=provider_name, @@ -564,7 +573,7 @@ class LLMNode(BaseNode[LLMNodeData]): filtered_prompt_messages.append(prompt_message) if not filtered_prompt_messages: - raise ValueError( + raise NoPromptFoundError( "No prompt found in the LLM configuration. " "Please ensure a prompt is properly configured before proceeding." ) @@ -636,7 +645,7 @@ class LLMNode(BaseNode[LLMNodeData]): variable_template_parser = VariableTemplateParser(template=prompt_template.text) variable_selectors = variable_template_parser.extract_variable_selectors() else: - raise ValueError(f"Invalid prompt template type: {type(prompt_template)}") + raise InvalidVariableTypeError(f"Invalid prompt template type: {type(prompt_template)}") variable_mapping = {} for variable_selector in variable_selectors: From 2d9632d8b9fa9d5fc22037a10b5b0006afa442ae Mon Sep 17 00:00:00 2001 From: -LAN- Date: Mon, 4 Nov 2024 15:23:08 +0800 Subject: [PATCH 23/48] refactor(list_operator): replace ValueError with InvalidKeyError (#10222) --- api/core/workflow/nodes/list_operator/node.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/core/workflow/nodes/list_operator/node.py b/api/core/workflow/nodes/list_operator/node.py index 6053a15d96..0406b97eb8 100644 --- a/api/core/workflow/nodes/list_operator/node.py +++ b/api/core/workflow/nodes/list_operator/node.py @@ -295,4 +295,4 @@ def _order_file(*, order: Literal["asc", "desc"], order_by: str = "", array: Seq extract_func = _get_file_extract_number_func(key=order_by) return sorted(array, key=lambda x: extract_func(x), reverse=order == "desc") else: - raise ValueError(f"Invalid order key: {order_by}") + raise InvalidKeyError(f"Invalid order key: {order_by}") From 1e2755786551ae93ca0b78de0e0c6a67a443b248 Mon Sep 17 00:00:00 2001 From: shisaru292 <87224749+shisaru292@users.noreply.github.com> Date: Mon, 4 Nov 2024 15:23:18 +0800 Subject: [PATCH 24/48] fix: missing working directory parameter in script (#10226) --- dev/reformat | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dev/reformat b/dev/reformat index ad83e897d9..94a7f3e6fe 100755 --- a/dev/reformat +++ b/dev/reformat @@ -9,10 +9,10 @@ if ! command -v ruff &> /dev/null || ! command -v dotenv-linter &> /dev/null; th fi # run ruff linter -ruff check --fix ./api +poetry run -C api ruff check --fix ./api # run ruff formatter -ruff format ./api +poetry run -C api ruff format ./api # run dotenv-linter linter -dotenv-linter ./api/.env.example ./web/.env.example +poetry run -C api dotenv-linter ./api/.env.example ./web/.env.example From c711c5e36efe22e5744d2bfdb814d817f934a9c9 Mon Sep 17 00:00:00 2001 From: -LAN- Date: Mon, 4 Nov 2024 15:55:34 +0800 Subject: [PATCH 25/48] feat(workflow): add configurable workflow file upload limit (#10176) Co-authored-by: JzoNg --- api/.env.example | 3 + api/configs/feature/__init__.py | 5 ++ api/controllers/common/fields.py | 24 ++++++ api/controllers/common/helpers.py | 39 +++++++++ api/controllers/console/explore/parameter.py | 81 +++---------------- api/controllers/console/files/__init__.py | 1 + api/controllers/service_api/app/app.py | 78 +++--------------- api/controllers/web/app.py | 78 +++--------------- .../features/file_upload/manager.py | 5 +- api/fields/file_fields.py | 1 + api/models/__init__.py | 2 - api/models/model.py | 2 +- docker/.env.example | 1 + docker/docker-compose.yaml | 1 + .../base/file-uploader/constants.ts | 1 + .../components/base/file-uploader/hooks.ts | 3 + .../_base/components/file-upload-setting.tsx | 10 ++- web/models/common.ts | 2 +- 18 files changed, 124 insertions(+), 213 deletions(-) create mode 100644 api/controllers/common/fields.py diff --git a/api/.env.example b/api/.env.example index fb785db174..d6661932db 100644 --- a/api/.env.example +++ b/api/.env.example @@ -327,6 +327,9 @@ SSRF_DEFAULT_MAX_RETRIES=3 BATCH_UPLOAD_LIMIT=10 KEYWORD_DATA_SOURCE_TYPE=database +# Workflow file upload limit +WORKFLOW_FILE_UPLOAD_LIMIT=10 + # CODE EXECUTION CONFIGURATION CODE_EXECUTION_ENDPOINT=http://127.0.0.1:8194 CODE_EXECUTION_API_KEY=dify-sandbox diff --git a/api/configs/feature/__init__.py b/api/configs/feature/__init__.py index f88019fbb6..4fedf6cd49 100644 --- a/api/configs/feature/__init__.py +++ b/api/configs/feature/__init__.py @@ -269,6 +269,11 @@ class FileUploadConfig(BaseSettings): default=20, ) + WORKFLOW_FILE_UPLOAD_LIMIT: PositiveInt = Field( + description="Maximum number of files allowed in a workflow upload operation", + default=10, + ) + class HttpConfig(BaseSettings): """ diff --git a/api/controllers/common/fields.py b/api/controllers/common/fields.py new file mode 100644 index 0000000000..79869916ed --- /dev/null +++ b/api/controllers/common/fields.py @@ -0,0 +1,24 @@ +from flask_restful import fields + +parameters__system_parameters = { + "image_file_size_limit": fields.Integer, + "video_file_size_limit": fields.Integer, + "audio_file_size_limit": fields.Integer, + "file_size_limit": fields.Integer, + "workflow_file_upload_limit": fields.Integer, +} + +parameters_fields = { + "opening_statement": fields.String, + "suggested_questions": fields.Raw, + "suggested_questions_after_answer": fields.Raw, + "speech_to_text": fields.Raw, + "text_to_speech": fields.Raw, + "retriever_resource": fields.Raw, + "annotation_reply": fields.Raw, + "more_like_this": fields.Raw, + "user_input_form": fields.Raw, + "sensitive_word_avoidance": fields.Raw, + "file_upload": fields.Raw, + "system_parameters": fields.Nested(parameters__system_parameters), +} diff --git a/api/controllers/common/helpers.py b/api/controllers/common/helpers.py index ed24b265ef..2bae203712 100644 --- a/api/controllers/common/helpers.py +++ b/api/controllers/common/helpers.py @@ -2,11 +2,15 @@ import mimetypes import os import re import urllib.parse +from collections.abc import Mapping +from typing import Any from uuid import uuid4 import httpx from pydantic import BaseModel +from configs import dify_config + class FileInfo(BaseModel): filename: str @@ -56,3 +60,38 @@ def guess_file_info_from_response(response: httpx.Response): mimetype=mimetype, size=int(response.headers.get("Content-Length", -1)), ) + + +def get_parameters_from_feature_dict(*, features_dict: Mapping[str, Any], user_input_form: list[dict[str, Any]]): + return { + "opening_statement": features_dict.get("opening_statement"), + "suggested_questions": features_dict.get("suggested_questions", []), + "suggested_questions_after_answer": features_dict.get("suggested_questions_after_answer", {"enabled": False}), + "speech_to_text": features_dict.get("speech_to_text", {"enabled": False}), + "text_to_speech": features_dict.get("text_to_speech", {"enabled": False}), + "retriever_resource": features_dict.get("retriever_resource", {"enabled": False}), + "annotation_reply": features_dict.get("annotation_reply", {"enabled": False}), + "more_like_this": features_dict.get("more_like_this", {"enabled": False}), + "user_input_form": user_input_form, + "sensitive_word_avoidance": features_dict.get( + "sensitive_word_avoidance", {"enabled": False, "type": "", "configs": []} + ), + "file_upload": features_dict.get( + "file_upload", + { + "image": { + "enabled": False, + "number_limits": 3, + "detail": "high", + "transfer_methods": ["remote_url", "local_file"], + } + }, + ), + "system_parameters": { + "image_file_size_limit": dify_config.UPLOAD_IMAGE_FILE_SIZE_LIMIT, + "video_file_size_limit": dify_config.UPLOAD_VIDEO_FILE_SIZE_LIMIT, + "audio_file_size_limit": dify_config.UPLOAD_AUDIO_FILE_SIZE_LIMIT, + "file_size_limit": dify_config.UPLOAD_FILE_SIZE_LIMIT, + "workflow_file_upload_limit": dify_config.WORKFLOW_FILE_UPLOAD_LIMIT, + }, + } diff --git a/api/controllers/console/explore/parameter.py b/api/controllers/console/explore/parameter.py index 7c7580e3c6..fee52248a6 100644 --- a/api/controllers/console/explore/parameter.py +++ b/api/controllers/console/explore/parameter.py @@ -1,6 +1,7 @@ -from flask_restful import fields, marshal_with +from flask_restful import marshal_with -from configs import dify_config +from controllers.common import fields +from controllers.common import helpers as controller_helpers from controllers.console import api from controllers.console.app.error import AppUnavailableError from controllers.console.explore.wraps import InstalledAppResource @@ -11,43 +12,14 @@ from services.app_service import AppService class AppParameterApi(InstalledAppResource): """Resource for app variables.""" - variable_fields = { - "key": fields.String, - "name": fields.String, - "description": fields.String, - "type": fields.String, - "default": fields.String, - "max_length": fields.Integer, - "options": fields.List(fields.String), - } - - system_parameters_fields = { - "image_file_size_limit": fields.Integer, - "video_file_size_limit": fields.Integer, - "audio_file_size_limit": fields.Integer, - "file_size_limit": fields.Integer, - } - - parameters_fields = { - "opening_statement": fields.String, - "suggested_questions": fields.Raw, - "suggested_questions_after_answer": fields.Raw, - "speech_to_text": fields.Raw, - "text_to_speech": fields.Raw, - "retriever_resource": fields.Raw, - "annotation_reply": fields.Raw, - "more_like_this": fields.Raw, - "user_input_form": fields.Raw, - "sensitive_word_avoidance": fields.Raw, - "file_upload": fields.Raw, - "system_parameters": fields.Nested(system_parameters_fields), - } - - @marshal_with(parameters_fields) + @marshal_with(fields.parameters_fields) def get(self, installed_app: InstalledApp): """Retrieve app parameters.""" app_model = installed_app.app + if app_model is None: + raise AppUnavailableError() + if app_model.mode in {AppMode.ADVANCED_CHAT.value, AppMode.WORKFLOW.value}: workflow = app_model.workflow if workflow is None: @@ -57,43 +29,16 @@ class AppParameterApi(InstalledAppResource): user_input_form = workflow.user_input_form(to_old_structure=True) else: app_model_config = app_model.app_model_config + if app_model_config is None: + raise AppUnavailableError() + features_dict = app_model_config.to_dict() user_input_form = features_dict.get("user_input_form", []) - return { - "opening_statement": features_dict.get("opening_statement"), - "suggested_questions": features_dict.get("suggested_questions", []), - "suggested_questions_after_answer": features_dict.get( - "suggested_questions_after_answer", {"enabled": False} - ), - "speech_to_text": features_dict.get("speech_to_text", {"enabled": False}), - "text_to_speech": features_dict.get("text_to_speech", {"enabled": False}), - "retriever_resource": features_dict.get("retriever_resource", {"enabled": False}), - "annotation_reply": features_dict.get("annotation_reply", {"enabled": False}), - "more_like_this": features_dict.get("more_like_this", {"enabled": False}), - "user_input_form": user_input_form, - "sensitive_word_avoidance": features_dict.get( - "sensitive_word_avoidance", {"enabled": False, "type": "", "configs": []} - ), - "file_upload": features_dict.get( - "file_upload", - { - "image": { - "enabled": False, - "number_limits": 3, - "detail": "high", - "transfer_methods": ["remote_url", "local_file"], - } - }, - ), - "system_parameters": { - "image_file_size_limit": dify_config.UPLOAD_IMAGE_FILE_SIZE_LIMIT, - "video_file_size_limit": dify_config.UPLOAD_VIDEO_FILE_SIZE_LIMIT, - "audio_file_size_limit": dify_config.UPLOAD_AUDIO_FILE_SIZE_LIMIT, - "file_size_limit": dify_config.UPLOAD_FILE_SIZE_LIMIT, - }, - } + return controller_helpers.get_parameters_from_feature_dict( + features_dict=features_dict, user_input_form=user_input_form + ) class ExploreAppMetaApi(InstalledAppResource): diff --git a/api/controllers/console/files/__init__.py b/api/controllers/console/files/__init__.py index 69ee7eaabd..6c7bd8acfd 100644 --- a/api/controllers/console/files/__init__.py +++ b/api/controllers/console/files/__init__.py @@ -37,6 +37,7 @@ class FileApi(Resource): "image_file_size_limit": dify_config.UPLOAD_IMAGE_FILE_SIZE_LIMIT, "video_file_size_limit": dify_config.UPLOAD_VIDEO_FILE_SIZE_LIMIT, "audio_file_size_limit": dify_config.UPLOAD_AUDIO_FILE_SIZE_LIMIT, + "workflow_file_upload_limit": dify_config.WORKFLOW_FILE_UPLOAD_LIMIT, }, 200 @setup_required diff --git a/api/controllers/service_api/app/app.py b/api/controllers/service_api/app/app.py index 9a4cdc26cd..88b13faa52 100644 --- a/api/controllers/service_api/app/app.py +++ b/api/controllers/service_api/app/app.py @@ -1,6 +1,7 @@ -from flask_restful import Resource, fields, marshal_with +from flask_restful import Resource, marshal_with -from configs import dify_config +from controllers.common import fields +from controllers.common import helpers as controller_helpers from controllers.service_api import api from controllers.service_api.app.error import AppUnavailableError from controllers.service_api.wraps import validate_app_token @@ -11,40 +12,8 @@ from services.app_service import AppService class AppParameterApi(Resource): """Resource for app variables.""" - variable_fields = { - "key": fields.String, - "name": fields.String, - "description": fields.String, - "type": fields.String, - "default": fields.String, - "max_length": fields.Integer, - "options": fields.List(fields.String), - } - - system_parameters_fields = { - "image_file_size_limit": fields.Integer, - "video_file_size_limit": fields.Integer, - "audio_file_size_limit": fields.Integer, - "file_size_limit": fields.Integer, - } - - parameters_fields = { - "opening_statement": fields.String, - "suggested_questions": fields.Raw, - "suggested_questions_after_answer": fields.Raw, - "speech_to_text": fields.Raw, - "text_to_speech": fields.Raw, - "retriever_resource": fields.Raw, - "annotation_reply": fields.Raw, - "more_like_this": fields.Raw, - "user_input_form": fields.Raw, - "sensitive_word_avoidance": fields.Raw, - "file_upload": fields.Raw, - "system_parameters": fields.Nested(system_parameters_fields), - } - @validate_app_token - @marshal_with(parameters_fields) + @marshal_with(fields.parameters_fields) def get(self, app_model: App): """Retrieve app parameters.""" if app_model.mode in {AppMode.ADVANCED_CHAT.value, AppMode.WORKFLOW.value}: @@ -56,43 +25,16 @@ class AppParameterApi(Resource): user_input_form = workflow.user_input_form(to_old_structure=True) else: app_model_config = app_model.app_model_config + if app_model_config is None: + raise AppUnavailableError() + features_dict = app_model_config.to_dict() user_input_form = features_dict.get("user_input_form", []) - return { - "opening_statement": features_dict.get("opening_statement"), - "suggested_questions": features_dict.get("suggested_questions", []), - "suggested_questions_after_answer": features_dict.get( - "suggested_questions_after_answer", {"enabled": False} - ), - "speech_to_text": features_dict.get("speech_to_text", {"enabled": False}), - "text_to_speech": features_dict.get("text_to_speech", {"enabled": False}), - "retriever_resource": features_dict.get("retriever_resource", {"enabled": False}), - "annotation_reply": features_dict.get("annotation_reply", {"enabled": False}), - "more_like_this": features_dict.get("more_like_this", {"enabled": False}), - "user_input_form": user_input_form, - "sensitive_word_avoidance": features_dict.get( - "sensitive_word_avoidance", {"enabled": False, "type": "", "configs": []} - ), - "file_upload": features_dict.get( - "file_upload", - { - "image": { - "enabled": False, - "number_limits": 3, - "detail": "high", - "transfer_methods": ["remote_url", "local_file"], - } - }, - ), - "system_parameters": { - "image_file_size_limit": dify_config.UPLOAD_IMAGE_FILE_SIZE_LIMIT, - "video_file_size_limit": dify_config.UPLOAD_VIDEO_FILE_SIZE_LIMIT, - "audio_file_size_limit": dify_config.UPLOAD_AUDIO_FILE_SIZE_LIMIT, - "file_size_limit": dify_config.UPLOAD_FILE_SIZE_LIMIT, - }, - } + return controller_helpers.get_parameters_from_feature_dict( + features_dict=features_dict, user_input_form=user_input_form + ) class AppMetaApi(Resource): diff --git a/api/controllers/web/app.py b/api/controllers/web/app.py index 974d2cff94..cc8255ccf4 100644 --- a/api/controllers/web/app.py +++ b/api/controllers/web/app.py @@ -1,6 +1,7 @@ -from flask_restful import fields, marshal_with +from flask_restful import marshal_with -from configs import dify_config +from controllers.common import fields +from controllers.common import helpers as controller_helpers from controllers.web import api from controllers.web.error import AppUnavailableError from controllers.web.wraps import WebApiResource @@ -11,39 +12,7 @@ from services.app_service import AppService class AppParameterApi(WebApiResource): """Resource for app variables.""" - variable_fields = { - "key": fields.String, - "name": fields.String, - "description": fields.String, - "type": fields.String, - "default": fields.String, - "max_length": fields.Integer, - "options": fields.List(fields.String), - } - - system_parameters_fields = { - "image_file_size_limit": fields.Integer, - "video_file_size_limit": fields.Integer, - "audio_file_size_limit": fields.Integer, - "file_size_limit": fields.Integer, - } - - parameters_fields = { - "opening_statement": fields.String, - "suggested_questions": fields.Raw, - "suggested_questions_after_answer": fields.Raw, - "speech_to_text": fields.Raw, - "text_to_speech": fields.Raw, - "retriever_resource": fields.Raw, - "annotation_reply": fields.Raw, - "more_like_this": fields.Raw, - "user_input_form": fields.Raw, - "sensitive_word_avoidance": fields.Raw, - "file_upload": fields.Raw, - "system_parameters": fields.Nested(system_parameters_fields), - } - - @marshal_with(parameters_fields) + @marshal_with(fields.parameters_fields) def get(self, app_model: App, end_user): """Retrieve app parameters.""" if app_model.mode in {AppMode.ADVANCED_CHAT.value, AppMode.WORKFLOW.value}: @@ -55,43 +24,16 @@ class AppParameterApi(WebApiResource): user_input_form = workflow.user_input_form(to_old_structure=True) else: app_model_config = app_model.app_model_config + if app_model_config is None: + raise AppUnavailableError() + features_dict = app_model_config.to_dict() user_input_form = features_dict.get("user_input_form", []) - return { - "opening_statement": features_dict.get("opening_statement"), - "suggested_questions": features_dict.get("suggested_questions", []), - "suggested_questions_after_answer": features_dict.get( - "suggested_questions_after_answer", {"enabled": False} - ), - "speech_to_text": features_dict.get("speech_to_text", {"enabled": False}), - "text_to_speech": features_dict.get("text_to_speech", {"enabled": False}), - "retriever_resource": features_dict.get("retriever_resource", {"enabled": False}), - "annotation_reply": features_dict.get("annotation_reply", {"enabled": False}), - "more_like_this": features_dict.get("more_like_this", {"enabled": False}), - "user_input_form": user_input_form, - "sensitive_word_avoidance": features_dict.get( - "sensitive_word_avoidance", {"enabled": False, "type": "", "configs": []} - ), - "file_upload": features_dict.get( - "file_upload", - { - "image": { - "enabled": False, - "number_limits": 3, - "detail": "high", - "transfer_methods": ["remote_url", "local_file"], - } - }, - ), - "system_parameters": { - "image_file_size_limit": dify_config.UPLOAD_IMAGE_FILE_SIZE_LIMIT, - "video_file_size_limit": dify_config.UPLOAD_VIDEO_FILE_SIZE_LIMIT, - "audio_file_size_limit": dify_config.UPLOAD_AUDIO_FILE_SIZE_LIMIT, - "file_size_limit": dify_config.UPLOAD_FILE_SIZE_LIMIT, - }, - } + return controller_helpers.get_parameters_from_feature_dict( + features_dict=features_dict, user_input_form=user_input_form + ) class AppMeta(WebApiResource): diff --git a/api/core/app/app_config/features/file_upload/manager.py b/api/core/app/app_config/features/file_upload/manager.py index 42beec2535..d0f75d0b75 100644 --- a/api/core/app/app_config/features/file_upload/manager.py +++ b/api/core/app/app_config/features/file_upload/manager.py @@ -1,8 +1,7 @@ from collections.abc import Mapping from typing import Any -from core.file.models import FileExtraConfig -from models import FileUploadConfig +from core.file import FileExtraConfig class FileUploadConfigManager: @@ -43,6 +42,6 @@ class FileUploadConfigManager: if not config.get("file_upload"): config["file_upload"] = {} else: - FileUploadConfig.model_validate(config["file_upload"]) + FileExtraConfig.model_validate(config["file_upload"]) return config, ["file_upload"] diff --git a/api/fields/file_fields.py b/api/fields/file_fields.py index 1cddc24b2c..afaacc0568 100644 --- a/api/fields/file_fields.py +++ b/api/fields/file_fields.py @@ -8,6 +8,7 @@ upload_config_fields = { "image_file_size_limit": fields.Integer, "video_file_size_limit": fields.Integer, "audio_file_size_limit": fields.Integer, + "workflow_file_upload_limit": fields.Integer, } file_fields = { diff --git a/api/models/__init__.py b/api/models/__init__.py index 1d8bae6cfa..cd6c7674da 100644 --- a/api/models/__init__.py +++ b/api/models/__init__.py @@ -6,7 +6,6 @@ from .model import ( AppMode, Conversation, EndUser, - FileUploadConfig, InstalledApp, Message, MessageAnnotation, @@ -50,6 +49,5 @@ __all__ = [ "Tenant", "Conversation", "MessageAnnotation", - "FileUploadConfig", "ToolFile", ] diff --git a/api/models/model.py b/api/models/model.py index 8a619d3f30..713ce80e12 100644 --- a/api/models/model.py +++ b/api/models/model.py @@ -121,7 +121,7 @@ class App(Base): return site @property - def app_model_config(self) -> Optional["AppModelConfig"]: + def app_model_config(self): if self.app_model_config_id: return db.session.query(AppModelConfig).filter(AppModelConfig.id == self.app_model_config_id).first() diff --git a/docker/.env.example b/docker/.env.example index 5b82d62d7b..aa5e102bd0 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -690,6 +690,7 @@ WORKFLOW_MAX_EXECUTION_STEPS=500 WORKFLOW_MAX_EXECUTION_TIME=1200 WORKFLOW_CALL_MAX_DEPTH=5 MAX_VARIABLE_SIZE=204800 +WORKFLOW_FILE_UPLOAD_LIMIT=10 # HTTP request node in workflow configuration HTTP_REQUEST_NODE_MAX_BINARY_SIZE=10485760 diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 12cdf25e70..a26838af10 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -1,4 +1,5 @@ x-shared-env: &shared-api-worker-env + WORKFLOW_FILE_UPLOAD_LIMIT: ${WORKFLOW_FILE_UPLOAD_LIMIT:-10} LOG_LEVEL: ${LOG_LEVEL:-INFO} LOG_FILE: ${LOG_FILE:-} LOG_FILE_MAX_SIZE: ${LOG_FILE_MAX_SIZE:-20} diff --git a/web/app/components/base/file-uploader/constants.ts b/web/app/components/base/file-uploader/constants.ts index 629fe2566b..a749d73c74 100644 --- a/web/app/components/base/file-uploader/constants.ts +++ b/web/app/components/base/file-uploader/constants.ts @@ -3,5 +3,6 @@ export const IMG_SIZE_LIMIT = 10 * 1024 * 1024 export const FILE_SIZE_LIMIT = 15 * 1024 * 1024 export const AUDIO_SIZE_LIMIT = 50 * 1024 * 1024 export const VIDEO_SIZE_LIMIT = 100 * 1024 * 1024 +export const MAX_FILE_UPLOAD_LIMIT = 10 export const FILE_URL_REGEX = /^(https?|ftp):\/\// diff --git a/web/app/components/base/file-uploader/hooks.ts b/web/app/components/base/file-uploader/hooks.ts index 088160691b..c735754ffe 100644 --- a/web/app/components/base/file-uploader/hooks.ts +++ b/web/app/components/base/file-uploader/hooks.ts @@ -18,6 +18,7 @@ import { AUDIO_SIZE_LIMIT, FILE_SIZE_LIMIT, IMG_SIZE_LIMIT, + MAX_FILE_UPLOAD_LIMIT, VIDEO_SIZE_LIMIT, } from '@/app/components/base/file-uploader/constants' import { useToastContext } from '@/app/components/base/toast' @@ -33,12 +34,14 @@ export const useFileSizeLimit = (fileUploadConfig?: FileUploadConfigResponse) => const docSizeLimit = Number(fileUploadConfig?.file_size_limit) * 1024 * 1024 || FILE_SIZE_LIMIT const audioSizeLimit = Number(fileUploadConfig?.audio_file_size_limit) * 1024 * 1024 || AUDIO_SIZE_LIMIT const videoSizeLimit = Number(fileUploadConfig?.video_file_size_limit) * 1024 * 1024 || VIDEO_SIZE_LIMIT + const maxFileUploadLimit = Number(fileUploadConfig?.workflow_file_upload_limit) || MAX_FILE_UPLOAD_LIMIT return { imgSizeLimit, docSizeLimit, audioSizeLimit, videoSizeLimit, + maxFileUploadLimit, } } diff --git a/web/app/components/workflow/nodes/_base/components/file-upload-setting.tsx b/web/app/components/workflow/nodes/_base/components/file-upload-setting.tsx index 82a3a906cf..42a7213f80 100644 --- a/web/app/components/workflow/nodes/_base/components/file-upload-setting.tsx +++ b/web/app/components/workflow/nodes/_base/components/file-upload-setting.tsx @@ -39,7 +39,13 @@ const FileUploadSetting: FC = ({ allowed_file_extensions, } = payload const { data: fileUploadConfigResponse } = useSWR({ url: '/files/upload' }, fetchFileUploadConfig) - const { imgSizeLimit, docSizeLimit, audioSizeLimit, videoSizeLimit } = useFileSizeLimit(fileUploadConfigResponse) + const { + imgSizeLimit, + docSizeLimit, + audioSizeLimit, + videoSizeLimit, + maxFileUploadLimit, + } = useFileSizeLimit(fileUploadConfigResponse) const handleSupportFileTypeChange = useCallback((type: SupportUploadFileTypes) => { const newPayload = produce(payload, (draft) => { @@ -156,7 +162,7 @@ const FileUploadSetting: FC = ({
diff --git a/web/models/common.ts b/web/models/common.ts index 9ab27a6018..dc2b1120b9 100644 --- a/web/models/common.ts +++ b/web/models/common.ts @@ -216,7 +216,7 @@ export type FileUploadConfigResponse = { file_size_limit: number // default is 15MB audio_file_size_limit?: number // default is 50MB video_file_size_limit?: number // default is 100MB - + workflow_file_upload_limit?: number // default is 10 } export type InvitationResult = { From 01d8d10f1c7e58cd8da2ea91aff036a85352eb1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=96=B9=E7=A8=8B?= Date: Mon, 4 Nov 2024 17:22:02 +0800 Subject: [PATCH 26/48] Using a dedicated interface to obtain the token credential for the gitee.ai provider (#10243) --- .../model_providers/gitee_ai/gitee_ai.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/api/core/model_runtime/model_providers/gitee_ai/gitee_ai.py b/api/core/model_runtime/model_providers/gitee_ai/gitee_ai.py index ca67594ce4..14aa811905 100644 --- a/api/core/model_runtime/model_providers/gitee_ai/gitee_ai.py +++ b/api/core/model_runtime/model_providers/gitee_ai/gitee_ai.py @@ -1,6 +1,7 @@ import logging -from core.model_runtime.entities.model_entities import ModelType +import requests + from core.model_runtime.errors.validate import CredentialsValidateFailedError from core.model_runtime.model_providers.__base.model_provider import ModelProvider @@ -16,8 +17,18 @@ class GiteeAIProvider(ModelProvider): :param credentials: provider credentials, credentials form defined in `provider_credential_schema`. """ try: - model_instance = self.get_model_instance(ModelType.LLM) - model_instance.validate_credentials(model="Qwen2-7B-Instruct", credentials=credentials) + api_key = credentials.get("api_key") + if not api_key: + raise CredentialsValidateFailedError("Credentials validation failed: api_key not given") + + # send a get request to validate the credentials + headers = {"Authorization": f"Bearer {api_key}"} + response = requests.get("https://ai.gitee.com/api/base/account/me", headers=headers, timeout=(10, 300)) + + if response.status_code != 200: + raise CredentialsValidateFailedError( + f"Credentials validation failed with status code {response.status_code}" + ) except CredentialsValidateFailedError as ex: raise ex except Exception as ex: From 0dda6820333bcaf20c580fe26b0f8b158f9b9058 Mon Sep 17 00:00:00 2001 From: -LAN- Date: Mon, 4 Nov 2024 17:48:10 +0800 Subject: [PATCH 27/48] chore(Dockerfile): upgrade zlib arm64 (#10244) --- api/Dockerfile | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/api/Dockerfile b/api/Dockerfile index c71317f797..ff34603004 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -55,12 +55,7 @@ RUN apt-get update \ && echo "deb http://deb.debian.org/debian testing main" > /etc/apt/sources.list \ && apt-get update \ # For Security - && apt-get install -y --no-install-recommends expat=2.6.3-2 libldap-2.5-0=2.5.18+dfsg-3+b1 perl=5.40.0-6 libsqlite3-0=3.46.1-1 \ - && if [ "$(dpkg --print-architecture)" = "amd64" ]; then \ - apt-get install -y --no-install-recommends zlib1g=1:1.3.dfsg+really1.3.1-1+b1; \ - else \ - apt-get install -y --no-install-recommends zlib1g=1:1.3.dfsg+really1.3.1-1; \ - fi \ + && apt-get install -y --no-install-recommends expat=2.6.3-2 libldap-2.5-0=2.5.18+dfsg-3+b1 perl=5.40.0-6 libsqlite3-0=3.46.1-1 zlib1g=1:1.3.dfsg+really1.3.1-1+b1 \ # install a chinese font to support the use of tools like matplotlib && apt-get install -y fonts-noto-cjk \ && apt-get autoremove -y \ From 794f495ef27a9e5d2bf46f46b722c83d651c1af6 Mon Sep 17 00:00:00 2001 From: -LAN- Date: Mon, 4 Nov 2024 18:34:55 +0800 Subject: [PATCH 28/48] fix(validation): allow to use 0 in the inputs form (#10255) --- api/core/app/apps/base_app_generator.py | 78 +++++++++++-------- .../core/app/apps/test_base_app_generator.py | 52 +++++++++++++ 2 files changed, 97 insertions(+), 33 deletions(-) create mode 100644 api/tests/unit_tests/core/app/apps/test_base_app_generator.py diff --git a/api/core/app/apps/base_app_generator.py b/api/core/app/apps/base_app_generator.py index 2ce7d5d96f..97615f0472 100644 --- a/api/core/app/apps/base_app_generator.py +++ b/api/core/app/apps/base_app_generator.py @@ -23,7 +23,10 @@ class BaseAppGenerator: user_inputs = user_inputs or {} # Filter input variables from form configuration, handle required fields, default values, and option values variables = app_config.variables - user_inputs = {var.variable: self._validate_input(inputs=user_inputs, var=var) for var in variables} + user_inputs = { + var.variable: self._validate_inputs(value=user_inputs.get(var.variable), variable_entity=var) + for var in variables + } user_inputs = {k: self._sanitize_value(v) for k, v in user_inputs.items()} # Convert files in inputs to File entity_dictionary = {item.variable: item for item in app_config.variables} @@ -75,57 +78,66 @@ class BaseAppGenerator: return user_inputs - def _validate_input(self, *, inputs: Mapping[str, Any], var: "VariableEntity"): - user_input_value = inputs.get(var.variable) + def _validate_inputs( + self, + *, + variable_entity: "VariableEntity", + value: Any, + ): + if value is None: + if variable_entity.required: + raise ValueError(f"{variable_entity.variable} is required in input form") + return value - if not user_input_value: - if var.required: - raise ValueError(f"{var.variable} is required in input form") - else: - return None - - if var.type in { + if variable_entity.type in { VariableEntityType.TEXT_INPUT, VariableEntityType.SELECT, VariableEntityType.PARAGRAPH, - } and not isinstance(user_input_value, str): - raise ValueError(f"(type '{var.type}') {var.variable} in input form must be a string") + } and not isinstance(value, str): + raise ValueError( + f"(type '{variable_entity.type}') {variable_entity.variable} in input form must be a string" + ) - if var.type == VariableEntityType.NUMBER and isinstance(user_input_value, str): + if variable_entity.type == VariableEntityType.NUMBER and isinstance(value, str): # may raise ValueError if user_input_value is not a valid number try: - if "." in user_input_value: - return float(user_input_value) + if "." in value: + return float(value) else: - return int(user_input_value) + return int(value) except ValueError: - raise ValueError(f"{var.variable} in input form must be a valid number") + raise ValueError(f"{variable_entity.variable} in input form must be a valid number") - match var.type: + match variable_entity.type: case VariableEntityType.SELECT: - if user_input_value not in var.options: - raise ValueError(f"{var.variable} in input form must be one of the following: {var.options}") + if value not in variable_entity.options: + raise ValueError( + f"{variable_entity.variable} in input form must be one of the following: " + f"{variable_entity.options}" + ) case VariableEntityType.TEXT_INPUT | VariableEntityType.PARAGRAPH: - if var.max_length and len(user_input_value) > var.max_length: - raise ValueError(f"{var.variable} in input form must be less than {var.max_length} characters") + if variable_entity.max_length and len(value) > variable_entity.max_length: + raise ValueError( + f"{variable_entity.variable} in input form must be less than {variable_entity.max_length} " + "characters" + ) case VariableEntityType.FILE: - if not isinstance(user_input_value, dict) and not isinstance(user_input_value, File): - raise ValueError(f"{var.variable} in input form must be a file") + if not isinstance(value, dict) and not isinstance(value, File): + raise ValueError(f"{variable_entity.variable} in input form must be a file") case VariableEntityType.FILE_LIST: # if number of files exceeds the limit, raise ValueError if not ( - isinstance(user_input_value, list) - and ( - all(isinstance(item, dict) for item in user_input_value) - or all(isinstance(item, File) for item in user_input_value) - ) + isinstance(value, list) + and (all(isinstance(item, dict) for item in value) or all(isinstance(item, File) for item in value)) ): - raise ValueError(f"{var.variable} in input form must be a list of files") + raise ValueError(f"{variable_entity.variable} in input form must be a list of files") - if var.max_length and len(user_input_value) > var.max_length: - raise ValueError(f"{var.variable} in input form must be less than {var.max_length} files") + if variable_entity.max_length and len(value) > variable_entity.max_length: + raise ValueError( + f"{variable_entity.variable} in input form must be less than {variable_entity.max_length} files" + ) - return user_input_value + return value def _sanitize_value(self, value: Any) -> Any: if isinstance(value, str): diff --git a/api/tests/unit_tests/core/app/apps/test_base_app_generator.py b/api/tests/unit_tests/core/app/apps/test_base_app_generator.py new file mode 100644 index 0000000000..a6bf43ab0c --- /dev/null +++ b/api/tests/unit_tests/core/app/apps/test_base_app_generator.py @@ -0,0 +1,52 @@ +import pytest + +from core.app.app_config.entities import VariableEntity, VariableEntityType +from core.app.apps.base_app_generator import BaseAppGenerator + + +def test_validate_inputs_with_zero(): + base_app_generator = BaseAppGenerator() + + var = VariableEntity( + variable="test_var", + label="test_var", + type=VariableEntityType.NUMBER, + required=True, + ) + + # Test with input 0 + result = base_app_generator._validate_inputs( + variable_entity=var, + value=0, + ) + + assert result == 0 + + # Test with input "0" (string) + result = base_app_generator._validate_inputs( + variable_entity=var, + value="0", + ) + + assert result == 0 + + +def test_validate_input_with_none_for_required_variable(): + base_app_generator = BaseAppGenerator() + + for var_type in VariableEntityType: + var = VariableEntity( + variable="test_var", + label="test_var", + type=var_type, + required=True, + ) + + # Test with input None + with pytest.raises(ValueError) as exc_info: + base_app_generator._validate_inputs( + variable_entity=var, + value=None, + ) + + assert str(exc_info.value) == "test_var is required in input form" From fe3cde973e05852e945cc89bb626330f0591958a Mon Sep 17 00:00:00 2001 From: -LAN- Date: Tue, 5 Nov 2024 09:27:51 +0800 Subject: [PATCH 29/48] refactor(parameter_extractor): implement custom error classes (#10260) --- .../workflow/nodes/parameter_extractor/exc.py | 50 ++++++++++++++++ .../parameter_extractor_node.py | 57 ++++++++++++------- 2 files changed, 86 insertions(+), 21 deletions(-) create mode 100644 api/core/workflow/nodes/parameter_extractor/exc.py diff --git a/api/core/workflow/nodes/parameter_extractor/exc.py b/api/core/workflow/nodes/parameter_extractor/exc.py new file mode 100644 index 0000000000..6511aba185 --- /dev/null +++ b/api/core/workflow/nodes/parameter_extractor/exc.py @@ -0,0 +1,50 @@ +class ParameterExtractorNodeError(ValueError): + """Base error for ParameterExtractorNode.""" + + +class InvalidModelTypeError(ParameterExtractorNodeError): + """Raised when the model is not a Large Language Model.""" + + +class ModelSchemaNotFoundError(ParameterExtractorNodeError): + """Raised when the model schema is not found.""" + + +class InvalidInvokeResultError(ParameterExtractorNodeError): + """Raised when the invoke result is invalid.""" + + +class InvalidTextContentTypeError(ParameterExtractorNodeError): + """Raised when the text content type is invalid.""" + + +class InvalidNumberOfParametersError(ParameterExtractorNodeError): + """Raised when the number of parameters is invalid.""" + + +class RequiredParameterMissingError(ParameterExtractorNodeError): + """Raised when a required parameter is missing.""" + + +class InvalidSelectValueError(ParameterExtractorNodeError): + """Raised when a select value is invalid.""" + + +class InvalidNumberValueError(ParameterExtractorNodeError): + """Raised when a number value is invalid.""" + + +class InvalidBoolValueError(ParameterExtractorNodeError): + """Raised when a bool value is invalid.""" + + +class InvalidStringValueError(ParameterExtractorNodeError): + """Raised when a string value is invalid.""" + + +class InvalidArrayValueError(ParameterExtractorNodeError): + """Raised when an array value is invalid.""" + + +class InvalidModelModeError(ParameterExtractorNodeError): + """Raised when the model mode is invalid.""" diff --git a/api/core/workflow/nodes/parameter_extractor/parameter_extractor_node.py b/api/core/workflow/nodes/parameter_extractor/parameter_extractor_node.py index 49546e9356..b64bde8ac5 100644 --- a/api/core/workflow/nodes/parameter_extractor/parameter_extractor_node.py +++ b/api/core/workflow/nodes/parameter_extractor/parameter_extractor_node.py @@ -32,6 +32,21 @@ from extensions.ext_database import db from models.workflow import WorkflowNodeExecutionStatus from .entities import ParameterExtractorNodeData +from .exc import ( + InvalidArrayValueError, + InvalidBoolValueError, + InvalidInvokeResultError, + InvalidModelModeError, + InvalidModelTypeError, + InvalidNumberOfParametersError, + InvalidNumberValueError, + InvalidSelectValueError, + InvalidStringValueError, + InvalidTextContentTypeError, + ModelSchemaNotFoundError, + ParameterExtractorNodeError, + RequiredParameterMissingError, +) from .prompts import ( CHAT_EXAMPLE, CHAT_GENERATE_JSON_USER_MESSAGE_TEMPLATE, @@ -85,7 +100,7 @@ class ParameterExtractorNode(LLMNode): model_instance, model_config = self._fetch_model_config(node_data.model) if not isinstance(model_instance.model_type_instance, LargeLanguageModel): - raise ValueError("Model is not a Large Language Model") + raise InvalidModelTypeError("Model is not a Large Language Model") llm_model = model_instance.model_type_instance model_schema = llm_model.get_model_schema( @@ -93,7 +108,7 @@ class ParameterExtractorNode(LLMNode): credentials=model_config.credentials, ) if not model_schema: - raise ValueError("Model schema not found") + raise ModelSchemaNotFoundError("Model schema not found") # fetch memory memory = self._fetch_memory( @@ -155,7 +170,7 @@ class ParameterExtractorNode(LLMNode): process_data["usage"] = jsonable_encoder(usage) process_data["tool_call"] = jsonable_encoder(tool_call) process_data["llm_text"] = text - except Exception as e: + except ParameterExtractorNodeError as e: return NodeRunResult( status=WorkflowNodeExecutionStatus.FAILED, inputs=inputs, @@ -177,7 +192,7 @@ class ParameterExtractorNode(LLMNode): try: result = self._validate_result(data=node_data, result=result or {}) - except Exception as e: + except ParameterExtractorNodeError as e: error = str(e) # transform result into standard format @@ -217,11 +232,11 @@ class ParameterExtractorNode(LLMNode): # handle invoke result if not isinstance(invoke_result, LLMResult): - raise ValueError(f"Invalid invoke result: {invoke_result}") + raise InvalidInvokeResultError(f"Invalid invoke result: {invoke_result}") text = invoke_result.message.content if not isinstance(text, str): - raise ValueError(f"Invalid text content type: {type(text)}. Expected str.") + raise InvalidTextContentTypeError(f"Invalid text content type: {type(text)}. Expected str.") usage = invoke_result.usage tool_call = invoke_result.message.tool_calls[0] if invoke_result.message.tool_calls else None @@ -344,7 +359,7 @@ class ParameterExtractorNode(LLMNode): files=files, ) else: - raise ValueError(f"Invalid model mode: {model_mode}") + raise InvalidModelModeError(f"Invalid model mode: {model_mode}") def _generate_prompt_engineering_completion_prompt( self, @@ -449,36 +464,36 @@ class ParameterExtractorNode(LLMNode): Validate result. """ if len(data.parameters) != len(result): - raise ValueError("Invalid number of parameters") + raise InvalidNumberOfParametersError("Invalid number of parameters") for parameter in data.parameters: if parameter.required and parameter.name not in result: - raise ValueError(f"Parameter {parameter.name} is required") + raise RequiredParameterMissingError(f"Parameter {parameter.name} is required") if parameter.type == "select" and parameter.options and result.get(parameter.name) not in parameter.options: - raise ValueError(f"Invalid `select` value for parameter {parameter.name}") + raise InvalidSelectValueError(f"Invalid `select` value for parameter {parameter.name}") if parameter.type == "number" and not isinstance(result.get(parameter.name), int | float): - raise ValueError(f"Invalid `number` value for parameter {parameter.name}") + raise InvalidNumberValueError(f"Invalid `number` value for parameter {parameter.name}") if parameter.type == "bool" and not isinstance(result.get(parameter.name), bool): - raise ValueError(f"Invalid `bool` value for parameter {parameter.name}") + raise InvalidBoolValueError(f"Invalid `bool` value for parameter {parameter.name}") if parameter.type == "string" and not isinstance(result.get(parameter.name), str): - raise ValueError(f"Invalid `string` value for parameter {parameter.name}") + raise InvalidStringValueError(f"Invalid `string` value for parameter {parameter.name}") if parameter.type.startswith("array"): parameters = result.get(parameter.name) if not isinstance(parameters, list): - raise ValueError(f"Invalid `array` value for parameter {parameter.name}") + raise InvalidArrayValueError(f"Invalid `array` value for parameter {parameter.name}") nested_type = parameter.type[6:-1] for item in parameters: if nested_type == "number" and not isinstance(item, int | float): - raise ValueError(f"Invalid `array[number]` value for parameter {parameter.name}") + raise InvalidArrayValueError(f"Invalid `array[number]` value for parameter {parameter.name}") if nested_type == "string" and not isinstance(item, str): - raise ValueError(f"Invalid `array[string]` value for parameter {parameter.name}") + raise InvalidArrayValueError(f"Invalid `array[string]` value for parameter {parameter.name}") if nested_type == "object" and not isinstance(item, dict): - raise ValueError(f"Invalid `array[object]` value for parameter {parameter.name}") + raise InvalidArrayValueError(f"Invalid `array[object]` value for parameter {parameter.name}") return result def _transform_result(self, data: ParameterExtractorNodeData, result: dict) -> dict: @@ -634,7 +649,7 @@ class ParameterExtractorNode(LLMNode): user_prompt_message = ChatModelMessage(role=PromptMessageRole.USER, text=input_text) return [system_prompt_messages, user_prompt_message] else: - raise ValueError(f"Model mode {model_mode} not support.") + raise InvalidModelModeError(f"Model mode {model_mode} not support.") def _get_prompt_engineering_prompt_template( self, @@ -669,7 +684,7 @@ class ParameterExtractorNode(LLMNode): .replace("}γγγ", "") ) else: - raise ValueError(f"Model mode {model_mode} not support.") + raise InvalidModelModeError(f"Model mode {model_mode} not support.") def _calculate_rest_token( self, @@ -683,12 +698,12 @@ class ParameterExtractorNode(LLMNode): model_instance, model_config = self._fetch_model_config(node_data.model) if not isinstance(model_instance.model_type_instance, LargeLanguageModel): - raise ValueError("Model is not a Large Language Model") + raise InvalidModelTypeError("Model is not a Large Language Model") llm_model = model_instance.model_type_instance model_schema = llm_model.get_model_schema(model_config.model, model_config.credentials) if not model_schema: - raise ValueError("Model schema not found") + raise ModelSchemaNotFoundError("Model schema not found") if set(model_schema.features or []) & {ModelFeature.MULTI_TOOL_CALL, ModelFeature.MULTI_TOOL_CALL}: prompt_template = self._get_function_calling_prompt_template(node_data, query, variable_pool, None, 2000) From 5d2c88ef596d3a6b4f1201f275025bb0b38162ac Mon Sep 17 00:00:00 2001 From: Matsuda Date: Tue, 5 Nov 2024 10:42:51 +0900 Subject: [PATCH 30/48] feat: support Claude 3.5 Haiku on Amazon Bedrock (#10265) --- .../llm/anthropic.claude-3-5-haiku-v1.yaml | 61 +++++++++++++++++++ .../llm/us.anthropic.claude-3-5-haiku-v1.yaml | 61 +++++++++++++++++++ 2 files changed, 122 insertions(+) create mode 100644 api/core/model_runtime/model_providers/bedrock/llm/anthropic.claude-3-5-haiku-v1.yaml create mode 100644 api/core/model_runtime/model_providers/bedrock/llm/us.anthropic.claude-3-5-haiku-v1.yaml diff --git a/api/core/model_runtime/model_providers/bedrock/llm/anthropic.claude-3-5-haiku-v1.yaml b/api/core/model_runtime/model_providers/bedrock/llm/anthropic.claude-3-5-haiku-v1.yaml new file mode 100644 index 0000000000..7c676136db --- /dev/null +++ b/api/core/model_runtime/model_providers/bedrock/llm/anthropic.claude-3-5-haiku-v1.yaml @@ -0,0 +1,61 @@ +model: anthropic.claude-3-5-haiku-20241022-v1:0 +label: + en_US: Claude 3.5 Haiku +model_type: llm +features: + - agent-thought + - vision + - tool-call + - stream-tool-call +model_properties: + mode: chat + context_size: 200000 +# docs: https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-anthropic-claude-messages.html +parameter_rules: + - name: max_tokens + use_template: max_tokens + required: true + type: int + default: 4096 + min: 1 + max: 4096 + help: + zh_Hans: 停止前生成的最大令牌数。请注意,Anthropic Claude 模型可能会在达到 max_tokens 的值之前停止生成令牌。不同的 Anthropic Claude 模型对此参数具有不同的最大值。 + en_US: The maximum number of tokens to generate before stopping. Note that Anthropic Claude models might stop generating tokens before reaching the value of max_tokens. Different Anthropic Claude models have different maximum values for this parameter. + # docs: https://docs.anthropic.com/claude/docs/system-prompts + - name: temperature + use_template: temperature + required: false + type: float + default: 1 + min: 0.0 + max: 1.0 + help: + zh_Hans: 生成内容的随机性。 + en_US: The amount of randomness injected into the response. + - name: top_p + required: false + type: float + default: 0.999 + min: 0.000 + max: 1.000 + help: + zh_Hans: 在核采样中,Anthropic Claude 按概率递减顺序计算每个后续标记的所有选项的累积分布,并在达到 top_p 指定的特定概率时将其切断。您应该更改温度或top_p,但不能同时更改两者。 + en_US: In nucleus sampling, Anthropic Claude computes the cumulative distribution over all the options for each subsequent token in decreasing probability order and cuts it off once it reaches a particular probability specified by top_p. You should alter either temperature or top_p, but not both. + - name: top_k + required: false + type: int + default: 0 + min: 0 + # tip docs from aws has error, max value is 500 + max: 500 + help: + zh_Hans: 对于每个后续标记,仅从前 K 个选项中进行采样。使用 top_k 删除长尾低概率响应。 + en_US: Only sample from the top K options for each subsequent token. Use top_k to remove long tail low probability responses. + - name: response_format + use_template: response_format +pricing: + input: '0.001' + output: '0.005' + unit: '0.001' + currency: USD diff --git a/api/core/model_runtime/model_providers/bedrock/llm/us.anthropic.claude-3-5-haiku-v1.yaml b/api/core/model_runtime/model_providers/bedrock/llm/us.anthropic.claude-3-5-haiku-v1.yaml new file mode 100644 index 0000000000..a9b66b1925 --- /dev/null +++ b/api/core/model_runtime/model_providers/bedrock/llm/us.anthropic.claude-3-5-haiku-v1.yaml @@ -0,0 +1,61 @@ +model: us.anthropic.claude-3-5-haiku-20241022-v1:0 +label: + en_US: Claude 3.5 Haiku(US.Cross Region Inference) +model_type: llm +features: + - agent-thought + - vision + - tool-call + - stream-tool-call +model_properties: + mode: chat + context_size: 200000 +# docs: https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-anthropic-claude-messages.html +parameter_rules: + - name: max_tokens + use_template: max_tokens + required: true + type: int + default: 4096 + min: 1 + max: 4096 + help: + zh_Hans: 停止前生成的最大令牌数。请注意,Anthropic Claude 模型可能会在达到 max_tokens 的值之前停止生成令牌。不同的 Anthropic Claude 模型对此参数具有不同的最大值。 + en_US: The maximum number of tokens to generate before stopping. Note that Anthropic Claude models might stop generating tokens before reaching the value of max_tokens. Different Anthropic Claude models have different maximum values for this parameter. + # docs: https://docs.anthropic.com/claude/docs/system-prompts + - name: temperature + use_template: temperature + required: false + type: float + default: 1 + min: 0.0 + max: 1.0 + help: + zh_Hans: 生成内容的随机性。 + en_US: The amount of randomness injected into the response. + - name: top_p + required: false + type: float + default: 0.999 + min: 0.000 + max: 1.000 + help: + zh_Hans: 在核采样中,Anthropic Claude 按概率递减顺序计算每个后续标记的所有选项的累积分布,并在达到 top_p 指定的特定概率时将其切断。您应该更改温度或top_p,但不能同时更改两者。 + en_US: In nucleus sampling, Anthropic Claude computes the cumulative distribution over all the options for each subsequent token in decreasing probability order and cuts it off once it reaches a particular probability specified by top_p. You should alter either temperature or top_p, but not both. + - name: top_k + required: false + type: int + default: 0 + min: 0 + # tip docs from aws has error, max value is 500 + max: 500 + help: + zh_Hans: 对于每个后续标记,仅从前 K 个选项中进行采样。使用 top_k 删除长尾低概率响应。 + en_US: Only sample from the top K options for each subsequent token. Use top_k to remove long tail low probability responses. + - name: response_format + use_template: response_format +pricing: + input: '0.001' + output: '0.005' + unit: '0.001' + currency: USD From 2cd976846a37264431a95eb129eaa25122508ce4 Mon Sep 17 00:00:00 2001 From: -LAN- Date: Tue, 5 Nov 2024 09:49:43 +0800 Subject: [PATCH 31/48] feat(document_extractor): support tool file in document extractor (#10217) --- api/core/workflow/nodes/document_extractor/node.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/api/core/workflow/nodes/document_extractor/node.py b/api/core/workflow/nodes/document_extractor/node.py index aacee94095..c90017d5e1 100644 --- a/api/core/workflow/nodes/document_extractor/node.py +++ b/api/core/workflow/nodes/document_extractor/node.py @@ -198,10 +198,8 @@ def _download_file_content(file: File) -> bytes: response = ssrf_proxy.get(file.remote_url) response.raise_for_status() return response.content - elif file.transfer_method == FileTransferMethod.LOCAL_FILE: - return file_manager.download(file) else: - raise ValueError(f"Unsupported transfer method: {file.transfer_method}") + return file_manager.download(file) except Exception as e: raise FileDownloadError(f"Error downloading file: {str(e)}") from e From 0858108423f75b992a5664aca64523fddb4a478d Mon Sep 17 00:00:00 2001 From: GeorgeCaoJ Date: Tue, 5 Nov 2024 09:56:41 +0800 Subject: [PATCH 32/48] fix(workflow): handle else condition branch addition error in if-else node (#10257) --- .../workflow/nodes/if-else/use-config.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/web/app/components/workflow/nodes/if-else/use-config.ts b/web/app/components/workflow/nodes/if-else/use-config.ts index d1210431a0..41e41f6b8b 100644 --- a/web/app/components/workflow/nodes/if-else/use-config.ts +++ b/web/app/components/workflow/nodes/if-else/use-config.ts @@ -78,24 +78,24 @@ const useConfig = (id: string, payload: IfElseNodeType) => { }) const handleAddCase = useCallback(() => { - const newInputs = produce(inputs, () => { - if (inputs.cases) { + const newInputs = produce(inputs, (draft) => { + if (draft.cases) { const case_id = uuid4() - inputs.cases.push({ + draft.cases.push({ case_id, logical_operator: LogicalOperator.and, conditions: [], }) - if (inputs._targetBranches) { - const elseCaseIndex = inputs._targetBranches.findIndex(branch => branch.id === 'false') + if (draft._targetBranches) { + const elseCaseIndex = draft._targetBranches.findIndex(branch => branch.id === 'false') if (elseCaseIndex > -1) { - inputs._targetBranches = branchNameCorrect([ - ...inputs._targetBranches.slice(0, elseCaseIndex), + draft._targetBranches = branchNameCorrect([ + ...draft._targetBranches.slice(0, elseCaseIndex), { id: case_id, name: '', }, - ...inputs._targetBranches.slice(elseCaseIndex), + ...draft._targetBranches.slice(elseCaseIndex), ]) } } From d330d31ee59422dd662f84dce35ae84d44c3b223 Mon Sep 17 00:00:00 2001 From: Novice Date: Tue, 5 Nov 2024 10:32:49 +0800 Subject: [PATCH 33/48] feat: Iteration node support parallel mode (#9493) --- .../advanced_chat/generate_task_pipeline.py | 3 +- .../apps/workflow/generate_task_pipeline.py | 3 +- api/core/app/apps/workflow_app_runner.py | 35 ++ api/core/app/entities/queue_entities.py | 37 +- api/core/app/entities/task_entities.py | 2 + .../task_pipeline/workflow_cycle_manage.py | 28 +- api/core/workflow/entities/node_entities.py | 1 + .../workflow/graph_engine/entities/event.py | 7 + .../workflow/graph_engine/graph_engine.py | 11 + api/core/workflow/nodes/iteration/entities.py | 10 + .../nodes/iteration/iteration_node.py | 412 ++++++++++++---- .../nodes/iteration/test_iteration.py | 449 +++++++++++++++++- web/app/components/base/select/index.tsx | 2 +- web/app/components/workflow/constants.ts | 4 +- .../workflow/hooks/use-nodes-interactions.ts | 5 + .../workflow/hooks/use-workflow-run.ts | 102 +++- .../workflow/nodes/_base/components/field.tsx | 6 +- .../components/workflow/nodes/_base/node.tsx | 24 +- .../workflow/nodes/iteration/default.ts | 39 +- .../workflow/nodes/iteration/node.tsx | 15 +- .../workflow/nodes/iteration/panel.tsx | 59 ++- .../workflow/nodes/iteration/types.ts | 5 + .../workflow/nodes/iteration/use-config.ts | 25 +- .../workflow/panel/debug-and-preview/hooks.ts | 12 +- web/app/components/workflow/run/index.tsx | 77 ++- .../workflow/run/iteration-result-panel.tsx | 20 +- web/app/components/workflow/run/node.tsx | 16 +- web/app/components/workflow/store.ts | 10 + web/app/components/workflow/types.ts | 6 +- web/app/components/workflow/utils.ts | 11 +- web/i18n/en-US/workflow.ts | 17 + web/i18n/zh-Hans/workflow.ts | 17 + web/types/workflow.ts | 5 + 33 files changed, 1283 insertions(+), 192 deletions(-) diff --git a/api/core/app/apps/advanced_chat/generate_task_pipeline.py b/api/core/app/apps/advanced_chat/generate_task_pipeline.py index e4cb3f8527..1fc7ffe2c7 100644 --- a/api/core/app/apps/advanced_chat/generate_task_pipeline.py +++ b/api/core/app/apps/advanced_chat/generate_task_pipeline.py @@ -20,6 +20,7 @@ from core.app.entities.queue_entities import ( QueueIterationStartEvent, QueueMessageReplaceEvent, QueueNodeFailedEvent, + QueueNodeInIterationFailedEvent, QueueNodeStartedEvent, QueueNodeSucceededEvent, QueueParallelBranchRunFailedEvent, @@ -314,7 +315,7 @@ class AdvancedChatAppGenerateTaskPipeline(BasedGenerateTaskPipeline, WorkflowCyc if response: yield response - elif isinstance(event, QueueNodeFailedEvent): + elif isinstance(event, QueueNodeFailedEvent | QueueNodeInIterationFailedEvent): workflow_node_execution = self._handle_workflow_node_execution_failed(event) response = self._workflow_node_finish_to_stream_response( diff --git a/api/core/app/apps/workflow/generate_task_pipeline.py b/api/core/app/apps/workflow/generate_task_pipeline.py index 419a5da806..d119d94a61 100644 --- a/api/core/app/apps/workflow/generate_task_pipeline.py +++ b/api/core/app/apps/workflow/generate_task_pipeline.py @@ -16,6 +16,7 @@ from core.app.entities.queue_entities import ( QueueIterationNextEvent, QueueIterationStartEvent, QueueNodeFailedEvent, + QueueNodeInIterationFailedEvent, QueueNodeStartedEvent, QueueNodeSucceededEvent, QueueParallelBranchRunFailedEvent, @@ -275,7 +276,7 @@ class WorkflowAppGenerateTaskPipeline(BasedGenerateTaskPipeline, WorkflowCycleMa if response: yield response - elif isinstance(event, QueueNodeFailedEvent): + elif isinstance(event, QueueNodeFailedEvent | QueueNodeInIterationFailedEvent): workflow_node_execution = self._handle_workflow_node_execution_failed(event) response = self._workflow_node_finish_to_stream_response( diff --git a/api/core/app/apps/workflow_app_runner.py b/api/core/app/apps/workflow_app_runner.py index ca23bbdd47..9a01e8a253 100644 --- a/api/core/app/apps/workflow_app_runner.py +++ b/api/core/app/apps/workflow_app_runner.py @@ -9,6 +9,7 @@ from core.app.entities.queue_entities import ( QueueIterationNextEvent, QueueIterationStartEvent, QueueNodeFailedEvent, + QueueNodeInIterationFailedEvent, QueueNodeStartedEvent, QueueNodeSucceededEvent, QueueParallelBranchRunFailedEvent, @@ -30,6 +31,7 @@ from core.workflow.graph_engine.entities.event import ( IterationRunNextEvent, IterationRunStartedEvent, IterationRunSucceededEvent, + NodeInIterationFailedEvent, NodeRunFailedEvent, NodeRunRetrieverResourceEvent, NodeRunStartedEvent, @@ -193,6 +195,7 @@ class WorkflowBasedAppRunner(AppRunner): node_run_index=event.route_node_state.index, predecessor_node_id=event.predecessor_node_id, in_iteration_id=event.in_iteration_id, + parallel_mode_run_id=event.parallel_mode_run_id, ) ) elif isinstance(event, NodeRunSucceededEvent): @@ -246,9 +249,40 @@ class WorkflowBasedAppRunner(AppRunner): error=event.route_node_state.node_run_result.error if event.route_node_state.node_run_result and event.route_node_state.node_run_result.error else "Unknown error", + execution_metadata=event.route_node_state.node_run_result.metadata + if event.route_node_state.node_run_result + else {}, in_iteration_id=event.in_iteration_id, ) ) + elif isinstance(event, NodeInIterationFailedEvent): + self._publish_event( + QueueNodeInIterationFailedEvent( + node_execution_id=event.id, + node_id=event.node_id, + node_type=event.node_type, + node_data=event.node_data, + parallel_id=event.parallel_id, + parallel_start_node_id=event.parallel_start_node_id, + parent_parallel_id=event.parent_parallel_id, + parent_parallel_start_node_id=event.parent_parallel_start_node_id, + start_at=event.route_node_state.start_at, + inputs=event.route_node_state.node_run_result.inputs + if event.route_node_state.node_run_result + else {}, + process_data=event.route_node_state.node_run_result.process_data + if event.route_node_state.node_run_result + else {}, + outputs=event.route_node_state.node_run_result.outputs + if event.route_node_state.node_run_result + else {}, + execution_metadata=event.route_node_state.node_run_result.metadata + if event.route_node_state.node_run_result + else {}, + in_iteration_id=event.in_iteration_id, + error=event.error, + ) + ) elif isinstance(event, NodeRunStreamChunkEvent): self._publish_event( QueueTextChunkEvent( @@ -326,6 +360,7 @@ class WorkflowBasedAppRunner(AppRunner): index=event.index, node_run_index=workflow_entry.graph_engine.graph_runtime_state.node_run_steps, output=event.pre_iteration_output, + parallel_mode_run_id=event.parallel_mode_run_id, ) ) elif isinstance(event, (IterationRunSucceededEvent | IterationRunFailedEvent)): diff --git a/api/core/app/entities/queue_entities.py b/api/core/app/entities/queue_entities.py index bc43baf8a5..f1542ec5d8 100644 --- a/api/core/app/entities/queue_entities.py +++ b/api/core/app/entities/queue_entities.py @@ -107,7 +107,8 @@ class QueueIterationNextEvent(AppQueueEvent): """parent parallel id if node is in parallel""" parent_parallel_start_node_id: Optional[str] = None """parent parallel start node id if node is in parallel""" - + parallel_mode_run_id: Optional[str] = None + """iteratoin run in parallel mode run id""" node_run_index: int output: Optional[Any] = None # output for the current iteration @@ -273,6 +274,8 @@ class QueueNodeStartedEvent(AppQueueEvent): in_iteration_id: Optional[str] = None """iteration id if node is in iteration""" start_at: datetime + parallel_mode_run_id: Optional[str] = None + """iteratoin run in parallel mode run id""" class QueueNodeSucceededEvent(AppQueueEvent): @@ -306,6 +309,37 @@ class QueueNodeSucceededEvent(AppQueueEvent): error: Optional[str] = None +class QueueNodeInIterationFailedEvent(AppQueueEvent): + """ + QueueNodeInIterationFailedEvent entity + """ + + event: QueueEvent = QueueEvent.NODE_FAILED + + node_execution_id: str + node_id: str + node_type: NodeType + node_data: BaseNodeData + parallel_id: Optional[str] = None + """parallel id if node is in parallel""" + parallel_start_node_id: Optional[str] = None + """parallel start node id if node is in parallel""" + parent_parallel_id: Optional[str] = None + """parent parallel id if node is in parallel""" + parent_parallel_start_node_id: Optional[str] = None + """parent parallel start node id if node is in parallel""" + in_iteration_id: Optional[str] = None + """iteration id if node is in iteration""" + start_at: datetime + + inputs: Optional[dict[str, Any]] = None + process_data: Optional[dict[str, Any]] = None + outputs: Optional[dict[str, Any]] = None + execution_metadata: Optional[dict[NodeRunMetadataKey, Any]] = None + + error: str + + class QueueNodeFailedEvent(AppQueueEvent): """ QueueNodeFailedEvent entity @@ -332,6 +366,7 @@ class QueueNodeFailedEvent(AppQueueEvent): inputs: Optional[dict[str, Any]] = None process_data: Optional[dict[str, Any]] = None outputs: Optional[dict[str, Any]] = None + execution_metadata: Optional[dict[NodeRunMetadataKey, Any]] = None error: str diff --git a/api/core/app/entities/task_entities.py b/api/core/app/entities/task_entities.py index 4b5f4716ed..7e9aad54be 100644 --- a/api/core/app/entities/task_entities.py +++ b/api/core/app/entities/task_entities.py @@ -244,6 +244,7 @@ class NodeStartStreamResponse(StreamResponse): parent_parallel_id: Optional[str] = None parent_parallel_start_node_id: Optional[str] = None iteration_id: Optional[str] = None + parallel_run_id: Optional[str] = None event: StreamEvent = StreamEvent.NODE_STARTED workflow_run_id: str @@ -432,6 +433,7 @@ class IterationNodeNextStreamResponse(StreamResponse): extras: dict = {} parallel_id: Optional[str] = None parallel_start_node_id: Optional[str] = None + parallel_mode_run_id: Optional[str] = None event: StreamEvent = StreamEvent.ITERATION_NEXT workflow_run_id: str diff --git a/api/core/app/task_pipeline/workflow_cycle_manage.py b/api/core/app/task_pipeline/workflow_cycle_manage.py index 2abee5bef5..b89edf9079 100644 --- a/api/core/app/task_pipeline/workflow_cycle_manage.py +++ b/api/core/app/task_pipeline/workflow_cycle_manage.py @@ -12,6 +12,7 @@ from core.app.entities.queue_entities import ( QueueIterationNextEvent, QueueIterationStartEvent, QueueNodeFailedEvent, + QueueNodeInIterationFailedEvent, QueueNodeStartedEvent, QueueNodeSucceededEvent, QueueParallelBranchRunFailedEvent, @@ -35,6 +36,7 @@ from core.model_runtime.utils.encoders import jsonable_encoder from core.ops.entities.trace_entity import TraceTaskName from core.ops.ops_trace_manager import TraceQueueManager, TraceTask from core.tools.tool_manager import ToolManager +from core.workflow.entities.node_entities import NodeRunMetadataKey from core.workflow.enums import SystemVariableKey from core.workflow.nodes import NodeType from core.workflow.nodes.tool.entities import ToolNodeData @@ -251,6 +253,12 @@ class WorkflowCycleManage: workflow_node_execution.status = WorkflowNodeExecutionStatus.RUNNING.value workflow_node_execution.created_by_role = workflow_run.created_by_role workflow_node_execution.created_by = workflow_run.created_by + workflow_node_execution.execution_metadata = json.dumps( + { + NodeRunMetadataKey.PARALLEL_MODE_RUN_ID: event.parallel_mode_run_id, + NodeRunMetadataKey.ITERATION_ID: event.in_iteration_id, + } + ) workflow_node_execution.created_at = datetime.now(timezone.utc).replace(tzinfo=None) session.add(workflow_node_execution) @@ -305,7 +313,9 @@ class WorkflowCycleManage: return workflow_node_execution - def _handle_workflow_node_execution_failed(self, event: QueueNodeFailedEvent) -> WorkflowNodeExecution: + def _handle_workflow_node_execution_failed( + self, event: QueueNodeFailedEvent | QueueNodeInIterationFailedEvent + ) -> WorkflowNodeExecution: """ Workflow node execution failed :param event: queue node failed event @@ -318,16 +328,19 @@ class WorkflowCycleManage: outputs = WorkflowEntry.handle_special_values(event.outputs) finished_at = datetime.now(timezone.utc).replace(tzinfo=None) elapsed_time = (finished_at - event.start_at).total_seconds() - + execution_metadata = ( + json.dumps(jsonable_encoder(event.execution_metadata)) if event.execution_metadata else None + ) db.session.query(WorkflowNodeExecution).filter(WorkflowNodeExecution.id == workflow_node_execution.id).update( { WorkflowNodeExecution.status: WorkflowNodeExecutionStatus.FAILED.value, WorkflowNodeExecution.error: event.error, WorkflowNodeExecution.inputs: json.dumps(inputs) if inputs else None, - WorkflowNodeExecution.process_data: json.dumps(process_data) if event.process_data else None, + WorkflowNodeExecution.process_data: json.dumps(event.process_data) if event.process_data else None, WorkflowNodeExecution.outputs: json.dumps(outputs) if outputs else None, WorkflowNodeExecution.finished_at: finished_at, WorkflowNodeExecution.elapsed_time: elapsed_time, + WorkflowNodeExecution.execution_metadata: execution_metadata, } ) @@ -342,6 +355,7 @@ class WorkflowCycleManage: workflow_node_execution.outputs = json.dumps(outputs) if outputs else None workflow_node_execution.finished_at = finished_at workflow_node_execution.elapsed_time = elapsed_time + workflow_node_execution.execution_metadata = execution_metadata self._wip_workflow_node_executions.pop(workflow_node_execution.node_execution_id) @@ -448,6 +462,7 @@ class WorkflowCycleManage: parent_parallel_id=event.parent_parallel_id, parent_parallel_start_node_id=event.parent_parallel_start_node_id, iteration_id=event.in_iteration_id, + parallel_run_id=event.parallel_mode_run_id, ), ) @@ -464,7 +479,7 @@ class WorkflowCycleManage: def _workflow_node_finish_to_stream_response( self, - event: QueueNodeSucceededEvent | QueueNodeFailedEvent, + event: QueueNodeSucceededEvent | QueueNodeFailedEvent | QueueNodeInIterationFailedEvent, task_id: str, workflow_node_execution: WorkflowNodeExecution, ) -> Optional[NodeFinishStreamResponse]: @@ -608,6 +623,7 @@ class WorkflowCycleManage: extras={}, parallel_id=event.parallel_id, parallel_start_node_id=event.parallel_start_node_id, + parallel_mode_run_id=event.parallel_mode_run_id, ), ) @@ -633,7 +649,9 @@ class WorkflowCycleManage: created_at=int(time.time()), extras={}, inputs=event.inputs or {}, - status=WorkflowNodeExecutionStatus.SUCCEEDED, + status=WorkflowNodeExecutionStatus.SUCCEEDED + if event.error is None + else WorkflowNodeExecutionStatus.FAILED, error=None, elapsed_time=(datetime.now(timezone.utc).replace(tzinfo=None) - event.start_at).total_seconds(), total_tokens=event.metadata.get("total_tokens", 0) if event.metadata else 0, diff --git a/api/core/workflow/entities/node_entities.py b/api/core/workflow/entities/node_entities.py index 0131bb342b..7e10cddc71 100644 --- a/api/core/workflow/entities/node_entities.py +++ b/api/core/workflow/entities/node_entities.py @@ -23,6 +23,7 @@ class NodeRunMetadataKey(str, Enum): PARALLEL_START_NODE_ID = "parallel_start_node_id" PARENT_PARALLEL_ID = "parent_parallel_id" PARENT_PARALLEL_START_NODE_ID = "parent_parallel_start_node_id" + PARALLEL_MODE_RUN_ID = "parallel_mode_run_id" class NodeRunResult(BaseModel): diff --git a/api/core/workflow/graph_engine/entities/event.py b/api/core/workflow/graph_engine/entities/event.py index 86d89e0a32..bacea191dd 100644 --- a/api/core/workflow/graph_engine/entities/event.py +++ b/api/core/workflow/graph_engine/entities/event.py @@ -59,6 +59,7 @@ class BaseNodeEvent(GraphEngineEvent): class NodeRunStartedEvent(BaseNodeEvent): predecessor_node_id: Optional[str] = None + parallel_mode_run_id: Optional[str] = None """predecessor node id""" @@ -81,6 +82,10 @@ class NodeRunFailedEvent(BaseNodeEvent): error: str = Field(..., description="error") +class NodeInIterationFailedEvent(BaseNodeEvent): + error: str = Field(..., description="error") + + ########################################### # Parallel Branch Events ########################################### @@ -129,6 +134,8 @@ class BaseIterationEvent(GraphEngineEvent): """parent parallel id if node is in parallel""" parent_parallel_start_node_id: Optional[str] = None """parent parallel start node id if node is in parallel""" + parallel_mode_run_id: Optional[str] = None + """iteratoin run in parallel mode run id""" class IterationRunStartedEvent(BaseIterationEvent): diff --git a/api/core/workflow/graph_engine/graph_engine.py b/api/core/workflow/graph_engine/graph_engine.py index 8f58af00ef..f07ad4de11 100644 --- a/api/core/workflow/graph_engine/graph_engine.py +++ b/api/core/workflow/graph_engine/graph_engine.py @@ -4,6 +4,7 @@ import time import uuid from collections.abc import Generator, Mapping from concurrent.futures import ThreadPoolExecutor, wait +from copy import copy, deepcopy from typing import Any, Optional from flask import Flask, current_app @@ -724,6 +725,16 @@ class GraphEngine: """ return time.perf_counter() - start_at > max_execution_time + def create_copy(self): + """ + create a graph engine copy + :return: with a new variable pool instance of graph engine + """ + new_instance = copy(self) + new_instance.graph_runtime_state = copy(self.graph_runtime_state) + new_instance.graph_runtime_state.variable_pool = deepcopy(self.graph_runtime_state.variable_pool) + return new_instance + class GraphRunFailedError(Exception): def __init__(self, error: str): diff --git a/api/core/workflow/nodes/iteration/entities.py b/api/core/workflow/nodes/iteration/entities.py index 4afc870e50..ebcb6f82fb 100644 --- a/api/core/workflow/nodes/iteration/entities.py +++ b/api/core/workflow/nodes/iteration/entities.py @@ -1,3 +1,4 @@ +from enum import Enum from typing import Any, Optional from pydantic import Field @@ -5,6 +6,12 @@ from pydantic import Field from core.workflow.nodes.base import BaseIterationNodeData, BaseIterationState, BaseNodeData +class ErrorHandleMode(str, Enum): + TERMINATED = "terminated" + CONTINUE_ON_ERROR = "continue-on-error" + REMOVE_ABNORMAL_OUTPUT = "remove-abnormal-output" + + class IterationNodeData(BaseIterationNodeData): """ Iteration Node Data. @@ -13,6 +20,9 @@ class IterationNodeData(BaseIterationNodeData): parent_loop_id: Optional[str] = None # redundant field, not used currently iterator_selector: list[str] # variable selector output_selector: list[str] # output selector + is_parallel: bool = False # open the parallel mode or not + parallel_nums: int = 10 # the numbers of parallel + error_handle_mode: ErrorHandleMode = ErrorHandleMode.TERMINATED # how to handle the error class IterationStartNodeData(BaseNodeData): diff --git a/api/core/workflow/nodes/iteration/iteration_node.py b/api/core/workflow/nodes/iteration/iteration_node.py index af79da9215..d121b0530a 100644 --- a/api/core/workflow/nodes/iteration/iteration_node.py +++ b/api/core/workflow/nodes/iteration/iteration_node.py @@ -1,12 +1,20 @@ import logging +import uuid from collections.abc import Generator, Mapping, Sequence +from concurrent.futures import Future, wait from datetime import datetime, timezone -from typing import Any, cast +from queue import Empty, Queue +from typing import TYPE_CHECKING, Any, Optional, cast + +from flask import Flask, current_app from configs import dify_config from core.model_runtime.utils.encoders import jsonable_encoder -from core.variables import IntegerSegment -from core.workflow.entities.node_entities import NodeRunMetadataKey, NodeRunResult +from core.workflow.entities.node_entities import ( + NodeRunMetadataKey, + NodeRunResult, +) +from core.workflow.entities.variable_pool import VariablePool from core.workflow.graph_engine.entities.event import ( BaseGraphEvent, BaseNodeEvent, @@ -17,6 +25,9 @@ from core.workflow.graph_engine.entities.event import ( IterationRunNextEvent, IterationRunStartedEvent, IterationRunSucceededEvent, + NodeInIterationFailedEvent, + NodeRunFailedEvent, + NodeRunStartedEvent, NodeRunStreamChunkEvent, NodeRunSucceededEvent, ) @@ -24,9 +35,11 @@ from core.workflow.graph_engine.entities.graph import Graph from core.workflow.nodes.base import BaseNode from core.workflow.nodes.enums import NodeType from core.workflow.nodes.event import NodeEvent, RunCompletedEvent -from core.workflow.nodes.iteration.entities import IterationNodeData +from core.workflow.nodes.iteration.entities import ErrorHandleMode, IterationNodeData from models.workflow import WorkflowNodeExecutionStatus +if TYPE_CHECKING: + from core.workflow.graph_engine.graph_engine import GraphEngine logger = logging.getLogger(__name__) @@ -38,6 +51,17 @@ class IterationNode(BaseNode[IterationNodeData]): _node_data_cls = IterationNodeData _node_type = NodeType.ITERATION + @classmethod + def get_default_config(cls, filters: Optional[dict] = None) -> dict: + return { + "type": "iteration", + "config": { + "is_parallel": False, + "parallel_nums": 10, + "error_handle_mode": ErrorHandleMode.TERMINATED.value, + }, + } + def _run(self) -> Generator[NodeEvent | InNodeEvent, None, None]: """ Run the node. @@ -83,7 +107,7 @@ class IterationNode(BaseNode[IterationNodeData]): variable_pool.add([self.node_id, "item"], iterator_list_value[0]) # init graph engine - from core.workflow.graph_engine.graph_engine import GraphEngine + from core.workflow.graph_engine.graph_engine import GraphEngine, GraphEngineThreadPool graph_engine = GraphEngine( tenant_id=self.tenant_id, @@ -123,108 +147,64 @@ class IterationNode(BaseNode[IterationNodeData]): index=0, pre_iteration_output=None, ) - outputs: list[Any] = [] try: - for _ in range(len(iterator_list_value)): - # run workflow - rst = graph_engine.run() - for event in rst: - if isinstance(event, (BaseNodeEvent | BaseParallelBranchEvent)) and not event.in_iteration_id: - event.in_iteration_id = self.node_id - - if ( - isinstance(event, BaseNodeEvent) - and event.node_type == NodeType.ITERATION_START - and not isinstance(event, NodeRunStreamChunkEvent) - ): + if self.node_data.is_parallel: + futures: list[Future] = [] + q = Queue() + thread_pool = GraphEngineThreadPool(max_workers=self.node_data.parallel_nums, max_submit_count=100) + for index, item in enumerate(iterator_list_value): + future: Future = thread_pool.submit( + self._run_single_iter_parallel, + current_app._get_current_object(), + q, + iterator_list_value, + inputs, + outputs, + start_at, + graph_engine, + iteration_graph, + index, + item, + ) + future.add_done_callback(thread_pool.task_done_callback) + futures.append(future) + succeeded_count = 0 + while True: + try: + event = q.get(timeout=1) + if event is None: + break + if isinstance(event, IterationRunNextEvent): + succeeded_count += 1 + if succeeded_count == len(futures): + q.put(None) + yield event + if isinstance(event, RunCompletedEvent): + q.put(None) + for f in futures: + if not f.done(): + f.cancel() + yield event + if isinstance(event, IterationRunFailedEvent): + q.put(None) + yield event + except Empty: continue - if isinstance(event, NodeRunSucceededEvent): - if event.route_node_state.node_run_result: - metadata = event.route_node_state.node_run_result.metadata - if not metadata: - metadata = {} - - if NodeRunMetadataKey.ITERATION_ID not in metadata: - metadata[NodeRunMetadataKey.ITERATION_ID] = self.node_id - index_variable = variable_pool.get([self.node_id, "index"]) - if not isinstance(index_variable, IntegerSegment): - yield RunCompletedEvent( - run_result=NodeRunResult( - status=WorkflowNodeExecutionStatus.FAILED, - error=f"Invalid index variable type: {type(index_variable)}", - ) - ) - return - metadata[NodeRunMetadataKey.ITERATION_INDEX] = index_variable.value - event.route_node_state.node_run_result.metadata = metadata - - yield event - elif isinstance(event, BaseGraphEvent): - if isinstance(event, GraphRunFailedEvent): - # iteration run failed - yield IterationRunFailedEvent( - iteration_id=self.id, - iteration_node_id=self.node_id, - iteration_node_type=self.node_type, - iteration_node_data=self.node_data, - start_at=start_at, - inputs=inputs, - outputs={"output": jsonable_encoder(outputs)}, - steps=len(iterator_list_value), - metadata={"total_tokens": graph_engine.graph_runtime_state.total_tokens}, - error=event.error, - ) - - yield RunCompletedEvent( - run_result=NodeRunResult( - status=WorkflowNodeExecutionStatus.FAILED, - error=event.error, - ) - ) - return - else: - event = cast(InNodeEvent, event) - yield event - - # append to iteration output variable list - current_iteration_output_variable = variable_pool.get(self.node_data.output_selector) - if current_iteration_output_variable is None: - yield RunCompletedEvent( - run_result=NodeRunResult( - status=WorkflowNodeExecutionStatus.FAILED, - error=f"Iteration output variable {self.node_data.output_selector} not found", - ) + # wait all threads + wait(futures) + else: + for _ in range(len(iterator_list_value)): + yield from self._run_single_iter( + iterator_list_value, + variable_pool, + inputs, + outputs, + start_at, + graph_engine, + iteration_graph, ) - return - current_iteration_output = current_iteration_output_variable.to_object() - outputs.append(current_iteration_output) - - # remove all nodes outputs from variable pool - for node_id in iteration_graph.node_ids: - variable_pool.remove([node_id]) - - # move to next iteration - current_index_variable = variable_pool.get([self.node_id, "index"]) - if not isinstance(current_index_variable, IntegerSegment): - raise ValueError(f"iteration {self.node_id} current index not found") - - next_index = current_index_variable.value + 1 - variable_pool.add([self.node_id, "index"], next_index) - - if next_index < len(iterator_list_value): - variable_pool.add([self.node_id, "item"], iterator_list_value[next_index]) - - yield IterationRunNextEvent( - iteration_id=self.id, - iteration_node_id=self.node_id, - iteration_node_type=self.node_type, - iteration_node_data=self.node_data, - index=next_index, - pre_iteration_output=jsonable_encoder(current_iteration_output), - ) - yield IterationRunSucceededEvent( iteration_id=self.id, iteration_node_id=self.node_id, @@ -330,3 +310,231 @@ class IterationNode(BaseNode[IterationNodeData]): } return variable_mapping + + def _handle_event_metadata( + self, event: BaseNodeEvent, iter_run_index: str, parallel_mode_run_id: str + ) -> NodeRunStartedEvent | BaseNodeEvent: + """ + add iteration metadata to event. + """ + if not isinstance(event, BaseNodeEvent): + return event + if self.node_data.is_parallel and isinstance(event, NodeRunStartedEvent): + event.parallel_mode_run_id = parallel_mode_run_id + return event + if event.route_node_state.node_run_result: + metadata = event.route_node_state.node_run_result.metadata + if not metadata: + metadata = {} + + if NodeRunMetadataKey.ITERATION_ID not in metadata: + metadata[NodeRunMetadataKey.ITERATION_ID] = self.node_id + if self.node_data.is_parallel: + metadata[NodeRunMetadataKey.PARALLEL_MODE_RUN_ID] = parallel_mode_run_id + else: + metadata[NodeRunMetadataKey.ITERATION_INDEX] = iter_run_index + event.route_node_state.node_run_result.metadata = metadata + return event + + def _run_single_iter( + self, + iterator_list_value: list[str], + variable_pool: VariablePool, + inputs: dict[str, list], + outputs: list, + start_at: datetime, + graph_engine: "GraphEngine", + iteration_graph: Graph, + parallel_mode_run_id: Optional[str] = None, + ) -> Generator[NodeEvent | InNodeEvent, None, None]: + """ + run single iteration + """ + try: + rst = graph_engine.run() + # get current iteration index + current_index = variable_pool.get([self.node_id, "index"]).value + next_index = int(current_index) + 1 + + if current_index is None: + raise ValueError(f"iteration {self.node_id} current index not found") + for event in rst: + if isinstance(event, (BaseNodeEvent | BaseParallelBranchEvent)) and not event.in_iteration_id: + event.in_iteration_id = self.node_id + + if ( + isinstance(event, BaseNodeEvent) + and event.node_type == NodeType.ITERATION_START + and not isinstance(event, NodeRunStreamChunkEvent) + ): + continue + + if isinstance(event, NodeRunSucceededEvent): + yield self._handle_event_metadata(event, current_index, parallel_mode_run_id) + elif isinstance(event, BaseGraphEvent): + if isinstance(event, GraphRunFailedEvent): + # iteration run failed + if self.node_data.is_parallel: + yield IterationRunFailedEvent( + iteration_id=self.id, + iteration_node_id=self.node_id, + iteration_node_type=self.node_type, + iteration_node_data=self.node_data, + parallel_mode_run_id=parallel_mode_run_id, + start_at=start_at, + inputs=inputs, + outputs={"output": jsonable_encoder(outputs)}, + steps=len(iterator_list_value), + metadata={"total_tokens": graph_engine.graph_runtime_state.total_tokens}, + error=event.error, + ) + else: + yield IterationRunFailedEvent( + iteration_id=self.id, + iteration_node_id=self.node_id, + iteration_node_type=self.node_type, + iteration_node_data=self.node_data, + start_at=start_at, + inputs=inputs, + outputs={"output": jsonable_encoder(outputs)}, + steps=len(iterator_list_value), + metadata={"total_tokens": graph_engine.graph_runtime_state.total_tokens}, + error=event.error, + ) + yield RunCompletedEvent( + run_result=NodeRunResult( + status=WorkflowNodeExecutionStatus.FAILED, + error=event.error, + ) + ) + return + else: + event = cast(InNodeEvent, event) + metadata_event = self._handle_event_metadata(event, current_index, parallel_mode_run_id) + if isinstance(event, NodeRunFailedEvent): + if self.node_data.error_handle_mode == ErrorHandleMode.CONTINUE_ON_ERROR: + yield NodeInIterationFailedEvent( + **metadata_event.model_dump(), + ) + outputs.insert(current_index, None) + variable_pool.add([self.node_id, "index"], next_index) + if next_index < len(iterator_list_value): + variable_pool.add([self.node_id, "item"], iterator_list_value[next_index]) + yield IterationRunNextEvent( + iteration_id=self.id, + iteration_node_id=self.node_id, + iteration_node_type=self.node_type, + iteration_node_data=self.node_data, + index=next_index, + parallel_mode_run_id=parallel_mode_run_id, + pre_iteration_output=None, + ) + return + elif self.node_data.error_handle_mode == ErrorHandleMode.REMOVE_ABNORMAL_OUTPUT: + yield NodeInIterationFailedEvent( + **metadata_event.model_dump(), + ) + variable_pool.add([self.node_id, "index"], next_index) + + if next_index < len(iterator_list_value): + variable_pool.add([self.node_id, "item"], iterator_list_value[next_index]) + yield IterationRunNextEvent( + iteration_id=self.id, + iteration_node_id=self.node_id, + iteration_node_type=self.node_type, + iteration_node_data=self.node_data, + index=next_index, + parallel_mode_run_id=parallel_mode_run_id, + pre_iteration_output=None, + ) + return + elif self.node_data.error_handle_mode == ErrorHandleMode.TERMINATED: + yield IterationRunFailedEvent( + iteration_id=self.id, + iteration_node_id=self.node_id, + iteration_node_type=self.node_type, + iteration_node_data=self.node_data, + start_at=start_at, + inputs=inputs, + outputs={"output": None}, + steps=len(iterator_list_value), + metadata={"total_tokens": graph_engine.graph_runtime_state.total_tokens}, + error=event.error, + ) + yield metadata_event + + current_iteration_output = variable_pool.get(self.node_data.output_selector).value + outputs.insert(current_index, current_iteration_output) + # remove all nodes outputs from variable pool + for node_id in iteration_graph.node_ids: + variable_pool.remove([node_id]) + + # move to next iteration + variable_pool.add([self.node_id, "index"], next_index) + + if next_index < len(iterator_list_value): + variable_pool.add([self.node_id, "item"], iterator_list_value[next_index]) + yield IterationRunNextEvent( + iteration_id=self.id, + iteration_node_id=self.node_id, + iteration_node_type=self.node_type, + iteration_node_data=self.node_data, + index=next_index, + parallel_mode_run_id=parallel_mode_run_id, + pre_iteration_output=jsonable_encoder(current_iteration_output) if current_iteration_output else None, + ) + + except Exception as e: + logger.exception(f"Iteration run failed:{str(e)}") + yield IterationRunFailedEvent( + iteration_id=self.id, + iteration_node_id=self.node_id, + iteration_node_type=self.node_type, + iteration_node_data=self.node_data, + start_at=start_at, + inputs=inputs, + outputs={"output": None}, + steps=len(iterator_list_value), + metadata={"total_tokens": graph_engine.graph_runtime_state.total_tokens}, + error=str(e), + ) + yield RunCompletedEvent( + run_result=NodeRunResult( + status=WorkflowNodeExecutionStatus.FAILED, + error=str(e), + ) + ) + + def _run_single_iter_parallel( + self, + flask_app: Flask, + q: Queue, + iterator_list_value: list[str], + inputs: dict[str, list], + outputs: list, + start_at: datetime, + graph_engine: "GraphEngine", + iteration_graph: Graph, + index: int, + item: Any, + ) -> Generator[NodeEvent | InNodeEvent, None, None]: + """ + run single iteration in parallel mode + """ + with flask_app.app_context(): + parallel_mode_run_id = uuid.uuid4().hex + graph_engine_copy = graph_engine.create_copy() + variable_pool_copy = graph_engine_copy.graph_runtime_state.variable_pool + variable_pool_copy.add([self.node_id, "index"], index) + variable_pool_copy.add([self.node_id, "item"], item) + for event in self._run_single_iter( + iterator_list_value=iterator_list_value, + variable_pool=variable_pool_copy, + inputs=inputs, + outputs=outputs, + start_at=start_at, + graph_engine=graph_engine_copy, + iteration_graph=iteration_graph, + parallel_mode_run_id=parallel_mode_run_id, + ): + q.put(event) diff --git a/api/tests/unit_tests/core/workflow/nodes/iteration/test_iteration.py b/api/tests/unit_tests/core/workflow/nodes/iteration/test_iteration.py index d755faee8a..29bd4d6c6c 100644 --- a/api/tests/unit_tests/core/workflow/nodes/iteration/test_iteration.py +++ b/api/tests/unit_tests/core/workflow/nodes/iteration/test_iteration.py @@ -10,6 +10,7 @@ from core.workflow.graph_engine.entities.graph import Graph from core.workflow.graph_engine.entities.graph_init_params import GraphInitParams from core.workflow.graph_engine.entities.graph_runtime_state import GraphRuntimeState from core.workflow.nodes.event import RunCompletedEvent +from core.workflow.nodes.iteration.entities import ErrorHandleMode from core.workflow.nodes.iteration.iteration_node import IterationNode from core.workflow.nodes.template_transform.template_transform_node import TemplateTransformNode from models.enums import UserFrom @@ -185,8 +186,6 @@ def test_run(): outputs={"output": "dify 123"}, ) - # print("") - with patch.object(TemplateTransformNode, "_run", new=tt_generator): # execute node result = iteration_node._run() @@ -404,18 +403,458 @@ def test_run_parallel(): outputs={"output": "dify 123"}, ) - # print("") - with patch.object(TemplateTransformNode, "_run", new=tt_generator): # execute node result = iteration_node._run() count = 0 for item in result: - # print(type(item), item) count += 1 if isinstance(item, RunCompletedEvent): assert item.run_result.status == WorkflowNodeExecutionStatus.SUCCEEDED assert item.run_result.outputs == {"output": ["dify 123", "dify 123"]} assert count == 32 + + +def test_iteration_run_in_parallel_mode(): + graph_config = { + "edges": [ + { + "id": "start-source-pe-target", + "source": "start", + "target": "pe", + }, + { + "id": "iteration-1-source-answer-3-target", + "source": "iteration-1", + "target": "answer-3", + }, + { + "id": "iteration-start-source-tt-target", + "source": "iteration-start", + "target": "tt", + }, + { + "id": "iteration-start-source-tt-2-target", + "source": "iteration-start", + "target": "tt-2", + }, + { + "id": "tt-source-if-else-target", + "source": "tt", + "target": "if-else", + }, + { + "id": "tt-2-source-if-else-target", + "source": "tt-2", + "target": "if-else", + }, + { + "id": "if-else-true-answer-2-target", + "source": "if-else", + "sourceHandle": "true", + "target": "answer-2", + }, + { + "id": "if-else-false-answer-4-target", + "source": "if-else", + "sourceHandle": "false", + "target": "answer-4", + }, + { + "id": "pe-source-iteration-1-target", + "source": "pe", + "target": "iteration-1", + }, + ], + "nodes": [ + {"data": {"title": "Start", "type": "start", "variables": []}, "id": "start"}, + { + "data": { + "iterator_selector": ["pe", "list_output"], + "output_selector": ["tt", "output"], + "output_type": "array[string]", + "startNodeType": "template-transform", + "start_node_id": "iteration-start", + "title": "iteration", + "type": "iteration", + }, + "id": "iteration-1", + }, + { + "data": { + "answer": "{{#tt.output#}}", + "iteration_id": "iteration-1", + "title": "answer 2", + "type": "answer", + }, + "id": "answer-2", + }, + { + "data": { + "iteration_id": "iteration-1", + "title": "iteration-start", + "type": "iteration-start", + }, + "id": "iteration-start", + }, + { + "data": { + "iteration_id": "iteration-1", + "template": "{{ arg1 }} 123", + "title": "template transform", + "type": "template-transform", + "variables": [{"value_selector": ["sys", "query"], "variable": "arg1"}], + }, + "id": "tt", + }, + { + "data": { + "iteration_id": "iteration-1", + "template": "{{ arg1 }} 321", + "title": "template transform", + "type": "template-transform", + "variables": [{"value_selector": ["sys", "query"], "variable": "arg1"}], + }, + "id": "tt-2", + }, + { + "data": {"answer": "{{#iteration-1.output#}}88888", "title": "answer 3", "type": "answer"}, + "id": "answer-3", + }, + { + "data": { + "conditions": [ + { + "comparison_operator": "is", + "id": "1721916275284", + "value": "hi", + "variable_selector": ["sys", "query"], + } + ], + "iteration_id": "iteration-1", + "logical_operator": "and", + "title": "if", + "type": "if-else", + }, + "id": "if-else", + }, + { + "data": {"answer": "no hi", "iteration_id": "iteration-1", "title": "answer 4", "type": "answer"}, + "id": "answer-4", + }, + { + "data": { + "instruction": "test1", + "model": { + "completion_params": {"temperature": 0.7}, + "mode": "chat", + "name": "gpt-4o", + "provider": "openai", + }, + "parameters": [ + {"description": "test", "name": "list_output", "required": False, "type": "array[string]"} + ], + "query": ["sys", "query"], + "reasoning_mode": "prompt", + "title": "pe", + "type": "parameter-extractor", + }, + "id": "pe", + }, + ], + } + + graph = Graph.init(graph_config=graph_config) + + init_params = GraphInitParams( + tenant_id="1", + app_id="1", + workflow_type=WorkflowType.CHAT, + workflow_id="1", + graph_config=graph_config, + user_id="1", + user_from=UserFrom.ACCOUNT, + invoke_from=InvokeFrom.DEBUGGER, + call_depth=0, + ) + + # construct variable pool + pool = VariablePool( + system_variables={ + SystemVariableKey.QUERY: "dify", + SystemVariableKey.FILES: [], + SystemVariableKey.CONVERSATION_ID: "abababa", + SystemVariableKey.USER_ID: "1", + }, + user_inputs={}, + environment_variables=[], + ) + pool.add(["pe", "list_output"], ["dify-1", "dify-2"]) + + parallel_iteration_node = IterationNode( + id=str(uuid.uuid4()), + graph_init_params=init_params, + graph=graph, + graph_runtime_state=GraphRuntimeState(variable_pool=pool, start_at=time.perf_counter()), + config={ + "data": { + "iterator_selector": ["pe", "list_output"], + "output_selector": ["tt", "output"], + "output_type": "array[string]", + "startNodeType": "template-transform", + "start_node_id": "iteration-start", + "title": "迭代", + "type": "iteration", + "is_parallel": True, + }, + "id": "iteration-1", + }, + ) + sequential_iteration_node = IterationNode( + id=str(uuid.uuid4()), + graph_init_params=init_params, + graph=graph, + graph_runtime_state=GraphRuntimeState(variable_pool=pool, start_at=time.perf_counter()), + config={ + "data": { + "iterator_selector": ["pe", "list_output"], + "output_selector": ["tt", "output"], + "output_type": "array[string]", + "startNodeType": "template-transform", + "start_node_id": "iteration-start", + "title": "迭代", + "type": "iteration", + "is_parallel": True, + }, + "id": "iteration-1", + }, + ) + + def tt_generator(self): + return NodeRunResult( + status=WorkflowNodeExecutionStatus.SUCCEEDED, + inputs={"iterator_selector": "dify"}, + outputs={"output": "dify 123"}, + ) + + with patch.object(TemplateTransformNode, "_run", new=tt_generator): + # execute node + parallel_result = parallel_iteration_node._run() + sequential_result = sequential_iteration_node._run() + assert parallel_iteration_node.node_data.parallel_nums == 10 + assert parallel_iteration_node.node_data.error_handle_mode == ErrorHandleMode.TERMINATED + count = 0 + parallel_arr = [] + sequential_arr = [] + for item in parallel_result: + count += 1 + parallel_arr.append(item) + if isinstance(item, RunCompletedEvent): + assert item.run_result.status == WorkflowNodeExecutionStatus.SUCCEEDED + assert item.run_result.outputs == {"output": ["dify 123", "dify 123"]} + assert count == 32 + + for item in sequential_result: + sequential_arr.append(item) + count += 1 + if isinstance(item, RunCompletedEvent): + assert item.run_result.status == WorkflowNodeExecutionStatus.SUCCEEDED + assert item.run_result.outputs == {"output": ["dify 123", "dify 123"]} + assert count == 64 + + +def test_iteration_run_error_handle(): + graph_config = { + "edges": [ + { + "id": "start-source-pe-target", + "source": "start", + "target": "pe", + }, + { + "id": "iteration-1-source-answer-3-target", + "source": "iteration-1", + "target": "answer-3", + }, + { + "id": "tt-source-if-else-target", + "source": "iteration-start", + "target": "if-else", + }, + { + "id": "if-else-true-answer-2-target", + "source": "if-else", + "sourceHandle": "true", + "target": "tt", + }, + { + "id": "if-else-false-answer-4-target", + "source": "if-else", + "sourceHandle": "false", + "target": "tt2", + }, + { + "id": "pe-source-iteration-1-target", + "source": "pe", + "target": "iteration-1", + }, + ], + "nodes": [ + {"data": {"title": "Start", "type": "start", "variables": []}, "id": "start"}, + { + "data": { + "iterator_selector": ["pe", "list_output"], + "output_selector": ["tt2", "output"], + "output_type": "array[string]", + "start_node_id": "if-else", + "title": "iteration", + "type": "iteration", + }, + "id": "iteration-1", + }, + { + "data": { + "iteration_id": "iteration-1", + "template": "{{ arg1.split(arg2) }}", + "title": "template transform", + "type": "template-transform", + "variables": [ + {"value_selector": ["iteration-1", "item"], "variable": "arg1"}, + {"value_selector": ["iteration-1", "index"], "variable": "arg2"}, + ], + }, + "id": "tt", + }, + { + "data": { + "iteration_id": "iteration-1", + "template": "{{ arg1 }}", + "title": "template transform", + "type": "template-transform", + "variables": [ + {"value_selector": ["iteration-1", "item"], "variable": "arg1"}, + ], + }, + "id": "tt2", + }, + { + "data": {"answer": "{{#iteration-1.output#}}88888", "title": "answer 3", "type": "answer"}, + "id": "answer-3", + }, + { + "data": { + "iteration_id": "iteration-1", + "title": "iteration-start", + "type": "iteration-start", + }, + "id": "iteration-start", + }, + { + "data": { + "conditions": [ + { + "comparison_operator": "is", + "id": "1721916275284", + "value": "1", + "variable_selector": ["iteration-1", "item"], + } + ], + "iteration_id": "iteration-1", + "logical_operator": "and", + "title": "if", + "type": "if-else", + }, + "id": "if-else", + }, + { + "data": { + "instruction": "test1", + "model": { + "completion_params": {"temperature": 0.7}, + "mode": "chat", + "name": "gpt-4o", + "provider": "openai", + }, + "parameters": [ + {"description": "test", "name": "list_output", "required": False, "type": "array[string]"} + ], + "query": ["sys", "query"], + "reasoning_mode": "prompt", + "title": "pe", + "type": "parameter-extractor", + }, + "id": "pe", + }, + ], + } + + graph = Graph.init(graph_config=graph_config) + + init_params = GraphInitParams( + tenant_id="1", + app_id="1", + workflow_type=WorkflowType.CHAT, + workflow_id="1", + graph_config=graph_config, + user_id="1", + user_from=UserFrom.ACCOUNT, + invoke_from=InvokeFrom.DEBUGGER, + call_depth=0, + ) + + # construct variable pool + pool = VariablePool( + system_variables={ + SystemVariableKey.QUERY: "dify", + SystemVariableKey.FILES: [], + SystemVariableKey.CONVERSATION_ID: "abababa", + SystemVariableKey.USER_ID: "1", + }, + user_inputs={}, + environment_variables=[], + ) + pool.add(["pe", "list_output"], ["1", "1"]) + iteration_node = IterationNode( + id=str(uuid.uuid4()), + graph_init_params=init_params, + graph=graph, + graph_runtime_state=GraphRuntimeState(variable_pool=pool, start_at=time.perf_counter()), + config={ + "data": { + "iterator_selector": ["pe", "list_output"], + "output_selector": ["tt", "output"], + "output_type": "array[string]", + "startNodeType": "template-transform", + "start_node_id": "iteration-start", + "title": "iteration", + "type": "iteration", + "is_parallel": True, + "error_handle_mode": ErrorHandleMode.CONTINUE_ON_ERROR, + }, + "id": "iteration-1", + }, + ) + # execute continue on error node + result = iteration_node._run() + result_arr = [] + count = 0 + for item in result: + result_arr.append(item) + count += 1 + if isinstance(item, RunCompletedEvent): + assert item.run_result.status == WorkflowNodeExecutionStatus.SUCCEEDED + assert item.run_result.outputs == {"output": [None, None]} + + assert count == 14 + # execute remove abnormal output + iteration_node.node_data.error_handle_mode = ErrorHandleMode.REMOVE_ABNORMAL_OUTPUT + result = iteration_node._run() + count = 0 + for item in result: + count += 1 + if isinstance(item, RunCompletedEvent): + assert item.run_result.status == WorkflowNodeExecutionStatus.SUCCEEDED + assert item.run_result.outputs == {"output": []} + assert count == 14 diff --git a/web/app/components/base/select/index.tsx b/web/app/components/base/select/index.tsx index ee5cee977b..c70cf24661 100644 --- a/web/app/components/base/select/index.tsx +++ b/web/app/components/base/select/index.tsx @@ -125,7 +125,7 @@ const Select: FC = ({
- {filteredItems.length > 0 && ( + {(filteredItems.length > 0 && open) && ( {filteredItems.map((item: Item) => ( { newNode.data.isInIteration = true newNode.data.iteration_id = prevNode.parentId newNode.zIndex = ITERATION_CHILDREN_Z_INDEX + if (newNode.data.type === BlockEnum.Answer || newNode.data.type === BlockEnum.Tool || newNode.data.type === BlockEnum.Assigner) { + const parentIterNodeIndex = nodes.findIndex(node => node.id === prevNode.parentId) + const iterNodeData: IterationNodeType = nodes[parentIterNodeIndex].data + iterNodeData._isShowTips = true + } } const newEdge: Edge = { diff --git a/web/app/components/workflow/hooks/use-workflow-run.ts b/web/app/components/workflow/hooks/use-workflow-run.ts index 0bbb1adab8..26654ef71e 100644 --- a/web/app/components/workflow/hooks/use-workflow-run.ts +++ b/web/app/components/workflow/hooks/use-workflow-run.ts @@ -14,6 +14,7 @@ import { NodeRunningStatus, WorkflowRunningStatus, } from '../types' +import { DEFAULT_ITER_TIMES } from '../constants' import { useWorkflowUpdate } from './use-workflow-interactions' import { useStore as useAppStore } from '@/app/components/app/store' import type { IOtherOptions } from '@/service/base' @@ -170,11 +171,13 @@ export const useWorkflowRun = () => { const { workflowRunningData, setWorkflowRunningData, + setIterParallelLogMap, } = workflowStore.getState() const { edges, setEdges, } = store.getState() + setIterParallelLogMap(new Map()) setWorkflowRunningData(produce(workflowRunningData!, (draft) => { draft.task_id = task_id draft.result = { @@ -244,6 +247,8 @@ export const useWorkflowRun = () => { const { workflowRunningData, setWorkflowRunningData, + iterParallelLogMap, + setIterParallelLogMap, } = workflowStore.getState() const { getNodes, @@ -259,10 +264,21 @@ export const useWorkflowRun = () => { const tracing = draft.tracing! const iterations = tracing.find(trace => trace.node_id === node?.parentId) const currIteration = iterations?.details![node.data.iteration_index] || iterations?.details![iterations.details!.length - 1] - currIteration?.push({ - ...data, - status: NodeRunningStatus.Running, - } as any) + if (!data.parallel_run_id) { + currIteration?.push({ + ...data, + status: NodeRunningStatus.Running, + } as any) + } + else { + if (!iterParallelLogMap.has(data.parallel_run_id)) + iterParallelLogMap.set(data.parallel_run_id, [{ ...data, status: NodeRunningStatus.Running } as any]) + else + iterParallelLogMap.get(data.parallel_run_id)!.push({ ...data, status: NodeRunningStatus.Running } as any) + setIterParallelLogMap(iterParallelLogMap) + if (iterations) + iterations.details = Array.from(iterParallelLogMap.values()) + } })) } else { @@ -309,6 +325,8 @@ export const useWorkflowRun = () => { const { workflowRunningData, setWorkflowRunningData, + iterParallelLogMap, + setIterParallelLogMap, } = workflowStore.getState() const { getNodes, @@ -317,21 +335,21 @@ export const useWorkflowRun = () => { const nodes = getNodes() const nodeParentId = nodes.find(node => node.id === data.node_id)!.parentId if (nodeParentId) { - setWorkflowRunningData(produce(workflowRunningData!, (draft) => { - const tracing = draft.tracing! - const iterations = tracing.find(trace => trace.node_id === nodeParentId) // the iteration node + if (!data.execution_metadata.parallel_mode_run_id) { + setWorkflowRunningData(produce(workflowRunningData!, (draft) => { + const tracing = draft.tracing! + const iterations = tracing.find(trace => trace.node_id === nodeParentId) // the iteration node - if (iterations && iterations.details) { - const iterationIndex = data.execution_metadata?.iteration_index || 0 - if (!iterations.details[iterationIndex]) - iterations.details[iterationIndex] = [] + if (iterations && iterations.details) { + const iterationIndex = data.execution_metadata?.iteration_index || 0 + if (!iterations.details[iterationIndex]) + iterations.details[iterationIndex] = [] - const currIteration = iterations.details[iterationIndex] - const nodeIndex = currIteration.findIndex(node => - node.node_id === data.node_id && ( - node.execution_metadata?.parallel_id === data.execution_metadata?.parallel_id || node.parallel_id === data.execution_metadata?.parallel_id), - ) - if (data.status === NodeRunningStatus.Succeeded) { + const currIteration = iterations.details[iterationIndex] + const nodeIndex = currIteration.findIndex(node => + node.node_id === data.node_id && ( + node.execution_metadata?.parallel_id === data.execution_metadata?.parallel_id || node.parallel_id === data.execution_metadata?.parallel_id), + ) if (nodeIndex !== -1) { currIteration[nodeIndex] = { ...currIteration[nodeIndex], @@ -344,8 +362,40 @@ export const useWorkflowRun = () => { } as any) } } - } - })) + })) + } + else { + // open parallel mode + setWorkflowRunningData(produce(workflowRunningData!, (draft) => { + const tracing = draft.tracing! + const iterations = tracing.find(trace => trace.node_id === nodeParentId) // the iteration node + + if (iterations && iterations.details) { + const iterRunID = data.execution_metadata?.parallel_mode_run_id + + const currIteration = iterParallelLogMap.get(iterRunID) + const nodeIndex = currIteration?.findIndex(node => + node.node_id === data.node_id && ( + node?.parallel_run_id === data.execution_metadata?.parallel_mode_run_id), + ) + if (currIteration) { + if (nodeIndex !== undefined && nodeIndex !== -1) { + currIteration[nodeIndex] = { + ...currIteration[nodeIndex], + ...data, + } as any + } + else { + currIteration.push({ + ...data, + } as any) + } + } + setIterParallelLogMap(iterParallelLogMap) + iterations.details = Array.from(iterParallelLogMap.values()) + } + })) + } } else { setWorkflowRunningData(produce(workflowRunningData!, (draft) => { @@ -379,6 +429,7 @@ export const useWorkflowRun = () => { const { workflowRunningData, setWorkflowRunningData, + setIterTimes, } = workflowStore.getState() const { getNodes, @@ -388,6 +439,7 @@ export const useWorkflowRun = () => { transform, } = store.getState() const nodes = getNodes() + setIterTimes(DEFAULT_ITER_TIMES) setWorkflowRunningData(produce(workflowRunningData!, (draft) => { draft.tracing!.push({ ...data, @@ -431,6 +483,8 @@ export const useWorkflowRun = () => { const { workflowRunningData, setWorkflowRunningData, + iterTimes, + setIterTimes, } = workflowStore.getState() const { data } = params @@ -445,13 +499,14 @@ export const useWorkflowRun = () => { if (iteration.details!.length >= iteration.metadata.iterator_length!) return } - iteration?.details!.push([]) + if (!data.parallel_mode_run_id) + iteration?.details!.push([]) })) const nodes = getNodes() const newNodes = produce(nodes, (draft) => { const currentNode = draft.find(node => node.id === data.node_id)! - - currentNode.data._iterationIndex = data.index > 0 ? data.index : 1 + currentNode.data._iterationIndex = iterTimes + setIterTimes(iterTimes + 1) }) setNodes(newNodes) @@ -464,6 +519,7 @@ export const useWorkflowRun = () => { const { workflowRunningData, setWorkflowRunningData, + setIterTimes, } = workflowStore.getState() const { getNodes, @@ -480,7 +536,7 @@ export const useWorkflowRun = () => { }) } })) - + setIterTimes(DEFAULT_ITER_TIMES) const newNodes = produce(nodes, (draft) => { const currentNode = draft.find(node => node.id === data.node_id)! diff --git a/web/app/components/workflow/nodes/_base/components/field.tsx b/web/app/components/workflow/nodes/_base/components/field.tsx index 6459cf8056..b2f815a325 100644 --- a/web/app/components/workflow/nodes/_base/components/field.tsx +++ b/web/app/components/workflow/nodes/_base/components/field.tsx @@ -12,15 +12,15 @@ import Tooltip from '@/app/components/base/tooltip' type Props = { className?: string title: JSX.Element | string | DefaultTFuncReturn + tooltip?: React.ReactNode isSubTitle?: boolean - tooltip?: string supportFold?: boolean children?: JSX.Element | string | null operations?: JSX.Element inline?: boolean } -const Filed: FC = ({ +const Field: FC = ({ className, title, isSubTitle, @@ -60,4 +60,4 @@ const Filed: FC = ({ ) } -export default React.memo(Filed) +export default React.memo(Field) diff --git a/web/app/components/workflow/nodes/_base/node.tsx b/web/app/components/workflow/nodes/_base/node.tsx index bd5921c735..e864c419e2 100644 --- a/web/app/components/workflow/nodes/_base/node.tsx +++ b/web/app/components/workflow/nodes/_base/node.tsx @@ -25,6 +25,7 @@ import { useToolIcon, } from '../../hooks' import { useNodeIterationInteractions } from '../iteration/use-interactions' +import type { IterationNodeType } from '../iteration/types' import { NodeSourceHandle, NodeTargetHandle, @@ -34,6 +35,7 @@ import NodeControl from './components/node-control' import AddVariablePopupWithPosition from './components/add-variable-popup-with-position' import cn from '@/utils/classnames' import BlockIcon from '@/app/components/workflow/block-icon' +import Tooltip from '@/app/components/base/tooltip' type BaseNodeProps = { children: ReactElement @@ -166,9 +168,27 @@ const BaseNode: FC = ({ />
- {data.title} +
+ {data.title} +
+ { + data.type === BlockEnum.Iteration && (data as IterationNodeType).is_parallel && ( + +
+ {t('workflow.nodes.iteration.parallelModeEnableTitle')} +
+ {t('workflow.nodes.iteration.parallelModeEnableDesc')} +
} + > +
+ {t('workflow.nodes.iteration.parallelModeUpper')} +
+ + ) + } { data._iterationLength && data._iterationIndex && data._runningStatus === NodeRunningStatus.Running && ( diff --git a/web/app/components/workflow/nodes/iteration/default.ts b/web/app/components/workflow/nodes/iteration/default.ts index 3afa52d06e..cdef268adb 100644 --- a/web/app/components/workflow/nodes/iteration/default.ts +++ b/web/app/components/workflow/nodes/iteration/default.ts @@ -1,7 +1,10 @@ -import { BlockEnum } from '../../types' +import { BlockEnum, ErrorHandleMode } from '../../types' import type { NodeDefault } from '../../types' import type { IterationNodeType } from './types' -import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants' +import { + ALL_CHAT_AVAILABLE_BLOCKS, + ALL_COMPLETION_AVAILABLE_BLOCKS, +} from '@/app/components/workflow/constants' const i18nPrefix = 'workflow' const nodeDefault: NodeDefault = { @@ -10,25 +13,45 @@ const nodeDefault: NodeDefault = { iterator_selector: [], output_selector: [], _children: [], + _isShowTips: false, + is_parallel: false, + parallel_nums: 10, + error_handle_mode: ErrorHandleMode.Terminated, }, getAvailablePrevNodes(isChatMode: boolean) { const nodes = isChatMode ? ALL_CHAT_AVAILABLE_BLOCKS - : ALL_COMPLETION_AVAILABLE_BLOCKS.filter(type => type !== BlockEnum.End) + : ALL_COMPLETION_AVAILABLE_BLOCKS.filter( + type => type !== BlockEnum.End, + ) return nodes }, getAvailableNextNodes(isChatMode: boolean) { - const nodes = isChatMode ? ALL_CHAT_AVAILABLE_BLOCKS : ALL_COMPLETION_AVAILABLE_BLOCKS + const nodes = isChatMode + ? ALL_CHAT_AVAILABLE_BLOCKS + : ALL_COMPLETION_AVAILABLE_BLOCKS return nodes }, checkValid(payload: IterationNodeType, t: any) { let errorMessages = '' - if (!errorMessages && (!payload.iterator_selector || payload.iterator_selector.length === 0)) - errorMessages = t(`${i18nPrefix}.errorMsg.fieldRequired`, { field: t(`${i18nPrefix}.nodes.iteration.input`) }) + if ( + !errorMessages + && (!payload.iterator_selector || payload.iterator_selector.length === 0) + ) { + errorMessages = t(`${i18nPrefix}.errorMsg.fieldRequired`, { + field: t(`${i18nPrefix}.nodes.iteration.input`), + }) + } - if (!errorMessages && (!payload.output_selector || payload.output_selector.length === 0)) - errorMessages = t(`${i18nPrefix}.errorMsg.fieldRequired`, { field: t(`${i18nPrefix}.nodes.iteration.output`) }) + if ( + !errorMessages + && (!payload.output_selector || payload.output_selector.length === 0) + ) { + errorMessages = t(`${i18nPrefix}.errorMsg.fieldRequired`, { + field: t(`${i18nPrefix}.nodes.iteration.output`), + }) + } return { isValid: !errorMessages, diff --git a/web/app/components/workflow/nodes/iteration/node.tsx b/web/app/components/workflow/nodes/iteration/node.tsx index 48a005a261..fda033b87a 100644 --- a/web/app/components/workflow/nodes/iteration/node.tsx +++ b/web/app/components/workflow/nodes/iteration/node.tsx @@ -8,12 +8,16 @@ import { useNodesInitialized, useViewport, } from 'reactflow' +import { useTranslation } from 'react-i18next' import { IterationStartNodeDumb } from '../iteration-start' import { useNodeIterationInteractions } from './use-interactions' import type { IterationNodeType } from './types' import AddBlock from './add-block' import cn from '@/utils/classnames' import type { NodeProps } from '@/app/components/workflow/types' +import Toast from '@/app/components/base/toast' + +const i18nPrefix = 'workflow.nodes.iteration' const Node: FC> = ({ id, @@ -22,11 +26,20 @@ const Node: FC> = ({ const { zoom } = useViewport() const nodesInitialized = useNodesInitialized() const { handleNodeIterationRerender } = useNodeIterationInteractions() + const { t } = useTranslation() useEffect(() => { if (nodesInitialized) handleNodeIterationRerender(id) - }, [nodesInitialized, id, handleNodeIterationRerender]) + if (data.is_parallel && data._isShowTips) { + Toast.notify({ + type: 'warning', + message: t(`${i18nPrefix}.answerNodeWarningDesc`), + duration: 5000, + }) + data._isShowTips = false + } + }, [nodesInitialized, id, handleNodeIterationRerender, data, t]) return (
> = ({ data, }) => { const { t } = useTranslation() - + const responseMethod = [ + { + value: ErrorHandleMode.Terminated, + name: t(`${i18nPrefix}.ErrorMethod.operationTerminated`), + }, + { + value: ErrorHandleMode.ContinueOnError, + name: t(`${i18nPrefix}.ErrorMethod.continueOnError`), + }, + { + value: ErrorHandleMode.RemoveAbnormalOutput, + name: t(`${i18nPrefix}.ErrorMethod.removeAbnormalOutput`), + }, + ] const { readOnly, inputs, @@ -47,6 +66,9 @@ const Panel: FC> = ({ setIterator, iteratorInputKey, iterationRunResult, + changeParallel, + changeErrorResponseMode, + changeParallelNums, } = useConfig(id, data) return ( @@ -87,6 +109,39 @@ const Panel: FC> = ({ />
+
+ {t(`${i18nPrefix}.parallelPanelDesc`)}
} inline> + + + + { + inputs.is_parallel && (
+ {t(`${i18nPrefix}.MaxParallelismDesc`)}
}> +
+ { changeParallelNums(Number(e.target.value)) }} /> + +
+ + + ) + } +
+ +
+ +
+ + + +
+ {isShowSingleRun && ( { @@ -184,6 +185,25 @@ const useConfig = (id: string, payload: IterationNodeType) => { }) }, [iteratorInputKey, runInputData, setRunInputData]) + const changeParallel = useCallback((value: boolean) => { + const newInputs = produce(inputs, (draft) => { + draft.is_parallel = value + }) + setInputs(newInputs) + }, [inputs, setInputs]) + + const changeErrorResponseMode = useCallback((item: Item) => { + const newInputs = produce(inputs, (draft) => { + draft.error_handle_mode = item.value as ErrorHandleMode + }) + setInputs(newInputs) + }, [inputs, setInputs]) + const changeParallelNums = useCallback((num: number) => { + const newInputs = produce(inputs, (draft) => { + draft.parallel_nums = num + }) + setInputs(newInputs) + }, [inputs, setInputs]) return { readOnly, inputs, @@ -210,6 +230,9 @@ const useConfig = (id: string, payload: IterationNodeType) => { setIterator, iteratorInputKey, iterationRunResult, + changeParallel, + changeErrorResponseMode, + changeParallelNums, } } diff --git a/web/app/components/workflow/panel/debug-and-preview/hooks.ts b/web/app/components/workflow/panel/debug-and-preview/hooks.ts index 58a4561e2c..5d932a1ba2 100644 --- a/web/app/components/workflow/panel/debug-and-preview/hooks.ts +++ b/web/app/components/workflow/panel/debug-and-preview/hooks.ts @@ -9,6 +9,8 @@ import { produce, setAutoFreeze } from 'immer' import { uniqBy } from 'lodash-es' import { useWorkflowRun } from '../../hooks' import { NodeRunningStatus, WorkflowRunningStatus } from '../../types' +import { useWorkflowStore } from '../../store' +import { DEFAULT_ITER_TIMES } from '../../constants' import type { ChatItem, Inputs, @@ -43,6 +45,7 @@ export const useChat = ( const { notify } = useToastContext() const { handleRun } = useWorkflowRun() const hasStopResponded = useRef(false) + const workflowStore = useWorkflowStore() const conversationId = useRef('') const taskIdRef = useRef('') const [chatList, setChatList] = useState(prevChatList || []) @@ -52,6 +55,9 @@ export const useChat = ( const [suggestedQuestions, setSuggestQuestions] = useState([]) const suggestedQuestionsAbortControllerRef = useRef(null) + const { + setIterTimes, + } = workflowStore.getState() useEffect(() => { setAutoFreeze(false) return () => { @@ -102,15 +108,16 @@ export const useChat = ( handleResponding(false) if (stopChat && taskIdRef.current) stopChat(taskIdRef.current) - + setIterTimes(DEFAULT_ITER_TIMES) if (suggestedQuestionsAbortControllerRef.current) suggestedQuestionsAbortControllerRef.current.abort() - }, [handleResponding, stopChat]) + }, [handleResponding, setIterTimes, stopChat]) const handleRestart = useCallback(() => { conversationId.current = '' taskIdRef.current = '' handleStop() + setIterTimes(DEFAULT_ITER_TIMES) const newChatList = config?.opening_statement ? [{ id: `${Date.now()}`, @@ -126,6 +133,7 @@ export const useChat = ( config, handleStop, handleUpdateChatList, + setIterTimes, ]) const updateCurrentQA = useCallback(({ diff --git a/web/app/components/workflow/run/index.tsx b/web/app/components/workflow/run/index.tsx index 9e636e902b..89db43fa35 100644 --- a/web/app/components/workflow/run/index.tsx +++ b/web/app/components/workflow/run/index.tsx @@ -60,36 +60,67 @@ const RunPanel: FC = ({ hideResult, activeTab = 'RESULT', runID, getRe }, [notify, getResultCallback]) const formatNodeList = useCallback((list: NodeTracing[]) => { - const allItems = list.reverse() + const allItems = [...list].reverse() const result: NodeTracing[] = [] - allItems.forEach((item) => { - const { node_type, execution_metadata } = item - if (node_type !== BlockEnum.Iteration) { - const isInIteration = !!execution_metadata?.iteration_id + const groupMap = new Map() - if (isInIteration) { - const iterationNode = result.find(node => node.node_id === execution_metadata?.iteration_id) - const iterationDetails = iterationNode?.details - const currentIterationIndex = execution_metadata?.iteration_index ?? 0 - - if (Array.isArray(iterationDetails)) { - if (iterationDetails.length === 0 || !iterationDetails[currentIterationIndex]) - iterationDetails[currentIterationIndex] = [item] - else - iterationDetails[currentIterationIndex].push(item) - } - return - } - // not in iteration - result.push(item) - - return - } + const processIterationNode = (item: NodeTracing) => { result.push({ ...item, details: [], }) + } + const updateParallelModeGroup = (runId: string, item: NodeTracing, iterationNode: NodeTracing) => { + if (!groupMap.has(runId)) + groupMap.set(runId, [item]) + else + groupMap.get(runId)!.push(item) + if (item.status === 'failed') { + iterationNode.status = 'failed' + iterationNode.error = item.error + } + + iterationNode.details = Array.from(groupMap.values()) + } + const updateSequentialModeGroup = (index: number, item: NodeTracing, iterationNode: NodeTracing) => { + const { details } = iterationNode + if (details) { + if (!details[index]) + details[index] = [item] + else + details[index].push(item) + } + + if (item.status === 'failed') { + iterationNode.status = 'failed' + iterationNode.error = item.error + } + } + const processNonIterationNode = (item: NodeTracing) => { + const { execution_metadata } = item + if (!execution_metadata?.iteration_id) { + result.push(item) + return + } + + const iterationNode = result.find(node => node.node_id === execution_metadata.iteration_id) + if (!iterationNode || !Array.isArray(iterationNode.details)) + return + + const { parallel_mode_run_id, iteration_index = 0 } = execution_metadata + + if (parallel_mode_run_id) + updateParallelModeGroup(parallel_mode_run_id, item, iterationNode) + else + updateSequentialModeGroup(iteration_index, item, iterationNode) + } + + allItems.forEach((item) => { + item.node_type === BlockEnum.Iteration + ? processIterationNode(item) + : processNonIterationNode(item) }) + return result }, []) diff --git a/web/app/components/workflow/run/iteration-result-panel.tsx b/web/app/components/workflow/run/iteration-result-panel.tsx index 7e2f6cbc00..c4cd909f2e 100644 --- a/web/app/components/workflow/run/iteration-result-panel.tsx +++ b/web/app/components/workflow/run/iteration-result-panel.tsx @@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next' import { RiArrowRightSLine, RiCloseLine, + RiErrorWarningLine, } from '@remixicon/react' import { ArrowNarrowLeft } from '../../base/icons/src/vender/line/arrows' import TracingPanel from './tracing-panel' @@ -27,7 +28,7 @@ const IterationResultPanel: FC = ({ noWrap, }) => { const { t } = useTranslation() - const [expandedIterations, setExpandedIterations] = useState>([]) + const [expandedIterations, setExpandedIterations] = useState>({}) const toggleIteration = useCallback((index: number) => { setExpandedIterations(prev => ({ @@ -71,10 +72,19 @@ const IterationResultPanel: FC = ({ {t(`${i18nPrefix}.iteration`)} {index + 1} - + { + iteration.some(item => item.status === 'failed') + ? ( + + ) + : (< RiArrowRightSLine className={ + cn( + 'w-4 h-4 text-text-tertiary transition-transform duration-200 flex-shrink-0', + expandedIterations[index] && 'transform rotate-90', + )} /> + ) + } + {expandedIterations[index] &&
= ({ return iteration_length } + const getErrorCount = (details: NodeTracing[][] | undefined) => { + if (!details || details.length === 0) + return 0 + return details.reduce((acc, iteration) => { + if (iteration.some(item => item.status === 'failed')) + acc++ + return acc + }, 0) + } useEffect(() => { setCollapseState(!nodeInfo.expand) }, [nodeInfo.expand, setCollapseState]) @@ -136,7 +145,12 @@ const NodePanel: FC = ({ onClick={handleOnShowIterationDetail} > -
{t('workflow.nodes.iteration.iteration', { count: getCount(nodeInfo.details?.length, nodeInfo.metadata?.iterator_length) })}
+
{t('workflow.nodes.iteration.iteration', { count: getCount(nodeInfo.details?.length, nodeInfo.metadata?.iterator_length) })}{getErrorCount(nodeInfo.details) > 0 && ( + <> + {t('workflow.nodes.iteration.comma')} + {t('workflow.nodes.iteration.error', { count: getErrorCount(nodeInfo.details) })} + + )}
{justShowIterationNavArrow ? ( diff --git a/web/app/components/workflow/store.ts b/web/app/components/workflow/store.ts index c2a6823e6b..c4a625c777 100644 --- a/web/app/components/workflow/store.ts +++ b/web/app/components/workflow/store.ts @@ -21,6 +21,7 @@ import type { WorkflowRunningData, } from './types' import { WorkflowContext } from './context' +import type { NodeTracing } from '@/types/workflow' // #TODO chatVar# // const MOCK_DATA = [ @@ -166,6 +167,10 @@ type Shape = { setShowImportDSLModal: (showImportDSLModal: boolean) => void showTips: string setShowTips: (showTips: string) => void + iterTimes: number + setIterTimes: (iterTimes: number) => void + iterParallelLogMap: Map + setIterParallelLogMap: (iterParallelLogMap: Map) => void } export const createWorkflowStore = () => { @@ -281,6 +286,11 @@ export const createWorkflowStore = () => { setShowImportDSLModal: showImportDSLModal => set(() => ({ showImportDSLModal })), showTips: '', setShowTips: showTips => set(() => ({ showTips })), + iterTimes: 1, + setIterTimes: iterTimes => set(() => ({ iterTimes })), + iterParallelLogMap: new Map(), + setIterParallelLogMap: iterParallelLogMap => set(() => ({ iterParallelLogMap })), + })) } diff --git a/web/app/components/workflow/types.ts b/web/app/components/workflow/types.ts index 81bec41eac..9b6ad033bf 100644 --- a/web/app/components/workflow/types.ts +++ b/web/app/components/workflow/types.ts @@ -36,7 +36,11 @@ export enum ControlMode { Pointer = 'pointer', Hand = 'hand', } - +export enum ErrorHandleMode { + Terminated = 'terminated', + ContinueOnError = 'continue-on-error', + RemoveAbnormalOutput = 'remove-abnormal-output', +} export type Branch = { id: string name: string diff --git a/web/app/components/workflow/utils.ts b/web/app/components/workflow/utils.ts index 91656e3bbc..aaf333f4d7 100644 --- a/web/app/components/workflow/utils.ts +++ b/web/app/components/workflow/utils.ts @@ -19,7 +19,7 @@ import type { ToolWithProvider, ValueSelector, } from './types' -import { BlockEnum } from './types' +import { BlockEnum, ErrorHandleMode } from './types' import { CUSTOM_NODE, ITERATION_CHILDREN_Z_INDEX, @@ -267,8 +267,13 @@ export const initialNodes = (originNodes: Node[], originEdges: Edge[]) => { }) } - if (node.data.type === BlockEnum.Iteration) - node.data._children = iterationNodeMap[node.id] || [] + if (node.data.type === BlockEnum.Iteration) { + const iterationNodeData = node.data as IterationNodeType + iterationNodeData._children = iterationNodeMap[node.id] || [] + iterationNodeData.is_parallel = iterationNodeData.is_parallel || false + iterationNodeData.parallel_nums = iterationNodeData.parallel_nums || 10 + iterationNodeData.error_handle_mode = iterationNodeData.error_handle_mode || ErrorHandleMode.Terminated + } return node }) diff --git a/web/i18n/en-US/workflow.ts b/web/i18n/en-US/workflow.ts index ea8355500a..1c6639aba0 100644 --- a/web/i18n/en-US/workflow.ts +++ b/web/i18n/en-US/workflow.ts @@ -556,6 +556,23 @@ const translation = { iteration_one: '{{count}} Iteration', iteration_other: '{{count}} Iterations', currentIteration: 'Current Iteration', + comma: ', ', + error_one: '{{count}} Error', + error_other: '{{count}} Errors', + parallelMode: 'Parallel Mode', + parallelModeUpper: 'PARALLEL MODE', + parallelModeEnableTitle: 'Parallel Mode Enabled', + parallelModeEnableDesc: 'In parallel mode, tasks within iterations support parallel execution. You can configure this in the properties panel on the right.', + parallelPanelDesc: 'In parallel mode, tasks in the iteration support parallel execution.', + MaxParallelismTitle: 'Maximum parallelism', + MaxParallelismDesc: 'The maximum parallelism is used to control the number of tasks executed simultaneously in a single iteration.', + errorResponseMethod: 'Error response method', + ErrorMethod: { + operationTerminated: 'terminated', + continueOnError: 'continue-on-error', + removeAbnormalOutput: 'remove-abnormal-output', + }, + answerNodeWarningDesc: 'Parallel mode warning: Answer nodes, conversation variable assignments, and persistent read/write operations within iterations may cause exceptions.', }, note: { addNote: 'Add Note', diff --git a/web/i18n/zh-Hans/workflow.ts b/web/i18n/zh-Hans/workflow.ts index 515d0fe235..1229ba8c03 100644 --- a/web/i18n/zh-Hans/workflow.ts +++ b/web/i18n/zh-Hans/workflow.ts @@ -556,6 +556,23 @@ const translation = { iteration_one: '{{count}}个迭代', iteration_other: '{{count}}个迭代', currentIteration: '当前迭代', + comma: ',', + error_one: '{{count}}个失败', + error_other: '{{count}}个失败', + parallelMode: '并行模式', + parallelModeUpper: '并行模式', + parallelModeEnableTitle: '并行模式启用', + parallelModeEnableDesc: '启用并行模式时迭代内的任务支持并行执行。你可以在右侧的属性面板中进行配置。', + parallelPanelDesc: '在并行模式下,迭代中的任务支持并行执行。', + MaxParallelismTitle: '最大并行度', + MaxParallelismDesc: '最大并行度用于控制单次迭代中同时执行的任务数量。', + errorResponseMethod: '错误响应方法', + ErrorMethod: { + operationTerminated: '错误时终止', + continueOnError: '忽略错误并继续', + removeAbnormalOutput: '移除错误输出', + }, + answerNodeWarningDesc: '并行模式警告:在迭代中,回答节点、会话变量赋值和工具持久读/写操作可能会导致异常。', }, note: { addNote: '添加注释', diff --git a/web/types/workflow.ts b/web/types/workflow.ts index 810026b084..3c0675b605 100644 --- a/web/types/workflow.ts +++ b/web/types/workflow.ts @@ -19,6 +19,7 @@ export type NodeTracing = { process_data: any outputs?: any status: string + parallel_run_id?: string error?: string elapsed_time: number execution_metadata: { @@ -31,6 +32,7 @@ export type NodeTracing = { parallel_start_node_id?: string parent_parallel_id?: string parent_parallel_start_node_id?: string + parallel_mode_run_id?: string } metadata: { iterator_length: number @@ -121,6 +123,7 @@ export type NodeStartedResponse = { id: string node_id: string iteration_id?: string + parallel_run_id?: string node_type: string index: number predecessor_node_id?: string @@ -166,6 +169,7 @@ export type NodeFinishedResponse = { parallel_start_node_id?: string iteration_index?: number iteration_id?: string + parallel_mode_run_id: string } created_at: number files?: FileResponse[] @@ -200,6 +204,7 @@ export type IterationNextResponse = { output: any extras?: any created_at: number + parallel_mode_run_id: string execution_metadata: { parallel_id?: string } From 56e19fd8f58f0675e6b2708dfe1eef97c3d1289a Mon Sep 17 00:00:00 2001 From: Benjamin Date: Tue, 5 Nov 2024 10:34:28 +0800 Subject: [PATCH 34/48] =?UTF-8?q?Updates:=20Add=20mplfonts=20library=20for?= =?UTF-8?q?=20customizing=20matplotlib=20fonts=20and=20Va=E2=80=A6=20(#990?= =?UTF-8?q?3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/poetry.lock | 70 ++++++++++++++++++++++++++++++++++++++++++---- api/pyproject.toml | 3 +- 2 files changed, 67 insertions(+), 6 deletions(-) diff --git a/api/poetry.lock b/api/poetry.lock index d7af124794..a697c02c45 100644 --- a/api/poetry.lock +++ b/api/poetry.lock @@ -2532,6 +2532,19 @@ files = [ {file = "filetype-1.2.0.tar.gz", hash = "sha256:66b56cd6474bf41d8c54660347d37afcc3f7d1970648de365c102ef77548aadb"}, ] +[[package]] +name = "fire" +version = "0.7.0" +description = "A library for automatically generating command line interfaces." +optional = false +python-versions = "*" +files = [ + {file = "fire-0.7.0.tar.gz", hash = "sha256:961550f07936eaf65ad1dc8360f2b2bf8408fad46abbfa4d2a3794f8d2a95cdf"}, +] + +[package.dependencies] +termcolor = "*" + [[package]] name = "flasgger" version = "0.9.7.1" @@ -2697,6 +2710,19 @@ files = [ {file = "flatbuffers-24.3.25.tar.gz", hash = "sha256:de2ec5b203f21441716617f38443e0a8ebf3d25bf0d9c0bb0ce68fa00ad546a4"}, ] +[[package]] +name = "fontmeta" +version = "1.6.1" +description = "An Utility to get ttf/otf font metadata" +optional = false +python-versions = "*" +files = [ + {file = "fontmeta-1.6.1.tar.gz", hash = "sha256:837e5bc4da879394b41bda1428a8a480eb7c4e993799a93cfb582bab771a9c24"}, +] + +[package.dependencies] +fonttools = "*" + [[package]] name = "fonttools" version = "4.54.1" @@ -5279,6 +5305,22 @@ files = [ {file = "monotonic-1.6.tar.gz", hash = "sha256:3a55207bcfed53ddd5c5bae174524062935efed17792e9de2ad0205ce9ad63f7"}, ] +[[package]] +name = "mplfonts" +version = "0.0.8" +description = "Fonts manager for matplotlib" +optional = false +python-versions = ">=3.7" +files = [ + {file = "mplfonts-0.0.8-py3-none-any.whl", hash = "sha256:b2182e5b0baa216cf016dec19942740e5b48956415708ad2d465e03952112ec1"}, + {file = "mplfonts-0.0.8.tar.gz", hash = "sha256:0abcb2fc0605645e1e7561c6923014d856f11676899b33b4d89757843f5e7c22"}, +] + +[package.dependencies] +fire = ">=0.4.0" +fontmeta = ">=1.6.1" +matplotlib = ">=3.4" + [[package]] name = "mpmath" version = "1.3.0" @@ -9300,6 +9342,20 @@ files = [ [package.dependencies] tencentcloud-sdk-python-common = "3.0.1257" +[[package]] +name = "termcolor" +version = "2.5.0" +description = "ANSI color formatting for output in terminal" +optional = false +python-versions = ">=3.9" +files = [ + {file = "termcolor-2.5.0-py3-none-any.whl", hash = "sha256:37b17b5fc1e604945c2642c872a3764b5d547a48009871aea3edd3afa180afb8"}, + {file = "termcolor-2.5.0.tar.gz", hash = "sha256:998d8d27da6d48442e8e1f016119076b690d962507531df4890fcd2db2ef8a6f"}, +] + +[package.extras] +tests = ["pytest", "pytest-cov"] + [[package]] name = "threadpoolctl" version = "3.5.0" @@ -10046,13 +10102,13 @@ files = [ [[package]] name = "vanna" -version = "0.7.3" +version = "0.7.5" description = "Generate SQL queries from natural language" optional = false python-versions = ">=3.9" files = [ - {file = "vanna-0.7.3-py3-none-any.whl", hash = "sha256:82ba39e5d6c503d1c8cca60835ed401d20ec3a3da98d487f529901dcb30061d6"}, - {file = "vanna-0.7.3.tar.gz", hash = "sha256:4590dd94d2fe180b4efc7a83c867b73144ef58794018910dc226857cfb703077"}, + {file = "vanna-0.7.5-py3-none-any.whl", hash = "sha256:07458c7befa49de517a8760c2d80a13147278b484c515d49a906acc88edcb835"}, + {file = "vanna-0.7.5.tar.gz", hash = "sha256:2fdffc58832898e4fc8e93c45b173424db59a22773b22ca348640161d391eacf"}, ] [package.dependencies] @@ -10073,7 +10129,7 @@ sqlparse = "*" tabulate = "*" [package.extras] -all = ["PyMySQL", "anthropic", "azure-common", "azure-identity", "azure-search-documents", "chromadb", "db-dtypes", "duckdb", "fastembed", "google-cloud-aiplatform", "google-cloud-bigquery", "google-generativeai", "httpx", "marqo", "mistralai (>=1.0.0)", "ollama", "openai", "opensearch-dsl", "opensearch-py", "pinecone-client", "psycopg2-binary", "pymilvus[model]", "qdrant-client", "qianfan", "snowflake-connector-python", "transformers", "weaviate-client", "zhipuai"] +all = ["PyMySQL", "anthropic", "azure-common", "azure-identity", "azure-search-documents", "boto", "boto3", "botocore", "chromadb", "db-dtypes", "duckdb", "faiss-cpu", "fastembed", "google-cloud-aiplatform", "google-cloud-bigquery", "google-generativeai", "httpx", "langchain_core", "langchain_postgres", "marqo", "mistralai (>=1.0.0)", "ollama", "openai", "opensearch-dsl", "opensearch-py", "pinecone-client", "psycopg2-binary", "pymilvus[model]", "qdrant-client", "qianfan", "snowflake-connector-python", "transformers", "weaviate-client", "xinference-client", "zhipuai"] anthropic = ["anthropic"] azuresearch = ["azure-common", "azure-identity", "azure-search-documents", "fastembed"] bedrock = ["boto3", "botocore"] @@ -10081,6 +10137,8 @@ bigquery = ["google-cloud-bigquery"] chromadb = ["chromadb"] clickhouse = ["clickhouse_connect"] duckdb = ["duckdb"] +faiss-cpu = ["faiss-cpu"] +faiss-gpu = ["faiss-gpu"] gemini = ["google-generativeai"] google = ["google-cloud-aiplatform", "google-generativeai"] hf = ["transformers"] @@ -10091,6 +10149,7 @@ mysql = ["PyMySQL"] ollama = ["httpx", "ollama"] openai = ["openai"] opensearch = ["opensearch-dsl", "opensearch-py"] +pgvector = ["langchain-postgres (>=0.0.12)"] pinecone = ["fastembed", "pinecone-client"] postgres = ["db-dtypes", "psycopg2-binary"] qdrant = ["fastembed", "qdrant-client"] @@ -10099,6 +10158,7 @@ snowflake = ["snowflake-connector-python"] test = ["tox"] vllm = ["vllm"] weaviate = ["weaviate-client"] +xinference-client = ["xinference-client"] zhipuai = ["zhipuai"] [[package]] @@ -10940,4 +11000,4 @@ cffi = ["cffi (>=1.11)"] [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.13" -content-hash = "ef927b98c33d704d680e08db0e5c7d9a4e05454c66fcd6a5f656a65eb08e886b" +content-hash = "e4794898403da4ad7b51f248a6c07632a949114c1b569406d3aa6a94c62510a5" diff --git a/api/pyproject.toml b/api/pyproject.toml index ef3dc14131..e42ca1b5a4 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -206,13 +206,14 @@ cloudscraper = "1.2.71" duckduckgo-search = "~6.3.0" jsonpath-ng = "1.6.1" matplotlib = "~3.8.2" +mplfonts = "~0.0.8" newspaper3k = "0.2.8" nltk = "3.9.1" numexpr = "~2.9.0" pydub = "~0.25.1" qrcode = "~7.4.2" twilio = "~9.0.4" -vanna = { version = "0.7.3", extras = ["postgres", "mysql", "clickhouse", "duckdb"] } +vanna = { version = "0.7.5", extras = ["postgres", "mysql", "clickhouse", "duckdb", "oracle"] } wikipedia = "1.4.0" yfinance = "~0.2.40" From 4e1af81e11977101b5dee2c229336ebdb177e594 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 5 Nov 2024 10:57:32 +0800 Subject: [PATCH 35/48] chore: translate i18n files (#10273) Co-authored-by: laipz8200 <16485841+laipz8200@users.noreply.github.com> Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com> --- web/i18n/de-DE/workflow.ts | 17 +++++++++++++++++ web/i18n/es-ES/workflow.ts | 17 +++++++++++++++++ web/i18n/fa-IR/workflow.ts | 17 +++++++++++++++++ web/i18n/fr-FR/workflow.ts | 17 +++++++++++++++++ web/i18n/hi-IN/workflow.ts | 17 +++++++++++++++++ web/i18n/it-IT/workflow.ts | 17 +++++++++++++++++ web/i18n/ja-JP/workflow.ts | 17 +++++++++++++++++ web/i18n/ko-KR/workflow.ts | 17 +++++++++++++++++ web/i18n/pl-PL/workflow.ts | 17 +++++++++++++++++ web/i18n/pt-BR/workflow.ts | 17 +++++++++++++++++ web/i18n/ro-RO/workflow.ts | 17 +++++++++++++++++ web/i18n/ru-RU/workflow.ts | 17 +++++++++++++++++ web/i18n/tr-TR/workflow.ts | 17 +++++++++++++++++ web/i18n/uk-UA/workflow.ts | 17 +++++++++++++++++ web/i18n/vi-VN/workflow.ts | 17 +++++++++++++++++ web/i18n/zh-Hant/workflow.ts | 17 +++++++++++++++++ 16 files changed, 272 insertions(+) diff --git a/web/i18n/de-DE/workflow.ts b/web/i18n/de-DE/workflow.ts index bde0250fcc..d05070c308 100644 --- a/web/i18n/de-DE/workflow.ts +++ b/web/i18n/de-DE/workflow.ts @@ -557,6 +557,23 @@ const translation = { iteration_one: '{{count}} Iteration', iteration_other: '{{count}} Iterationen', currentIteration: 'Aktuelle Iteration', + ErrorMethod: { + operationTerminated: 'beendet', + removeAbnormalOutput: 'remove-abnormale_ausgabe', + continueOnError: 'Fehler "Fortfahren bei"', + }, + MaxParallelismTitle: 'Maximale Parallelität', + parallelMode: 'Paralleler Modus', + errorResponseMethod: 'Methode der Fehlerantwort', + error_one: '{{Anzahl}} Fehler', + error_other: '{{Anzahl}} Irrtümer', + MaxParallelismDesc: 'Die maximale Parallelität wird verwendet, um die Anzahl der Aufgaben zu steuern, die gleichzeitig in einer einzigen Iteration ausgeführt werden.', + parallelPanelDesc: 'Im parallelen Modus unterstützen Aufgaben in der Iteration die parallele Ausführung.', + parallelModeEnableDesc: 'Im parallelen Modus unterstützen Aufgaben innerhalb von Iterationen die parallele Ausführung. Sie können dies im Eigenschaftenbereich auf der rechten Seite konfigurieren.', + answerNodeWarningDesc: 'Warnung im parallelen Modus: Antwortknoten, Zuweisungen von Konversationsvariablen und persistente Lese-/Schreibvorgänge innerhalb von Iterationen können Ausnahmen verursachen.', + parallelModeEnableTitle: 'Paralleler Modus aktiviert', + parallelModeUpper: 'PARALLELER MODUS', + comma: ',', }, note: { editor: { diff --git a/web/i18n/es-ES/workflow.ts b/web/i18n/es-ES/workflow.ts index 59a330e7f4..6c9af49c4d 100644 --- a/web/i18n/es-ES/workflow.ts +++ b/web/i18n/es-ES/workflow.ts @@ -557,6 +557,23 @@ const translation = { iteration_one: '{{count}} Iteración', iteration_other: '{{count}} Iteraciones', currentIteration: 'Iteración actual', + ErrorMethod: { + operationTerminated: 'Terminado', + continueOnError: 'Continuar en el error', + removeAbnormalOutput: 'eliminar-salida-anormal', + }, + comma: ',', + errorResponseMethod: 'Método de respuesta a errores', + error_one: '{{conteo}} Error', + parallelPanelDesc: 'En el modo paralelo, las tareas de la iteración admiten la ejecución en paralelo.', + MaxParallelismTitle: 'Máximo paralelismo', + error_other: '{{conteo}} Errores', + parallelMode: 'Modo paralelo', + parallelModeEnableDesc: 'En el modo paralelo, las tareas dentro de las iteraciones admiten la ejecución en paralelo. Puede configurar esto en el panel de propiedades a la derecha.', + parallelModeUpper: 'MODO PARALELO', + MaxParallelismDesc: 'El paralelismo máximo se utiliza para controlar el número de tareas ejecutadas simultáneamente en una sola iteración.', + answerNodeWarningDesc: 'Advertencia de modo paralelo: Los nodos de respuesta, las asignaciones de variables de conversación y las operaciones de lectura/escritura persistentes dentro de las iteraciones pueden provocar excepciones.', + parallelModeEnableTitle: 'Modo paralelo habilitado', }, note: { addNote: 'Agregar nota', diff --git a/web/i18n/fa-IR/workflow.ts b/web/i18n/fa-IR/workflow.ts index b1f9384159..4b00390663 100644 --- a/web/i18n/fa-IR/workflow.ts +++ b/web/i18n/fa-IR/workflow.ts @@ -557,6 +557,23 @@ const translation = { iteration_one: '{{count}} تکرار', iteration_other: '{{count}} تکرارها', currentIteration: 'تکرار فعلی', + ErrorMethod: { + continueOnError: 'ادامه در خطا', + operationTerminated: 'فسخ', + removeAbnormalOutput: 'حذف خروجی غیرطبیعی', + }, + error_one: '{{تعداد}} خطا', + error_other: '{{تعداد}} خطاهای', + parallelMode: 'حالت موازی', + errorResponseMethod: 'روش پاسخ به خطا', + parallelModeEnableTitle: 'حالت موازی فعال است', + parallelModeUpper: 'حالت موازی', + comma: ',', + parallelModeEnableDesc: 'در حالت موازی، وظایف درون تکرارها از اجرای موازی پشتیبانی می کنند. می توانید این را در پانل ویژگی ها در سمت راست پیکربندی کنید.', + MaxParallelismTitle: 'حداکثر موازی سازی', + parallelPanelDesc: 'در حالت موازی، وظایف در تکرار از اجرای موازی پشتیبانی می کنند.', + MaxParallelismDesc: 'حداکثر موازی سازی برای کنترل تعداد وظایف اجرا شده به طور همزمان در یک تکرار واحد استفاده می شود.', + answerNodeWarningDesc: 'هشدار حالت موازی: گره های پاسخ، تکالیف متغیر مکالمه و عملیات خواندن/نوشتن مداوم در تکرارها ممکن است باعث استثنائات شود.', }, note: { addNote: 'افزودن یادداشت', diff --git a/web/i18n/fr-FR/workflow.ts b/web/i18n/fr-FR/workflow.ts index e56932455f..e736e2cb07 100644 --- a/web/i18n/fr-FR/workflow.ts +++ b/web/i18n/fr-FR/workflow.ts @@ -557,6 +557,23 @@ const translation = { iteration_one: '{{count}} Itération', iteration_other: '{{count}} Itérations', currentIteration: 'Itération actuelle', + ErrorMethod: { + operationTerminated: 'Terminé', + removeAbnormalOutput: 'remove-abnormal-output', + continueOnError: 'continuer sur l’erreur', + }, + comma: ',', + error_one: '{{compte}} Erreur', + error_other: '{{compte}} Erreurs', + parallelModeEnableDesc: 'En mode parallèle, les tâches au sein des itérations prennent en charge l’exécution parallèle. Vous pouvez le configurer dans le panneau des propriétés à droite.', + parallelModeUpper: 'MODE PARALLÈLE', + parallelPanelDesc: 'En mode parallèle, les tâches de l’itération prennent en charge l’exécution parallèle.', + MaxParallelismDesc: 'Le parallélisme maximal est utilisé pour contrôler le nombre de tâches exécutées simultanément en une seule itération.', + errorResponseMethod: 'Méthode de réponse aux erreurs', + MaxParallelismTitle: 'Parallélisme maximal', + answerNodeWarningDesc: 'Avertissement en mode parallèle : les nœuds de réponse, les affectations de variables de conversation et les opérations de lecture/écriture persistantes au sein des itérations peuvent provoquer des exceptions.', + parallelModeEnableTitle: 'Mode parallèle activé', + parallelMode: 'Mode parallèle', }, note: { addNote: 'Ajouter note', diff --git a/web/i18n/hi-IN/workflow.ts b/web/i18n/hi-IN/workflow.ts index 1473f78ccd..4112643488 100644 --- a/web/i18n/hi-IN/workflow.ts +++ b/web/i18n/hi-IN/workflow.ts @@ -577,6 +577,23 @@ const translation = { iteration_one: '{{count}} इटरेशन', iteration_other: '{{count}} इटरेशन्स', currentIteration: 'वर्तमान इटरेशन', + ErrorMethod: { + operationTerminated: 'समाप्त', + continueOnError: 'जारी रखें-पर-त्रुटि', + removeAbnormalOutput: 'निकालें-असामान्य-आउटपुट', + }, + comma: ',', + error_other: '{{गिनती}} त्रुटियों', + error_one: '{{गिनती}} चूक', + parallelMode: 'समानांतर मोड', + parallelModeUpper: 'समानांतर मोड', + errorResponseMethod: 'त्रुटि प्रतिक्रिया विधि', + MaxParallelismTitle: 'अधिकतम समांतरता', + parallelModeEnableTitle: 'समानांतर मोड सक्षम किया गया', + parallelModeEnableDesc: 'समानांतर मोड में, पुनरावृत्तियों के भीतर कार्य समानांतर निष्पादन का समर्थन करते हैं। आप इसे दाईं ओर गुण पैनल में कॉन्फ़िगर कर सकते हैं।', + parallelPanelDesc: 'समानांतर मोड में, पुनरावृत्ति में कार्य समानांतर निष्पादन का समर्थन करते हैं।', + MaxParallelismDesc: 'अधिकतम समांतरता का उपयोग एकल पुनरावृत्ति में एक साथ निष्पादित कार्यों की संख्या को नियंत्रित करने के लिए किया जाता है।', + answerNodeWarningDesc: 'समानांतर मोड चेतावनी: उत्तर नोड्स, वार्तालाप चर असाइनमेंट, और पुनरावृत्तियों के भीतर लगातार पढ़ने/लिखने की कार्रवाई अपवाद पैदा कर सकती है।', }, note: { addNote: 'नोट जोड़ें', diff --git a/web/i18n/it-IT/workflow.ts b/web/i18n/it-IT/workflow.ts index 19fa7bfbb5..756fb665af 100644 --- a/web/i18n/it-IT/workflow.ts +++ b/web/i18n/it-IT/workflow.ts @@ -584,6 +584,23 @@ const translation = { iteration_one: '{{count}} Iterazione', iteration_other: '{{count}} Iterazioni', currentIteration: 'Iterazione Corrente', + ErrorMethod: { + operationTerminated: 'Terminato', + continueOnError: 'continua sull\'errore', + removeAbnormalOutput: 'rimuovi-output-anomalo', + }, + error_one: '{{conteggio}} Errore', + parallelMode: 'Modalità parallela', + MaxParallelismTitle: 'Parallelismo massimo', + error_other: '{{conteggio}} Errori', + parallelModeEnableDesc: 'In modalità parallela, le attività all\'interno delle iterazioni supportano l\'esecuzione parallela. È possibile configurare questa opzione nel pannello delle proprietà a destra.', + MaxParallelismDesc: 'Il parallelismo massimo viene utilizzato per controllare il numero di attività eseguite contemporaneamente in una singola iterazione.', + errorResponseMethod: 'Metodo di risposta all\'errore', + parallelModeEnableTitle: 'Modalità parallela abilitata', + parallelModeUpper: 'MODALITÀ PARALLELA', + comma: ',', + parallelPanelDesc: 'In modalità parallela, le attività nell\'iterazione supportano l\'esecuzione parallela.', + answerNodeWarningDesc: 'Avviso in modalità parallela: i nodi di risposta, le assegnazioni di variabili di conversazione e le operazioni di lettura/scrittura persistenti all\'interno delle iterazioni possono causare eccezioni.', }, note: { addNote: 'Aggiungi Nota', diff --git a/web/i18n/ja-JP/workflow.ts b/web/i18n/ja-JP/workflow.ts index b6c7786081..a82ba71e48 100644 --- a/web/i18n/ja-JP/workflow.ts +++ b/web/i18n/ja-JP/workflow.ts @@ -558,6 +558,23 @@ const translation = { iteration_one: '{{count}} イテレーション', iteration_other: '{{count}} イテレーション', currentIteration: '現在のイテレーション', + ErrorMethod: { + operationTerminated: '終了', + continueOnError: 'エラー時に続行', + removeAbnormalOutput: 'アブノーマルアウトプットの削除', + }, + comma: ',', + error_other: '{{カウント}}エラー', + error_one: '{{カウント}}エラー', + parallelModeUpper: 'パラレルモード', + parallelMode: 'パラレルモード', + MaxParallelismTitle: '最大並列処理', + errorResponseMethod: 'エラー応答方式', + parallelPanelDesc: '並列モードでは、イテレーションのタスクは並列実行をサポートします。', + parallelModeEnableDesc: '並列モードでは、イテレーション内のタスクは並列実行をサポートします。これは、右側のプロパティパネルで構成できます。', + parallelModeEnableTitle: 'パラレルモード有効', + MaxParallelismDesc: '最大並列処理は、1 回の反復で同時に実行されるタスクの数を制御するために使用されます。', + answerNodeWarningDesc: '並列モードの警告: 応答ノード、会話変数の割り当て、およびイテレーション内の永続的な読み取り/書き込み操作により、例外が発生する可能性があります。', }, note: { addNote: 'コメントを追加', diff --git a/web/i18n/ko-KR/workflow.ts b/web/i18n/ko-KR/workflow.ts index b62aff2068..589831401c 100644 --- a/web/i18n/ko-KR/workflow.ts +++ b/web/i18n/ko-KR/workflow.ts @@ -557,6 +557,23 @@ const translation = { iteration_one: '{{count}} 반복', iteration_other: '{{count}} 반복', currentIteration: '현재 반복', + ErrorMethod: { + operationTerminated: '종료', + continueOnError: '오류 발생 시 계속', + removeAbnormalOutput: '비정상 출력 제거', + }, + comma: ',', + error_one: '{{개수}} 오류', + parallelMode: '병렬 모드', + errorResponseMethod: '오류 응답 방법', + parallelModeUpper: '병렬 모드', + MaxParallelismTitle: '최대 병렬 처리', + error_other: '{{개수}} 오류', + parallelModeEnableTitle: 'Parallel Mode Enabled(병렬 모드 사용)', + parallelPanelDesc: '병렬 모드에서 반복의 작업은 병렬 실행을 지원합니다.', + parallelModeEnableDesc: '병렬 모드에서는 반복 내의 작업이 병렬 실행을 지원합니다. 오른쪽의 속성 패널에서 이를 구성할 수 있습니다.', + MaxParallelismDesc: '최대 병렬 처리는 단일 반복에서 동시에 실행되는 작업 수를 제어하는 데 사용됩니다.', + answerNodeWarningDesc: '병렬 모드 경고: 응답 노드, 대화 변수 할당 및 반복 내의 지속적인 읽기/쓰기 작업으로 인해 예외가 발생할 수 있습니다.', }, note: { editor: { diff --git a/web/i18n/pl-PL/workflow.ts b/web/i18n/pl-PL/workflow.ts index aace1b2642..f118f7945c 100644 --- a/web/i18n/pl-PL/workflow.ts +++ b/web/i18n/pl-PL/workflow.ts @@ -557,6 +557,23 @@ const translation = { iteration_one: '{{count}} Iteracja', iteration_other: '{{count}} Iteracje', currentIteration: 'Bieżąca iteracja', + ErrorMethod: { + continueOnError: 'kontynuacja w przypadku błędu', + operationTerminated: 'Zakończone', + removeAbnormalOutput: 'usuń-nieprawidłowe-wyjście', + }, + comma: ',', + parallelModeUpper: 'TRYB RÓWNOLEGŁY', + parallelModeEnableTitle: 'Włączony tryb równoległy', + MaxParallelismTitle: 'Maksymalna równoległość', + error_one: '{{liczba}} Błąd', + error_other: '{{liczba}} Błędy', + parallelPanelDesc: 'W trybie równoległym zadania w iteracji obsługują wykonywanie równoległe.', + parallelMode: 'Tryb równoległy', + MaxParallelismDesc: 'Maksymalna równoległość służy do kontrolowania liczby zadań wykonywanych jednocześnie w jednej iteracji.', + parallelModeEnableDesc: 'W trybie równoległym zadania w iteracjach obsługują wykonywanie równoległe. Możesz to skonfigurować w panelu właściwości po prawej stronie.', + answerNodeWarningDesc: 'Ostrzeżenie w trybie równoległym: węzły odpowiedzi, przypisania zmiennych konwersacji i trwałe operacje odczytu/zapisu w iteracjach mogą powodować wyjątki.', + errorResponseMethod: 'Metoda odpowiedzi na błąd', }, note: { editor: { diff --git a/web/i18n/pt-BR/workflow.ts b/web/i18n/pt-BR/workflow.ts index f0f2fec0e2..44afda5cd4 100644 --- a/web/i18n/pt-BR/workflow.ts +++ b/web/i18n/pt-BR/workflow.ts @@ -557,6 +557,23 @@ const translation = { iteration_one: '{{count}} Iteração', iteration_other: '{{count}} Iterações', currentIteration: 'Iteração atual', + ErrorMethod: { + continueOnError: 'continuar em erro', + removeAbnormalOutput: 'saída anormal de remoção', + operationTerminated: 'Terminada', + }, + MaxParallelismTitle: 'Paralelismo máximo', + parallelModeEnableTitle: 'Modo paralelo ativado', + errorResponseMethod: 'Método de resposta de erro', + error_other: '{{contagem}} Erros', + parallelMode: 'Modo paralelo', + parallelModeUpper: 'MODO PARALELO', + error_one: '{{contagem}} Erro', + parallelModeEnableDesc: 'No modo paralelo, as tarefas dentro das iterações dão suporte à execução paralela. Você pode configurar isso no painel de propriedades à direita.', + comma: ',', + MaxParallelismDesc: 'O paralelismo máximo é usado para controlar o número de tarefas executadas simultaneamente em uma única iteração.', + answerNodeWarningDesc: 'Aviso de modo paralelo: nós de resposta, atribuições de variáveis de conversação e operações persistentes de leitura/gravação em iterações podem causar exceções.', + parallelPanelDesc: 'No modo paralelo, as tarefas na iteração dão suporte à execução paralela.', }, note: { editor: { diff --git a/web/i18n/ro-RO/workflow.ts b/web/i18n/ro-RO/workflow.ts index ab0100d347..d8cd84f730 100644 --- a/web/i18n/ro-RO/workflow.ts +++ b/web/i18n/ro-RO/workflow.ts @@ -557,6 +557,23 @@ const translation = { iteration_one: '{{count}} Iterație', iteration_other: '{{count}} Iterații', currentIteration: 'Iterație curentă', + ErrorMethod: { + operationTerminated: 'Încheiată', + continueOnError: 'continuare-la-eroare', + removeAbnormalOutput: 'elimină-ieșire-anormală', + }, + parallelModeEnableTitle: 'Modul paralel activat', + errorResponseMethod: 'Metoda de răspuns la eroare', + comma: ',', + parallelModeEnableDesc: 'În modul paralel, sarcinile din iterații acceptă execuția paralelă. Puteți configura acest lucru în panoul de proprietăți din dreapta.', + parallelModeUpper: 'MOD PARALEL', + MaxParallelismTitle: 'Paralelism maxim', + parallelMode: 'Mod paralel', + error_other: '{{număr}} Erori', + error_one: '{{număr}} Eroare', + parallelPanelDesc: 'În modul paralel, activitățile din iterație acceptă execuția paralelă.', + MaxParallelismDesc: 'Paralelismul maxim este utilizat pentru a controla numărul de sarcini executate simultan într-o singură iterație.', + answerNodeWarningDesc: 'Avertisment modul paralel: Nodurile de răspuns, atribuirea variabilelor de conversație și operațiunile persistente de citire/scriere în iterații pot cauza excepții.', }, note: { editor: { diff --git a/web/i18n/ru-RU/workflow.ts b/web/i18n/ru-RU/workflow.ts index 27735fbb7d..c822f8c3e5 100644 --- a/web/i18n/ru-RU/workflow.ts +++ b/web/i18n/ru-RU/workflow.ts @@ -557,6 +557,23 @@ const translation = { iteration_one: '{{count}} Итерация', iteration_other: '{{count}} Итераций', currentIteration: 'Текущая итерация', + ErrorMethod: { + operationTerminated: 'Прекращено', + continueOnError: 'продолжить по ошибке', + removeAbnormalOutput: 'удалить аномальный вывод', + }, + comma: ',', + error_other: '{{Количество}} Ошибки', + errorResponseMethod: 'Метод реагирования на ошибку', + MaxParallelismTitle: 'Максимальный параллелизм', + parallelModeUpper: 'ПАРАЛЛЕЛЬНЫЙ РЕЖИМ', + error_one: '{{Количество}} Ошибка', + parallelModeEnableTitle: 'Параллельный режим включен', + parallelMode: 'Параллельный режим', + parallelPanelDesc: 'В параллельном режиме задачи в итерации поддерживают параллельное выполнение.', + parallelModeEnableDesc: 'В параллельном режиме задачи в итерациях поддерживают параллельное выполнение. Вы можете настроить это на панели свойств справа.', + MaxParallelismDesc: 'Максимальный параллелизм используется для управления количеством задач, выполняемых одновременно в одной итерации.', + answerNodeWarningDesc: 'Предупреждение о параллельном режиме: узлы ответов, присвоение переменных диалога и постоянные операции чтения и записи в итерациях могут вызывать исключения.', }, note: { addNote: 'Добавить заметку', diff --git a/web/i18n/tr-TR/workflow.ts b/web/i18n/tr-TR/workflow.ts index 82718ebc03..e6e25f6d0e 100644 --- a/web/i18n/tr-TR/workflow.ts +++ b/web/i18n/tr-TR/workflow.ts @@ -558,6 +558,23 @@ const translation = { iteration_one: '{{count}} Yineleme', iteration_other: '{{count}} Yineleme', currentIteration: 'Mevcut Yineleme', + ErrorMethod: { + operationTerminated: 'Sonlandırıldı', + continueOnError: 'Hata Üzerine Devam Et', + removeAbnormalOutput: 'anormal çıktıyı kaldır', + }, + parallelModeUpper: 'PARALEL MOD', + parallelMode: 'Paralel Mod', + MaxParallelismTitle: 'Maksimum paralellik', + error_one: '{{sayı}} Hata', + errorResponseMethod: 'Hata yanıtı yöntemi', + comma: ',', + parallelModeEnableTitle: 'Paralel Mod Etkin', + error_other: '{{sayı}} Hata', + parallelPanelDesc: 'Paralel modda, yinelemedeki görevler paralel yürütmeyi destekler.', + answerNodeWarningDesc: 'Paralel mod uyarısı: Yinelemeler içindeki yanıt düğümleri, konuşma değişkeni atamaları ve kalıcı okuma/yazma işlemleri özel durumlara neden olabilir.', + parallelModeEnableDesc: 'Paralel modda, yinelemeler içindeki görevler paralel yürütmeyi destekler. Bunu sağdaki özellikler panelinde yapılandırabilirsiniz.', + MaxParallelismDesc: 'Maksimum paralellik, tek bir yinelemede aynı anda yürütülen görevlerin sayısını kontrol etmek için kullanılır.', }, note: { addNote: 'Not Ekle', diff --git a/web/i18n/uk-UA/workflow.ts b/web/i18n/uk-UA/workflow.ts index 1828b6499f..663b5e4c13 100644 --- a/web/i18n/uk-UA/workflow.ts +++ b/web/i18n/uk-UA/workflow.ts @@ -557,6 +557,23 @@ const translation = { iteration_one: '{{count}} Ітерація', iteration_other: '{{count}} Ітерацій', currentIteration: 'Поточна ітерація', + ErrorMethod: { + operationTerminated: 'Припинено', + continueOnError: 'Продовжити після помилки', + removeAbnormalOutput: 'видалити-ненормальний-вивід', + }, + error_one: '{{count}} Помилка', + comma: ',', + MaxParallelismTitle: 'Максимальна паралельність', + parallelModeUpper: 'ПАРАЛЕЛЬНИЙ РЕЖИМ', + error_other: '{{count}} Помилки', + parallelMode: 'Паралельний режим', + parallelModeEnableTitle: 'Увімкнено паралельний режим', + errorResponseMethod: 'Метод реагування на помилку', + parallelPanelDesc: 'У паралельному режимі завдання в ітерації підтримують паралельне виконання.', + parallelModeEnableDesc: 'У паралельному режимі завдання всередині ітерацій підтримують паралельне виконання. Ви можете налаштувати це на панелі властивостей праворуч.', + MaxParallelismDesc: 'Максимальний паралелізм використовується для контролю числа завдань, що виконуються одночасно за одну ітерацію.', + answerNodeWarningDesc: 'Попередження в паралельному режимі: вузли відповідей, призначення змінних розмови та постійні операції читання/запису в межах ітерацій можуть спричинити винятки.', }, note: { editor: { diff --git a/web/i18n/vi-VN/workflow.ts b/web/i18n/vi-VN/workflow.ts index 2866af8a2a..1176fdd2b5 100644 --- a/web/i18n/vi-VN/workflow.ts +++ b/web/i18n/vi-VN/workflow.ts @@ -557,6 +557,23 @@ const translation = { iteration_one: '{{count}} Lặp', iteration_other: '{{count}} Lặp', currentIteration: 'Lặp hiện tại', + ErrorMethod: { + operationTerminated: 'Chấm dứt', + removeAbnormalOutput: 'loại bỏ-bất thường-đầu ra', + continueOnError: 'Tiếp tục lỗi', + }, + comma: ',', + error_other: '{{đếm}} Lỗi', + error_one: '{{đếm}} Lỗi', + MaxParallelismTitle: 'Song song tối đa', + parallelPanelDesc: 'Ở chế độ song song, các tác vụ trong quá trình lặp hỗ trợ thực thi song song.', + parallelMode: 'Chế độ song song', + parallelModeEnableTitle: 'Đã bật Chế độ song song', + errorResponseMethod: 'Phương pháp phản hồi lỗi', + MaxParallelismDesc: 'Tính song song tối đa được sử dụng để kiểm soát số lượng tác vụ được thực hiện đồng thời trong một lần lặp.', + answerNodeWarningDesc: 'Cảnh báo chế độ song song: Các nút trả lời, bài tập biến hội thoại và các thao tác đọc/ghi liên tục trong các lần lặp có thể gây ra ngoại lệ.', + parallelModeEnableDesc: 'Trong chế độ song song, các tác vụ trong các lần lặp hỗ trợ thực thi song song. Bạn có thể định cấu hình điều này trong bảng thuộc tính ở bên phải.', + parallelModeUpper: 'CHẾ ĐỘ SONG SONG', }, note: { editor: { diff --git a/web/i18n/zh-Hant/workflow.ts b/web/i18n/zh-Hant/workflow.ts index d65b3999d2..f3fbfdedc2 100644 --- a/web/i18n/zh-Hant/workflow.ts +++ b/web/i18n/zh-Hant/workflow.ts @@ -557,6 +557,23 @@ const translation = { iteration_one: '{{count}}個迭代', iteration_other: '{{count}}個迭代', currentIteration: '當前迭代', + ErrorMethod: { + operationTerminated: '終止', + removeAbnormalOutput: 'remove-abnormal-output', + continueOnError: '出錯時繼續', + }, + comma: ',', + parallelMode: '並行模式', + parallelModeEnableTitle: 'Parallel Mode 已啟用', + MaxParallelismTitle: '最大並行度', + parallelModeUpper: '並行模式', + parallelPanelDesc: '在並行模式下,反覆運算中的任務支援並行執行。', + error_one: '{{count}}錯誤', + errorResponseMethod: '錯誤回應方法', + parallelModeEnableDesc: '在並行模式下,反覆運算中的任務支援並行執行。您可以在右側的 properties 面板中進行配置。', + answerNodeWarningDesc: '並行模式警告:反覆運算中的應答節點、對話變數賦值和持久讀/寫操作可能會導致異常。', + error_other: '{{count}}錯誤', + MaxParallelismDesc: '最大並行度用於控制在單個反覆運算中同時執行的任務數。', }, note: { editor: { From 3040d538f7a20497b24e2242df68ca14f0d9cacd Mon Sep 17 00:00:00 2001 From: NFish Date: Tue, 5 Nov 2024 12:38:31 +0800 Subject: [PATCH 36/48] refactor the logic of refreshing access_token (#10068) --- web/app/account/avatar.tsx | 5 +- .../header/account-dropdown/index.tsx | 5 +- web/app/components/swr-initor.tsx | 39 ++---- web/app/signin/normalForm.tsx | 5 +- web/hooks/use-refresh-token.ts | 99 -------------- web/service/base.ts | 128 +++++++++++------- web/service/refresh-token.ts | 75 ++++++++++ 7 files changed, 171 insertions(+), 185 deletions(-) delete mode 100644 web/hooks/use-refresh-token.ts create mode 100644 web/service/refresh-token.ts diff --git a/web/app/account/avatar.tsx b/web/app/account/avatar.tsx index 2b9aeba5da..544e43ab27 100644 --- a/web/app/account/avatar.tsx +++ b/web/app/account/avatar.tsx @@ -23,8 +23,9 @@ export default function AppSelector() { params: {}, }) - if (localStorage?.getItem('console_token')) - localStorage.removeItem('console_token') + localStorage.removeItem('setup_status') + localStorage.removeItem('console_token') + localStorage.removeItem('refresh_token') router.push('/signin') } diff --git a/web/app/components/header/account-dropdown/index.tsx b/web/app/components/header/account-dropdown/index.tsx index 712906ebae..14f079c0f2 100644 --- a/web/app/components/header/account-dropdown/index.tsx +++ b/web/app/components/header/account-dropdown/index.tsx @@ -47,8 +47,9 @@ export default function AppSelector({ isMobile }: IAppSelector) { params: {}, }) - if (localStorage?.getItem('console_token')) - localStorage.removeItem('console_token') + localStorage.removeItem('setup_status') + localStorage.removeItem('console_token') + localStorage.removeItem('refresh_token') router.push('/signin') } diff --git a/web/app/components/swr-initor.tsx b/web/app/components/swr-initor.tsx index ff9a7b832f..2a119df996 100644 --- a/web/app/components/swr-initor.tsx +++ b/web/app/components/swr-initor.tsx @@ -4,7 +4,6 @@ import { SWRConfig } from 'swr' import { useCallback, useEffect, useState } from 'react' import type { ReactNode } from 'react' import { usePathname, useRouter, useSearchParams } from 'next/navigation' -import useRefreshToken from '@/hooks/use-refresh-token' import { fetchSetupStatus } from '@/service/common' type SwrInitorProps = { @@ -15,12 +14,11 @@ const SwrInitor = ({ }: SwrInitorProps) => { const router = useRouter() const searchParams = useSearchParams() - const pathname = usePathname() - const { getNewAccessToken } = useRefreshToken() - const consoleToken = searchParams.get('access_token') - const refreshToken = searchParams.get('refresh_token') + const consoleToken = decodeURIComponent(searchParams.get('access_token') || '') + const refreshToken = decodeURIComponent(searchParams.get('refresh_token') || '') const consoleTokenFromLocalStorage = localStorage?.getItem('console_token') const refreshTokenFromLocalStorage = localStorage?.getItem('refresh_token') + const pathname = usePathname() const [init, setInit] = useState(false) const isSetupFinished = useCallback(async () => { @@ -41,25 +39,6 @@ const SwrInitor = ({ } }, []) - const setRefreshToken = useCallback(async () => { - try { - if (!(consoleToken || refreshToken || consoleTokenFromLocalStorage || refreshTokenFromLocalStorage)) - return Promise.reject(new Error('No token found')) - - if (consoleTokenFromLocalStorage && refreshTokenFromLocalStorage) - await getNewAccessToken() - - if (consoleToken && refreshToken) { - localStorage.setItem('console_token', consoleToken) - localStorage.setItem('refresh_token', refreshToken) - await getNewAccessToken() - } - } - catch (error) { - return Promise.reject(error) - } - }, [consoleToken, refreshToken, consoleTokenFromLocalStorage, refreshTokenFromLocalStorage, getNewAccessToken]) - useEffect(() => { (async () => { try { @@ -68,9 +47,15 @@ const SwrInitor = ({ router.replace('/install') return } - await setRefreshToken() - if (searchParams.has('access_token') || searchParams.has('refresh_token')) + if (!((consoleToken && refreshToken) || (consoleTokenFromLocalStorage && refreshTokenFromLocalStorage))) { + router.replace('/signin') + return + } + if (searchParams.has('access_token') || searchParams.has('refresh_token')) { + consoleToken && localStorage.setItem('console_token', consoleToken) + refreshToken && localStorage.setItem('refresh_token', refreshToken) router.replace(pathname) + } setInit(true) } @@ -78,7 +63,7 @@ const SwrInitor = ({ router.replace('/signin') } })() - }, [isSetupFinished, setRefreshToken, router, pathname, searchParams]) + }, [isSetupFinished, router, pathname, searchParams, consoleToken, refreshToken, consoleTokenFromLocalStorage, refreshTokenFromLocalStorage]) return init ? ( diff --git a/web/app/signin/normalForm.tsx b/web/app/signin/normalForm.tsx index c0f2d89b37..f4f46c68ba 100644 --- a/web/app/signin/normalForm.tsx +++ b/web/app/signin/normalForm.tsx @@ -12,11 +12,9 @@ import cn from '@/utils/classnames' import { getSystemFeatures, invitationCheck } from '@/service/common' import { defaultSystemFeatures } from '@/types/feature' import Toast from '@/app/components/base/toast' -import useRefreshToken from '@/hooks/use-refresh-token' import { IS_CE_EDITION } from '@/config' const NormalForm = () => { - const { getNewAccessToken } = useRefreshToken() const { t } = useTranslation() const router = useRouter() const searchParams = useSearchParams() @@ -38,7 +36,6 @@ const NormalForm = () => { if (consoleToken && refreshToken) { localStorage.setItem('console_token', consoleToken) localStorage.setItem('refresh_token', refreshToken) - getNewAccessToken() router.replace('/apps') return } @@ -71,7 +68,7 @@ const NormalForm = () => { setSystemFeatures(defaultSystemFeatures) } finally { setIsLoading(false) } - }, [consoleToken, refreshToken, message, router, invite_token, isInviteLink, getNewAccessToken]) + }, [consoleToken, refreshToken, message, router, invite_token, isInviteLink]) useEffect(() => { init() }, [init]) diff --git a/web/hooks/use-refresh-token.ts b/web/hooks/use-refresh-token.ts deleted file mode 100644 index 53dc4faf00..0000000000 --- a/web/hooks/use-refresh-token.ts +++ /dev/null @@ -1,99 +0,0 @@ -'use client' -import { useCallback, useEffect, useRef } from 'react' -import { jwtDecode } from 'jwt-decode' -import dayjs from 'dayjs' -import utc from 'dayjs/plugin/utc' -import { useRouter } from 'next/navigation' -import type { CommonResponse } from '@/models/common' -import { fetchNewToken } from '@/service/common' -import { fetchWithRetry } from '@/utils' - -dayjs.extend(utc) - -const useRefreshToken = () => { - const router = useRouter() - const timer = useRef() - const advanceTime = useRef(5 * 60 * 1000) - - const getExpireTime = useCallback((token: string) => { - if (!token) - return 0 - const decoded = jwtDecode(token) - return (decoded.exp || 0) * 1000 - }, []) - - const getCurrentTimeStamp = useCallback(() => { - return dayjs.utc().valueOf() - }, []) - - const handleError = useCallback(() => { - localStorage?.removeItem('is_refreshing') - localStorage?.removeItem('console_token') - localStorage?.removeItem('refresh_token') - router.replace('/signin') - }, []) - - const getNewAccessToken = useCallback(async () => { - const currentAccessToken = localStorage?.getItem('console_token') - const currentRefreshToken = localStorage?.getItem('refresh_token') - if (!currentAccessToken || !currentRefreshToken) { - handleError() - return new Error('No access token or refresh token found') - } - if (localStorage?.getItem('is_refreshing') === '1') { - clearTimeout(timer.current) - timer.current = setTimeout(() => { - getNewAccessToken() - }, 1000) - return null - } - const currentTokenExpireTime = getExpireTime(currentAccessToken) - if (getCurrentTimeStamp() + advanceTime.current > currentTokenExpireTime) { - localStorage?.setItem('is_refreshing', '1') - const [e, res] = await fetchWithRetry(fetchNewToken({ - body: { refresh_token: currentRefreshToken }, - }) as Promise) - if (e) { - handleError() - return e - } - const { access_token, refresh_token } = res.data - localStorage?.setItem('is_refreshing', '0') - localStorage?.setItem('console_token', access_token) - localStorage?.setItem('refresh_token', refresh_token) - const newTokenExpireTime = getExpireTime(access_token) - clearTimeout(timer.current) - timer.current = setTimeout(() => { - getNewAccessToken() - }, newTokenExpireTime - advanceTime.current - getCurrentTimeStamp()) - } - else { - const newTokenExpireTime = getExpireTime(currentAccessToken) - clearTimeout(timer.current) - timer.current = setTimeout(() => { - getNewAccessToken() - }, newTokenExpireTime - advanceTime.current - getCurrentTimeStamp()) - } - return null - }, [getExpireTime, getCurrentTimeStamp, handleError]) - - const handleVisibilityChange = useCallback(() => { - if (document.visibilityState === 'visible') - getNewAccessToken() - }, []) - - useEffect(() => { - window.addEventListener('visibilitychange', handleVisibilityChange) - return () => { - window.removeEventListener('visibilitychange', handleVisibilityChange) - clearTimeout(timer.current) - localStorage?.removeItem('is_refreshing') - } - }, []) - - return { - getNewAccessToken, - } -} - -export default useRefreshToken diff --git a/web/service/base.ts b/web/service/base.ts index fbdd5c1fd3..fcf8d8bd7d 100644 --- a/web/service/base.ts +++ b/web/service/base.ts @@ -1,3 +1,4 @@ +import { refreshAccessTokenOrRelogin } from './refresh-token' import { API_PREFIX, IS_CE_EDITION, PUBLIC_API_PREFIX } from '@/config' import Toast from '@/app/components/base/toast' import type { AnnotationReply, MessageEnd, MessageReplace, ThoughtItem } from '@/app/components/base/chat/chat/type' @@ -356,39 +357,8 @@ const baseFetch = ( if (!/^(2|3)\d{2}$/.test(String(res.status))) { const bodyJson = res.json() switch (res.status) { - case 401: { - if (isPublicAPI) { - return bodyJson.then((data: ResponseError) => { - if (data.code === 'web_sso_auth_required') - requiredWebSSOLogin() - - if (data.code === 'unauthorized') { - removeAccessToken() - globalThis.location.reload() - } - - return Promise.reject(data) - }) - } - const loginUrl = `${globalThis.location.origin}/signin` - bodyJson.then((data: ResponseError) => { - if (data.code === 'init_validate_failed' && IS_CE_EDITION && !silent) - Toast.notify({ type: 'error', message: data.message, duration: 4000 }) - else if (data.code === 'not_init_validated' && IS_CE_EDITION) - globalThis.location.href = `${globalThis.location.origin}/init` - else if (data.code === 'not_setup' && IS_CE_EDITION) - globalThis.location.href = `${globalThis.location.origin}/install` - else if (location.pathname !== '/signin' || !IS_CE_EDITION) - globalThis.location.href = loginUrl - else if (!silent) - Toast.notify({ type: 'error', message: data.message }) - }).catch(() => { - // Handle any other errors - globalThis.location.href = loginUrl - }) - - break - } + case 401: + return Promise.reject(resClone) case 403: bodyJson.then((data: ResponseError) => { if (!silent) @@ -484,7 +454,9 @@ export const upload = (options: any, isPublicAPI?: boolean, url?: string, search export const ssePost = ( url: string, fetchOptions: FetchOptionType, - { + otherOptions: IOtherOptions, +) => { + const { isPublicAPI = false, onData, onCompleted, @@ -507,8 +479,7 @@ export const ssePost = ( onTextReplace, onError, getAbortController, - }: IOtherOptions, -) => { + } = otherOptions const abortController = new AbortController() const options = Object.assign({}, baseOptions, { @@ -532,21 +503,29 @@ export const ssePost = ( globalThis.fetch(urlWithPrefix, options as RequestInit) .then((res) => { if (!/^(2|3)\d{2}$/.test(String(res.status))) { - res.json().then((data: any) => { - if (isPublicAPI) { - if (data.code === 'web_sso_auth_required') - requiredWebSSOLogin() + if (res.status === 401) { + refreshAccessTokenOrRelogin(TIME_OUT).then(() => { + ssePost(url, fetchOptions, otherOptions) + }).catch(() => { + res.json().then((data: any) => { + if (isPublicAPI) { + if (data.code === 'web_sso_auth_required') + requiredWebSSOLogin() - if (data.code === 'unauthorized') { - removeAccessToken() - globalThis.location.reload() - } - if (res.status === 401) - return - } - Toast.notify({ type: 'error', message: data.message || 'Server Error' }) - }) - onError?.('Server Error') + if (data.code === 'unauthorized') { + removeAccessToken() + globalThis.location.reload() + } + } + }) + }) + } + else { + res.json().then((data) => { + Toast.notify({ type: 'error', message: data.message || 'Server Error' }) + }) + onError?.('Server Error') + } return } return handleStream(res, (str: string, isFirstMessage: boolean, moreInfo: IOnDataMoreInfo) => { @@ -568,7 +547,54 @@ export const ssePost = ( // base request export const request = (url: string, options = {}, otherOptions?: IOtherOptions) => { - return baseFetch(url, options, otherOptions || {}) + return new Promise((resolve, reject) => { + const otherOptionsForBaseFetch = otherOptions || {} + baseFetch(url, options, otherOptionsForBaseFetch).then(resolve).catch((errResp) => { + if (errResp?.status === 401) { + return refreshAccessTokenOrRelogin(TIME_OUT).then(() => { + baseFetch(url, options, otherOptionsForBaseFetch).then(resolve).catch(reject) + }).catch(() => { + const { + isPublicAPI = false, + silent, + } = otherOptionsForBaseFetch + const bodyJson = errResp.json() + if (isPublicAPI) { + return bodyJson.then((data: ResponseError) => { + if (data.code === 'web_sso_auth_required') + requiredWebSSOLogin() + + if (data.code === 'unauthorized') { + removeAccessToken() + globalThis.location.reload() + } + + return Promise.reject(data) + }) + } + const loginUrl = `${globalThis.location.origin}/signin` + bodyJson.then((data: ResponseError) => { + if (data.code === 'init_validate_failed' && IS_CE_EDITION && !silent) + Toast.notify({ type: 'error', message: data.message, duration: 4000 }) + else if (data.code === 'not_init_validated' && IS_CE_EDITION) + globalThis.location.href = `${globalThis.location.origin}/init` + else if (data.code === 'not_setup' && IS_CE_EDITION) + globalThis.location.href = `${globalThis.location.origin}/install` + else if (location.pathname !== '/signin' || !IS_CE_EDITION) + globalThis.location.href = loginUrl + else if (!silent) + Toast.notify({ type: 'error', message: data.message }) + }).catch(() => { + // Handle any other errors + globalThis.location.href = loginUrl + }) + }) + } + else { + reject(errResp) + } + }) + }) } // request methods diff --git a/web/service/refresh-token.ts b/web/service/refresh-token.ts new file mode 100644 index 0000000000..8bd2215041 --- /dev/null +++ b/web/service/refresh-token.ts @@ -0,0 +1,75 @@ +import { apiPrefix } from '@/config' +import { fetchWithRetry } from '@/utils' + +let isRefreshing = false +function waitUntilTokenRefreshed() { + return new Promise((resolve, reject) => { + function _check() { + const isRefreshingSign = localStorage.getItem('is_refreshing') + if ((isRefreshingSign && isRefreshingSign === '1') || isRefreshing) { + setTimeout(() => { + _check() + }, 1000) + } + else { + resolve() + } + } + _check() + }) +} + +// only one request can send +async function getNewAccessToken(): Promise { + try { + const isRefreshingSign = localStorage.getItem('is_refreshing') + if ((isRefreshingSign && isRefreshingSign === '1') || isRefreshing) { + await waitUntilTokenRefreshed() + } + else { + globalThis.localStorage.setItem('is_refreshing', '1') + isRefreshing = true + const refresh_token = globalThis.localStorage.getItem('refresh_token') + + // Do not use baseFetch to refresh tokens. + // If a 401 response occurs and baseFetch itself attempts to refresh the token, + // it can lead to an infinite loop if the refresh attempt also returns 401. + // To avoid this, handle token refresh separately in a dedicated function + // that does not call baseFetch and uses a single retry mechanism. + const [error, ret] = await fetchWithRetry(globalThis.fetch(`${apiPrefix}/refresh-token`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json;utf-8', + }, + body: JSON.stringify({ refresh_token }), + })) + if (error) { + return Promise.reject(error) + } + else { + if (ret.status === 401) + return Promise.reject(ret) + + const { data } = await ret.json() + globalThis.localStorage.setItem('console_token', data.access_token) + globalThis.localStorage.setItem('refresh_token', data.refresh_token) + } + } + } + catch (error) { + console.error(error) + return Promise.reject(error) + } + finally { + isRefreshing = false + globalThis.localStorage.removeItem('is_refreshing') + } +} + +export async function refreshAccessTokenOrRelogin(timeout: number) { + return Promise.race([new Promise((resolve, reject) => setTimeout(() => { + isRefreshing = false + globalThis.localStorage.removeItem('is_refreshing') + reject(new Error('request timeout')) + }, timeout)), getNewAccessToken()]) +} From 33219e850a462f693879122a5e9b54a11fccd63e Mon Sep 17 00:00:00 2001 From: -LAN- Date: Tue, 5 Nov 2024 14:23:18 +0800 Subject: [PATCH 37/48] fix(node): correct file property name in function switch (#10284) --- api/core/workflow/nodes/list_operator/node.py | 2 +- .../core/workflow/nodes/test_list_operator.py | 49 +++++++++++++++++-- 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/api/core/workflow/nodes/list_operator/node.py b/api/core/workflow/nodes/list_operator/node.py index 0406b97eb8..49e7ca85fd 100644 --- a/api/core/workflow/nodes/list_operator/node.py +++ b/api/core/workflow/nodes/list_operator/node.py @@ -157,7 +157,7 @@ def _get_file_extract_string_func(*, key: str) -> Callable[[File], str]: return lambda x: x.type case "extension": return lambda x: x.extension or "" - case "mimetype": + case "mime_type": return lambda x: x.mime_type or "" case "transfer_method": return lambda x: x.transfer_method diff --git a/api/tests/unit_tests/core/workflow/nodes/test_list_operator.py b/api/tests/unit_tests/core/workflow/nodes/test_list_operator.py index 53e3c93fcc..0f5c8bf51b 100644 --- a/api/tests/unit_tests/core/workflow/nodes/test_list_operator.py +++ b/api/tests/unit_tests/core/workflow/nodes/test_list_operator.py @@ -2,11 +2,11 @@ from unittest.mock import MagicMock import pytest -from core.file import File -from core.file.models import FileTransferMethod, FileType +from core.file import File, FileTransferMethod, FileType from core.variables import ArrayFileSegment from core.workflow.nodes.list_operator.entities import FilterBy, FilterCondition, Limit, ListOperatorNodeData, OrderBy -from core.workflow.nodes.list_operator.node import ListOperatorNode +from core.workflow.nodes.list_operator.exc import InvalidKeyError +from core.workflow.nodes.list_operator.node import ListOperatorNode, _get_file_extract_string_func from models.workflow import WorkflowNodeExecutionStatus @@ -109,3 +109,46 @@ def test_filter_files_by_type(list_operator_node): assert expected_file["tenant_id"] == result_file.tenant_id assert expected_file["transfer_method"] == result_file.transfer_method assert expected_file["related_id"] == result_file.related_id + + +def test_get_file_extract_string_func(): + # Create a File object + file = File( + tenant_id="test_tenant", + type=FileType.DOCUMENT, + transfer_method=FileTransferMethod.LOCAL_FILE, + filename="test_file.txt", + extension=".txt", + mime_type="text/plain", + remote_url="https://example.com/test_file.txt", + related_id="test_related_id", + ) + + # Test each case + assert _get_file_extract_string_func(key="name")(file) == "test_file.txt" + assert _get_file_extract_string_func(key="type")(file) == "document" + assert _get_file_extract_string_func(key="extension")(file) == ".txt" + assert _get_file_extract_string_func(key="mime_type")(file) == "text/plain" + assert _get_file_extract_string_func(key="transfer_method")(file) == "local_file" + assert _get_file_extract_string_func(key="url")(file) == "https://example.com/test_file.txt" + + # Test with empty values + empty_file = File( + tenant_id="test_tenant", + type=FileType.DOCUMENT, + transfer_method=FileTransferMethod.LOCAL_FILE, + filename=None, + extension=None, + mime_type=None, + remote_url=None, + related_id="test_related_id", + ) + + assert _get_file_extract_string_func(key="name")(empty_file) == "" + assert _get_file_extract_string_func(key="extension")(empty_file) == "" + assert _get_file_extract_string_func(key="mime_type")(empty_file) == "" + assert _get_file_extract_string_func(key="url")(empty_file) == "" + + # Test invalid key + with pytest.raises(InvalidKeyError): + _get_file_extract_string_func(key="invalid_key") From b88145096f86e240fd221bdfc50e37fd82a18f72 Mon Sep 17 00:00:00 2001 From: -LAN- Date: Tue, 5 Nov 2024 14:40:57 +0800 Subject: [PATCH 38/48] feat(model): add validation for custom disclaimer length (#10287) --- api/models/model.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/api/models/model.py b/api/models/model.py index 713ce80e12..0b3793cc62 100644 --- a/api/models/model.py +++ b/api/models/model.py @@ -1320,7 +1320,7 @@ class Site(Base): privacy_policy = db.Column(db.String(255)) show_workflow_steps = db.Column(db.Boolean, nullable=False, server_default=db.text("true")) use_icon_as_answer_icon = db.Column(db.Boolean, nullable=False, server_default=db.text("false")) - custom_disclaimer: Mapped[str] = mapped_column(sa.TEXT, default="") + _custom_disclaimer: Mapped[str] = mapped_column("custom_disclaimer", sa.TEXT, default="") customize_domain = db.Column(db.String(255)) customize_token_strategy = db.Column(db.String(255), nullable=False) prompt_public = db.Column(db.Boolean, nullable=False, server_default=db.text("false")) @@ -1331,6 +1331,16 @@ class Site(Base): updated_at = db.Column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)")) code = db.Column(db.String(255)) + @property + def custom_disclaimer(self): + return self._custom_disclaimer + + @custom_disclaimer.setter + def custom_disclaimer(self, value: str): + if len(value) > 512: + raise ValueError("Custom disclaimer cannot exceed 512 characters.") + self._custom_disclaimer = value + @staticmethod def generate_code(n): while True: From f65d577f54050d525227f94fc98ef3733888fb8e Mon Sep 17 00:00:00 2001 From: Matsuda Date: Tue, 5 Nov 2024 15:41:15 +0900 Subject: [PATCH 39/48] fix(model_runtime): fix wrong max_tokens for Claude 3.5 Haiku on Amazon Bedrock (#10286) --- .../bedrock/llm/anthropic.claude-3-5-haiku-v1.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/core/model_runtime/model_providers/bedrock/llm/anthropic.claude-3-5-haiku-v1.yaml b/api/core/model_runtime/model_providers/bedrock/llm/anthropic.claude-3-5-haiku-v1.yaml index 7c676136db..35fc8d0d11 100644 --- a/api/core/model_runtime/model_providers/bedrock/llm/anthropic.claude-3-5-haiku-v1.yaml +++ b/api/core/model_runtime/model_providers/bedrock/llm/anthropic.claude-3-5-haiku-v1.yaml @@ -16,9 +16,9 @@ parameter_rules: use_template: max_tokens required: true type: int - default: 4096 + default: 8192 min: 1 - max: 4096 + max: 8192 help: zh_Hans: 停止前生成的最大令牌数。请注意,Anthropic Claude 模型可能会在达到 max_tokens 的值之前停止生成令牌。不同的 Anthropic Claude 模型对此参数具有不同的最大值。 en_US: The maximum number of tokens to generate before stopping. Note that Anthropic Claude models might stop generating tokens before reaching the value of max_tokens. Different Anthropic Claude models have different maximum values for this parameter. From 43c7739b8883c996a8f64f53e736a43b1fbd98bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9D=9E=E6=B3=95=E6=93=8D=E4=BD=9C?= Date: Tue, 5 Nov 2024 14:42:47 +0800 Subject: [PATCH 40/48] feat: add xAI model provider (#10272) --- .../model_providers/x/__init__.py | 0 .../model_providers/x/_assets/x-ai-logo.svg | 1 + .../model_providers/x/llm/__init__.py | 0 .../model_providers/x/llm/grok-beta.yaml | 63 ++++++ .../model_providers/x/llm/llm.py | 37 ++++ api/core/model_runtime/model_providers/x/x.py | 25 +++ .../model_runtime/model_providers/x/x.yaml | 38 ++++ api/tests/integration_tests/.env.example | 4 + .../model_runtime/x/__init__.py | 0 .../model_runtime/x/test_llm.py | 204 ++++++++++++++++++ 10 files changed, 372 insertions(+) create mode 100644 api/core/model_runtime/model_providers/x/__init__.py create mode 100644 api/core/model_runtime/model_providers/x/_assets/x-ai-logo.svg create mode 100644 api/core/model_runtime/model_providers/x/llm/__init__.py create mode 100644 api/core/model_runtime/model_providers/x/llm/grok-beta.yaml create mode 100644 api/core/model_runtime/model_providers/x/llm/llm.py create mode 100644 api/core/model_runtime/model_providers/x/x.py create mode 100644 api/core/model_runtime/model_providers/x/x.yaml create mode 100644 api/tests/integration_tests/model_runtime/x/__init__.py create mode 100644 api/tests/integration_tests/model_runtime/x/test_llm.py diff --git a/api/core/model_runtime/model_providers/x/__init__.py b/api/core/model_runtime/model_providers/x/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/core/model_runtime/model_providers/x/_assets/x-ai-logo.svg b/api/core/model_runtime/model_providers/x/_assets/x-ai-logo.svg new file mode 100644 index 0000000000..f8b745cb13 --- /dev/null +++ b/api/core/model_runtime/model_providers/x/_assets/x-ai-logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/api/core/model_runtime/model_providers/x/llm/__init__.py b/api/core/model_runtime/model_providers/x/llm/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/core/model_runtime/model_providers/x/llm/grok-beta.yaml b/api/core/model_runtime/model_providers/x/llm/grok-beta.yaml new file mode 100644 index 0000000000..7c305735b9 --- /dev/null +++ b/api/core/model_runtime/model_providers/x/llm/grok-beta.yaml @@ -0,0 +1,63 @@ +model: grok-beta +label: + en_US: Grok beta +model_type: llm +features: + - multi-tool-call +model_properties: + mode: chat + context_size: 131072 +parameter_rules: + - name: temperature + label: + en_US: "Temperature" + zh_Hans: "采样温度" + type: float + default: 0.7 + min: 0.0 + max: 2.0 + precision: 1 + required: true + help: + en_US: "The randomness of the sampling temperature control output. The temperature value is within the range of [0.0, 1.0]. The higher the value, the more random and creative the output; the lower the value, the more stable it is. It is recommended to adjust either top_p or temperature parameters according to your needs to avoid adjusting both at the same time." + zh_Hans: "采样温度控制输出的随机性。温度值在 [0.0, 1.0] 范围内,值越高,输出越随机和创造性;值越低,输出越稳定。建议根据需求调整 top_p 或 temperature 参数,避免同时调整两者。" + + - name: top_p + label: + en_US: "Top P" + zh_Hans: "Top P" + type: float + default: 0.7 + min: 0.0 + max: 1.0 + precision: 1 + required: true + help: + en_US: "The value range of the sampling method is [0.0, 1.0]. The top_p value determines that the model selects tokens from the top p% of candidate words with the highest probability; when top_p is 0, this parameter is invalid. It is recommended to adjust either top_p or temperature parameters according to your needs to avoid adjusting both at the same time." + zh_Hans: "采样方法的取值范围为 [0.0,1.0]。top_p 值确定模型从概率最高的前p%的候选词中选取 tokens;当 top_p 为 0 时,此参数无效。建议根据需求调整 top_p 或 temperature 参数,避免同时调整两者。" + + - name: frequency_penalty + use_template: frequency_penalty + label: + en_US: "Frequency Penalty" + zh_Hans: "频率惩罚" + type: float + default: 0 + min: 0 + max: 2.0 + precision: 1 + required: false + help: + en_US: "Number between 0 and 2.0. Positive values penalize new tokens based on their existing frequency in the text so far, decreasing the model's likelihood to repeat the same line verbatim." + zh_Hans: "介于0和2.0之间的数字。正值会根据新标记在文本中迄今为止的现有频率来惩罚它们,从而降低模型一字不差地重复同一句话的可能性。" + + - name: user + use_template: text + label: + en_US: "User" + zh_Hans: "用户" + type: string + required: false + help: + en_US: "Used to track and differentiate conversation requests from different users." + zh_Hans: "用于追踪和区分不同用户的对话请求。" diff --git a/api/core/model_runtime/model_providers/x/llm/llm.py b/api/core/model_runtime/model_providers/x/llm/llm.py new file mode 100644 index 0000000000..3f5325a857 --- /dev/null +++ b/api/core/model_runtime/model_providers/x/llm/llm.py @@ -0,0 +1,37 @@ +from collections.abc import Generator +from typing import Optional, Union + +from yarl import URL + +from core.model_runtime.entities.llm_entities import LLMMode, LLMResult +from core.model_runtime.entities.message_entities import ( + PromptMessage, + PromptMessageTool, +) +from core.model_runtime.model_providers.openai_api_compatible.llm.llm import OAIAPICompatLargeLanguageModel + + +class XAILargeLanguageModel(OAIAPICompatLargeLanguageModel): + def _invoke( + self, + model: str, + credentials: dict, + prompt_messages: list[PromptMessage], + model_parameters: dict, + tools: Optional[list[PromptMessageTool]] = None, + stop: Optional[list[str]] = None, + stream: bool = True, + user: Optional[str] = None, + ) -> Union[LLMResult, Generator]: + self._add_custom_parameters(credentials) + return super()._invoke(model, credentials, prompt_messages, model_parameters, tools, stop, stream) + + def validate_credentials(self, model: str, credentials: dict) -> None: + self._add_custom_parameters(credentials) + super().validate_credentials(model, credentials) + + @staticmethod + def _add_custom_parameters(credentials) -> None: + credentials["endpoint_url"] = str(URL(credentials["endpoint_url"])) or "https://api.x.ai/v1" + credentials["mode"] = LLMMode.CHAT.value + credentials["function_calling_type"] = "tool_call" diff --git a/api/core/model_runtime/model_providers/x/x.py b/api/core/model_runtime/model_providers/x/x.py new file mode 100644 index 0000000000..e3f2b8eeba --- /dev/null +++ b/api/core/model_runtime/model_providers/x/x.py @@ -0,0 +1,25 @@ +import logging + +from core.model_runtime.entities.model_entities import ModelType +from core.model_runtime.errors.validate import CredentialsValidateFailedError +from core.model_runtime.model_providers.__base.model_provider import ModelProvider + +logger = logging.getLogger(__name__) + + +class XAIProvider(ModelProvider): + def validate_provider_credentials(self, credentials: dict) -> None: + """ + Validate provider credentials + if validate failed, raise exception + + :param credentials: provider credentials, credentials form defined in `provider_credential_schema`. + """ + try: + model_instance = self.get_model_instance(ModelType.LLM) + model_instance.validate_credentials(model="grok-beta", credentials=credentials) + except CredentialsValidateFailedError as ex: + raise ex + except Exception as ex: + logger.exception(f"{self.get_provider_schema().provider} credentials validate failed") + raise ex diff --git a/api/core/model_runtime/model_providers/x/x.yaml b/api/core/model_runtime/model_providers/x/x.yaml new file mode 100644 index 0000000000..90d1cbfe7e --- /dev/null +++ b/api/core/model_runtime/model_providers/x/x.yaml @@ -0,0 +1,38 @@ +provider: x +label: + en_US: xAI +description: + en_US: xAI is a company working on building artificial intelligence to accelerate human scientific discovery. We are guided by our mission to advance our collective understanding of the universe. +icon_small: + en_US: x-ai-logo.svg +icon_large: + en_US: x-ai-logo.svg +help: + title: + en_US: Get your token from xAI + zh_Hans: 从 xAI 获取 token + url: + en_US: https://x.ai/api +supported_model_types: + - llm +configurate_methods: + - predefined-model +provider_credential_schema: + credential_form_schemas: + - variable: api_key + label: + en_US: API Key + type: secret-input + required: true + placeholder: + zh_Hans: 在此输入您的 API Key + en_US: Enter your API Key + - variable: endpoint_url + label: + en_US: API Base + type: text-input + required: false + default: https://api.x.ai/v1 + placeholder: + zh_Hans: 在此输入您的 API Base + en_US: Enter your API Base diff --git a/api/tests/integration_tests/.env.example b/api/tests/integration_tests/.env.example index baa100531f..95cca83b44 100644 --- a/api/tests/integration_tests/.env.example +++ b/api/tests/integration_tests/.env.example @@ -102,3 +102,7 @@ GPUSTACK_API_KEY= # Gitee AI Credentials GITEE_AI_API_KEY= + +# xAI Credentials +XAI_API_KEY= +XAI_API_BASE= diff --git a/api/tests/integration_tests/model_runtime/x/__init__.py b/api/tests/integration_tests/model_runtime/x/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/tests/integration_tests/model_runtime/x/test_llm.py b/api/tests/integration_tests/model_runtime/x/test_llm.py new file mode 100644 index 0000000000..647a2f6480 --- /dev/null +++ b/api/tests/integration_tests/model_runtime/x/test_llm.py @@ -0,0 +1,204 @@ +import os +from collections.abc import Generator + +import pytest + +from core.model_runtime.entities.llm_entities import LLMResult, LLMResultChunk, LLMResultChunkDelta +from core.model_runtime.entities.message_entities import ( + AssistantPromptMessage, + PromptMessageTool, + SystemPromptMessage, + UserPromptMessage, +) +from core.model_runtime.entities.model_entities import AIModelEntity +from core.model_runtime.errors.validate import CredentialsValidateFailedError +from core.model_runtime.model_providers.x.llm.llm import XAILargeLanguageModel + +"""FOR MOCK FIXTURES, DO NOT REMOVE""" +from tests.integration_tests.model_runtime.__mock.openai import setup_openai_mock + + +def test_predefined_models(): + model = XAILargeLanguageModel() + model_schemas = model.predefined_models() + + assert len(model_schemas) >= 1 + assert isinstance(model_schemas[0], AIModelEntity) + + +@pytest.mark.parametrize("setup_openai_mock", [["chat"]], indirect=True) +def test_validate_credentials_for_chat_model(setup_openai_mock): + model = XAILargeLanguageModel() + + with pytest.raises(CredentialsValidateFailedError): + # model name to gpt-3.5-turbo because of mocking + model.validate_credentials( + model="gpt-3.5-turbo", + credentials={"api_key": "invalid_key", "endpoint_url": os.environ.get("XAI_API_BASE"), "mode": "chat"}, + ) + + model.validate_credentials( + model="grok-beta", + credentials={ + "api_key": os.environ.get("XAI_API_KEY"), + "endpoint_url": os.environ.get("XAI_API_BASE"), + "mode": "chat", + }, + ) + + +@pytest.mark.parametrize("setup_openai_mock", [["chat"]], indirect=True) +def test_invoke_chat_model(setup_openai_mock): + model = XAILargeLanguageModel() + + result = model.invoke( + model="grok-beta", + credentials={ + "api_key": os.environ.get("XAI_API_KEY"), + "endpoint_url": os.environ.get("XAI_API_BASE"), + "mode": "chat", + }, + prompt_messages=[ + SystemPromptMessage( + content="You are a helpful AI assistant.", + ), + UserPromptMessage(content="Hello World!"), + ], + model_parameters={ + "temperature": 0.0, + "top_p": 1.0, + "presence_penalty": 0.0, + "frequency_penalty": 0.0, + "max_tokens": 10, + }, + stop=["How"], + stream=False, + user="foo", + ) + + assert isinstance(result, LLMResult) + assert len(result.message.content) > 0 + + +@pytest.mark.parametrize("setup_openai_mock", [["chat"]], indirect=True) +def test_invoke_chat_model_with_tools(setup_openai_mock): + model = XAILargeLanguageModel() + + result = model.invoke( + model="grok-beta", + credentials={ + "api_key": os.environ.get("XAI_API_KEY"), + "endpoint_url": os.environ.get("XAI_API_BASE"), + "mode": "chat", + }, + prompt_messages=[ + SystemPromptMessage( + content="You are a helpful AI assistant.", + ), + UserPromptMessage( + content="what's the weather today in London?", + ), + ], + model_parameters={"temperature": 0.0, "max_tokens": 100}, + tools=[ + PromptMessageTool( + name="get_weather", + description="Determine weather in my location", + parameters={ + "type": "object", + "properties": { + "location": {"type": "string", "description": "The city and state e.g. San Francisco, CA"}, + "unit": {"type": "string", "enum": ["c", "f"]}, + }, + "required": ["location"], + }, + ), + PromptMessageTool( + name="get_stock_price", + description="Get the current stock price", + parameters={ + "type": "object", + "properties": {"symbol": {"type": "string", "description": "The stock symbol"}}, + "required": ["symbol"], + }, + ), + ], + stream=False, + user="foo", + ) + + assert isinstance(result, LLMResult) + assert isinstance(result.message, AssistantPromptMessage) + + +@pytest.mark.parametrize("setup_openai_mock", [["chat"]], indirect=True) +def test_invoke_stream_chat_model(setup_openai_mock): + model = XAILargeLanguageModel() + + result = model.invoke( + model="grok-beta", + credentials={ + "api_key": os.environ.get("XAI_API_KEY"), + "endpoint_url": os.environ.get("XAI_API_BASE"), + "mode": "chat", + }, + prompt_messages=[ + SystemPromptMessage( + content="You are a helpful AI assistant.", + ), + UserPromptMessage(content="Hello World!"), + ], + model_parameters={"temperature": 0.0, "max_tokens": 100}, + stream=True, + user="foo", + ) + + assert isinstance(result, Generator) + + for chunk in result: + assert isinstance(chunk, LLMResultChunk) + assert isinstance(chunk.delta, LLMResultChunkDelta) + assert isinstance(chunk.delta.message, AssistantPromptMessage) + assert len(chunk.delta.message.content) > 0 if chunk.delta.finish_reason is None else True + if chunk.delta.finish_reason is not None: + assert chunk.delta.usage is not None + assert chunk.delta.usage.completion_tokens > 0 + + +def test_get_num_tokens(): + model = XAILargeLanguageModel() + + num_tokens = model.get_num_tokens( + model="grok-beta", + credentials={"api_key": os.environ.get("XAI_API_KEY"), "endpoint_url": os.environ.get("XAI_API_BASE")}, + prompt_messages=[UserPromptMessage(content="Hello World!")], + ) + + assert num_tokens == 10 + + num_tokens = model.get_num_tokens( + model="grok-beta", + credentials={"api_key": os.environ.get("XAI_API_KEY"), "endpoint_url": os.environ.get("XAI_API_BASE")}, + prompt_messages=[ + SystemPromptMessage( + content="You are a helpful AI assistant.", + ), + UserPromptMessage(content="Hello World!"), + ], + tools=[ + PromptMessageTool( + name="get_weather", + description="Determine weather in my location", + parameters={ + "type": "object", + "properties": { + "location": {"type": "string", "description": "The city and state e.g. San Francisco, CA"}, + "unit": {"type": "string", "enum": ["c", "f"]}, + }, + "required": ["location"], + }, + ), + ], + ) + + assert num_tokens == 77 From b9198639e26b56833e5d84209c87b7852ffb50c8 Mon Sep 17 00:00:00 2001 From: eux Date: Tue, 5 Nov 2024 14:42:59 +0800 Subject: [PATCH 41/48] fix: borken faq url in CONTRIBUTING.md (#10275) --- CONTRIBUTING.md | 2 +- CONTRIBUTING_VI.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8f57cd545e..da2928d189 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -81,7 +81,7 @@ Dify requires the following dependencies to build, make sure they're installed o Dify is composed of a backend and a frontend. Navigate to the backend directory by `cd api/`, then follow the [Backend README](api/README.md) to install it. In a separate terminal, navigate to the frontend directory by `cd web/`, then follow the [Frontend README](web/README.md) to install. -Check the [installation FAQ](https://docs.dify.ai/learn-more/faq/self-host-faq) for a list of common issues and steps to troubleshoot. +Check the [installation FAQ](https://docs.dify.ai/learn-more/faq/install-faq) for a list of common issues and steps to troubleshoot. ### 5. Visit dify in your browser diff --git a/CONTRIBUTING_VI.md b/CONTRIBUTING_VI.md index 80e68a046e..a77239ff38 100644 --- a/CONTRIBUTING_VI.md +++ b/CONTRIBUTING_VI.md @@ -79,7 +79,7 @@ Dify yêu cầu các phụ thuộc sau để build, hãy đảm bảo chúng đ Dify bao gồm một backend và một frontend. Đi đến thư mục backend bằng lệnh `cd api/`, sau đó làm theo hướng dẫn trong [README của Backend](api/README.md) để cài đặt. Trong một terminal khác, đi đến thư mục frontend bằng lệnh `cd web/`, sau đó làm theo hướng dẫn trong [README của Frontend](web/README.md) để cài đặt. -Kiểm tra [FAQ về cài đặt](https://docs.dify.ai/learn-more/faq/self-host-faq) để xem danh sách các vấn đề thường gặp và các bước khắc phục. +Kiểm tra [FAQ về cài đặt](https://docs.dify.ai/learn-more/faq/install-faq) để xem danh sách các vấn đề thường gặp và các bước khắc phục. ### 5. Truy cập Dify trong trình duyệt của bạn From 2d1e5fb4e0f459102e9057d79441303ecd492b13 Mon Sep 17 00:00:00 2001 From: pinsily <13160724868@163.com> Date: Tue, 5 Nov 2024 14:47:15 +0800 Subject: [PATCH 42/48] fix: handle KeyError when accessing rules in CleanProcessor.clean (#10258) --- api/core/indexing_runner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/core/indexing_runner.py b/api/core/indexing_runner.py index fb9fe8f210..e2a94073cf 100644 --- a/api/core/indexing_runner.py +++ b/api/core/indexing_runner.py @@ -598,7 +598,7 @@ class IndexingRunner: rules = DatasetProcessRule.AUTOMATIC_RULES else: rules = json.loads(processing_rule.rules) if processing_rule.rules else {} - document_text = CleanProcessor.clean(text, rules) + document_text = CleanProcessor.clean(text, {"rules": rules}) return document_text From 507bb3549aae33397bfa762086667cea9197e215 Mon Sep 17 00:00:00 2001 From: Matsuda Date: Tue, 5 Nov 2024 17:09:53 +0900 Subject: [PATCH 43/48] fix typo: writeOpner to writeOpener (#10290) --- web/i18n/pl-PL/app-debug.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/i18n/pl-PL/app-debug.ts b/web/i18n/pl-PL/app-debug.ts index 7cf6c77cb4..cf7232e563 100644 --- a/web/i18n/pl-PL/app-debug.ts +++ b/web/i18n/pl-PL/app-debug.ts @@ -355,7 +355,7 @@ const translation = { openingStatement: { title: 'Wstęp do rozmowy', add: 'Dodaj', - writeOpner: 'Napisz wstęp', + writeOpener: 'Napisz wstęp', placeholder: 'Tutaj napisz swoją wiadomość wprowadzającą, możesz użyć zmiennych, spróbuj wpisać {{variable}}.', openingQuestion: 'Pytania otwierające', From baab81714e88cf0fda66fa73ffa3d2ecdd89d32f Mon Sep 17 00:00:00 2001 From: -LAN- Date: Tue, 5 Nov 2024 16:30:23 +0800 Subject: [PATCH 44/48] fix(http_request): improve parameter initialization and reorganize tests (#10297) --- .../workflow/nodes/http_request/executor.py | 6 +- .../test_http_request_executor.py | 198 ++++++++++++++++++ .../test_http_request_node.py | 169 +-------------- 3 files changed, 203 insertions(+), 170 deletions(-) create mode 100644 api/tests/unit_tests/core/workflow/nodes/http_request/test_http_request_executor.py rename api/tests/unit_tests/core/workflow/nodes/{ => http_request}/test_http_request_node.py (52%) diff --git a/api/core/workflow/nodes/http_request/executor.py b/api/core/workflow/nodes/http_request/executor.py index 6204fc2644..d90dfcc766 100644 --- a/api/core/workflow/nodes/http_request/executor.py +++ b/api/core/workflow/nodes/http_request/executor.py @@ -88,8 +88,10 @@ class Executor: self.url = self.variable_pool.convert_template(self.node_data.url).text def _init_params(self): - params = self.variable_pool.convert_template(self.node_data.params).text - self.params = _plain_text_to_dict(params) + params = _plain_text_to_dict(self.node_data.params) + for key in params: + params[key] = self.variable_pool.convert_template(params[key]).text + self.params = params def _init_headers(self): headers = self.variable_pool.convert_template(self.node_data.headers).text diff --git a/api/tests/unit_tests/core/workflow/nodes/http_request/test_http_request_executor.py b/api/tests/unit_tests/core/workflow/nodes/http_request/test_http_request_executor.py new file mode 100644 index 0000000000..12c469a81a --- /dev/null +++ b/api/tests/unit_tests/core/workflow/nodes/http_request/test_http_request_executor.py @@ -0,0 +1,198 @@ +from core.workflow.entities.variable_pool import VariablePool +from core.workflow.nodes.http_request import ( + BodyData, + HttpRequestNodeAuthorization, + HttpRequestNodeBody, + HttpRequestNodeData, +) +from core.workflow.nodes.http_request.entities import HttpRequestNodeTimeout +from core.workflow.nodes.http_request.executor import Executor + + +def test_executor_with_json_body_and_number_variable(): + # Prepare the variable pool + variable_pool = VariablePool( + system_variables={}, + user_inputs={}, + ) + variable_pool.add(["pre_node_id", "number"], 42) + + # Prepare the node data + node_data = HttpRequestNodeData( + title="Test JSON Body with Number Variable", + method="post", + url="https://api.example.com/data", + authorization=HttpRequestNodeAuthorization(type="no-auth"), + headers="Content-Type: application/json", + params="", + body=HttpRequestNodeBody( + type="json", + data=[ + BodyData( + key="", + type="text", + value='{"number": {{#pre_node_id.number#}}}', + ) + ], + ), + ) + + # Initialize the Executor + executor = Executor( + node_data=node_data, + timeout=HttpRequestNodeTimeout(connect=10, read=30, write=30), + variable_pool=variable_pool, + ) + + # Check the executor's data + assert executor.method == "post" + assert executor.url == "https://api.example.com/data" + assert executor.headers == {"Content-Type": "application/json"} + assert executor.params == {} + assert executor.json == {"number": 42} + assert executor.data is None + assert executor.files is None + assert executor.content is None + + # Check the raw request (to_log method) + raw_request = executor.to_log() + assert "POST /data HTTP/1.1" in raw_request + assert "Host: api.example.com" in raw_request + assert "Content-Type: application/json" in raw_request + assert '{"number": 42}' in raw_request + + +def test_executor_with_json_body_and_object_variable(): + # Prepare the variable pool + variable_pool = VariablePool( + system_variables={}, + user_inputs={}, + ) + variable_pool.add(["pre_node_id", "object"], {"name": "John Doe", "age": 30, "email": "john@example.com"}) + + # Prepare the node data + node_data = HttpRequestNodeData( + title="Test JSON Body with Object Variable", + method="post", + url="https://api.example.com/data", + authorization=HttpRequestNodeAuthorization(type="no-auth"), + headers="Content-Type: application/json", + params="", + body=HttpRequestNodeBody( + type="json", + data=[ + BodyData( + key="", + type="text", + value="{{#pre_node_id.object#}}", + ) + ], + ), + ) + + # Initialize the Executor + executor = Executor( + node_data=node_data, + timeout=HttpRequestNodeTimeout(connect=10, read=30, write=30), + variable_pool=variable_pool, + ) + + # Check the executor's data + assert executor.method == "post" + assert executor.url == "https://api.example.com/data" + assert executor.headers == {"Content-Type": "application/json"} + assert executor.params == {} + assert executor.json == {"name": "John Doe", "age": 30, "email": "john@example.com"} + assert executor.data is None + assert executor.files is None + assert executor.content is None + + # Check the raw request (to_log method) + raw_request = executor.to_log() + assert "POST /data HTTP/1.1" in raw_request + assert "Host: api.example.com" in raw_request + assert "Content-Type: application/json" in raw_request + assert '"name": "John Doe"' in raw_request + assert '"age": 30' in raw_request + assert '"email": "john@example.com"' in raw_request + + +def test_executor_with_json_body_and_nested_object_variable(): + # Prepare the variable pool + variable_pool = VariablePool( + system_variables={}, + user_inputs={}, + ) + variable_pool.add(["pre_node_id", "object"], {"name": "John Doe", "age": 30, "email": "john@example.com"}) + + # Prepare the node data + node_data = HttpRequestNodeData( + title="Test JSON Body with Nested Object Variable", + method="post", + url="https://api.example.com/data", + authorization=HttpRequestNodeAuthorization(type="no-auth"), + headers="Content-Type: application/json", + params="", + body=HttpRequestNodeBody( + type="json", + data=[ + BodyData( + key="", + type="text", + value='{"object": {{#pre_node_id.object#}}}', + ) + ], + ), + ) + + # Initialize the Executor + executor = Executor( + node_data=node_data, + timeout=HttpRequestNodeTimeout(connect=10, read=30, write=30), + variable_pool=variable_pool, + ) + + # Check the executor's data + assert executor.method == "post" + assert executor.url == "https://api.example.com/data" + assert executor.headers == {"Content-Type": "application/json"} + assert executor.params == {} + assert executor.json == {"object": {"name": "John Doe", "age": 30, "email": "john@example.com"}} + assert executor.data is None + assert executor.files is None + assert executor.content is None + + # Check the raw request (to_log method) + raw_request = executor.to_log() + assert "POST /data HTTP/1.1" in raw_request + assert "Host: api.example.com" in raw_request + assert "Content-Type: application/json" in raw_request + assert '"object": {' in raw_request + assert '"name": "John Doe"' in raw_request + assert '"age": 30' in raw_request + assert '"email": "john@example.com"' in raw_request + + +def test_extract_selectors_from_template_with_newline(): + variable_pool = VariablePool() + variable_pool.add(("node_id", "custom_query"), "line1\nline2") + node_data = HttpRequestNodeData( + title="Test JSON Body with Nested Object Variable", + method="post", + url="https://api.example.com/data", + authorization=HttpRequestNodeAuthorization(type="no-auth"), + headers="Content-Type: application/json", + params="test: {{#node_id.custom_query#}}", + body=HttpRequestNodeBody( + type="none", + data=[], + ), + ) + + executor = Executor( + node_data=node_data, + timeout=HttpRequestNodeTimeout(connect=10, read=30, write=30), + variable_pool=variable_pool, + ) + + assert executor.params == {"test": "line1\nline2"} diff --git a/api/tests/unit_tests/core/workflow/nodes/test_http_request_node.py b/api/tests/unit_tests/core/workflow/nodes/http_request/test_http_request_node.py similarity index 52% rename from api/tests/unit_tests/core/workflow/nodes/test_http_request_node.py rename to api/tests/unit_tests/core/workflow/nodes/http_request/test_http_request_node.py index 720037d05f..741a3a1894 100644 --- a/api/tests/unit_tests/core/workflow/nodes/test_http_request_node.py +++ b/api/tests/unit_tests/core/workflow/nodes/http_request/test_http_request_node.py @@ -1,5 +1,3 @@ -import json - import httpx from core.app.entities.app_invoke_entities import InvokeFrom @@ -16,8 +14,7 @@ from core.workflow.nodes.http_request import ( HttpRequestNodeBody, HttpRequestNodeData, ) -from core.workflow.nodes.http_request.entities import HttpRequestNodeTimeout -from core.workflow.nodes.http_request.executor import Executor, _plain_text_to_dict +from core.workflow.nodes.http_request.executor import _plain_text_to_dict from models.enums import UserFrom from models.workflow import WorkflowNodeExecutionStatus, WorkflowType @@ -203,167 +200,3 @@ def test_http_request_node_form_with_file(monkeypatch): assert result.status == WorkflowNodeExecutionStatus.SUCCEEDED assert result.outputs is not None assert result.outputs["body"] == "" - - -def test_executor_with_json_body_and_number_variable(): - # Prepare the variable pool - variable_pool = VariablePool( - system_variables={}, - user_inputs={}, - ) - variable_pool.add(["pre_node_id", "number"], 42) - - # Prepare the node data - node_data = HttpRequestNodeData( - title="Test JSON Body with Number Variable", - method="post", - url="https://api.example.com/data", - authorization=HttpRequestNodeAuthorization(type="no-auth"), - headers="Content-Type: application/json", - params="", - body=HttpRequestNodeBody( - type="json", - data=[ - BodyData( - key="", - type="text", - value='{"number": {{#pre_node_id.number#}}}', - ) - ], - ), - ) - - # Initialize the Executor - executor = Executor( - node_data=node_data, - timeout=HttpRequestNodeTimeout(connect=10, read=30, write=30), - variable_pool=variable_pool, - ) - - # Check the executor's data - assert executor.method == "post" - assert executor.url == "https://api.example.com/data" - assert executor.headers == {"Content-Type": "application/json"} - assert executor.params == {} - assert executor.json == {"number": 42} - assert executor.data is None - assert executor.files is None - assert executor.content is None - - # Check the raw request (to_log method) - raw_request = executor.to_log() - assert "POST /data HTTP/1.1" in raw_request - assert "Host: api.example.com" in raw_request - assert "Content-Type: application/json" in raw_request - assert '{"number": 42}' in raw_request - - -def test_executor_with_json_body_and_object_variable(): - # Prepare the variable pool - variable_pool = VariablePool( - system_variables={}, - user_inputs={}, - ) - variable_pool.add(["pre_node_id", "object"], {"name": "John Doe", "age": 30, "email": "john@example.com"}) - - # Prepare the node data - node_data = HttpRequestNodeData( - title="Test JSON Body with Object Variable", - method="post", - url="https://api.example.com/data", - authorization=HttpRequestNodeAuthorization(type="no-auth"), - headers="Content-Type: application/json", - params="", - body=HttpRequestNodeBody( - type="json", - data=[ - BodyData( - key="", - type="text", - value="{{#pre_node_id.object#}}", - ) - ], - ), - ) - - # Initialize the Executor - executor = Executor( - node_data=node_data, - timeout=HttpRequestNodeTimeout(connect=10, read=30, write=30), - variable_pool=variable_pool, - ) - - # Check the executor's data - assert executor.method == "post" - assert executor.url == "https://api.example.com/data" - assert executor.headers == {"Content-Type": "application/json"} - assert executor.params == {} - assert executor.json == {"name": "John Doe", "age": 30, "email": "john@example.com"} - assert executor.data is None - assert executor.files is None - assert executor.content is None - - # Check the raw request (to_log method) - raw_request = executor.to_log() - assert "POST /data HTTP/1.1" in raw_request - assert "Host: api.example.com" in raw_request - assert "Content-Type: application/json" in raw_request - assert '"name": "John Doe"' in raw_request - assert '"age": 30' in raw_request - assert '"email": "john@example.com"' in raw_request - - -def test_executor_with_json_body_and_nested_object_variable(): - # Prepare the variable pool - variable_pool = VariablePool( - system_variables={}, - user_inputs={}, - ) - variable_pool.add(["pre_node_id", "object"], {"name": "John Doe", "age": 30, "email": "john@example.com"}) - - # Prepare the node data - node_data = HttpRequestNodeData( - title="Test JSON Body with Nested Object Variable", - method="post", - url="https://api.example.com/data", - authorization=HttpRequestNodeAuthorization(type="no-auth"), - headers="Content-Type: application/json", - params="", - body=HttpRequestNodeBody( - type="json", - data=[ - BodyData( - key="", - type="text", - value='{"object": {{#pre_node_id.object#}}}', - ) - ], - ), - ) - - # Initialize the Executor - executor = Executor( - node_data=node_data, - timeout=HttpRequestNodeTimeout(connect=10, read=30, write=30), - variable_pool=variable_pool, - ) - - # Check the executor's data - assert executor.method == "post" - assert executor.url == "https://api.example.com/data" - assert executor.headers == {"Content-Type": "application/json"} - assert executor.params == {} - assert executor.json == {"object": {"name": "John Doe", "age": 30, "email": "john@example.com"}} - assert executor.data is None - assert executor.files is None - assert executor.content is None - - # Check the raw request (to_log method) - raw_request = executor.to_log() - assert "POST /data HTTP/1.1" in raw_request - assert "Host: api.example.com" in raw_request - assert "Content-Type: application/json" in raw_request - assert '"object": {' in raw_request - assert '"name": "John Doe"' in raw_request - assert '"age": 30' in raw_request - assert '"email": "john@example.com"' in raw_request From 3c89d45a2d415b911d88351c2dac98d4a0d69053 Mon Sep 17 00:00:00 2001 From: Novice <857526207@qq.com> Date: Tue, 5 Nov 2024 16:31:49 +0800 Subject: [PATCH 45/48] fix: iteration none output error (#10295) --- api/factories/variable_factory.py | 2 ++ api/tests/unit_tests/core/app/segments/test_factory.py | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/api/factories/variable_factory.py b/api/factories/variable_factory.py index d0c8c7e84f..0191102b90 100644 --- a/api/factories/variable_factory.py +++ b/api/factories/variable_factory.py @@ -91,6 +91,8 @@ def build_segment(value: Any, /) -> Segment: return ArrayObjectSegment(value=value) case SegmentType.FILE: return ArrayFileSegment(value=value) + case SegmentType.NONE: + return ArrayAnySegment(value=value) case _: raise ValueError(f"not supported value {value}") raise ValueError(f"not supported value {value}") diff --git a/api/tests/unit_tests/core/app/segments/test_factory.py b/api/tests/unit_tests/core/app/segments/test_factory.py index 72d277fad4..882a87239b 100644 --- a/api/tests/unit_tests/core/app/segments/test_factory.py +++ b/api/tests/unit_tests/core/app/segments/test_factory.py @@ -13,6 +13,7 @@ from core.variables import ( StringVariable, ) from core.variables.exc import VariableError +from core.variables.segments import ArrayAnySegment from factories import variable_factory @@ -156,3 +157,9 @@ def test_variable_cannot_large_than_200_kb(): "value": "a" * 1024 * 201, } ) + + +def test_array_none_variable(): + var = variable_factory.build_segment([None, None, None, None]) + assert isinstance(var, ArrayAnySegment) + assert var.value == [None, None, None, None] From e9d69f020a97de5b94f1b401df8f266e7bbacec9 Mon Sep 17 00:00:00 2001 From: Yeuoly Date: Tue, 5 Nov 2024 20:26:55 +0800 Subject: [PATCH 46/48] feat: cast files into correct type while invoking --- api/core/tools/entities/tool_entities.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/api/core/tools/entities/tool_entities.py b/api/core/tools/entities/tool_entities.py index 94a0c783e1..8b86ba7473 100644 --- a/api/core/tools/entities/tool_entities.py +++ b/api/core/tools/entities/tool_entities.py @@ -258,11 +258,18 @@ class ToolParameter(BaseModel): return float(value) else: return int(value) - case ( - ToolParameter.ToolParameterType.SYSTEM_FILES - | ToolParameter.ToolParameterType.FILE - | ToolParameter.ToolParameterType.FILES - ): + case ToolParameter.ToolParameterType.SYSTEM_FILES | ToolParameter.ToolParameterType.FILES: + if not isinstance(value, list): + return [value] + return value + case ToolParameter.ToolParameterType.FILE: + if isinstance(value, list): + if len(value) != 1: + raise ValueError( + "This parameter only accepts one file but got multiple files while invoking." + ) + else: + return value[0] return value case _: return str(value) From 6baa98f16605e3ff0f3c7684e2ea97a8df300509 Mon Sep 17 00:00:00 2001 From: Yeuoly Date: Wed, 6 Nov 2024 17:13:05 +0800 Subject: [PATCH 47/48] feat: support app-selector, model-selector and tool-selector as parameters --- api/core/entities/parameter_entities.py | 12 ++++++++++-- api/core/entities/provider_entities.py | 4 ++-- api/core/tools/entities/tool_entities.py | 20 ++++++++++++++++++-- 3 files changed, 30 insertions(+), 6 deletions(-) diff --git a/api/core/entities/parameter_entities.py b/api/core/entities/parameter_entities.py index 74d052ad11..990e84867e 100644 --- a/api/core/entities/parameter_entities.py +++ b/api/core/entities/parameter_entities.py @@ -12,7 +12,8 @@ class CommonParameterType(Enum): SYSTEM_FILES = "system-files" BOOLEAN = "boolean" APP_SELECTOR = "app-selector" - MODEL_CONFIG = "model-config" + TOOL_SELECTOR = "tool-selector" + MODEL_SELECTOR = "model-selector" class AppSelectorScope(Enum): @@ -22,7 +23,7 @@ class AppSelectorScope(Enum): COMPLETION = "completion" -class ModelConfigScope(Enum): +class ModelSelectorScope(Enum): LLM = "llm" TEXT_EMBEDDING = "text-embedding" RERANK = "rerank" @@ -30,3 +31,10 @@ class ModelConfigScope(Enum): SPEECH2TEXT = "speech2text" MODERATION = "moderation" VISION = "vision" + + +class ToolSelectorScope(Enum): + ALL = "all" + CUSTOM = "custom" + BUILTIN = "builtin" + WORKFLOW = "workflow" diff --git a/api/core/entities/provider_entities.py b/api/core/entities/provider_entities.py index 596a841e74..76fcff149e 100644 --- a/api/core/entities/provider_entities.py +++ b/api/core/entities/provider_entities.py @@ -3,7 +3,7 @@ from typing import Optional, Union from pydantic import BaseModel, ConfigDict, Field -from core.entities.parameter_entities import AppSelectorScope, CommonParameterType, ModelConfigScope +from core.entities.parameter_entities import AppSelectorScope, CommonParameterType, ModelSelectorScope from core.model_runtime.entities.model_entities import ModelType from core.tools.entities.common_entities import I18nObject @@ -168,7 +168,7 @@ class ProviderConfig(BasicProviderConfig): value: str = Field(..., description="The value of the option") label: I18nObject = Field(..., description="The label of the option") - scope: AppSelectorScope | ModelConfigScope | None = None + scope: AppSelectorScope | ModelSelectorScope | None = None required: bool = False default: Optional[Union[int, str]] = None options: Optional[list[Option]] = None diff --git a/api/core/tools/entities/tool_entities.py b/api/core/tools/entities/tool_entities.py index 8b86ba7473..26c9caaa43 100644 --- a/api/core/tools/entities/tool_entities.py +++ b/api/core/tools/entities/tool_entities.py @@ -5,7 +5,12 @@ from typing import Any, Optional, Union from pydantic import BaseModel, ConfigDict, Field, ValidationInfo, field_serializer, field_validator -from core.entities.parameter_entities import AppSelectorScope, CommonParameterType, ModelConfigScope +from core.entities.parameter_entities import ( + AppSelectorScope, + CommonParameterType, + ModelSelectorScope, + ToolSelectorScope, +) from core.entities.provider_entities import ProviderConfig from core.tools.entities.common_entities import I18nObject @@ -209,6 +214,9 @@ class ToolParameter(BaseModel): SECRET_INPUT = CommonParameterType.SECRET_INPUT.value FILE = CommonParameterType.FILE.value FILES = CommonParameterType.FILES.value + APP_SELECTOR = CommonParameterType.APP_SELECTOR.value + TOOL_SELECTOR = CommonParameterType.TOOL_SELECTOR.value + MODEL_SELECTOR = CommonParameterType.MODEL_SELECTOR.value # deprecated, should not use. SYSTEM_FILES = CommonParameterType.SYSTEM_FILES.value @@ -271,6 +279,14 @@ class ToolParameter(BaseModel): else: return value[0] return value + case ( + ToolParameter.ToolParameterType.TOOL_SELECTOR + | ToolParameter.ToolParameterType.MODEL_SELECTOR + | ToolParameter.ToolParameterType.APP_SELECTOR + ): + if not isinstance(value, dict): + raise ValueError("The selector must be a dictionary.") + return value case _: return str(value) @@ -287,7 +303,7 @@ class ToolParameter(BaseModel): human_description: Optional[I18nObject] = Field(default=None, description="The description presented to the user") placeholder: Optional[I18nObject] = Field(default=None, description="The placeholder presented to the user") type: ToolParameterType = Field(..., description="The type of the parameter") - scope: AppSelectorScope | ModelConfigScope | None = None + scope: AppSelectorScope | ModelSelectorScope | ToolSelectorScope | None = None form: ToolParameterForm = Field(..., description="The form of the parameter, schema/form/llm") llm_description: Optional[str] = None required: Optional[bool] = False From 28c9ec3f4f66a4387e30764c5dbfc12d1417681f Mon Sep 17 00:00:00 2001 From: Yeuoly Date: Wed, 6 Nov 2024 17:30:50 +0800 Subject: [PATCH 48/48] feat: support fetch tool provider info --- .../console/workspace/tool_providers.py | 14 ++++++++ api/core/entities/provider_entities.py | 12 +++++-- .../tools/builtin_tools_manage_service.py | 35 ++++++++++++++++++- 3 files changed, 57 insertions(+), 4 deletions(-) diff --git a/api/controllers/console/workspace/tool_providers.py b/api/controllers/console/workspace/tool_providers.py index 910b991de1..ad8dee09f9 100644 --- a/api/controllers/console/workspace/tool_providers.py +++ b/api/controllers/console/workspace/tool_providers.py @@ -61,6 +61,19 @@ class ToolBuiltinProviderListToolsApi(Resource): ) +class ToolBuiltinProviderInfoApi(Resource): + @setup_required + @login_required + @account_initialization_required + def get(self, provider): + user = current_user + + user_id = user.id + tenant_id = user.current_tenant_id + + return jsonable_encoder(BuiltinToolManageService.get_builtin_tool_provider_info(user_id, tenant_id, provider)) + + class ToolBuiltinProviderDeleteApi(Resource): @setup_required @login_required @@ -604,6 +617,7 @@ api.add_resource(ToolProviderListApi, "/workspaces/current/tool-providers") # builtin tool provider api.add_resource(ToolBuiltinProviderListToolsApi, "/workspaces/current/tool-provider/builtin//tools") +api.add_resource(ToolBuiltinProviderInfoApi, "/workspaces/current/tool-provider/builtin//info") api.add_resource(ToolBuiltinProviderDeleteApi, "/workspaces/current/tool-provider/builtin//delete") api.add_resource(ToolBuiltinProviderUpdateApi, "/workspaces/current/tool-provider/builtin//update") api.add_resource( diff --git a/api/core/entities/provider_entities.py b/api/core/entities/provider_entities.py index 76fcff149e..b5de05d2fb 100644 --- a/api/core/entities/provider_entities.py +++ b/api/core/entities/provider_entities.py @@ -3,7 +3,12 @@ from typing import Optional, Union from pydantic import BaseModel, ConfigDict, Field -from core.entities.parameter_entities import AppSelectorScope, CommonParameterType, ModelSelectorScope +from core.entities.parameter_entities import ( + AppSelectorScope, + CommonParameterType, + ModelSelectorScope, + ToolSelectorScope, +) from core.model_runtime.entities.model_entities import ModelType from core.tools.entities.common_entities import I18nObject @@ -140,7 +145,8 @@ class BasicProviderConfig(BaseModel): SELECT = CommonParameterType.SELECT.value BOOLEAN = CommonParameterType.BOOLEAN.value APP_SELECTOR = CommonParameterType.APP_SELECTOR.value - MODEL_CONFIG = CommonParameterType.MODEL_CONFIG.value + MODEL_SELECTOR = CommonParameterType.MODEL_SELECTOR.value + TOOL_SELECTOR = CommonParameterType.TOOL_SELECTOR.value @classmethod def value_of(cls, value: str) -> "ProviderConfig.Type": @@ -168,7 +174,7 @@ class ProviderConfig(BasicProviderConfig): value: str = Field(..., description="The value of the option") label: I18nObject = Field(..., description="The label of the option") - scope: AppSelectorScope | ModelSelectorScope | None = None + scope: AppSelectorScope | ModelSelectorScope | ToolSelectorScope | None = None required: bool = False default: Optional[Union[int, str]] = None options: Optional[list[Option]] = None diff --git a/api/services/tools/builtin_tools_manage_service.py b/api/services/tools/builtin_tools_manage_service.py index 35cc59b22e..3acae74db6 100644 --- a/api/services/tools/builtin_tools_manage_service.py +++ b/api/services/tools/builtin_tools_manage_service.py @@ -62,6 +62,37 @@ class BuiltinToolManageService: return result + @staticmethod + def get_builtin_tool_provider_info(user_id: str, tenant_id: str, provider: str): + """ + get builtin tool provider info + """ + provider_controller = ToolManager.get_builtin_provider(provider, tenant_id) + tool_provider_configurations = ProviderConfigEncrypter( + tenant_id=tenant_id, + config=[x.to_basic_provider_config() for x in provider_controller.get_credentials_schema()], + provider_type=provider_controller.provider_type.value, + provider_identity=provider_controller.entity.identity.name, + ) + # check if user has added the provider + builtin_provider = BuiltinToolManageService._fetch_builtin_provider(provider, tenant_id) + + credentials = {} + if builtin_provider is not None: + # get credentials + credentials = builtin_provider.credentials + credentials = tool_provider_configurations.decrypt(credentials) + + entity = ToolTransformService.builtin_provider_to_user_provider( + provider_controller=provider_controller, + db_provider=builtin_provider, + decrypt_credentials=True, + ) + + entity.original_credentials = {} + + return entity + @staticmethod def list_builtin_provider_credentials_schema(provider_name: str, tenant_id: str): """ @@ -255,6 +286,7 @@ class BuiltinToolManageService: @staticmethod def _fetch_builtin_provider(provider_name: str, tenant_id: str) -> BuiltinToolProvider | None: try: + full_provider_name = provider_name provider_id_entity = ToolProviderID(provider_name) provider_name = provider_id_entity.provider_name if provider_id_entity.organization != "langgenius": @@ -264,7 +296,8 @@ class BuiltinToolManageService: db.session.query(BuiltinToolProvider) .filter( BuiltinToolProvider.tenant_id == tenant_id, - (BuiltinToolProvider.provider == provider_name) | (BuiltinToolProvider.provider == provider_name), + (BuiltinToolProvider.provider == provider_name) + | (BuiltinToolProvider.provider == full_provider_name), ) .first() )