mirror of
https://git.mirrors.martin98.com/https://github.com/langgenius/dify.git
synced 2025-08-18 23:45:51 +08:00
Merge branch 'main' into fix/chore-fix
This commit is contained in:
commit
16b49ac436
@ -1,5 +1,6 @@
|
|||||||
from datetime import UTC, datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
|
from flask import request
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
from flask_restful import Resource, inputs, marshal_with, reqparse
|
from flask_restful import Resource, inputs, marshal_with, reqparse
|
||||||
from sqlalchemy import and_
|
from sqlalchemy import and_
|
||||||
@ -20,8 +21,17 @@ class InstalledAppsListApi(Resource):
|
|||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@marshal_with(installed_app_list_fields)
|
@marshal_with(installed_app_list_fields)
|
||||||
def get(self):
|
def get(self):
|
||||||
|
app_id = request.args.get("app_id", default=None, type=str)
|
||||||
current_tenant_id = current_user.current_tenant_id
|
current_tenant_id = current_user.current_tenant_id
|
||||||
installed_apps = db.session.query(InstalledApp).filter(InstalledApp.tenant_id == current_tenant_id).all()
|
|
||||||
|
if app_id:
|
||||||
|
installed_apps = (
|
||||||
|
db.session.query(InstalledApp)
|
||||||
|
.filter(and_(InstalledApp.tenant_id == current_tenant_id, InstalledApp.app_id == app_id))
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
installed_apps = db.session.query(InstalledApp).filter(InstalledApp.tenant_id == current_tenant_id).all()
|
||||||
|
|
||||||
current_user.role = TenantService.get_user_role(current_user, current_user.current_tenant)
|
current_user.role = TenantService.get_user_role(current_user, current_user.current_tenant)
|
||||||
installed_apps = [
|
installed_apps = [
|
||||||
|
@ -413,6 +413,7 @@ class ToolWorkflowProviderCreateApi(Resource):
|
|||||||
description=args["description"],
|
description=args["description"],
|
||||||
parameters=args["parameters"],
|
parameters=args["parameters"],
|
||||||
privacy_policy=args["privacy_policy"],
|
privacy_policy=args["privacy_policy"],
|
||||||
|
labels=args["labels"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ from datetime import datetime
|
|||||||
from enum import Enum, StrEnum
|
from enum import Enum, StrEnum
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
from pydantic import BaseModel, field_validator
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from core.model_runtime.entities.llm_entities import LLMResult, LLMResultChunk
|
from core.model_runtime.entities.llm_entities import LLMResult, LLMResultChunk
|
||||||
from core.workflow.entities.node_entities import NodeRunMetadataKey
|
from core.workflow.entities.node_entities import NodeRunMetadataKey
|
||||||
@ -113,18 +113,6 @@ class QueueIterationNextEvent(AppQueueEvent):
|
|||||||
output: Optional[Any] = None # output for the current iteration
|
output: Optional[Any] = None # output for the current iteration
|
||||||
duration: Optional[float] = None
|
duration: Optional[float] = None
|
||||||
|
|
||||||
@field_validator("output", mode="before")
|
|
||||||
@classmethod
|
|
||||||
def set_output(cls, v):
|
|
||||||
"""
|
|
||||||
Set output
|
|
||||||
"""
|
|
||||||
if v is None:
|
|
||||||
return None
|
|
||||||
if isinstance(v, int | float | str | bool | dict | list):
|
|
||||||
return v
|
|
||||||
raise ValueError("output must be a valid type")
|
|
||||||
|
|
||||||
|
|
||||||
class QueueIterationCompletedEvent(AppQueueEvent):
|
class QueueIterationCompletedEvent(AppQueueEvent):
|
||||||
"""
|
"""
|
||||||
|
@ -0,0 +1,38 @@
|
|||||||
|
model: gemini-exp-1206
|
||||||
|
label:
|
||||||
|
en_US: Gemini exp 1206
|
||||||
|
model_type: llm
|
||||||
|
features:
|
||||||
|
- agent-thought
|
||||||
|
- vision
|
||||||
|
- tool-call
|
||||||
|
- stream-tool-call
|
||||||
|
model_properties:
|
||||||
|
mode: chat
|
||||||
|
context_size: 2097152
|
||||||
|
parameter_rules:
|
||||||
|
- name: temperature
|
||||||
|
use_template: temperature
|
||||||
|
- name: top_p
|
||||||
|
use_template: top_p
|
||||||
|
- name: top_k
|
||||||
|
label:
|
||||||
|
zh_Hans: 取样数量
|
||||||
|
en_US: Top k
|
||||||
|
type: int
|
||||||
|
help:
|
||||||
|
zh_Hans: 仅从每个后续标记的前 K 个选项中采样。
|
||||||
|
en_US: Only sample from the top K options for each subsequent token.
|
||||||
|
required: false
|
||||||
|
- name: max_output_tokens
|
||||||
|
use_template: max_tokens
|
||||||
|
default: 8192
|
||||||
|
min: 1
|
||||||
|
max: 8192
|
||||||
|
- name: json_schema
|
||||||
|
use_template: json_schema
|
||||||
|
pricing:
|
||||||
|
input: '0.00'
|
||||||
|
output: '0.00'
|
||||||
|
unit: '0.000001'
|
||||||
|
currency: USD
|
@ -162,7 +162,7 @@ class TidbService:
|
|||||||
clusters = []
|
clusters = []
|
||||||
tidb_serverless_list_map = {item.cluster_id: item for item in tidb_serverless_list}
|
tidb_serverless_list_map = {item.cluster_id: item for item in tidb_serverless_list}
|
||||||
cluster_ids = [item.cluster_id for item in tidb_serverless_list]
|
cluster_ids = [item.cluster_id for item in tidb_serverless_list]
|
||||||
params = {"clusterIds": cluster_ids, "view": "FULL"}
|
params = {"clusterIds": cluster_ids, "view": "BASIC"}
|
||||||
response = requests.get(
|
response = requests.get(
|
||||||
f"{api_url}/clusters:batchGet", params=params, auth=HTTPDigestAuth(public_key, private_key)
|
f"{api_url}/clusters:batchGet", params=params, auth=HTTPDigestAuth(public_key, private_key)
|
||||||
)
|
)
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
from collections.abc import Mapping
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
@ -140,8 +141,8 @@ class BaseIterationEvent(GraphEngineEvent):
|
|||||||
|
|
||||||
class IterationRunStartedEvent(BaseIterationEvent):
|
class IterationRunStartedEvent(BaseIterationEvent):
|
||||||
start_at: datetime = Field(..., description="start at")
|
start_at: datetime = Field(..., description="start at")
|
||||||
inputs: Optional[dict[str, Any]] = None
|
inputs: Optional[Mapping[str, Any]] = None
|
||||||
metadata: Optional[dict[str, Any]] = None
|
metadata: Optional[Mapping[str, Any]] = None
|
||||||
predecessor_node_id: Optional[str] = None
|
predecessor_node_id: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
@ -153,18 +154,18 @@ class IterationRunNextEvent(BaseIterationEvent):
|
|||||||
|
|
||||||
class IterationRunSucceededEvent(BaseIterationEvent):
|
class IterationRunSucceededEvent(BaseIterationEvent):
|
||||||
start_at: datetime = Field(..., description="start at")
|
start_at: datetime = Field(..., description="start at")
|
||||||
inputs: Optional[dict[str, Any]] = None
|
inputs: Optional[Mapping[str, Any]] = None
|
||||||
outputs: Optional[dict[str, Any]] = None
|
outputs: Optional[Mapping[str, Any]] = None
|
||||||
metadata: Optional[dict[str, Any]] = None
|
metadata: Optional[Mapping[str, Any]] = None
|
||||||
steps: int = 0
|
steps: int = 0
|
||||||
iteration_duration_map: Optional[dict[str, float]] = None
|
iteration_duration_map: Optional[dict[str, float]] = None
|
||||||
|
|
||||||
|
|
||||||
class IterationRunFailedEvent(BaseIterationEvent):
|
class IterationRunFailedEvent(BaseIterationEvent):
|
||||||
start_at: datetime = Field(..., description="start at")
|
start_at: datetime = Field(..., description="start at")
|
||||||
inputs: Optional[dict[str, Any]] = None
|
inputs: Optional[Mapping[str, Any]] = None
|
||||||
outputs: Optional[dict[str, Any]] = None
|
outputs: Optional[Mapping[str, Any]] = None
|
||||||
metadata: Optional[dict[str, Any]] = None
|
metadata: Optional[Mapping[str, Any]] = None
|
||||||
steps: int = 0
|
steps: int = 0
|
||||||
error: str = Field(..., description="failed reason")
|
error: str = Field(..., description="failed reason")
|
||||||
|
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import csv
|
import csv
|
||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
|
||||||
import docx
|
import docx
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
@ -264,14 +266,20 @@ def _extract_text_from_ppt(file_content: bytes) -> str:
|
|||||||
|
|
||||||
def _extract_text_from_pptx(file_content: bytes) -> str:
|
def _extract_text_from_pptx(file_content: bytes) -> str:
|
||||||
try:
|
try:
|
||||||
with io.BytesIO(file_content) as file:
|
if dify_config.UNSTRUCTURED_API_URL and dify_config.UNSTRUCTURED_API_KEY:
|
||||||
if dify_config.UNSTRUCTURED_API_URL and dify_config.UNSTRUCTURED_API_KEY:
|
with tempfile.NamedTemporaryFile(suffix=".pptx", delete=False) as temp_file:
|
||||||
elements = partition_via_api(
|
temp_file.write(file_content)
|
||||||
file=file,
|
temp_file.flush()
|
||||||
api_url=dify_config.UNSTRUCTURED_API_URL,
|
with open(temp_file.name, "rb") as file:
|
||||||
api_key=dify_config.UNSTRUCTURED_API_KEY,
|
elements = partition_via_api(
|
||||||
)
|
file=file,
|
||||||
else:
|
metadata_filename=temp_file.name,
|
||||||
|
api_url=dify_config.UNSTRUCTURED_API_URL,
|
||||||
|
api_key=dify_config.UNSTRUCTURED_API_KEY,
|
||||||
|
)
|
||||||
|
os.unlink(temp_file.name)
|
||||||
|
else:
|
||||||
|
with io.BytesIO(file_content) as file:
|
||||||
elements = partition_pptx(file=file)
|
elements = partition_pptx(file=file)
|
||||||
return "\n".join([getattr(element, "text", "") for element in elements])
|
return "\n".join([getattr(element, "text", "") for element in elements])
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
@ -10,7 +10,7 @@ from typing import TYPE_CHECKING, Any, Optional, cast
|
|||||||
from flask import Flask, current_app
|
from flask import Flask, current_app
|
||||||
|
|
||||||
from configs import dify_config
|
from configs import dify_config
|
||||||
from core.model_runtime.utils.encoders import jsonable_encoder
|
from core.variables import IntegerVariable
|
||||||
from core.workflow.entities.node_entities import (
|
from core.workflow.entities.node_entities import (
|
||||||
NodeRunMetadataKey,
|
NodeRunMetadataKey,
|
||||||
NodeRunResult,
|
NodeRunResult,
|
||||||
@ -156,33 +156,35 @@ class IterationNode(BaseNode[IterationNodeData]):
|
|||||||
iteration_node_data=self.node_data,
|
iteration_node_data=self.node_data,
|
||||||
index=0,
|
index=0,
|
||||||
pre_iteration_output=None,
|
pre_iteration_output=None,
|
||||||
|
duration=None,
|
||||||
)
|
)
|
||||||
iter_run_map: dict[str, float] = {}
|
iter_run_map: dict[str, float] = {}
|
||||||
outputs: list[Any] = [None] * len(iterator_list_value)
|
outputs: list[Any] = [None] * len(iterator_list_value)
|
||||||
try:
|
try:
|
||||||
if self.node_data.is_parallel:
|
if self.node_data.is_parallel:
|
||||||
futures: list[Future] = []
|
futures: list[Future] = []
|
||||||
q = Queue()
|
q: Queue = Queue()
|
||||||
thread_pool = GraphEngineThreadPool(max_workers=self.node_data.parallel_nums, max_submit_count=100)
|
thread_pool = GraphEngineThreadPool(max_workers=self.node_data.parallel_nums, max_submit_count=100)
|
||||||
for index, item in enumerate(iterator_list_value):
|
for index, item in enumerate(iterator_list_value):
|
||||||
future: Future = thread_pool.submit(
|
future: Future = thread_pool.submit(
|
||||||
self._run_single_iter_parallel,
|
self._run_single_iter_parallel,
|
||||||
current_app._get_current_object(), # type: ignore
|
flask_app=current_app._get_current_object(), # type: ignore
|
||||||
contextvars.copy_context(),
|
q=q,
|
||||||
q,
|
context=contextvars.copy_context(),
|
||||||
iterator_list_value,
|
iterator_list_value=iterator_list_value,
|
||||||
inputs,
|
inputs=inputs,
|
||||||
outputs,
|
outputs=outputs,
|
||||||
start_at,
|
start_at=start_at,
|
||||||
graph_engine,
|
graph_engine=graph_engine,
|
||||||
iteration_graph,
|
iteration_graph=iteration_graph,
|
||||||
index,
|
index=index,
|
||||||
item,
|
item=item,
|
||||||
iter_run_map,
|
iter_run_map=iter_run_map,
|
||||||
)
|
)
|
||||||
future.add_done_callback(thread_pool.task_done_callback)
|
future.add_done_callback(thread_pool.task_done_callback)
|
||||||
futures.append(future)
|
futures.append(future)
|
||||||
succeeded_count = 0
|
succeeded_count = 0
|
||||||
|
empty_count = 0
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
event = q.get(timeout=1)
|
event = q.get(timeout=1)
|
||||||
@ -210,17 +212,22 @@ class IterationNode(BaseNode[IterationNodeData]):
|
|||||||
else:
|
else:
|
||||||
for _ in range(len(iterator_list_value)):
|
for _ in range(len(iterator_list_value)):
|
||||||
yield from self._run_single_iter(
|
yield from self._run_single_iter(
|
||||||
iterator_list_value,
|
iterator_list_value=iterator_list_value,
|
||||||
variable_pool,
|
variable_pool=variable_pool,
|
||||||
inputs,
|
inputs=inputs,
|
||||||
outputs,
|
outputs=outputs,
|
||||||
start_at,
|
start_at=start_at,
|
||||||
graph_engine,
|
graph_engine=graph_engine,
|
||||||
iteration_graph,
|
iteration_graph=iteration_graph,
|
||||||
iter_run_map,
|
iter_run_map=iter_run_map,
|
||||||
)
|
)
|
||||||
if self.node_data.error_handle_mode == ErrorHandleMode.REMOVE_ABNORMAL_OUTPUT:
|
if self.node_data.error_handle_mode == ErrorHandleMode.REMOVE_ABNORMAL_OUTPUT:
|
||||||
outputs = [output for output in outputs if output is not None]
|
outputs = [output for output in outputs if output is not None]
|
||||||
|
|
||||||
|
# Flatten the list of lists
|
||||||
|
if isinstance(outputs, list) and all(isinstance(output, list) for output in outputs):
|
||||||
|
outputs = [item for sublist in outputs for item in sublist]
|
||||||
|
|
||||||
yield IterationRunSucceededEvent(
|
yield IterationRunSucceededEvent(
|
||||||
iteration_id=self.id,
|
iteration_id=self.id,
|
||||||
iteration_node_id=self.node_id,
|
iteration_node_id=self.node_id,
|
||||||
@ -228,7 +235,7 @@ class IterationNode(BaseNode[IterationNodeData]):
|
|||||||
iteration_node_data=self.node_data,
|
iteration_node_data=self.node_data,
|
||||||
start_at=start_at,
|
start_at=start_at,
|
||||||
inputs=inputs,
|
inputs=inputs,
|
||||||
outputs={"output": jsonable_encoder(outputs)},
|
outputs={"output": outputs},
|
||||||
steps=len(iterator_list_value),
|
steps=len(iterator_list_value),
|
||||||
metadata={"total_tokens": graph_engine.graph_runtime_state.total_tokens},
|
metadata={"total_tokens": graph_engine.graph_runtime_state.total_tokens},
|
||||||
)
|
)
|
||||||
@ -236,8 +243,11 @@ class IterationNode(BaseNode[IterationNodeData]):
|
|||||||
yield RunCompletedEvent(
|
yield RunCompletedEvent(
|
||||||
run_result=NodeRunResult(
|
run_result=NodeRunResult(
|
||||||
status=WorkflowNodeExecutionStatus.SUCCEEDED,
|
status=WorkflowNodeExecutionStatus.SUCCEEDED,
|
||||||
outputs={"output": jsonable_encoder(outputs)},
|
outputs={"output": outputs},
|
||||||
metadata={NodeRunMetadataKey.ITERATION_DURATION_MAP: iter_run_map},
|
metadata={
|
||||||
|
NodeRunMetadataKey.ITERATION_DURATION_MAP: iter_run_map,
|
||||||
|
NodeRunMetadataKey.TOTAL_TOKENS: graph_engine.graph_runtime_state.total_tokens,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
except IterationNodeError as e:
|
except IterationNodeError as e:
|
||||||
@ -250,7 +260,7 @@ class IterationNode(BaseNode[IterationNodeData]):
|
|||||||
iteration_node_data=self.node_data,
|
iteration_node_data=self.node_data,
|
||||||
start_at=start_at,
|
start_at=start_at,
|
||||||
inputs=inputs,
|
inputs=inputs,
|
||||||
outputs={"output": jsonable_encoder(outputs)},
|
outputs={"output": outputs},
|
||||||
steps=len(iterator_list_value),
|
steps=len(iterator_list_value),
|
||||||
metadata={"total_tokens": graph_engine.graph_runtime_state.total_tokens},
|
metadata={"total_tokens": graph_engine.graph_runtime_state.total_tokens},
|
||||||
error=str(e),
|
error=str(e),
|
||||||
@ -282,7 +292,7 @@ class IterationNode(BaseNode[IterationNodeData]):
|
|||||||
:param node_data: node data
|
:param node_data: node data
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
variable_mapping = {
|
variable_mapping: dict[str, Sequence[str]] = {
|
||||||
f"{node_id}.input_selector": node_data.iterator_selector,
|
f"{node_id}.input_selector": node_data.iterator_selector,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -310,7 +320,7 @@ class IterationNode(BaseNode[IterationNodeData]):
|
|||||||
sub_node_variable_mapping = node_cls.extract_variable_selector_to_variable_mapping(
|
sub_node_variable_mapping = node_cls.extract_variable_selector_to_variable_mapping(
|
||||||
graph_config=graph_config, config=sub_node_config
|
graph_config=graph_config, config=sub_node_config
|
||||||
)
|
)
|
||||||
sub_node_variable_mapping = cast(dict[str, list[str]], sub_node_variable_mapping)
|
sub_node_variable_mapping = cast(dict[str, Sequence[str]], sub_node_variable_mapping)
|
||||||
except NotImplementedError:
|
except NotImplementedError:
|
||||||
sub_node_variable_mapping = {}
|
sub_node_variable_mapping = {}
|
||||||
|
|
||||||
@ -331,8 +341,12 @@ class IterationNode(BaseNode[IterationNodeData]):
|
|||||||
return variable_mapping
|
return variable_mapping
|
||||||
|
|
||||||
def _handle_event_metadata(
|
def _handle_event_metadata(
|
||||||
self, event: BaseNodeEvent, iter_run_index: str, parallel_mode_run_id: str
|
self,
|
||||||
) -> NodeRunStartedEvent | BaseNodeEvent:
|
*,
|
||||||
|
event: BaseNodeEvent | InNodeEvent,
|
||||||
|
iter_run_index: int,
|
||||||
|
parallel_mode_run_id: str | None,
|
||||||
|
) -> NodeRunStartedEvent | BaseNodeEvent | InNodeEvent:
|
||||||
"""
|
"""
|
||||||
add iteration metadata to event.
|
add iteration metadata to event.
|
||||||
"""
|
"""
|
||||||
@ -357,9 +371,10 @@ class IterationNode(BaseNode[IterationNodeData]):
|
|||||||
|
|
||||||
def _run_single_iter(
|
def _run_single_iter(
|
||||||
self,
|
self,
|
||||||
iterator_list_value: list[str],
|
*,
|
||||||
|
iterator_list_value: Sequence[str],
|
||||||
variable_pool: VariablePool,
|
variable_pool: VariablePool,
|
||||||
inputs: dict[str, list],
|
inputs: Mapping[str, list],
|
||||||
outputs: list,
|
outputs: list,
|
||||||
start_at: datetime,
|
start_at: datetime,
|
||||||
graph_engine: "GraphEngine",
|
graph_engine: "GraphEngine",
|
||||||
@ -375,15 +390,12 @@ class IterationNode(BaseNode[IterationNodeData]):
|
|||||||
try:
|
try:
|
||||||
rst = graph_engine.run()
|
rst = graph_engine.run()
|
||||||
# get current iteration index
|
# get current iteration index
|
||||||
variable = variable_pool.get([self.node_id, "index"])
|
index_variable = variable_pool.get([self.node_id, "index"])
|
||||||
if variable is None:
|
if not isinstance(index_variable, IntegerVariable):
|
||||||
raise IterationIndexNotFoundError(f"iteration {self.node_id} current index not found")
|
raise IterationIndexNotFoundError(f"iteration {self.node_id} current index not found")
|
||||||
current_index = variable.value
|
current_index = index_variable.value
|
||||||
iteration_run_id = parallel_mode_run_id if parallel_mode_run_id is not None else f"{current_index}"
|
iteration_run_id = parallel_mode_run_id if parallel_mode_run_id is not None else f"{current_index}"
|
||||||
next_index = int(current_index) + 1
|
next_index = int(current_index) + 1
|
||||||
|
|
||||||
if current_index is None:
|
|
||||||
raise IterationIndexNotFoundError(f"iteration {self.node_id} current index not found")
|
|
||||||
for event in rst:
|
for event in rst:
|
||||||
if isinstance(event, (BaseNodeEvent | BaseParallelBranchEvent)) and not event.in_iteration_id:
|
if isinstance(event, (BaseNodeEvent | BaseParallelBranchEvent)) and not event.in_iteration_id:
|
||||||
event.in_iteration_id = self.node_id
|
event.in_iteration_id = self.node_id
|
||||||
@ -396,7 +408,9 @@ class IterationNode(BaseNode[IterationNodeData]):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
if isinstance(event, NodeRunSucceededEvent):
|
if isinstance(event, NodeRunSucceededEvent):
|
||||||
yield self._handle_event_metadata(event, current_index, parallel_mode_run_id)
|
yield self._handle_event_metadata(
|
||||||
|
event=event, iter_run_index=current_index, parallel_mode_run_id=parallel_mode_run_id
|
||||||
|
)
|
||||||
elif isinstance(event, BaseGraphEvent):
|
elif isinstance(event, BaseGraphEvent):
|
||||||
if isinstance(event, GraphRunFailedEvent):
|
if isinstance(event, GraphRunFailedEvent):
|
||||||
# iteration run failed
|
# iteration run failed
|
||||||
@ -409,7 +423,7 @@ class IterationNode(BaseNode[IterationNodeData]):
|
|||||||
parallel_mode_run_id=parallel_mode_run_id,
|
parallel_mode_run_id=parallel_mode_run_id,
|
||||||
start_at=start_at,
|
start_at=start_at,
|
||||||
inputs=inputs,
|
inputs=inputs,
|
||||||
outputs={"output": jsonable_encoder(outputs)},
|
outputs={"output": outputs},
|
||||||
steps=len(iterator_list_value),
|
steps=len(iterator_list_value),
|
||||||
metadata={"total_tokens": graph_engine.graph_runtime_state.total_tokens},
|
metadata={"total_tokens": graph_engine.graph_runtime_state.total_tokens},
|
||||||
error=event.error,
|
error=event.error,
|
||||||
@ -422,7 +436,7 @@ class IterationNode(BaseNode[IterationNodeData]):
|
|||||||
iteration_node_data=self.node_data,
|
iteration_node_data=self.node_data,
|
||||||
start_at=start_at,
|
start_at=start_at,
|
||||||
inputs=inputs,
|
inputs=inputs,
|
||||||
outputs={"output": jsonable_encoder(outputs)},
|
outputs={"output": outputs},
|
||||||
steps=len(iterator_list_value),
|
steps=len(iterator_list_value),
|
||||||
metadata={"total_tokens": graph_engine.graph_runtime_state.total_tokens},
|
metadata={"total_tokens": graph_engine.graph_runtime_state.total_tokens},
|
||||||
error=event.error,
|
error=event.error,
|
||||||
@ -434,9 +448,11 @@ class IterationNode(BaseNode[IterationNodeData]):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
else:
|
elif isinstance(event, InNodeEvent):
|
||||||
event = cast(InNodeEvent, event)
|
# event = cast(InNodeEvent, event)
|
||||||
metadata_event = self._handle_event_metadata(event, current_index, parallel_mode_run_id)
|
metadata_event = self._handle_event_metadata(
|
||||||
|
event=event, iter_run_index=current_index, parallel_mode_run_id=parallel_mode_run_id
|
||||||
|
)
|
||||||
if isinstance(event, NodeRunFailedEvent):
|
if isinstance(event, NodeRunFailedEvent):
|
||||||
if self.node_data.error_handle_mode == ErrorHandleMode.CONTINUE_ON_ERROR:
|
if self.node_data.error_handle_mode == ErrorHandleMode.CONTINUE_ON_ERROR:
|
||||||
yield NodeInIterationFailedEvent(
|
yield NodeInIterationFailedEvent(
|
||||||
@ -518,7 +534,7 @@ class IterationNode(BaseNode[IterationNodeData]):
|
|||||||
iteration_node_data=self.node_data,
|
iteration_node_data=self.node_data,
|
||||||
index=next_index,
|
index=next_index,
|
||||||
parallel_mode_run_id=parallel_mode_run_id,
|
parallel_mode_run_id=parallel_mode_run_id,
|
||||||
pre_iteration_output=jsonable_encoder(current_iteration_output) if current_iteration_output else None,
|
pre_iteration_output=current_iteration_output or None,
|
||||||
duration=duration,
|
duration=duration,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -545,11 +561,12 @@ class IterationNode(BaseNode[IterationNodeData]):
|
|||||||
|
|
||||||
def _run_single_iter_parallel(
|
def _run_single_iter_parallel(
|
||||||
self,
|
self,
|
||||||
|
*,
|
||||||
flask_app: Flask,
|
flask_app: Flask,
|
||||||
context: contextvars.Context,
|
context: contextvars.Context,
|
||||||
q: Queue,
|
q: Queue,
|
||||||
iterator_list_value: list[str],
|
iterator_list_value: Sequence[str],
|
||||||
inputs: dict[str, list],
|
inputs: Mapping[str, list],
|
||||||
outputs: list,
|
outputs: list,
|
||||||
start_at: datetime,
|
start_at: datetime,
|
||||||
graph_engine: "GraphEngine",
|
graph_engine: "GraphEngine",
|
||||||
@ -557,7 +574,7 @@ class IterationNode(BaseNode[IterationNodeData]):
|
|||||||
index: int,
|
index: int,
|
||||||
item: Any,
|
item: Any,
|
||||||
iter_run_map: dict[str, float],
|
iter_run_map: dict[str, float],
|
||||||
) -> Generator[NodeEvent | InNodeEvent, None, None]:
|
):
|
||||||
"""
|
"""
|
||||||
run single iteration in parallel mode
|
run single iteration in parallel mode
|
||||||
"""
|
"""
|
||||||
|
@ -253,6 +253,8 @@ class NotionOAuth(OAuthDataSource):
|
|||||||
}
|
}
|
||||||
response = requests.get(url=f"{self._NOTION_BLOCK_SEARCH}/{block_id}", headers=headers)
|
response = requests.get(url=f"{self._NOTION_BLOCK_SEARCH}/{block_id}", headers=headers)
|
||||||
response_json = response.json()
|
response_json = response.json()
|
||||||
|
if response.status_code != 200:
|
||||||
|
raise ValueError(f"Error fetching block parent page ID: {response_json.message}")
|
||||||
parent = response_json["parent"]
|
parent = response_json["parent"]
|
||||||
parent_type = parent["type"]
|
parent_type = parent["type"]
|
||||||
if parent_type == "block_id":
|
if parent_type == "block_id":
|
||||||
|
@ -82,6 +82,10 @@ class WorkflowToolManageService:
|
|||||||
db.session.add(workflow_tool_provider)
|
db.session.add(workflow_tool_provider)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
if labels is not None:
|
||||||
|
ToolLabelManager.update_tool_labels(
|
||||||
|
ToolTransformService.workflow_provider_to_controller(workflow_tool_provider), labels
|
||||||
|
)
|
||||||
return {"result": "success"}
|
return {"result": "success"}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -37,7 +37,11 @@ def test_dify_config_undefined_entry(example_env_file):
|
|||||||
assert config["LOG_LEVEL"] == "INFO"
|
assert config["LOG_LEVEL"] == "INFO"
|
||||||
|
|
||||||
|
|
||||||
|
# NOTE: If there is a `.env` file in your Workspace, this test might not succeed as expected.
|
||||||
|
# This is due to `pymilvus` loading all the variables from the `.env` file into `os.environ`.
|
||||||
def test_dify_config(example_env_file):
|
def test_dify_config(example_env_file):
|
||||||
|
# clear system environment variables
|
||||||
|
os.environ.clear()
|
||||||
# load dotenv file with pydantic-settings
|
# load dotenv file with pydantic-settings
|
||||||
config = DifyConfig(_env_file=example_env_file)
|
config = DifyConfig(_env_file=example_env_file)
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ import s from './style.module.css'
|
|||||||
import cn from '@/utils/classnames'
|
import cn from '@/utils/classnames'
|
||||||
import type { App } from '@/types/app'
|
import type { App } from '@/types/app'
|
||||||
import Confirm from '@/app/components/base/confirm'
|
import Confirm from '@/app/components/base/confirm'
|
||||||
import { ToastContext } from '@/app/components/base/toast'
|
import Toast, { ToastContext } from '@/app/components/base/toast'
|
||||||
import { copyApp, deleteApp, exportAppConfig, updateAppInfo } from '@/service/apps'
|
import { copyApp, deleteApp, exportAppConfig, updateAppInfo } from '@/service/apps'
|
||||||
import DuplicateAppModal from '@/app/components/app/duplicate-modal'
|
import DuplicateAppModal from '@/app/components/app/duplicate-modal'
|
||||||
import type { DuplicateAppModalProps } from '@/app/components/app/duplicate-modal'
|
import type { DuplicateAppModalProps } from '@/app/components/app/duplicate-modal'
|
||||||
@ -31,6 +31,7 @@ import TagSelector from '@/app/components/base/tag-management/selector'
|
|||||||
import type { EnvironmentVariable } from '@/app/components/workflow/types'
|
import type { EnvironmentVariable } from '@/app/components/workflow/types'
|
||||||
import DSLExportConfirmModal from '@/app/components/workflow/dsl-export-confirm-modal'
|
import DSLExportConfirmModal from '@/app/components/workflow/dsl-export-confirm-modal'
|
||||||
import { fetchWorkflowDraft } from '@/service/workflow'
|
import { fetchWorkflowDraft } from '@/service/workflow'
|
||||||
|
import { fetchInstalledAppList } from '@/service/explore'
|
||||||
|
|
||||||
export type AppCardProps = {
|
export type AppCardProps = {
|
||||||
app: App
|
app: App
|
||||||
@ -209,6 +210,21 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
|
|||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
setShowConfirmDelete(true)
|
setShowConfirmDelete(true)
|
||||||
}
|
}
|
||||||
|
const onClickInstalledApp = async (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
props.onClick?.()
|
||||||
|
e.preventDefault()
|
||||||
|
try {
|
||||||
|
const { installed_apps }: any = await fetchInstalledAppList(app.id) || {}
|
||||||
|
if (installed_apps?.length > 0)
|
||||||
|
window.open(`/explore/installed/${installed_apps[0].id}`, '_blank')
|
||||||
|
else
|
||||||
|
throw new Error('No app found in Explore')
|
||||||
|
}
|
||||||
|
catch (e: any) {
|
||||||
|
Toast.notify({ type: 'error', message: `${e.message || e}` })
|
||||||
|
}
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<div className="relative w-full py-1" onMouseLeave={onMouseLeave}>
|
<div className="relative w-full py-1" onMouseLeave={onMouseLeave}>
|
||||||
<button className={s.actionItem} onClick={onClickSettings}>
|
<button className={s.actionItem} onClick={onClickSettings}>
|
||||||
@ -233,6 +249,10 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<Divider className="!my-1" />
|
<Divider className="!my-1" />
|
||||||
|
<button className={s.actionItem} onClick={onClickInstalledApp}>
|
||||||
|
<span className={s.actionName}>{t('app.openInExplore')}</span>
|
||||||
|
</button>
|
||||||
|
<Divider className="!my-1" />
|
||||||
<div
|
<div
|
||||||
className={cn(s.actionItem, s.deleteActionItem, 'group')}
|
className={cn(s.actionItem, s.deleteActionItem, 'group')}
|
||||||
onClick={onClickDelete}
|
onClick={onClickDelete}
|
||||||
@ -353,10 +373,10 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
|
|||||||
}
|
}
|
||||||
popupClassName={
|
popupClassName={
|
||||||
(app.mode === 'completion' || app.mode === 'chat')
|
(app.mode === 'completion' || app.mode === 'chat')
|
||||||
? '!w-[238px] translate-x-[-110px]'
|
? '!w-[256px] translate-x-[-224px]'
|
||||||
: ''
|
: '!w-[160px] translate-x-[-128px]'
|
||||||
}
|
}
|
||||||
className={'!w-[128px] h-fit !z-20'}
|
className={'h-fit !z-20'}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
@ -5,7 +5,8 @@ import {
|
|||||||
} from 'react'
|
} from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { RiArrowDownSLine } from '@remixicon/react'
|
import { RiArrowDownSLine, RiPlanetLine } from '@remixicon/react'
|
||||||
|
import Toast from '../../base/toast'
|
||||||
import type { ModelAndParameter } from '../configuration/debug/types'
|
import type { ModelAndParameter } from '../configuration/debug/types'
|
||||||
import SuggestedAction from './suggested-action'
|
import SuggestedAction from './suggested-action'
|
||||||
import PublishWithMultipleModel from './publish-with-multiple-model'
|
import PublishWithMultipleModel from './publish-with-multiple-model'
|
||||||
@ -15,6 +16,7 @@ import {
|
|||||||
PortalToFollowElemContent,
|
PortalToFollowElemContent,
|
||||||
PortalToFollowElemTrigger,
|
PortalToFollowElemTrigger,
|
||||||
} from '@/app/components/base/portal-to-follow-elem'
|
} from '@/app/components/base/portal-to-follow-elem'
|
||||||
|
import { fetchInstalledAppList } from '@/service/explore'
|
||||||
import EmbeddedModal from '@/app/components/app/overview/embedded'
|
import EmbeddedModal from '@/app/components/app/overview/embedded'
|
||||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||||
import { useGetLanguage } from '@/context/i18n'
|
import { useGetLanguage } from '@/context/i18n'
|
||||||
@ -105,6 +107,19 @@ const AppPublisher = ({
|
|||||||
setPublished(false)
|
setPublished(false)
|
||||||
}, [disabled, onToggle, open])
|
}, [disabled, onToggle, open])
|
||||||
|
|
||||||
|
const handleOpenInExplore = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const { installed_apps }: any = await fetchInstalledAppList(appDetail?.id) || {}
|
||||||
|
if (installed_apps?.length > 0)
|
||||||
|
window.open(`/explore/installed/${installed_apps[0].id}`, '_blank')
|
||||||
|
else
|
||||||
|
throw new Error('No app found in Explore')
|
||||||
|
}
|
||||||
|
catch (e: any) {
|
||||||
|
Toast.notify({ type: 'error', message: `${e.message || e}` })
|
||||||
|
}
|
||||||
|
}, [appDetail?.id])
|
||||||
|
|
||||||
const [embeddingModalOpen, setEmbeddingModalOpen] = useState(false)
|
const [embeddingModalOpen, setEmbeddingModalOpen] = useState(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -205,6 +220,15 @@ const AppPublisher = ({
|
|||||||
{t('workflow.common.embedIntoSite')}
|
{t('workflow.common.embedIntoSite')}
|
||||||
</SuggestedAction>
|
</SuggestedAction>
|
||||||
)}
|
)}
|
||||||
|
<SuggestedAction
|
||||||
|
onClick={() => {
|
||||||
|
handleOpenInExplore()
|
||||||
|
}}
|
||||||
|
disabled={!publishedAt}
|
||||||
|
icon={<RiPlanetLine className='w-4 h-4' />}
|
||||||
|
>
|
||||||
|
{t('workflow.common.openInExplore')}
|
||||||
|
</SuggestedAction>
|
||||||
<SuggestedAction disabled={!publishedAt} link='./develop' icon={<FileText className='w-4 h-4' />}>{t('workflow.common.accessAPIReference')}</SuggestedAction>
|
<SuggestedAction disabled={!publishedAt} link='./develop' icon={<FileText className='w-4 h-4' />}>{t('workflow.common.accessAPIReference')}</SuggestedAction>
|
||||||
{appDetail?.mode === 'workflow' && (
|
{appDetail?.mode === 'workflow' && (
|
||||||
<WorkflowToolConfigureButton
|
<WorkflowToolConfigureButton
|
||||||
|
@ -11,16 +11,19 @@ import { useDraggableUploader } from './hooks'
|
|||||||
import { checkIsAnimatedImage } from './utils'
|
import { checkIsAnimatedImage } from './utils'
|
||||||
import { ALLOW_FILE_EXTENSIONS } from '@/types/app'
|
import { ALLOW_FILE_EXTENSIONS } from '@/types/app'
|
||||||
|
|
||||||
type UploaderProps = {
|
export type OnImageInput = {
|
||||||
className?: string
|
(isCropped: true, tempUrl: string, croppedAreaPixels: Area, fileName: string): void
|
||||||
onImageCropped?: (tempUrl: string, croppedAreaPixels: Area, fileName: string) => void
|
(isCropped: false, file: File): void
|
||||||
onUpload?: (file?: File) => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const Uploader: FC<UploaderProps> = ({
|
type UploaderProps = {
|
||||||
|
className?: string
|
||||||
|
onImageInput?: OnImageInput
|
||||||
|
}
|
||||||
|
|
||||||
|
const ImageInput: FC<UploaderProps> = ({
|
||||||
className,
|
className,
|
||||||
onImageCropped,
|
onImageInput,
|
||||||
onUpload,
|
|
||||||
}) => {
|
}) => {
|
||||||
const [inputImage, setInputImage] = useState<{ file: File; url: string }>()
|
const [inputImage, setInputImage] = useState<{ file: File; url: string }>()
|
||||||
const [isAnimatedImage, setIsAnimatedImage] = useState<boolean>(false)
|
const [isAnimatedImage, setIsAnimatedImage] = useState<boolean>(false)
|
||||||
@ -37,8 +40,7 @@ const Uploader: FC<UploaderProps> = ({
|
|||||||
const onCropComplete = async (_: Area, croppedAreaPixels: Area) => {
|
const onCropComplete = async (_: Area, croppedAreaPixels: Area) => {
|
||||||
if (!inputImage)
|
if (!inputImage)
|
||||||
return
|
return
|
||||||
onImageCropped?.(inputImage.url, croppedAreaPixels, inputImage.file.name)
|
onImageInput?.(true, inputImage.url, croppedAreaPixels, inputImage.file.name)
|
||||||
onUpload?.(undefined)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleLocalFileInput = (e: ChangeEvent<HTMLInputElement>) => {
|
const handleLocalFileInput = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
@ -48,7 +50,7 @@ const Uploader: FC<UploaderProps> = ({
|
|||||||
checkIsAnimatedImage(file).then((isAnimatedImage) => {
|
checkIsAnimatedImage(file).then((isAnimatedImage) => {
|
||||||
setIsAnimatedImage(!!isAnimatedImage)
|
setIsAnimatedImage(!!isAnimatedImage)
|
||||||
if (isAnimatedImage)
|
if (isAnimatedImage)
|
||||||
onUpload?.(file)
|
onImageInput?.(false, file)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -117,4 +119,4 @@ const Uploader: FC<UploaderProps> = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Uploader
|
export default ImageInput
|
@ -8,12 +8,14 @@ import Button from '../button'
|
|||||||
import { ImagePlus } from '../icons/src/vender/line/images'
|
import { ImagePlus } from '../icons/src/vender/line/images'
|
||||||
import { useLocalFileUploader } from '../image-uploader/hooks'
|
import { useLocalFileUploader } from '../image-uploader/hooks'
|
||||||
import EmojiPickerInner from '../emoji-picker/Inner'
|
import EmojiPickerInner from '../emoji-picker/Inner'
|
||||||
import Uploader from './Uploader'
|
import type { OnImageInput } from './ImageInput'
|
||||||
|
import ImageInput from './ImageInput'
|
||||||
import s from './style.module.css'
|
import s from './style.module.css'
|
||||||
import getCroppedImg from './utils'
|
import getCroppedImg from './utils'
|
||||||
import type { AppIconType, ImageFile } from '@/types/app'
|
import type { AppIconType, ImageFile } from '@/types/app'
|
||||||
import cn from '@/utils/classnames'
|
import cn from '@/utils/classnames'
|
||||||
import { DISABLE_UPLOAD_IMAGE_AS_ICON } from '@/config'
|
import { DISABLE_UPLOAD_IMAGE_AS_ICON } from '@/config'
|
||||||
|
|
||||||
export type AppIconEmojiSelection = {
|
export type AppIconEmojiSelection = {
|
||||||
type: 'emoji'
|
type: 'emoji'
|
||||||
icon: string
|
icon: string
|
||||||
@ -69,14 +71,15 @@ const AppIconPicker: FC<AppIconPickerProps> = ({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const [imageCropInfo, setImageCropInfo] = useState<{ tempUrl: string; croppedAreaPixels: Area; fileName: string }>()
|
type InputImageInfo = { file: File } | { tempUrl: string; croppedAreaPixels: Area; fileName: string }
|
||||||
const handleImageCropped = async (tempUrl: string, croppedAreaPixels: Area, fileName: string) => {
|
const [inputImageInfo, setInputImageInfo] = useState<InputImageInfo>()
|
||||||
setImageCropInfo({ tempUrl, croppedAreaPixels, fileName })
|
|
||||||
}
|
|
||||||
|
|
||||||
const [uploadImageInfo, setUploadImageInfo] = useState<{ file?: File }>()
|
const handleImageInput: OnImageInput = async (isCropped: boolean, fileOrTempUrl: string | File, croppedAreaPixels?: Area, fileName?: string) => {
|
||||||
const handleUpload = async (file?: File) => {
|
setInputImageInfo(
|
||||||
setUploadImageInfo({ file })
|
isCropped
|
||||||
|
? { tempUrl: fileOrTempUrl as string, croppedAreaPixels: croppedAreaPixels!, fileName: fileName! }
|
||||||
|
: { file: fileOrTempUrl as File },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSelect = async () => {
|
const handleSelect = async () => {
|
||||||
@ -90,15 +93,15 @@ const AppIconPicker: FC<AppIconPickerProps> = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if (!imageCropInfo && !uploadImageInfo)
|
if (!inputImageInfo)
|
||||||
return
|
return
|
||||||
setUploading(true)
|
setUploading(true)
|
||||||
if (imageCropInfo.file) {
|
if ('file' in inputImageInfo) {
|
||||||
handleLocalFileUpload(imageCropInfo.file)
|
handleLocalFileUpload(inputImageInfo.file)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const blob = await getCroppedImg(imageCropInfo.tempUrl, imageCropInfo.croppedAreaPixels, imageCropInfo.fileName)
|
const blob = await getCroppedImg(inputImageInfo.tempUrl, inputImageInfo.croppedAreaPixels, inputImageInfo.fileName)
|
||||||
const file = new File([blob], imageCropInfo.fileName, { type: blob.type })
|
const file = new File([blob], inputImageInfo.fileName, { type: blob.type })
|
||||||
handleLocalFileUpload(file)
|
handleLocalFileUpload(file)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -127,10 +130,8 @@ const AppIconPicker: FC<AppIconPickerProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>}
|
</div>}
|
||||||
|
|
||||||
<Divider className='m-0' />
|
<EmojiPickerInner className={cn(activeTab === 'emoji' ? 'block' : 'hidden', 'pt-2')} onSelect={handleSelectEmoji} />
|
||||||
|
<ImageInput className={activeTab === 'image' ? 'block' : 'hidden'} onImageInput={handleImageInput} />
|
||||||
<EmojiPickerInner className={activeTab === 'emoji' ? 'block' : 'hidden'} onSelect={handleSelectEmoji} />
|
|
||||||
<Uploader className={activeTab === 'image' ? 'block' : 'hidden'} onImageCropped={handleImageCropped} onUpload={handleUpload}/>
|
|
||||||
|
|
||||||
<Divider className='m-0' />
|
<Divider className='m-0' />
|
||||||
<div className='w-full flex items-center justify-center p-3 gap-2'>
|
<div className='w-full flex items-center justify-center p-3 gap-2'>
|
||||||
|
@ -116,12 +116,12 @@ export default async function getCroppedImg(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function checkIsAnimatedImage(file) {
|
export function checkIsAnimatedImage(file: File): Promise<boolean> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const fileReader = new FileReader()
|
const fileReader = new FileReader()
|
||||||
|
|
||||||
fileReader.onload = function (e) {
|
fileReader.onload = function (e) {
|
||||||
const arr = new Uint8Array(e.target.result)
|
const arr = new Uint8Array(e.target?.result as ArrayBuffer)
|
||||||
|
|
||||||
// Check file extension
|
// Check file extension
|
||||||
const fileName = file.name.toLowerCase()
|
const fileName = file.name.toLowerCase()
|
||||||
@ -148,7 +148,7 @@ export function checkIsAnimatedImage(file) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Function to check for WebP signature
|
// Function to check for WebP signature
|
||||||
function isWebP(arr) {
|
function isWebP(arr: Uint8Array) {
|
||||||
return (
|
return (
|
||||||
arr[0] === 0x52 && arr[1] === 0x49 && arr[2] === 0x46 && arr[3] === 0x46
|
arr[0] === 0x52 && arr[1] === 0x49 && arr[2] === 0x46 && arr[3] === 0x46
|
||||||
&& arr[8] === 0x57 && arr[9] === 0x45 && arr[10] === 0x42 && arr[11] === 0x50
|
&& arr[8] === 0x57 && arr[9] === 0x45 && arr[10] === 0x42 && arr[11] === 0x50
|
||||||
@ -156,7 +156,7 @@ function isWebP(arr) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Function to check if the WebP is animated (contains ANIM chunk)
|
// Function to check if the WebP is animated (contains ANIM chunk)
|
||||||
function checkWebPAnimation(arr) {
|
function checkWebPAnimation(arr: Uint8Array) {
|
||||||
// Search for the ANIM chunk in WebP to determine if it's animated
|
// Search for the ANIM chunk in WebP to determine if it's animated
|
||||||
for (let i = 12; i < arr.length - 4; i++) {
|
for (let i = 12; i < arr.length - 4; i++) {
|
||||||
if (arr[i] === 0x41 && arr[i + 1] === 0x4E && arr[i + 2] === 0x49 && arr[i + 3] === 0x4D)
|
if (arr[i] === 0x41 && arr[i + 1] === 0x4E && arr[i + 2] === 0x49 && arr[i + 3] === 0x4D)
|
||||||
|
@ -68,7 +68,7 @@ const EmojiPickerInner: FC<IEmojiPickerInnerProps> = ({
|
|||||||
}, [onSelect, selectedEmoji, selectedBackground])
|
}, [onSelect, selectedEmoji, selectedBackground])
|
||||||
|
|
||||||
return <div className={cn(className)}>
|
return <div className={cn(className)}>
|
||||||
<div className='flex flex-col items-center w-full px-3'>
|
<div className='flex flex-col items-center w-full px-3 pb-2'>
|
||||||
<div className="relative w-full">
|
<div className="relative w-full">
|
||||||
<div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
|
<div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
|
||||||
<MagnifyingGlassIcon className="w-5 h-5 text-gray-400" aria-hidden="true" />
|
<MagnifyingGlassIcon className="w-5 h-5 text-gray-400" aria-hidden="true" />
|
||||||
|
@ -158,13 +158,13 @@ export const isAllowedFileExtension = (fileName: string, fileMimetype: string, a
|
|||||||
|
|
||||||
export const getFilesInLogs = (rawData: any) => {
|
export const getFilesInLogs = (rawData: any) => {
|
||||||
const result = Object.keys(rawData || {}).map((key) => {
|
const result = Object.keys(rawData || {}).map((key) => {
|
||||||
if (typeof rawData[key] === 'object' && rawData[key].dify_model_identity === '__dify__file__') {
|
if (typeof rawData[key] === 'object' && rawData[key]?.dify_model_identity === '__dify__file__') {
|
||||||
return {
|
return {
|
||||||
varName: key,
|
varName: key,
|
||||||
list: getProcessedFilesFromResponse([rawData[key]]),
|
list: getProcessedFilesFromResponse([rawData[key]]),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (Array.isArray(rawData[key]) && rawData[key].some(item => item.dify_model_identity === '__dify__file__')) {
|
if (Array.isArray(rawData[key]) && rawData[key].some(item => item?.dify_model_identity === '__dify__file__')) {
|
||||||
return {
|
return {
|
||||||
varName: key,
|
varName: key,
|
||||||
list: getProcessedFilesFromResponse(rawData[key]),
|
list: getProcessedFilesFromResponse(rawData[key]),
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
import mermaid from 'mermaid'
|
import mermaid from 'mermaid'
|
||||||
import { usePrevious } from 'ahooks'
|
import { usePrevious } from 'ahooks'
|
||||||
import CryptoJS from 'crypto-js'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { ExclamationTriangleIcon } from '@heroicons/react/24/outline'
|
import { ExclamationTriangleIcon } from '@heroicons/react/24/outline'
|
||||||
import LoadingAnim from '@/app/components/base/chat/chat/loading-anim'
|
import LoadingAnim from '@/app/components/base/chat/chat/loading-anim'
|
||||||
@ -14,12 +13,6 @@ mermaidAPI = null
|
|||||||
if (typeof window !== 'undefined')
|
if (typeof window !== 'undefined')
|
||||||
mermaidAPI = mermaid.mermaidAPI
|
mermaidAPI = mermaid.mermaidAPI
|
||||||
|
|
||||||
const style = {
|
|
||||||
minWidth: '480px',
|
|
||||||
height: 'auto',
|
|
||||||
overflow: 'auto',
|
|
||||||
}
|
|
||||||
|
|
||||||
const svgToBase64 = (svgGraph: string) => {
|
const svgToBase64 = (svgGraph: string) => {
|
||||||
const svgBytes = new TextEncoder().encode(svgGraph)
|
const svgBytes = new TextEncoder().encode(svgGraph)
|
||||||
const blob = new Blob([svgBytes], { type: 'image/svg+xml;charset=utf-8' })
|
const blob = new Blob([svgBytes], { type: 'image/svg+xml;charset=utf-8' })
|
||||||
@ -38,7 +31,6 @@ const Flowchart = React.forwardRef((props: {
|
|||||||
const [svgCode, setSvgCode] = useState(null)
|
const [svgCode, setSvgCode] = useState(null)
|
||||||
const [look, setLook] = useState<'classic' | 'handDrawn'>('classic')
|
const [look, setLook] = useState<'classic' | 'handDrawn'>('classic')
|
||||||
|
|
||||||
const chartId = useRef(`flowchart_${CryptoJS.MD5(props.PrimitiveCode).toString()}`)
|
|
||||||
const prevPrimitiveCode = usePrevious(props.PrimitiveCode)
|
const prevPrimitiveCode = usePrevious(props.PrimitiveCode)
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
const timeRef = useRef<NodeJS.Timeout>()
|
const timeRef = useRef<NodeJS.Timeout>()
|
||||||
@ -51,12 +43,10 @@ const Flowchart = React.forwardRef((props: {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (typeof window !== 'undefined' && mermaidAPI) {
|
if (typeof window !== 'undefined' && mermaidAPI) {
|
||||||
const svgGraph = await mermaidAPI.render(chartId.current, PrimitiveCode)
|
const svgGraph = await mermaidAPI.render('flowchart', PrimitiveCode)
|
||||||
const base64Svg: any = await svgToBase64(svgGraph.svg)
|
const base64Svg: any = await svgToBase64(svgGraph.svg)
|
||||||
setSvgCode(base64Svg)
|
setSvgCode(base64Svg)
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
if (chartId.current && base64Svg)
|
|
||||||
localStorage.setItem(chartId.current, base64Svg)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
@ -79,19 +69,11 @@ const Flowchart = React.forwardRef((props: {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
localStorage.removeItem(chartId.current)
|
|
||||||
renderFlowchart(props.PrimitiveCode)
|
renderFlowchart(props.PrimitiveCode)
|
||||||
}
|
}
|
||||||
}, [look])
|
}, [look])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const cachedSvg: any = localStorage.getItem(chartId.current)
|
|
||||||
|
|
||||||
if (cachedSvg) {
|
|
||||||
setSvgCode(cachedSvg)
|
|
||||||
setIsLoading(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (timeRef.current)
|
if (timeRef.current)
|
||||||
clearTimeout(timeRef.current)
|
clearTimeout(timeRef.current)
|
||||||
|
|
||||||
@ -130,8 +112,8 @@ const Flowchart = React.forwardRef((props: {
|
|||||||
</div>
|
</div>
|
||||||
{
|
{
|
||||||
svgCode
|
svgCode
|
||||||
&& <div className="mermaid cursor-pointer" style={style} onClick={() => setImagePreviewUrl(svgCode)}>
|
&& <div className="mermaid cursor-pointer h-auto w-full object-fit: cover" onClick={() => setImagePreviewUrl(svgCode)}>
|
||||||
{svgCode && <img src={svgCode} style={{ width: '100%', height: 'auto' }} alt="mermaid_chart" />}
|
{svgCode && <img src={svgCode} alt="mermaid_chart" />}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
{isLoading
|
{isLoading
|
||||||
|
@ -72,7 +72,7 @@ const VariableTag = ({
|
|||||||
{isEnv && <Env className='shrink-0 mr-0.5 w-3.5 h-3.5 text-util-colors-violet-violet-600' />}
|
{isEnv && <Env className='shrink-0 mr-0.5 w-3.5 h-3.5 text-util-colors-violet-violet-600' />}
|
||||||
{isChatVar && <BubbleX className='w-3.5 h-3.5 text-util-colors-teal-teal-700' />}
|
{isChatVar && <BubbleX className='w-3.5 h-3.5 text-util-colors-teal-teal-700' />}
|
||||||
<div
|
<div
|
||||||
className={cn('truncate text-text-accent font-medium', (isEnv || isChatVar) && 'text-text-secondary')}
|
className={cn('truncate ml-0.5 text-text-accent font-medium', (isEnv || isChatVar) && 'text-text-secondary')}
|
||||||
title={variableName}
|
title={variableName}
|
||||||
>
|
>
|
||||||
{variableName}
|
{variableName}
|
||||||
|
@ -274,7 +274,7 @@ const VarReferenceVars: FC<Props> = ({
|
|||||||
{
|
{
|
||||||
!hideSearch && (
|
!hideSearch && (
|
||||||
<>
|
<>
|
||||||
<div className={cn('mb-2 mx-1', searchBoxClassName)} onClick={e => e.stopPropagation()}>
|
<div className={cn('mb-1 mx-2 mt-2', searchBoxClassName)} onClick={e => e.stopPropagation()}>
|
||||||
<Input
|
<Input
|
||||||
showLeftIcon
|
showLeftIcon
|
||||||
showClearIcon
|
showClearIcon
|
||||||
|
@ -25,10 +25,12 @@ import { FILE_TYPE_OPTIONS, SUB_VARIABLES, TRANSFER_METHOD } from '../../default
|
|||||||
import ConditionWrap from '../condition-wrap'
|
import ConditionWrap from '../condition-wrap'
|
||||||
import ConditionOperator from './condition-operator'
|
import ConditionOperator from './condition-operator'
|
||||||
import ConditionInput from './condition-input'
|
import ConditionInput from './condition-input'
|
||||||
import VariableTag from '@/app/components/workflow/nodes/_base/components/variable-tag'
|
|
||||||
|
import ConditionVarSelector from './condition-var-selector'
|
||||||
import type {
|
import type {
|
||||||
Node,
|
Node,
|
||||||
NodeOutPutVar,
|
NodeOutPutVar,
|
||||||
|
ValueSelector,
|
||||||
Var,
|
Var,
|
||||||
} from '@/app/components/workflow/types'
|
} from '@/app/components/workflow/types'
|
||||||
import { VarType } from '@/app/components/workflow/types'
|
import { VarType } from '@/app/components/workflow/types'
|
||||||
@ -82,6 +84,7 @@ const ConditionItem = ({
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const [isHovered, setIsHovered] = useState(false)
|
const [isHovered, setIsHovered] = useState(false)
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
|
||||||
const doUpdateCondition = useCallback((newCondition: Condition) => {
|
const doUpdateCondition = useCallback((newCondition: Condition) => {
|
||||||
if (isSubVariableKey)
|
if (isSubVariableKey)
|
||||||
@ -190,6 +193,17 @@ const ConditionItem = ({
|
|||||||
onRemoveCondition?.(caseId, condition.id)
|
onRemoveCondition?.(caseId, condition.id)
|
||||||
}, [caseId, condition, conditionId, isSubVariableKey, onRemoveCondition, onRemoveSubVariableCondition])
|
}, [caseId, condition, conditionId, isSubVariableKey, onRemoveCondition, onRemoveSubVariableCondition])
|
||||||
|
|
||||||
|
const handleVarChange = useCallback((valueSelector: ValueSelector, varItem: Var) => {
|
||||||
|
const newCondition = produce(condition, (draft) => {
|
||||||
|
draft.variable_selector = valueSelector
|
||||||
|
draft.varType = varItem.type
|
||||||
|
draft.value = ''
|
||||||
|
draft.comparison_operator = getOperators(varItem.type)[0]
|
||||||
|
})
|
||||||
|
doUpdateCondition(newCondition)
|
||||||
|
setOpen(false)
|
||||||
|
}, [condition, doUpdateCondition])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('flex mb-1 last-of-type:mb-0', className)}>
|
<div className={cn('flex mb-1 last-of-type:mb-0', className)}>
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
@ -221,11 +235,14 @@ const ConditionItem = ({
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
: (
|
: (
|
||||||
<VariableTag
|
<ConditionVarSelector
|
||||||
|
open={open}
|
||||||
|
onOpenChange={setOpen}
|
||||||
valueSelector={condition.variable_selector || []}
|
valueSelector={condition.variable_selector || []}
|
||||||
varType={condition.varType}
|
varType={condition.varType}
|
||||||
availableNodes={availableNodes}
|
availableNodes={availableNodes}
|
||||||
isShort
|
nodesOutputVars={nodesOutputVars}
|
||||||
|
onChange={handleVarChange}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
@ -0,0 +1,58 @@
|
|||||||
|
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem'
|
||||||
|
import VariableTag from '@/app/components/workflow/nodes/_base/components/variable-tag'
|
||||||
|
import VarReferenceVars from '@/app/components/workflow/nodes/_base/components/variable/var-reference-vars'
|
||||||
|
import type { Node, NodeOutPutVar, ValueSelector, Var, VarType } from '@/app/components/workflow/types'
|
||||||
|
|
||||||
|
type ConditionVarSelectorProps = {
|
||||||
|
open: boolean
|
||||||
|
onOpenChange: (open: boolean) => void
|
||||||
|
valueSelector: ValueSelector
|
||||||
|
varType: VarType
|
||||||
|
availableNodes: Node[]
|
||||||
|
nodesOutputVars: NodeOutPutVar[]
|
||||||
|
onChange: (valueSelector: ValueSelector, varItem: Var) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const ConditionVarSelector = ({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
valueSelector,
|
||||||
|
varType,
|
||||||
|
availableNodes,
|
||||||
|
nodesOutputVars,
|
||||||
|
onChange,
|
||||||
|
}: ConditionVarSelectorProps) => {
|
||||||
|
return (
|
||||||
|
<PortalToFollowElem
|
||||||
|
open={open}
|
||||||
|
onOpenChange={onOpenChange}
|
||||||
|
placement='bottom-start'
|
||||||
|
offset={{
|
||||||
|
mainAxis: 4,
|
||||||
|
crossAxis: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PortalToFollowElemTrigger onClick={() => onOpenChange(!open)}>
|
||||||
|
<div className="cursor-pointer">
|
||||||
|
<VariableTag
|
||||||
|
valueSelector={valueSelector}
|
||||||
|
varType={varType}
|
||||||
|
availableNodes={availableNodes}
|
||||||
|
isShort
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</PortalToFollowElemTrigger>
|
||||||
|
<PortalToFollowElemContent className='z-[1000]'>
|
||||||
|
<div className='w-[296px] bg-components-panel-bg-blur rounded-lg border-[0.5px] border-components-panel-border shadow-lg'>
|
||||||
|
<VarReferenceVars
|
||||||
|
vars={nodesOutputVars}
|
||||||
|
isSupportFileVar
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</PortalToFollowElemContent>
|
||||||
|
</PortalToFollowElem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ConditionVarSelector
|
@ -73,7 +73,7 @@ const ConditionValue = ({
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'shrink-0 truncate text-xs font-medium text-text-accent',
|
'shrink-0 ml-0.5 truncate text-xs font-medium text-text-accent',
|
||||||
!notHasValue && 'max-w-[70px]',
|
!notHasValue && 'max-w-[70px]',
|
||||||
)}
|
)}
|
||||||
title={variableName}
|
title={variableName}
|
||||||
|
@ -35,12 +35,12 @@ const OutputPanel: FC<OutputPanelProps> = ({
|
|||||||
for (const key in outputs) {
|
for (const key in outputs) {
|
||||||
if (Array.isArray(outputs[key])) {
|
if (Array.isArray(outputs[key])) {
|
||||||
outputs[key].map((output: any) => {
|
outputs[key].map((output: any) => {
|
||||||
if (output.dify_model_identity === '__dify__file__')
|
if (output?.dify_model_identity === '__dify__file__')
|
||||||
fileList.push(output)
|
fileList.push(output)
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
else if (outputs[key].dify_model_identity === '__dify__file__') {
|
else if (outputs[key]?.dify_model_identity === '__dify__file__') {
|
||||||
fileList.push(outputs[key])
|
fileList.push(outputs[key])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -101,6 +101,7 @@ const translation = {
|
|||||||
switchLabel: 'The app copy to be created',
|
switchLabel: 'The app copy to be created',
|
||||||
removeOriginal: 'Delete the original app',
|
removeOriginal: 'Delete the original app',
|
||||||
switchStart: 'Start switch',
|
switchStart: 'Start switch',
|
||||||
|
openInExplore: 'Open in Explore',
|
||||||
typeSelector: {
|
typeSelector: {
|
||||||
all: 'ALL Types',
|
all: 'ALL Types',
|
||||||
chatbot: 'Chatbot',
|
chatbot: 'Chatbot',
|
||||||
|
@ -32,6 +32,7 @@ const translation = {
|
|||||||
restore: 'Restore',
|
restore: 'Restore',
|
||||||
runApp: 'Run App',
|
runApp: 'Run App',
|
||||||
batchRunApp: 'Batch Run App',
|
batchRunApp: 'Batch Run App',
|
||||||
|
openInExplore: 'Open in Explore',
|
||||||
accessAPIReference: 'Access API Reference',
|
accessAPIReference: 'Access API Reference',
|
||||||
embedIntoSite: 'Embed Into Site',
|
embedIntoSite: 'Embed Into Site',
|
||||||
addTitle: 'Add title...',
|
addTitle: 'Add title...',
|
||||||
|
@ -80,7 +80,7 @@ const translation = {
|
|||||||
title: '会話ログ',
|
title: '会話ログ',
|
||||||
workflowTitle: 'ログの詳細',
|
workflowTitle: 'ログの詳細',
|
||||||
fileListLabel: 'ファイルの詳細',
|
fileListLabel: 'ファイルの詳細',
|
||||||
fileListDetail: 'ディテール',
|
fileListDetail: '詳細',
|
||||||
},
|
},
|
||||||
promptLog: 'プロンプトログ',
|
promptLog: 'プロンプトログ',
|
||||||
agentLog: 'エージェントログ',
|
agentLog: 'エージェントログ',
|
||||||
|
@ -93,6 +93,7 @@ const translation = {
|
|||||||
switchLabel: '作成されるアプリのコピー',
|
switchLabel: '作成されるアプリのコピー',
|
||||||
removeOriginal: '元のアプリを削除する',
|
removeOriginal: '元のアプリを削除する',
|
||||||
switchStart: '切り替えを開始する',
|
switchStart: '切り替えを開始する',
|
||||||
|
openInExplore: '"探索" で開く',
|
||||||
typeSelector: {
|
typeSelector: {
|
||||||
all: 'すべてのタイプ',
|
all: 'すべてのタイプ',
|
||||||
chatbot: 'チャットボット',
|
chatbot: 'チャットボット',
|
||||||
|
@ -32,6 +32,7 @@ const translation = {
|
|||||||
restore: '復元',
|
restore: '復元',
|
||||||
runApp: 'アプリを実行',
|
runApp: 'アプリを実行',
|
||||||
batchRunApp: 'バッチでアプリを実行',
|
batchRunApp: 'バッチでアプリを実行',
|
||||||
|
openInExplore: '"探索" で開く',
|
||||||
accessAPIReference: 'APIリファレンスにアクセス',
|
accessAPIReference: 'APIリファレンスにアクセス',
|
||||||
embedIntoSite: 'サイトに埋め込む',
|
embedIntoSite: 'サイトに埋め込む',
|
||||||
addTitle: 'タイトルを追加...',
|
addTitle: 'タイトルを追加...',
|
||||||
|
@ -12,8 +12,8 @@ export const fetchAppDetail = (id: string): Promise<any> => {
|
|||||||
return get(`/explore/apps/${id}`)
|
return get(`/explore/apps/${id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const fetchInstalledAppList = () => {
|
export const fetchInstalledAppList = (app_id?: string | null) => {
|
||||||
return get('/installed-apps')
|
return get(`/installed-apps${app_id ? `?app_id=${app_id}` : ''}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const installApp = (id: string) => {
|
export const installApp = (id: string) => {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user