Compare commits
1 Commits
main
...
chore/auto
| Author | SHA1 | Date |
|---|---|---|
|
|
a65d255a32 | 11 months ago |
@ -0,0 +1,78 @@
|
|||||||
|
from .agent import AgentNodeType
|
||||||
|
from .answer import AnswerNodeType
|
||||||
|
from .assigner import AssignerNodeType
|
||||||
|
from .code import CodeLanguage, CodeNodeType, OutputVar
|
||||||
|
from .common import (
|
||||||
|
BlockEnum,
|
||||||
|
CommonEdgeType,
|
||||||
|
CommonNodeType,
|
||||||
|
CompleteEdge,
|
||||||
|
CompleteNode,
|
||||||
|
Context,
|
||||||
|
InputVar,
|
||||||
|
InputVarType,
|
||||||
|
Memory,
|
||||||
|
ModelConfig,
|
||||||
|
PromptItem,
|
||||||
|
PromptRole,
|
||||||
|
ValueSelector,
|
||||||
|
Variable,
|
||||||
|
VarType,
|
||||||
|
VisionSetting,
|
||||||
|
)
|
||||||
|
from .end import EndNodeType
|
||||||
|
from .http import HttpNodeType
|
||||||
|
from .if_else import IfElseNodeType
|
||||||
|
from .iteration import IterationNodeType
|
||||||
|
from .iteration_start import IterationStartNodeType
|
||||||
|
from .knowledge_retrieval import KnowledgeRetrievalNodeType
|
||||||
|
from .list_operator import ListFilterNodeType
|
||||||
|
from .llm import LLMNodeType, VisionConfig
|
||||||
|
from .note_node import NoteNodeType
|
||||||
|
from .parameter_extractor import ParameterExtractorNodeType
|
||||||
|
from .question_classifier import QuestionClassifierNodeType
|
||||||
|
from .start import StartNodeType
|
||||||
|
from .template_transform import TemplateTransformNodeType
|
||||||
|
from .tool import ToolNodeType
|
||||||
|
from .variable_assigner import VariableAssignerNodeType
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"AgentNodeType",
|
||||||
|
"AnswerNodeType",
|
||||||
|
"AssignerNodeType",
|
||||||
|
"BlockEnum",
|
||||||
|
"CodeLanguage",
|
||||||
|
"CodeNodeType",
|
||||||
|
"CommonEdgeType",
|
||||||
|
"CommonNodeType",
|
||||||
|
"CompleteEdge",
|
||||||
|
"CompleteNode",
|
||||||
|
"Context",
|
||||||
|
"EndNodeType",
|
||||||
|
"HttpNodeType",
|
||||||
|
"IfElseNodeType",
|
||||||
|
"InputVar",
|
||||||
|
"InputVarType",
|
||||||
|
"IterationNodeType",
|
||||||
|
"IterationStartNodeType",
|
||||||
|
"KnowledgeRetrievalNodeType",
|
||||||
|
"LLMNodeType",
|
||||||
|
"ListFilterNodeType",
|
||||||
|
"Memory",
|
||||||
|
"ModelConfig",
|
||||||
|
"NoteNodeType",
|
||||||
|
"OutputVar",
|
||||||
|
"ParameterExtractorNodeType",
|
||||||
|
"PromptItem",
|
||||||
|
"PromptRole",
|
||||||
|
"QuestionClassifierNodeType",
|
||||||
|
"StartNodeType",
|
||||||
|
"TemplateTransformNodeType",
|
||||||
|
"ToolNodeType",
|
||||||
|
"ValueSelector",
|
||||||
|
"VarType",
|
||||||
|
"Variable",
|
||||||
|
"VariableAssignerNodeType",
|
||||||
|
"VisionConfig",
|
||||||
|
"VisionSetting",
|
||||||
|
]
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from .common import BlockEnum, CommonNodeType
|
||||||
|
|
||||||
|
# Introduce previously defined CommonNodeType and ToolVarInputs
|
||||||
|
# Assume they are defined in the same module
|
||||||
|
|
||||||
|
|
||||||
|
class ToolVarInputs(BaseModel):
|
||||||
|
variable_name: Optional[str] = None
|
||||||
|
default_value: Optional[Any] = None
|
||||||
|
|
||||||
|
|
||||||
|
class AgentNodeType(CommonNodeType):
|
||||||
|
agent_strategy_provider_name: Optional[str] = None
|
||||||
|
agent_strategy_name: Optional[str] = None
|
||||||
|
agent_strategy_label: Optional[str] = None
|
||||||
|
agent_parameters: Optional[ToolVarInputs] = None
|
||||||
|
output_schema: dict[str, Any]
|
||||||
|
plugin_unique_identifier: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
# 示例用法
|
||||||
|
if __name__ == "__main__":
|
||||||
|
example_node = AgentNodeType(
|
||||||
|
title="Example Agent",
|
||||||
|
desc="An agent node example",
|
||||||
|
type=BlockEnum.agent,
|
||||||
|
output_schema={"key": "value"},
|
||||||
|
agent_parameters=ToolVarInputs(variable_name="example_var", default_value="default"),
|
||||||
|
)
|
||||||
|
print(example_node)
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
from .common import BlockEnum, CommonNodeType, Variable
|
||||||
|
|
||||||
|
|
||||||
|
class AnswerNodeType(CommonNodeType):
|
||||||
|
variables: list[Variable]
|
||||||
|
answer: str
|
||||||
|
|
||||||
|
|
||||||
|
# Example usage
|
||||||
|
if __name__ == "__main__":
|
||||||
|
example_node = AnswerNodeType(
|
||||||
|
title="Example Answer Node",
|
||||||
|
desc="An answer node example",
|
||||||
|
type=BlockEnum.answer,
|
||||||
|
answer="This is the answer",
|
||||||
|
variables=[
|
||||||
|
Variable(variable="var1", value_selector=["node1", "key1"]),
|
||||||
|
Variable(variable="var2", value_selector=["node2", "key2"]),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
print(example_node)
|
||||||
@ -0,0 +1,62 @@
|
|||||||
|
from enum import Enum
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
from .common import BlockEnum, CommonNodeType
|
||||||
|
|
||||||
|
# Import previously defined CommonNodeType and ValueSelector
|
||||||
|
# Assume they are defined in the same module
|
||||||
|
|
||||||
|
|
||||||
|
class WriteMode(str, Enum):
|
||||||
|
overwrite = "over-write"
|
||||||
|
clear = "clear"
|
||||||
|
append = "append"
|
||||||
|
extend = "extend"
|
||||||
|
set = "set"
|
||||||
|
increment = "+="
|
||||||
|
decrement = "-="
|
||||||
|
multiply = "*="
|
||||||
|
divide = "/="
|
||||||
|
|
||||||
|
|
||||||
|
class AssignerNodeInputType(str, Enum):
|
||||||
|
variable = "variable"
|
||||||
|
constant = "constant"
|
||||||
|
|
||||||
|
|
||||||
|
class AssignerNodeOperation(BaseModel):
|
||||||
|
variable_selector: Any # Placeholder for ValueSelector type
|
||||||
|
input_type: AssignerNodeInputType
|
||||||
|
operation: WriteMode
|
||||||
|
value: Any
|
||||||
|
|
||||||
|
|
||||||
|
class AssignerNodeType(CommonNodeType):
|
||||||
|
version: Optional[str] = Field(None, pattern="^[12]$") # Version is '1' or '2'
|
||||||
|
items: list[AssignerNodeOperation]
|
||||||
|
|
||||||
|
|
||||||
|
# Example usage
|
||||||
|
if __name__ == "__main__":
|
||||||
|
example_node = AssignerNodeType(
|
||||||
|
title="Example Assigner Node",
|
||||||
|
desc="An assigner node example",
|
||||||
|
type=BlockEnum.variable_assigner,
|
||||||
|
items=[
|
||||||
|
AssignerNodeOperation(
|
||||||
|
variable_selector={"nodeId": "node1", "key": "value"}, # Example ValueSelector
|
||||||
|
input_type=AssignerNodeInputType.variable,
|
||||||
|
operation=WriteMode.set,
|
||||||
|
value="newValue",
|
||||||
|
),
|
||||||
|
AssignerNodeOperation(
|
||||||
|
variable_selector={"nodeId": "node2", "key": "value"},
|
||||||
|
input_type=AssignerNodeInputType.constant,
|
||||||
|
operation=WriteMode.increment,
|
||||||
|
value=1,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
print(example_node)
|
||||||
@ -0,0 +1,56 @@
|
|||||||
|
from enum import Enum
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from core.auto.node_types.common import BlockEnum, CommonNodeType, Variable, VarType
|
||||||
|
|
||||||
|
# 引入之前定义的 CommonNodeType、VarType 和 Variable
|
||||||
|
# 假设它们在同一模块中定义
|
||||||
|
|
||||||
|
|
||||||
|
class CodeLanguage(str, Enum):
|
||||||
|
python3 = "python3"
|
||||||
|
javascript = "javascript"
|
||||||
|
json = "json"
|
||||||
|
|
||||||
|
|
||||||
|
class OutputVar(BaseModel):
|
||||||
|
type: VarType
|
||||||
|
children: Optional[None] = None # 未来支持嵌套
|
||||||
|
|
||||||
|
def dict(self, *args, **kwargs):
|
||||||
|
"""自定义序列化方法,确保正确序列化"""
|
||||||
|
result = {"type": self.type.value if isinstance(self.type, Enum) else self.type}
|
||||||
|
|
||||||
|
if self.children is not None:
|
||||||
|
result["children"] = self.children
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class CodeNodeType(CommonNodeType):
|
||||||
|
variables: list[Variable]
|
||||||
|
code_language: CodeLanguage
|
||||||
|
code: str
|
||||||
|
outputs: dict[str, OutputVar]
|
||||||
|
|
||||||
|
|
||||||
|
# 示例用法
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# 创建示例节点
|
||||||
|
example_node = CodeNodeType(
|
||||||
|
title="Example Code Node",
|
||||||
|
desc="A code node example",
|
||||||
|
type=BlockEnum.code,
|
||||||
|
code_language=CodeLanguage.python3,
|
||||||
|
code="print('Hello, World!')",
|
||||||
|
outputs={
|
||||||
|
"output1": OutputVar(type=VarType.string),
|
||||||
|
"output2": OutputVar(type=VarType.number),
|
||||||
|
},
|
||||||
|
variables=[
|
||||||
|
Variable(variable="var1", value_selector=["node1", "key1"]),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
print(example_node.get_all_required_fields())
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
from .common import BlockEnum, CommonNodeType, Variable
|
||||||
|
|
||||||
|
# Import previously defined CommonNodeType and Variable
|
||||||
|
# Assume they are defined in the same module
|
||||||
|
|
||||||
|
|
||||||
|
class EndNodeType(CommonNodeType):
|
||||||
|
outputs: list[Variable]
|
||||||
|
|
||||||
|
|
||||||
|
# Example usage
|
||||||
|
if __name__ == "__main__":
|
||||||
|
example_node = EndNodeType(
|
||||||
|
title="Example End Node",
|
||||||
|
desc="An end node example",
|
||||||
|
type=BlockEnum.end,
|
||||||
|
outputs=[
|
||||||
|
Variable(variable="outputVar1", value_selector=["node1", "key1"]),
|
||||||
|
Variable(variable="outputVar2", value_selector=["node2", "key2"]),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
print(example_node)
|
||||||
@ -0,0 +1,127 @@
|
|||||||
|
from enum import Enum
|
||||||
|
from typing import Optional, Union
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from .common import BlockEnum, CommonNodeType, ValueSelector, Variable
|
||||||
|
|
||||||
|
# Import previously defined CommonNodeType, ValueSelector, and Variable
|
||||||
|
# Assume they are defined in the same module
|
||||||
|
|
||||||
|
|
||||||
|
class Method(str, Enum):
|
||||||
|
"""HTTP request methods."""
|
||||||
|
|
||||||
|
get = "get"
|
||||||
|
post = "post"
|
||||||
|
head = "head"
|
||||||
|
patch = "patch"
|
||||||
|
put = "put"
|
||||||
|
delete = "delete"
|
||||||
|
|
||||||
|
|
||||||
|
class BodyType(str, Enum):
|
||||||
|
"""HTTP request body types."""
|
||||||
|
|
||||||
|
none = "none"
|
||||||
|
formData = "form-data"
|
||||||
|
xWwwFormUrlencoded = "x-www-form-urlencoded"
|
||||||
|
rawText = "raw-text"
|
||||||
|
json = "json"
|
||||||
|
binary = "binary"
|
||||||
|
|
||||||
|
|
||||||
|
class BodyPayloadValueType(str, Enum):
|
||||||
|
"""Types of values in body payload."""
|
||||||
|
|
||||||
|
text = "text"
|
||||||
|
file = "file"
|
||||||
|
|
||||||
|
|
||||||
|
class BodyPayload(BaseModel):
|
||||||
|
"""Body payload item for HTTP requests."""
|
||||||
|
|
||||||
|
id: Optional[str] = None
|
||||||
|
key: Optional[str] = None
|
||||||
|
type: BodyPayloadValueType
|
||||||
|
file: Optional[ValueSelector] = None # Used when type is file
|
||||||
|
value: Optional[str] = None # Used when type is text
|
||||||
|
|
||||||
|
|
||||||
|
class Body(BaseModel):
|
||||||
|
"""HTTP request body configuration."""
|
||||||
|
|
||||||
|
type: BodyType
|
||||||
|
data: Union[str, list[BodyPayload]] # string is deprecated, will convert to BodyPayload
|
||||||
|
|
||||||
|
|
||||||
|
class AuthorizationType(str, Enum):
|
||||||
|
"""HTTP authorization types."""
|
||||||
|
|
||||||
|
none = "no-auth"
|
||||||
|
apiKey = "api-key"
|
||||||
|
|
||||||
|
|
||||||
|
class APIType(str, Enum):
|
||||||
|
"""API key types."""
|
||||||
|
|
||||||
|
basic = "basic"
|
||||||
|
bearer = "bearer"
|
||||||
|
custom = "custom"
|
||||||
|
|
||||||
|
|
||||||
|
class AuthConfig(BaseModel):
|
||||||
|
"""Authorization configuration."""
|
||||||
|
|
||||||
|
type: APIType
|
||||||
|
api_key: str
|
||||||
|
header: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class Authorization(BaseModel):
|
||||||
|
"""HTTP authorization settings."""
|
||||||
|
|
||||||
|
type: AuthorizationType
|
||||||
|
config: Optional[AuthConfig] = None
|
||||||
|
|
||||||
|
|
||||||
|
class Timeout(BaseModel):
|
||||||
|
"""HTTP request timeout settings."""
|
||||||
|
|
||||||
|
connect: Optional[int] = None
|
||||||
|
read: Optional[int] = None
|
||||||
|
write: Optional[int] = None
|
||||||
|
max_connect_timeout: Optional[int] = None
|
||||||
|
max_read_timeout: Optional[int] = None
|
||||||
|
max_write_timeout: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
|
class HttpNodeType(CommonNodeType):
|
||||||
|
"""HTTP request node type implementation."""
|
||||||
|
|
||||||
|
variables: list[Variable]
|
||||||
|
method: Method
|
||||||
|
url: str
|
||||||
|
headers: str
|
||||||
|
params: str
|
||||||
|
body: Body
|
||||||
|
authorization: Authorization
|
||||||
|
timeout: Timeout
|
||||||
|
|
||||||
|
|
||||||
|
# Example usage
|
||||||
|
if __name__ == "__main__":
|
||||||
|
example_node = HttpNodeType(
|
||||||
|
title="Example HTTP Node",
|
||||||
|
desc="An HTTP request node example",
|
||||||
|
type=BlockEnum.http_request,
|
||||||
|
variables=[Variable(variable="var1", value_selector=["node1", "key1"])],
|
||||||
|
method=Method.get,
|
||||||
|
url="https://api.example.com/data",
|
||||||
|
headers="{}",
|
||||||
|
params="{}",
|
||||||
|
body=Body(type=BodyType.none, data=[]),
|
||||||
|
authorization=Authorization(type=AuthorizationType.none),
|
||||||
|
timeout=Timeout(connect=30, read=30, write=30),
|
||||||
|
)
|
||||||
|
print(example_node)
|
||||||
@ -0,0 +1,99 @@
|
|||||||
|
from enum import Enum
|
||||||
|
from typing import Optional, Union
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from .common import BlockEnum, CommonNodeType, ValueSelector, VarType
|
||||||
|
from .tool import VarType as NumberVarType
|
||||||
|
|
||||||
|
# Import previously defined CommonNodeType, ValueSelector, Var, and VarType
|
||||||
|
# Assume they are defined in the same module
|
||||||
|
|
||||||
|
|
||||||
|
class LogicalOperator(str, Enum):
|
||||||
|
and_ = "and"
|
||||||
|
or_ = "or"
|
||||||
|
|
||||||
|
|
||||||
|
class ComparisonOperator(str, Enum):
|
||||||
|
contains = "contains"
|
||||||
|
notContains = "not contains"
|
||||||
|
startWith = "start with"
|
||||||
|
endWith = "end with"
|
||||||
|
is_ = "is"
|
||||||
|
isNot = "is not"
|
||||||
|
empty = "empty"
|
||||||
|
notEmpty = "not empty"
|
||||||
|
equal = "="
|
||||||
|
notEqual = "≠"
|
||||||
|
largerThan = ">"
|
||||||
|
lessThan = "<"
|
||||||
|
largerThanOrEqual = "≥"
|
||||||
|
lessThanOrEqual = "≤"
|
||||||
|
isNull = "is null"
|
||||||
|
isNotNull = "is not null"
|
||||||
|
in_ = "in"
|
||||||
|
notIn = "not in"
|
||||||
|
allOf = "all of"
|
||||||
|
exists = "exists"
|
||||||
|
notExists = "not exists"
|
||||||
|
equals = "=" # Alias for equal for compatibility
|
||||||
|
|
||||||
|
|
||||||
|
class Condition(BaseModel):
|
||||||
|
id: str
|
||||||
|
varType: VarType
|
||||||
|
variable_selector: Optional[ValueSelector]
|
||||||
|
key: Optional[str] = None # Sub variable key
|
||||||
|
comparison_operator: Optional[ComparisonOperator] = None
|
||||||
|
value: Union[str, list[str]]
|
||||||
|
numberVarType: Optional[NumberVarType]
|
||||||
|
sub_variable_condition: Optional["CaseItem"] = None # Recursive reference
|
||||||
|
|
||||||
|
|
||||||
|
class CaseItem(BaseModel):
|
||||||
|
case_id: str
|
||||||
|
logical_operator: LogicalOperator
|
||||||
|
conditions: list[Condition]
|
||||||
|
|
||||||
|
|
||||||
|
class IfElseNodeType(CommonNodeType):
|
||||||
|
logical_operator: Optional[LogicalOperator] = None
|
||||||
|
conditions: Optional[list[Condition]] = None
|
||||||
|
cases: list[CaseItem]
|
||||||
|
isInIteration: bool
|
||||||
|
|
||||||
|
|
||||||
|
# Example usage
|
||||||
|
if __name__ == "__main__":
|
||||||
|
example_node = IfElseNodeType(
|
||||||
|
title="Example IfElse Node",
|
||||||
|
desc="An if-else node example",
|
||||||
|
type=BlockEnum.if_else,
|
||||||
|
logical_operator=LogicalOperator.and_,
|
||||||
|
conditions=[
|
||||||
|
Condition(
|
||||||
|
id="condition1",
|
||||||
|
varType=VarType.string,
|
||||||
|
variable_selector={"nodeId": "varNode", "key": "value"},
|
||||||
|
comparison_operator=ComparisonOperator.is_,
|
||||||
|
value="exampleValue",
|
||||||
|
)
|
||||||
|
],
|
||||||
|
cases=[
|
||||||
|
CaseItem(
|
||||||
|
case_id="case1",
|
||||||
|
logical_operator=LogicalOperator.or_,
|
||||||
|
conditions=[
|
||||||
|
Condition(
|
||||||
|
id="condition2",
|
||||||
|
varType=VarType.number,
|
||||||
|
value="10",
|
||||||
|
comparison_operator=ComparisonOperator.largerThan,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
isInIteration=True,
|
||||||
|
)
|
||||||
|
print(example_node)
|
||||||
@ -0,0 +1,45 @@
|
|||||||
|
from enum import Enum
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from .common import BlockEnum, CommonNodeType, ValueSelector, VarType
|
||||||
|
|
||||||
|
|
||||||
|
class ErrorHandleMode(str, Enum):
|
||||||
|
"""Error handling modes for iteration."""
|
||||||
|
|
||||||
|
terminated = "terminated"
|
||||||
|
continue_on_error = "continue-on-error"
|
||||||
|
remove_abnormal_output = "remove-abnormal-output"
|
||||||
|
|
||||||
|
|
||||||
|
class IterationNodeType(CommonNodeType):
|
||||||
|
"""Iteration node type implementation."""
|
||||||
|
|
||||||
|
startNodeType: Optional[BlockEnum] = None
|
||||||
|
start_node_id: str # Start node ID in the iteration
|
||||||
|
iteration_id: Optional[str] = None
|
||||||
|
iterator_selector: ValueSelector
|
||||||
|
output_selector: ValueSelector
|
||||||
|
output_type: VarType # Output type
|
||||||
|
is_parallel: bool # Open the parallel mode or not
|
||||||
|
parallel_nums: int # The numbers of parallel
|
||||||
|
error_handle_mode: ErrorHandleMode # How to handle error in the iteration
|
||||||
|
_isShowTips: bool # Show tips when answer node in parallel mode iteration
|
||||||
|
|
||||||
|
|
||||||
|
# 示例用法
|
||||||
|
if __name__ == "__main__":
|
||||||
|
example_node = IterationNodeType(
|
||||||
|
title="Example Iteration Node",
|
||||||
|
desc="An iteration node example",
|
||||||
|
type=BlockEnum.iteration,
|
||||||
|
start_node_id="startNode1",
|
||||||
|
iterator_selector=ValueSelector(value=["iteratorNode", "value"]),
|
||||||
|
output_selector=ValueSelector(value=["outputNode", "value"]),
|
||||||
|
output_type=VarType.string,
|
||||||
|
is_parallel=True,
|
||||||
|
parallel_nums=5,
|
||||||
|
error_handle_mode=ErrorHandleMode.continue_on_error,
|
||||||
|
_isShowTips=True,
|
||||||
|
)
|
||||||
|
print(example_node)
|
||||||
@ -0,0 +1,115 @@
|
|||||||
|
from enum import Enum
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from .common import BlockEnum, CommonNodeType, ModelConfig, ValueSelector
|
||||||
|
|
||||||
|
|
||||||
|
class RetrieveType(str, Enum):
|
||||||
|
"""Retrieval mode types."""
|
||||||
|
|
||||||
|
single = "single"
|
||||||
|
multiple = "multiple"
|
||||||
|
|
||||||
|
|
||||||
|
class RerankingModeEnum(str, Enum):
|
||||||
|
"""Reranking mode types."""
|
||||||
|
|
||||||
|
simple = "simple"
|
||||||
|
advanced = "advanced"
|
||||||
|
|
||||||
|
|
||||||
|
class VectorSetting(BaseModel):
|
||||||
|
"""Vector weight settings."""
|
||||||
|
|
||||||
|
vector_weight: float
|
||||||
|
embedding_provider_name: str
|
||||||
|
embedding_model_name: str
|
||||||
|
|
||||||
|
|
||||||
|
class KeywordSetting(BaseModel):
|
||||||
|
"""Keyword weight settings."""
|
||||||
|
|
||||||
|
keyword_weight: float
|
||||||
|
|
||||||
|
|
||||||
|
class Weights(BaseModel):
|
||||||
|
"""Weight configuration for retrieval."""
|
||||||
|
|
||||||
|
vector_setting: VectorSetting
|
||||||
|
keyword_setting: KeywordSetting
|
||||||
|
|
||||||
|
|
||||||
|
class RerankingModel(BaseModel):
|
||||||
|
"""Reranking model configuration."""
|
||||||
|
|
||||||
|
provider: str
|
||||||
|
model: str
|
||||||
|
|
||||||
|
|
||||||
|
class MultipleRetrievalConfig(BaseModel):
|
||||||
|
"""Configuration for multiple retrieval mode."""
|
||||||
|
|
||||||
|
top_k: int
|
||||||
|
score_threshold: Optional[float] = None
|
||||||
|
reranking_model: Optional[RerankingModel] = None
|
||||||
|
reranking_mode: Optional[RerankingModeEnum] = None
|
||||||
|
weights: Optional[Weights] = None
|
||||||
|
reranking_enable: Optional[bool] = None
|
||||||
|
|
||||||
|
|
||||||
|
class SingleRetrievalConfig(BaseModel):
|
||||||
|
"""Configuration for single retrieval mode."""
|
||||||
|
|
||||||
|
model: ModelConfig
|
||||||
|
|
||||||
|
|
||||||
|
class DataSet(BaseModel):
|
||||||
|
"""Dataset information."""
|
||||||
|
|
||||||
|
id: str
|
||||||
|
name: str
|
||||||
|
description: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class KnowledgeRetrievalNodeType(CommonNodeType):
|
||||||
|
"""Knowledge retrieval node type implementation."""
|
||||||
|
|
||||||
|
query_variable_selector: ValueSelector
|
||||||
|
dataset_ids: list[str]
|
||||||
|
retrieval_mode: RetrieveType
|
||||||
|
multiple_retrieval_config: Optional[MultipleRetrievalConfig] = None
|
||||||
|
single_retrieval_config: Optional[SingleRetrievalConfig] = None
|
||||||
|
_datasets: Optional[list[DataSet]] = None
|
||||||
|
|
||||||
|
|
||||||
|
# Example usage
|
||||||
|
if __name__ == "__main__":
|
||||||
|
example_node = KnowledgeRetrievalNodeType(
|
||||||
|
title="Example Knowledge Retrieval Node",
|
||||||
|
desc="A knowledge retrieval node example",
|
||||||
|
type=BlockEnum.knowledge_retrieval,
|
||||||
|
query_variable_selector=ValueSelector(value=["queryNode", "query"]),
|
||||||
|
dataset_ids=["dataset1", "dataset2"],
|
||||||
|
retrieval_mode=RetrieveType.multiple,
|
||||||
|
multiple_retrieval_config=MultipleRetrievalConfig(
|
||||||
|
top_k=10,
|
||||||
|
score_threshold=0.5,
|
||||||
|
reranking_model=RerankingModel(provider="example_provider", model="example_model"),
|
||||||
|
reranking_mode=RerankingModeEnum.simple,
|
||||||
|
weights=Weights(
|
||||||
|
vector_setting=VectorSetting(
|
||||||
|
vector_weight=0.7, embedding_provider_name="provider1", embedding_model_name="model1"
|
||||||
|
),
|
||||||
|
keyword_setting=KeywordSetting(keyword_weight=0.3),
|
||||||
|
),
|
||||||
|
reranking_enable=True,
|
||||||
|
),
|
||||||
|
single_retrieval_config=SingleRetrievalConfig(
|
||||||
|
model=ModelConfig(
|
||||||
|
provider="example_provider", name="example_model", mode="chat", completion_params={"temperature": 0.7}
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
print(example_node)
|
||||||
@ -0,0 +1,73 @@
|
|||||||
|
from enum import Enum
|
||||||
|
from typing import Optional, Union
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from .common import BlockEnum, CommonNodeType, ValueSelector, VarType
|
||||||
|
|
||||||
|
# Import ComparisonOperator from if_else.py
|
||||||
|
from .if_else import ComparisonOperator
|
||||||
|
|
||||||
|
|
||||||
|
class OrderBy(str, Enum):
|
||||||
|
ASC = "asc"
|
||||||
|
DESC = "desc"
|
||||||
|
|
||||||
|
|
||||||
|
class Limit(BaseModel):
|
||||||
|
enabled: bool
|
||||||
|
size: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
|
class Condition(BaseModel):
|
||||||
|
key: str
|
||||||
|
comparison_operator: ComparisonOperator
|
||||||
|
value: Union[str, int, list[str]]
|
||||||
|
|
||||||
|
|
||||||
|
class FilterBy(BaseModel):
|
||||||
|
enabled: bool
|
||||||
|
conditions: list[Condition]
|
||||||
|
|
||||||
|
|
||||||
|
class ExtractBy(BaseModel):
|
||||||
|
enabled: bool
|
||||||
|
serial: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class OrderByConfig(BaseModel):
|
||||||
|
enabled: bool
|
||||||
|
key: Union[ValueSelector, str]
|
||||||
|
value: OrderBy
|
||||||
|
|
||||||
|
|
||||||
|
class ListFilterNodeType(CommonNodeType):
|
||||||
|
"""List filter/operator node type implementation."""
|
||||||
|
|
||||||
|
variable: ValueSelector
|
||||||
|
var_type: VarType
|
||||||
|
item_var_type: VarType
|
||||||
|
filter_by: FilterBy
|
||||||
|
extract_by: ExtractBy
|
||||||
|
order_by: OrderByConfig
|
||||||
|
limit: Limit
|
||||||
|
|
||||||
|
|
||||||
|
# 示例用法
|
||||||
|
if __name__ == "__main__":
|
||||||
|
example_node = ListFilterNodeType(
|
||||||
|
title="Example List Filter Node",
|
||||||
|
desc="A list filter node example",
|
||||||
|
type=BlockEnum.list_operator, # Fixed: use list_operator instead of list_filter
|
||||||
|
variable=ValueSelector(value=["varNode", "value"]),
|
||||||
|
var_type=VarType.string,
|
||||||
|
item_var_type=VarType.number,
|
||||||
|
filter_by=FilterBy(
|
||||||
|
enabled=True,
|
||||||
|
conditions=[Condition(key="status", comparison_operator=ComparisonOperator.equals, value="active")],
|
||||||
|
),
|
||||||
|
extract_by=ExtractBy(enabled=True, serial="serial_1"),
|
||||||
|
order_by=OrderByConfig(enabled=True, key="created_at", value=OrderBy.DESC),
|
||||||
|
limit=Limit(enabled=True, size=100),
|
||||||
|
)
|
||||||
|
print(example_node)
|
||||||
@ -0,0 +1,66 @@
|
|||||||
|
from typing import Optional, Union
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from .common import (
|
||||||
|
BlockEnum,
|
||||||
|
CommonNodeType,
|
||||||
|
Context,
|
||||||
|
Memory,
|
||||||
|
ModelConfig,
|
||||||
|
PromptItem,
|
||||||
|
Variable,
|
||||||
|
VisionSetting,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PromptConfig(BaseModel):
|
||||||
|
"""Configuration for prompt template variables."""
|
||||||
|
|
||||||
|
jinja2_variables: Optional[list[Variable]] = None
|
||||||
|
|
||||||
|
|
||||||
|
class VisionConfig(BaseModel):
|
||||||
|
"""Configuration for vision settings."""
|
||||||
|
|
||||||
|
enabled: bool = False
|
||||||
|
configs: Optional[VisionSetting] = None
|
||||||
|
|
||||||
|
def dict(self, *args, **kwargs):
|
||||||
|
"""自定义序列化方法,确保正确序列化"""
|
||||||
|
result = {"enabled": self.enabled}
|
||||||
|
|
||||||
|
if self.configs:
|
||||||
|
result["configs"] = self.configs.dict()
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class LLMNodeType(CommonNodeType):
|
||||||
|
"""LLM node type implementation."""
|
||||||
|
|
||||||
|
model: ModelConfig
|
||||||
|
prompt_template: Union[list[PromptItem], PromptItem]
|
||||||
|
prompt_config: Optional[PromptConfig] = None
|
||||||
|
memory: Optional[Memory] = None
|
||||||
|
context: Optional[Context] = Context(enabled=False, variable_selector=None)
|
||||||
|
vision: Optional[VisionConfig] = VisionConfig(enabled=False)
|
||||||
|
|
||||||
|
|
||||||
|
# 示例用法
|
||||||
|
if __name__ == "__main__":
|
||||||
|
example_node = LLMNodeType(
|
||||||
|
title="Example LLM Node",
|
||||||
|
desc="A LLM node example",
|
||||||
|
type=BlockEnum.llm,
|
||||||
|
model=ModelConfig(provider="zhipuai", name="glm-4-flash", mode="chat", completion_params={"temperature": 0.7}),
|
||||||
|
prompt_template=[
|
||||||
|
PromptItem(
|
||||||
|
id="system-id", role="system", text="你是一个代码工程师,你会根据用户的需求给出用户所需要的函数"
|
||||||
|
),
|
||||||
|
PromptItem(id="user-id", role="user", text="给出两数相加的python 函数代码,函数名 func 不要添加其他内容"),
|
||||||
|
],
|
||||||
|
context=Context(enabled=False, variable_selector=None),
|
||||||
|
vision=VisionConfig(enabled=False),
|
||||||
|
)
|
||||||
|
print(example_node)
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
from .common import BlockEnum, CommonNodeType
|
||||||
|
|
||||||
|
# Import previously defined CommonNodeType
|
||||||
|
# Assume it is defined in the same module
|
||||||
|
|
||||||
|
|
||||||
|
class NoteTheme(str, Enum):
|
||||||
|
blue = "blue"
|
||||||
|
cyan = "cyan"
|
||||||
|
green = "green"
|
||||||
|
yellow = "yellow"
|
||||||
|
pink = "pink"
|
||||||
|
violet = "violet"
|
||||||
|
|
||||||
|
|
||||||
|
class NoteNodeType(CommonNodeType):
|
||||||
|
"""Custom note node type implementation."""
|
||||||
|
|
||||||
|
text: str
|
||||||
|
theme: NoteTheme
|
||||||
|
author: str
|
||||||
|
showAuthor: bool
|
||||||
|
|
||||||
|
|
||||||
|
# Example usage
|
||||||
|
if __name__ == "__main__":
|
||||||
|
example_node = NoteNodeType(
|
||||||
|
title="Example Note Node",
|
||||||
|
desc="A note node example",
|
||||||
|
type=BlockEnum.custom_note,
|
||||||
|
text="This is a note.",
|
||||||
|
theme=NoteTheme.green,
|
||||||
|
author="John Doe",
|
||||||
|
showAuthor=True,
|
||||||
|
)
|
||||||
|
print(example_node)
|
||||||
@ -0,0 +1,85 @@
|
|||||||
|
from enum import Enum
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from .common import BlockEnum, CommonNodeType, Memory, ModelConfig, ValueSelector, VisionSetting
|
||||||
|
|
||||||
|
# Import previously defined CommonNodeType, Memory, ModelConfig, ValueSelector, and VisionSetting
|
||||||
|
# Assume they are defined in the same module
|
||||||
|
|
||||||
|
|
||||||
|
class ParamType(str, Enum):
|
||||||
|
"""Parameter types for extraction."""
|
||||||
|
|
||||||
|
string = "string"
|
||||||
|
number = "number"
|
||||||
|
bool = "bool"
|
||||||
|
select = "select"
|
||||||
|
arrayString = "array[string]"
|
||||||
|
arrayNumber = "array[number]"
|
||||||
|
arrayObject = "array[object]"
|
||||||
|
|
||||||
|
|
||||||
|
class Param(BaseModel):
|
||||||
|
"""Parameter definition for extraction."""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
type: ParamType
|
||||||
|
options: Optional[list[str]] = None
|
||||||
|
description: str
|
||||||
|
required: Optional[bool] = None
|
||||||
|
|
||||||
|
|
||||||
|
class ReasoningModeType(str, Enum):
|
||||||
|
"""Reasoning mode types for parameter extraction."""
|
||||||
|
|
||||||
|
prompt = "prompt"
|
||||||
|
functionCall = "function_call"
|
||||||
|
|
||||||
|
|
||||||
|
class VisionConfig(BaseModel):
|
||||||
|
"""Vision configuration."""
|
||||||
|
|
||||||
|
enabled: bool
|
||||||
|
configs: Optional[VisionSetting] = None
|
||||||
|
|
||||||
|
|
||||||
|
class ParameterExtractorNodeType(CommonNodeType):
|
||||||
|
"""Parameter extractor node type implementation."""
|
||||||
|
|
||||||
|
model: ModelConfig
|
||||||
|
query: ValueSelector
|
||||||
|
reasoning_mode: ReasoningModeType
|
||||||
|
parameters: List[Param]
|
||||||
|
instruction: str
|
||||||
|
memory: Optional[Memory] = None
|
||||||
|
vision: VisionConfig
|
||||||
|
|
||||||
|
|
||||||
|
# Example usage
|
||||||
|
if __name__ == "__main__":
|
||||||
|
example_node = ParameterExtractorNodeType(
|
||||||
|
title="Example Parameter Extractor Node",
|
||||||
|
desc="A parameter extractor node example",
|
||||||
|
type=BlockEnum.parameter_extractor,
|
||||||
|
model=ModelConfig(
|
||||||
|
provider="example_provider", name="example_model", mode="chat", completion_params={"temperature": 0.7}
|
||||||
|
),
|
||||||
|
query=ValueSelector(value=["queryNode", "value"]),
|
||||||
|
reasoning_mode=ReasoningModeType.prompt,
|
||||||
|
parameters=[
|
||||||
|
Param(name="param1", type=ParamType.string, description="This is a string parameter", required=True),
|
||||||
|
Param(
|
||||||
|
name="param2",
|
||||||
|
type=ParamType.number,
|
||||||
|
options=["1", "2", "3"],
|
||||||
|
description="This is a number parameter",
|
||||||
|
required=False,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
instruction="Please extract the parameters from the input.",
|
||||||
|
memory=Memory(window={"enabled": True, "size": 10}, query_prompt_template="Extract parameters from: {{query}}"),
|
||||||
|
vision=VisionConfig(enabled=True, configs={"setting": "example_setting"}),
|
||||||
|
)
|
||||||
|
print(example_node)
|
||||||
@ -0,0 +1,51 @@
|
|||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from .common import BlockEnum, CommonNodeType, Memory, ModelConfig, ValueSelector, VisionSetting
|
||||||
|
|
||||||
|
# Import previously defined CommonNodeType, Memory, ModelConfig, ValueSelector, and VisionSetting
|
||||||
|
# Assume they are defined in the same module
|
||||||
|
|
||||||
|
|
||||||
|
class Topic(BaseModel):
|
||||||
|
"""Topic for classification."""
|
||||||
|
|
||||||
|
id: str
|
||||||
|
name: str
|
||||||
|
|
||||||
|
|
||||||
|
class VisionConfig(BaseModel):
|
||||||
|
"""Vision configuration."""
|
||||||
|
|
||||||
|
enabled: bool
|
||||||
|
configs: Optional[VisionSetting] = None
|
||||||
|
|
||||||
|
|
||||||
|
class QuestionClassifierNodeType(CommonNodeType):
|
||||||
|
"""Question classifier node type implementation."""
|
||||||
|
|
||||||
|
query_variable_selector: ValueSelector
|
||||||
|
model: ModelConfig
|
||||||
|
classes: list[Topic]
|
||||||
|
instruction: str
|
||||||
|
memory: Optional[Memory] = None
|
||||||
|
vision: VisionConfig
|
||||||
|
|
||||||
|
|
||||||
|
# Example usage
|
||||||
|
if __name__ == "__main__":
|
||||||
|
example_node = QuestionClassifierNodeType(
|
||||||
|
title="Example Question Classifier Node",
|
||||||
|
desc="A question classifier node example",
|
||||||
|
type=BlockEnum.question_classifier,
|
||||||
|
query_variable_selector=ValueSelector(value=["queryNode", "value"]),
|
||||||
|
model=ModelConfig(
|
||||||
|
provider="example_provider", name="example_model", mode="chat", completion_params={"temperature": 0.7}
|
||||||
|
),
|
||||||
|
classes=[Topic(id="1", name="Science"), Topic(id="2", name="Mathematics"), Topic(id="3", name="Literature")],
|
||||||
|
instruction="Classify the given question into the appropriate topic.",
|
||||||
|
memory=Memory(window={"enabled": True, "size": 10}, query_prompt_template="Classify this question: {{query}}"),
|
||||||
|
vision=VisionConfig(enabled=True, configs={"setting": "example_setting"}),
|
||||||
|
)
|
||||||
|
print(example_node)
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
from .common import BlockEnum, CommonNodeType, InputVar
|
||||||
|
|
||||||
|
# Import previously defined CommonNodeType and InputVar
|
||||||
|
# Assume they are defined in the same module
|
||||||
|
|
||||||
|
|
||||||
|
class StartNodeType(CommonNodeType):
|
||||||
|
variables: list[InputVar]
|
||||||
|
|
||||||
|
|
||||||
|
# Example usage
|
||||||
|
if __name__ == "__main__":
|
||||||
|
example_node = StartNodeType(
|
||||||
|
title="Example Start Node",
|
||||||
|
desc="A start node example",
|
||||||
|
type=BlockEnum.start,
|
||||||
|
variables=[
|
||||||
|
InputVar(type="text-input", label="Input 1", variable="input1", required=True),
|
||||||
|
InputVar(type="number", label="Input 2", variable="input2", required=True),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
print(example_node)
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
from .common import BlockEnum, CommonNodeType, Variable
|
||||||
|
|
||||||
|
# 引入之前定义的 CommonNodeType 和 Variable
|
||||||
|
# 假设它们在同一模块中定义
|
||||||
|
|
||||||
|
|
||||||
|
class TemplateTransformNodeType(CommonNodeType):
|
||||||
|
"""Template transform node type implementation."""
|
||||||
|
|
||||||
|
variables: list[Variable]
|
||||||
|
template: str
|
||||||
|
|
||||||
|
|
||||||
|
# 示例用法
|
||||||
|
if __name__ == "__main__":
|
||||||
|
example_node = TemplateTransformNodeType(
|
||||||
|
title="Example Template Transform Node",
|
||||||
|
desc="A template transform node example",
|
||||||
|
type=BlockEnum.template_transform,
|
||||||
|
variables=[
|
||||||
|
Variable(variable="var1", value_selector=["node1", "key1"]),
|
||||||
|
Variable(variable="var2", value_selector=["node2", "key2"]),
|
||||||
|
],
|
||||||
|
template="Hello, {{ var1 }}! You have {{ var2 }} new messages.",
|
||||||
|
)
|
||||||
|
print(example_node)
|
||||||
@ -0,0 +1,54 @@
|
|||||||
|
from enum import Enum
|
||||||
|
from typing import Any, Optional, Union
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from .common import BlockEnum, CommonNodeType, ValueSelector
|
||||||
|
|
||||||
|
# Import previously defined CommonNodeType and ValueSelector
|
||||||
|
# Assume they are defined in the same module
|
||||||
|
|
||||||
|
|
||||||
|
class VarType(str, Enum):
|
||||||
|
variable = "variable"
|
||||||
|
constant = "constant"
|
||||||
|
mixed = "mixed"
|
||||||
|
|
||||||
|
|
||||||
|
class ToolVarInputs(BaseModel):
|
||||||
|
type: VarType
|
||||||
|
value: Optional[Union[str, ValueSelector, Any]] = None
|
||||||
|
|
||||||
|
|
||||||
|
class ToolNodeType(CommonNodeType):
|
||||||
|
"""Tool node type implementation."""
|
||||||
|
|
||||||
|
provider_id: str
|
||||||
|
provider_type: Any # Placeholder for CollectionType
|
||||||
|
provider_name: str
|
||||||
|
tool_name: str
|
||||||
|
tool_label: str
|
||||||
|
tool_parameters: dict[str, ToolVarInputs]
|
||||||
|
tool_configurations: dict[str, Any]
|
||||||
|
output_schema: dict[str, Any]
|
||||||
|
|
||||||
|
|
||||||
|
# Example usage
|
||||||
|
if __name__ == "__main__":
|
||||||
|
example_node = ToolNodeType(
|
||||||
|
title="Example Tool Node",
|
||||||
|
desc="A tool node example",
|
||||||
|
type=BlockEnum.tool,
|
||||||
|
provider_id="12345",
|
||||||
|
provider_type="some_collection_type", # Placeholder for CollectionType
|
||||||
|
provider_name="Example Provider",
|
||||||
|
tool_name="Example Tool",
|
||||||
|
tool_label="Example Tool Label",
|
||||||
|
tool_parameters={
|
||||||
|
"input1": ToolVarInputs(type=VarType.variable, value="some_value"),
|
||||||
|
"input2": ToolVarInputs(type=VarType.constant, value="constant_value"),
|
||||||
|
},
|
||||||
|
tool_configurations={"config1": "value1", "config2": {"nested": "value2"}},
|
||||||
|
output_schema={"output1": "string", "output2": "number"},
|
||||||
|
)
|
||||||
|
print(example_node.json(indent=2)) # Print as JSON format for viewing
|
||||||
@ -0,0 +1,56 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from .common import BlockEnum, CommonNodeType, ValueSelector, VarType
|
||||||
|
|
||||||
|
|
||||||
|
class VarGroupItem(BaseModel):
|
||||||
|
"""Variable group item configuration."""
|
||||||
|
|
||||||
|
output_type: VarType
|
||||||
|
variables: list[ValueSelector]
|
||||||
|
|
||||||
|
|
||||||
|
class GroupConfig(VarGroupItem):
|
||||||
|
"""Group configuration for advanced settings."""
|
||||||
|
|
||||||
|
group_name: str
|
||||||
|
groupId: str
|
||||||
|
|
||||||
|
|
||||||
|
class AdvancedSettings(BaseModel):
|
||||||
|
"""Advanced settings for variable assigner."""
|
||||||
|
|
||||||
|
group_enabled: bool
|
||||||
|
groups: list[GroupConfig]
|
||||||
|
|
||||||
|
|
||||||
|
class VariableAssignerNodeType(CommonNodeType, VarGroupItem):
|
||||||
|
"""Variable assigner node type implementation."""
|
||||||
|
|
||||||
|
advanced_settings: AdvancedSettings
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
arbitrary_types_allowed = True
|
||||||
|
|
||||||
|
|
||||||
|
# Example usage
|
||||||
|
if __name__ == "__main__":
|
||||||
|
example_node = VariableAssignerNodeType(
|
||||||
|
title="Example Variable Assigner Node",
|
||||||
|
desc="A variable assigner node example",
|
||||||
|
type=BlockEnum.variable_assigner,
|
||||||
|
output_type=VarType.string,
|
||||||
|
variables=[ValueSelector(value=["varNode1", "value1"]), ValueSelector(value=["varNode2", "value2"])],
|
||||||
|
advanced_settings=AdvancedSettings(
|
||||||
|
group_enabled=True,
|
||||||
|
groups=[
|
||||||
|
GroupConfig(
|
||||||
|
group_name="Group 1",
|
||||||
|
groupId="group1",
|
||||||
|
output_type=VarType.number,
|
||||||
|
variables=[ValueSelector(value=["varNode3", "value3"])],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
print(example_node.json(indent=2)) # Print as JSON format for viewing
|
||||||
@ -0,0 +1,262 @@
|
|||||||
|
app:
|
||||||
|
description: ''
|
||||||
|
icon: 🤖
|
||||||
|
icon_background: '#FFEAD5'
|
||||||
|
mode: workflow
|
||||||
|
name: 文本分析工作流
|
||||||
|
use_icon_as_answer_icon: false
|
||||||
|
kind: app
|
||||||
|
version: 0.1.2
|
||||||
|
workflow:
|
||||||
|
conversation_variables: []
|
||||||
|
environment_variables: []
|
||||||
|
features:
|
||||||
|
file_upload:
|
||||||
|
allowed_file_extensions:
|
||||||
|
- .JPG
|
||||||
|
- .JPEG
|
||||||
|
- .PNG
|
||||||
|
- .GIF
|
||||||
|
- .WEBP
|
||||||
|
- .SVG
|
||||||
|
allowed_file_types:
|
||||||
|
- image
|
||||||
|
allowed_file_upload_methods:
|
||||||
|
- local_file
|
||||||
|
- remote_url
|
||||||
|
enabled: false
|
||||||
|
fileUploadConfig:
|
||||||
|
audio_file_size_limit: 50
|
||||||
|
batch_count_limit: 5
|
||||||
|
file_size_limit: 15
|
||||||
|
image_file_size_limit: 10
|
||||||
|
video_file_size_limit: 100
|
||||||
|
image:
|
||||||
|
enabled: false
|
||||||
|
number_limits: 3
|
||||||
|
transfer_methods:
|
||||||
|
- local_file
|
||||||
|
- remote_url
|
||||||
|
number_limits: 3
|
||||||
|
opening_statement: ''
|
||||||
|
retriever_resource:
|
||||||
|
enabled: true
|
||||||
|
sensitive_word_avoidance:
|
||||||
|
enabled: false
|
||||||
|
speech_to_text:
|
||||||
|
enabled: false
|
||||||
|
suggested_questions: []
|
||||||
|
suggested_questions_after_answer:
|
||||||
|
enabled: false
|
||||||
|
text_to_speech:
|
||||||
|
enabled: false
|
||||||
|
language: ''
|
||||||
|
voice: ''
|
||||||
|
graph:
|
||||||
|
edges:
|
||||||
|
- id: 1740019130520-source-1740019130521-target
|
||||||
|
source: '1740019130520'
|
||||||
|
sourceHandle: source
|
||||||
|
target: '1740019130521'
|
||||||
|
targetHandle: target
|
||||||
|
type: custom
|
||||||
|
zIndex: 0
|
||||||
|
data:
|
||||||
|
isInIteration: false
|
||||||
|
sourceType: start
|
||||||
|
targetType: llm
|
||||||
|
- id: 1740019130520-source-1740019130522-target
|
||||||
|
source: '1740019130520'
|
||||||
|
sourceHandle: source
|
||||||
|
target: '1740019130522'
|
||||||
|
targetHandle: target
|
||||||
|
type: custom
|
||||||
|
zIndex: 0
|
||||||
|
data:
|
||||||
|
isInIteration: false
|
||||||
|
sourceType: start
|
||||||
|
targetType: code
|
||||||
|
- id: 1740019130521-source-1740019130523-target
|
||||||
|
source: '1740019130521'
|
||||||
|
sourceHandle: source
|
||||||
|
target: '1740019130523'
|
||||||
|
targetHandle: target
|
||||||
|
type: custom
|
||||||
|
zIndex: 0
|
||||||
|
data:
|
||||||
|
isInIteration: false
|
||||||
|
sourceType: llm
|
||||||
|
targetType: template-transform
|
||||||
|
- id: 1740019130522-source-1740019130523-target
|
||||||
|
source: '1740019130522'
|
||||||
|
sourceHandle: source
|
||||||
|
target: '1740019130523'
|
||||||
|
targetHandle: target
|
||||||
|
type: custom
|
||||||
|
zIndex: 0
|
||||||
|
data:
|
||||||
|
isInIteration: false
|
||||||
|
sourceType: code
|
||||||
|
targetType: template-transform
|
||||||
|
- id: 1740019130523-source-1740019130524-target
|
||||||
|
source: '1740019130523'
|
||||||
|
sourceHandle: source
|
||||||
|
target: '1740019130524'
|
||||||
|
targetHandle: target
|
||||||
|
type: custom
|
||||||
|
zIndex: 0
|
||||||
|
data:
|
||||||
|
isInIteration: false
|
||||||
|
sourceType: template-transform
|
||||||
|
targetType: end
|
||||||
|
nodes:
|
||||||
|
- id: '1740019130520'
|
||||||
|
position:
|
||||||
|
x: 80
|
||||||
|
y: 282
|
||||||
|
height: 116
|
||||||
|
width: 244
|
||||||
|
positionAbsolute:
|
||||||
|
x: 80
|
||||||
|
y: 282
|
||||||
|
selected: false
|
||||||
|
sourcePosition: right
|
||||||
|
targetPosition: left
|
||||||
|
type: custom
|
||||||
|
data:
|
||||||
|
title: 开始节点
|
||||||
|
desc: 接收用户输入的文本参数
|
||||||
|
type: start
|
||||||
|
variables:
|
||||||
|
- type: text-input
|
||||||
|
label: user_text
|
||||||
|
variable: user_text
|
||||||
|
required: true
|
||||||
|
max_length: 48
|
||||||
|
options: []
|
||||||
|
- id: '1740019130521'
|
||||||
|
position:
|
||||||
|
x: 380
|
||||||
|
y: 282
|
||||||
|
height: 98
|
||||||
|
width: 244
|
||||||
|
positionAbsolute:
|
||||||
|
x: 380
|
||||||
|
y: 282
|
||||||
|
selected: false
|
||||||
|
sourcePosition: right
|
||||||
|
targetPosition: left
|
||||||
|
type: custom
|
||||||
|
data:
|
||||||
|
title: LLM节点
|
||||||
|
desc: 使用大语言模型进行情感分析,返回文本的情感结果
|
||||||
|
type: llm
|
||||||
|
model:
|
||||||
|
provider: zhipuai
|
||||||
|
name: glm-4-flash
|
||||||
|
mode: chat
|
||||||
|
completion_params:
|
||||||
|
temperature: 0.7
|
||||||
|
prompt_template:
|
||||||
|
- id: 1740019130521-system
|
||||||
|
text: 请分析以下文本的情感,返回积极、消极或中性
|
||||||
|
role: system
|
||||||
|
- id: 1740019130521-user
|
||||||
|
text: '{{user_text}}'
|
||||||
|
role: user
|
||||||
|
context:
|
||||||
|
enabled: false
|
||||||
|
variable_selector: []
|
||||||
|
vision:
|
||||||
|
enabled: false
|
||||||
|
- id: '1740019130522'
|
||||||
|
position:
|
||||||
|
x: 680
|
||||||
|
y: 282
|
||||||
|
height: 54
|
||||||
|
width: 244
|
||||||
|
positionAbsolute:
|
||||||
|
x: 680
|
||||||
|
y: 282
|
||||||
|
selected: false
|
||||||
|
sourcePosition: right
|
||||||
|
targetPosition: left
|
||||||
|
type: custom
|
||||||
|
data:
|
||||||
|
title: 代码节点
|
||||||
|
desc: 计算文本的统计信息,包括字符数、单词数和句子数
|
||||||
|
type: code
|
||||||
|
variables:
|
||||||
|
- variable: text_for_analysis
|
||||||
|
value_selector:
|
||||||
|
- '1740019130520'
|
||||||
|
- user_text
|
||||||
|
code_language: python3
|
||||||
|
code: "import re\n\ndef main(text):\n char_count = len(text)\n word_count\
|
||||||
|
\ = len(text.split())\n sentence_count = len(re.findall(r'[.!?]', text))\n\
|
||||||
|
\ return {'char_count': char_count, 'word_count': word_count, 'sentence_count':\
|
||||||
|
\ sentence_count}"
|
||||||
|
outputs:
|
||||||
|
text_statistics:
|
||||||
|
type: object
|
||||||
|
- id: '1740019130523'
|
||||||
|
position:
|
||||||
|
x: 980
|
||||||
|
y: 282
|
||||||
|
height: 54
|
||||||
|
width: 244
|
||||||
|
positionAbsolute:
|
||||||
|
x: 980
|
||||||
|
y: 282
|
||||||
|
selected: false
|
||||||
|
sourcePosition: right
|
||||||
|
targetPosition: left
|
||||||
|
type: custom
|
||||||
|
data:
|
||||||
|
title: 模板节点
|
||||||
|
desc: 将情感分析结果和统计信息组合成格式化报告
|
||||||
|
type: template-transform
|
||||||
|
variables:
|
||||||
|
- variable: sentiment_result
|
||||||
|
value_selector:
|
||||||
|
- '1740019130521'
|
||||||
|
- sentiment_result
|
||||||
|
- variable: text_statistics
|
||||||
|
value_selector:
|
||||||
|
- '1740019130522'
|
||||||
|
- text_statistics
|
||||||
|
template: '情感分析结果:{{sentiment_result}}
|
||||||
|
|
||||||
|
文本统计信息:
|
||||||
|
|
||||||
|
字符数:{{text_statistics.char_count}}
|
||||||
|
|
||||||
|
单词数:{{text_statistics.word_count}}
|
||||||
|
|
||||||
|
句子数:{{text_statistics.sentence_count}}'
|
||||||
|
- id: '1740019130524'
|
||||||
|
position:
|
||||||
|
x: 1280
|
||||||
|
y: 282
|
||||||
|
height: 90
|
||||||
|
width: 244
|
||||||
|
positionAbsolute:
|
||||||
|
x: 1280
|
||||||
|
y: 282
|
||||||
|
selected: false
|
||||||
|
sourcePosition: right
|
||||||
|
targetPosition: left
|
||||||
|
type: custom
|
||||||
|
data:
|
||||||
|
title: 结束节点
|
||||||
|
desc: 返回最终的分析报告
|
||||||
|
type: end
|
||||||
|
outputs:
|
||||||
|
- variable: final_report
|
||||||
|
value_selector:
|
||||||
|
- '1740019130523'
|
||||||
|
- output
|
||||||
|
viewport:
|
||||||
|
x: 92.96659905656679
|
||||||
|
y: 79.13437154762897
|
||||||
|
zoom: 0.9002006986311041
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
"""
|
||||||
|
工作流生成器包
|
||||||
|
用于根据用户需求生成Dify工作流
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .workflow_generator import WorkflowGenerator
|
||||||
|
|
||||||
|
__all__ = ["WorkflowGenerator"]
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
"""
|
||||||
|
节点和边生成器包
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .edge_generator import EdgeGenerator
|
||||||
|
from .layout_engine import LayoutEngine
|
||||||
|
from .node_generator import NodeGenerator
|
||||||
|
|
||||||
|
__all__ = ["EdgeGenerator", "LayoutEngine", "NodeGenerator"]
|
||||||
@ -0,0 +1,101 @@
|
|||||||
|
"""
|
||||||
|
Edge Generator
|
||||||
|
Used to generate edges in the workflow
|
||||||
|
"""
|
||||||
|
|
||||||
|
from core.auto.node_types.common import CommonEdgeType, CompleteEdge, CompleteNode
|
||||||
|
from core.auto.workflow_generator.models.workflow_description import ConnectionDescription
|
||||||
|
|
||||||
|
|
||||||
|
class EdgeGenerator:
|
||||||
|
"""Edge generator for creating workflow edges"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_edges(nodes: list[CompleteNode], connections: list[ConnectionDescription]) -> list[CompleteEdge]:
|
||||||
|
"""
|
||||||
|
Create edges based on nodes and connection information
|
||||||
|
|
||||||
|
Args:
|
||||||
|
nodes: list of nodes
|
||||||
|
connections: list of connection descriptions
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list of edges
|
||||||
|
"""
|
||||||
|
edges = []
|
||||||
|
|
||||||
|
# If connection information is provided, create edges based on it
|
||||||
|
if connections:
|
||||||
|
for connection in connections:
|
||||||
|
source_id = connection.source
|
||||||
|
target_id = connection.target
|
||||||
|
|
||||||
|
if not source_id or not target_id:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Find source and target nodes
|
||||||
|
source_node = next((node for node in nodes if node.id == source_id), None)
|
||||||
|
target_node = next((node for node in nodes if node.id == target_id), None)
|
||||||
|
|
||||||
|
if not source_node or not target_node:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Get node types
|
||||||
|
source_type = source_node.data.type
|
||||||
|
target_type = target_node.data.type
|
||||||
|
|
||||||
|
# Create edge
|
||||||
|
edge_id = f"{source_id}-source-{target_id}-target"
|
||||||
|
|
||||||
|
# Create edge data
|
||||||
|
edge_data = CommonEdgeType(isInIteration=False, sourceType=source_type, targetType=target_type)
|
||||||
|
|
||||||
|
# Create complete edge
|
||||||
|
edge = CompleteEdge(
|
||||||
|
id=edge_id,
|
||||||
|
source=source_id,
|
||||||
|
sourceHandle="source",
|
||||||
|
target=target_id,
|
||||||
|
targetHandle="target",
|
||||||
|
type="custom",
|
||||||
|
zIndex=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add edge data
|
||||||
|
edge.add_data(edge_data)
|
||||||
|
|
||||||
|
edges.append(edge)
|
||||||
|
# If no connection information is provided, automatically create edges
|
||||||
|
else:
|
||||||
|
# Create edges based on node order
|
||||||
|
for i in range(len(nodes) - 1):
|
||||||
|
source_node = nodes[i]
|
||||||
|
target_node = nodes[i + 1]
|
||||||
|
|
||||||
|
# Get node types
|
||||||
|
source_type = source_node.data.type
|
||||||
|
target_type = target_node.data.type
|
||||||
|
|
||||||
|
# Create edge
|
||||||
|
edge_id = f"{source_node.id}-source-{target_node.id}-target"
|
||||||
|
|
||||||
|
# Create edge data
|
||||||
|
edge_data = CommonEdgeType(isInIteration=False, sourceType=source_type, targetType=target_type)
|
||||||
|
|
||||||
|
# Create complete edge
|
||||||
|
edge = CompleteEdge(
|
||||||
|
id=edge_id,
|
||||||
|
source=source_node.id,
|
||||||
|
sourceHandle="source",
|
||||||
|
target=target_node.id,
|
||||||
|
targetHandle="target",
|
||||||
|
type="custom",
|
||||||
|
zIndex=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add edge data
|
||||||
|
edge.add_data(edge_data)
|
||||||
|
|
||||||
|
edges.append(edge)
|
||||||
|
|
||||||
|
return edges
|
||||||
@ -0,0 +1,77 @@
|
|||||||
|
"""
|
||||||
|
Layout Engine
|
||||||
|
Used to arrange the positions of nodes and edges
|
||||||
|
"""
|
||||||
|
|
||||||
|
from core.auto.node_types.common import CompleteEdge, CompleteNode
|
||||||
|
|
||||||
|
|
||||||
|
class LayoutEngine:
|
||||||
|
"""Layout engine"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def apply_layout(nodes: list[CompleteNode]) -> None:
|
||||||
|
"""
|
||||||
|
Apply layout, arranging nodes in a row
|
||||||
|
|
||||||
|
Args:
|
||||||
|
nodes: list of nodes
|
||||||
|
"""
|
||||||
|
# Simple linear layout, arranging nodes from left to right
|
||||||
|
x_position = 80
|
||||||
|
y_position = 282
|
||||||
|
|
||||||
|
for node in nodes:
|
||||||
|
node.position = {"x": x_position, "y": y_position}
|
||||||
|
node.positionAbsolute = {"x": x_position, "y": y_position}
|
||||||
|
|
||||||
|
# Update position for the next node
|
||||||
|
x_position += 300 # Horizontal spacing between nodes
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def apply_topological_layout(nodes: list[CompleteNode], edges: list[CompleteEdge]) -> None:
|
||||||
|
"""
|
||||||
|
Apply topological sort layout, arranging nodes based on their dependencies
|
||||||
|
|
||||||
|
Args:
|
||||||
|
nodes: list of nodes
|
||||||
|
edges: list of edges
|
||||||
|
"""
|
||||||
|
# Create mapping from node ID to node
|
||||||
|
node_map = {node.id: node for node in nodes}
|
||||||
|
|
||||||
|
# Create adjacency list
|
||||||
|
adjacency_list = {node.id: [] for node in nodes}
|
||||||
|
for edge in edges:
|
||||||
|
adjacency_list[edge.source].append(edge.target)
|
||||||
|
|
||||||
|
# Create in-degree table
|
||||||
|
in_degree = {node.id: 0 for node in nodes}
|
||||||
|
for source, targets in adjacency_list.items():
|
||||||
|
for target in targets:
|
||||||
|
in_degree[target] += 1
|
||||||
|
|
||||||
|
# Topological sort
|
||||||
|
queue = [node_id for node_id, degree in in_degree.items() if degree == 0]
|
||||||
|
sorted_nodes = []
|
||||||
|
|
||||||
|
while queue:
|
||||||
|
current = queue.pop(0)
|
||||||
|
sorted_nodes.append(current)
|
||||||
|
|
||||||
|
for neighbor in adjacency_list[current]:
|
||||||
|
in_degree[neighbor] -= 1
|
||||||
|
if in_degree[neighbor] == 0:
|
||||||
|
queue.append(neighbor)
|
||||||
|
|
||||||
|
# Apply layout
|
||||||
|
x_position = 80
|
||||||
|
y_position = 282
|
||||||
|
|
||||||
|
for node_id in sorted_nodes:
|
||||||
|
node = node_map[node_id]
|
||||||
|
node.position = {"x": x_position, "y": y_position}
|
||||||
|
node.positionAbsolute = {"x": x_position, "y": y_position}
|
||||||
|
|
||||||
|
# Update position for the next node
|
||||||
|
x_position += 300 # Horizontal spacing between nodes
|
||||||
@ -0,0 +1,446 @@
|
|||||||
|
"""
|
||||||
|
Node Generator
|
||||||
|
Generate nodes based on workflow description
|
||||||
|
"""
|
||||||
|
|
||||||
|
from core.auto.node_types.code import CodeLanguage, CodeNodeType, OutputVar
|
||||||
|
from core.auto.node_types.common import (
|
||||||
|
BlockEnum,
|
||||||
|
CompleteNode,
|
||||||
|
Context,
|
||||||
|
InputVar,
|
||||||
|
ModelConfig,
|
||||||
|
PromptItem,
|
||||||
|
PromptRole,
|
||||||
|
ValueSelector,
|
||||||
|
Variable,
|
||||||
|
)
|
||||||
|
from core.auto.node_types.end import EndNodeType
|
||||||
|
from core.auto.node_types.llm import LLMNodeType, VisionConfig
|
||||||
|
from core.auto.node_types.start import StartNodeType
|
||||||
|
from core.auto.node_types.template_transform import TemplateTransformNodeType
|
||||||
|
from core.auto.workflow_generator.models.workflow_description import NodeDescription
|
||||||
|
from core.auto.workflow_generator.utils.prompts import DEFAULT_MODEL_CONFIG, DEFAULT_SYSTEM_PROMPT
|
||||||
|
from core.auto.workflow_generator.utils.type_mapper import map_string_to_var_type, map_var_type_to_input_type
|
||||||
|
|
||||||
|
|
||||||
|
class NodeGenerator:
|
||||||
|
"""Node generator for creating workflow nodes"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_nodes(node_descriptions: list[NodeDescription]) -> list[CompleteNode]:
|
||||||
|
"""
|
||||||
|
Create nodes based on node descriptions
|
||||||
|
|
||||||
|
Args:
|
||||||
|
node_descriptions: list of node descriptions
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list of nodes
|
||||||
|
"""
|
||||||
|
nodes = []
|
||||||
|
|
||||||
|
for node_desc in node_descriptions:
|
||||||
|
node_type = node_desc.type
|
||||||
|
|
||||||
|
if node_type == "start":
|
||||||
|
node = NodeGenerator._create_start_node(node_desc)
|
||||||
|
elif node_type == "llm":
|
||||||
|
node = NodeGenerator._create_llm_node(node_desc)
|
||||||
|
elif node_type == "code":
|
||||||
|
node = NodeGenerator._create_code_node(node_desc)
|
||||||
|
elif node_type == "template":
|
||||||
|
node = NodeGenerator._create_template_node(node_desc)
|
||||||
|
elif node_type == "end":
|
||||||
|
node = NodeGenerator._create_end_node(node_desc)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unsupported node type: {node_type}")
|
||||||
|
|
||||||
|
nodes.append(node)
|
||||||
|
|
||||||
|
return nodes
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _create_start_node(node_desc: NodeDescription) -> CompleteNode:
|
||||||
|
"""Create start node"""
|
||||||
|
variables = []
|
||||||
|
|
||||||
|
for var in node_desc.variables or []:
|
||||||
|
input_var = InputVar(
|
||||||
|
type=map_var_type_to_input_type(var.type),
|
||||||
|
label=var.name,
|
||||||
|
variable=var.name,
|
||||||
|
required=var.required,
|
||||||
|
max_length=48,
|
||||||
|
options=[],
|
||||||
|
)
|
||||||
|
variables.append(input_var)
|
||||||
|
|
||||||
|
start_node = StartNodeType(
|
||||||
|
title=node_desc.title, desc=node_desc.description or "", type=BlockEnum.start, variables=variables
|
||||||
|
)
|
||||||
|
|
||||||
|
return CompleteNode(
|
||||||
|
id=node_desc.id,
|
||||||
|
type="custom",
|
||||||
|
position={"x": 0, "y": 0}, # Temporary position, will be updated later
|
||||||
|
height=118, # Increase height to match reference file
|
||||||
|
width=244,
|
||||||
|
positionAbsolute={"x": 0, "y": 0},
|
||||||
|
selected=False,
|
||||||
|
sourcePosition="right",
|
||||||
|
targetPosition="left",
|
||||||
|
data=start_node,
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _create_llm_node(node_desc: NodeDescription) -> CompleteNode:
|
||||||
|
"""Create LLM node"""
|
||||||
|
# Build prompt template
|
||||||
|
prompt_template = []
|
||||||
|
|
||||||
|
# Add system prompt
|
||||||
|
system_prompt = node_desc.system_prompt or DEFAULT_SYSTEM_PROMPT
|
||||||
|
prompt_template.append(PromptItem(id=f"{node_desc.id}-system", role=PromptRole.system, text=system_prompt))
|
||||||
|
|
||||||
|
# Add user prompt
|
||||||
|
user_prompt = node_desc.user_prompt or "Please answer these questions:"
|
||||||
|
|
||||||
|
# Build variable list
|
||||||
|
variables = []
|
||||||
|
for var in node_desc.variables or []:
|
||||||
|
source_node = var.source_node or ""
|
||||||
|
source_variable = var.source_variable or ""
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"DEBUG: Processing variable {var.name}, source_node={source_node}, source_variable={source_variable}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# If source node is an LLM node, ensure source_variable is 'text'
|
||||||
|
if source_node:
|
||||||
|
# Check if the source node is an LLM node by checking connections
|
||||||
|
# This is a simple heuristic - if the source node is connected to a node with 'llm' in its ID
|
||||||
|
# or if the source node has 'llm' in its ID, assume it's an LLM node
|
||||||
|
if "llm" in source_node.lower():
|
||||||
|
print(f"DEBUG: Found LLM node {source_node}")
|
||||||
|
if source_variable != "text":
|
||||||
|
old_var = source_variable
|
||||||
|
source_variable = "text" # LLM nodes output variable is always 'text'
|
||||||
|
print(
|
||||||
|
f"Auto-fixing: Changed source variable from '{old_var}' to 'text' for LLM node {source_node}" # noqa: E501
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if the user prompt already contains correctly formatted variable references
|
||||||
|
# Variable references in LLM nodes should be in the format {{#nodeID.variableName#}}
|
||||||
|
correct_format = f"{{{{#{source_node}.{source_variable}#}}}}"
|
||||||
|
simple_format = f"{{{{{var.name}}}}}"
|
||||||
|
|
||||||
|
# If simple format is used in the prompt, replace it with the correct format
|
||||||
|
if simple_format in user_prompt and source_node and source_variable:
|
||||||
|
user_prompt = user_prompt.replace(simple_format, correct_format)
|
||||||
|
|
||||||
|
variable = Variable(variable=var.name, value_selector=[source_node, source_variable])
|
||||||
|
variables.append(variable)
|
||||||
|
|
||||||
|
# Update user prompt
|
||||||
|
prompt_template.append(PromptItem(id=f"{node_desc.id}-user", role=PromptRole.user, text=user_prompt))
|
||||||
|
|
||||||
|
# Use default model configuration, prioritize configuration in node description
|
||||||
|
provider = node_desc.provider or DEFAULT_MODEL_CONFIG["provider"]
|
||||||
|
model = node_desc.model or DEFAULT_MODEL_CONFIG["model"]
|
||||||
|
|
||||||
|
llm_node = LLMNodeType(
|
||||||
|
title=node_desc.title,
|
||||||
|
desc=node_desc.description or "",
|
||||||
|
type=BlockEnum.llm,
|
||||||
|
model=ModelConfig(
|
||||||
|
provider=provider,
|
||||||
|
name=model,
|
||||||
|
mode=DEFAULT_MODEL_CONFIG["mode"],
|
||||||
|
completion_params=DEFAULT_MODEL_CONFIG["completion_params"],
|
||||||
|
),
|
||||||
|
prompt_template=prompt_template,
|
||||||
|
variables=variables,
|
||||||
|
context=Context(enabled=False, variable_selector=ValueSelector(value=[])),
|
||||||
|
vision=VisionConfig(enabled=False),
|
||||||
|
)
|
||||||
|
|
||||||
|
return CompleteNode(
|
||||||
|
id=node_desc.id,
|
||||||
|
type="custom",
|
||||||
|
position={"x": 0, "y": 0}, # Temporary position, will be updated later
|
||||||
|
height=126, # Increase height to match reference file
|
||||||
|
width=244,
|
||||||
|
positionAbsolute={"x": 0, "y": 0},
|
||||||
|
selected=False,
|
||||||
|
sourcePosition="right",
|
||||||
|
targetPosition="left",
|
||||||
|
data=llm_node,
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _create_code_node(node_desc: NodeDescription) -> CompleteNode:
|
||||||
|
"""Create code node"""
|
||||||
|
# Build variable list and function parameter names
|
||||||
|
variables = []
|
||||||
|
var_names = []
|
||||||
|
var_mapping = {} # Used to store mapping from variable names to function parameter names
|
||||||
|
|
||||||
|
# First, identify all LLM nodes in the workflow
|
||||||
|
llm_nodes = set()
|
||||||
|
for connection in node_desc.workflow_description.connections:
|
||||||
|
for node in node_desc.workflow_description.nodes:
|
||||||
|
if node.id == connection.source and node.type.lower() == "llm":
|
||||||
|
llm_nodes.add(node.id)
|
||||||
|
|
||||||
|
for var in node_desc.variables or []:
|
||||||
|
source_node = var.source_node or ""
|
||||||
|
source_variable = var.source_variable or ""
|
||||||
|
|
||||||
|
# Check if source node is an LLM node and warn if source_variable is not 'text'
|
||||||
|
if source_node in llm_nodes and source_variable != "text":
|
||||||
|
print(
|
||||||
|
f"WARNING: LLM node {source_node} output variable should be 'text', but got '{source_variable}'. This may cause issues in Dify." # noqa: E501
|
||||||
|
)
|
||||||
|
print(" Consider changing the source_variable to 'text' in your workflow description.")
|
||||||
|
# Auto-fix: Always use 'text' as the source variable for LLM nodes
|
||||||
|
old_var = source_variable
|
||||||
|
source_variable = "text"
|
||||||
|
print(f"Auto-fixing: Changed source variable from '{old_var}' to 'text' for LLM node {source_node}")
|
||||||
|
elif source_node and "llm" in source_node.lower() and source_variable != "text":
|
||||||
|
# Fallback heuristic check based on node ID
|
||||||
|
print(
|
||||||
|
f"WARNING: Node {source_node} appears to be an LLM node based on its ID, but source_variable is not 'text'." # noqa: E501
|
||||||
|
)
|
||||||
|
print(" Consider changing the source_variable to 'text' in your workflow description.")
|
||||||
|
# Auto-fix: Always use 'text' as the source variable for LLM nodes
|
||||||
|
old_var = source_variable
|
||||||
|
source_variable = "text"
|
||||||
|
print(f"Auto-fixing: Changed source variable from '{old_var}' to 'text' for LLM node {source_node}")
|
||||||
|
|
||||||
|
# Use variable name as function parameter name
|
||||||
|
variable_name = var.name # Variable name defined in this node
|
||||||
|
param_name = variable_name # Function parameter name must match variable name
|
||||||
|
|
||||||
|
# Validate variable name format
|
||||||
|
if not variable_name.replace("_", "").isalnum():
|
||||||
|
raise ValueError(
|
||||||
|
f"Invalid variable name: {variable_name}. Variable names must only contain letters, numbers, and underscores." # noqa: E501
|
||||||
|
)
|
||||||
|
if not variable_name[0].isalpha() and variable_name[0] != "_":
|
||||||
|
raise ValueError(
|
||||||
|
f"Invalid variable name: {variable_name}. Variable names must start with a letter or underscore."
|
||||||
|
)
|
||||||
|
|
||||||
|
var_names.append(param_name)
|
||||||
|
var_mapping[variable_name] = param_name
|
||||||
|
|
||||||
|
variable = Variable(variable=variable_name, value_selector=[source_node, source_variable])
|
||||||
|
variables.append(variable)
|
||||||
|
|
||||||
|
# Build output
|
||||||
|
outputs = {}
|
||||||
|
for output in node_desc.outputs or []:
|
||||||
|
# Validate output variable name format
|
||||||
|
if not output.name.replace("_", "").isalnum():
|
||||||
|
raise ValueError(
|
||||||
|
f"Invalid output variable name: {output.name}. Output names must only contain letters, numbers, and underscores." # noqa: E501
|
||||||
|
)
|
||||||
|
if not output.name[0].isalpha() and output.name[0] != "_":
|
||||||
|
raise ValueError(
|
||||||
|
f"Invalid output variable name: {output.name}. Output names must start with a letter or underscore."
|
||||||
|
)
|
||||||
|
|
||||||
|
outputs[output.name] = OutputVar(type=map_string_to_var_type(output.type))
|
||||||
|
|
||||||
|
# Generate code, ensure function parameters match variable names, return values match output names
|
||||||
|
output_names = [output.name for output in node_desc.outputs or []]
|
||||||
|
|
||||||
|
# Build function parameter list
|
||||||
|
params_str = ", ".join(var_names) if var_names else ""
|
||||||
|
|
||||||
|
# Build return value dictionary
|
||||||
|
return_dict = {}
|
||||||
|
for output_name in output_names:
|
||||||
|
# Use the first variable as the return value by default
|
||||||
|
return_dict[output_name] = var_names[0] if var_names else f'"{output_name}"'
|
||||||
|
|
||||||
|
return_dict_str = ", ".join([f'"{k}": {v}' for k, v in return_dict.items()])
|
||||||
|
|
||||||
|
# Default code template, ensure return dictionary type matches output variable
|
||||||
|
default_code = f"""def main({params_str}):
|
||||||
|
# Write your code here
|
||||||
|
# Process input variables
|
||||||
|
|
||||||
|
# Return a dictionary, key names must match variable names defined in outputs
|
||||||
|
return {{{return_dict_str}}}"""
|
||||||
|
|
||||||
|
# If custom code is provided, ensure it meets the specifications
|
||||||
|
if node_desc.code:
|
||||||
|
custom_code = node_desc.code
|
||||||
|
# Check if it contains main function definition
|
||||||
|
if not custom_code.strip().startswith("def main("):
|
||||||
|
# Try to fix the code by adding main function with correct parameters
|
||||||
|
custom_code = f"def main({params_str}):\n" + custom_code.strip()
|
||||||
|
else:
|
||||||
|
# Extract function parameters from the existing main function
|
||||||
|
import re
|
||||||
|
|
||||||
|
func_params = re.search(r"def\s+main\s*\((.*?)\)", custom_code)
|
||||||
|
if func_params:
|
||||||
|
existing_params = [p.strip() for p in func_params.group(1).split(",") if p.strip()]
|
||||||
|
# Verify that all required parameters are present
|
||||||
|
missing_params = set(var_names) - set(existing_params)
|
||||||
|
if missing_params:
|
||||||
|
# 尝试修复代码,将函数参数替换为正确的参数名
|
||||||
|
old_params = func_params.group(1)
|
||||||
|
new_params = params_str
|
||||||
|
custom_code = custom_code.replace(f"def main({old_params})", f"def main({new_params})")
|
||||||
|
print(
|
||||||
|
f"Warning: Fixed missing parameters in code node: {', '.join(missing_params)}. Function parameters must match variable names defined in this node." # noqa: E501
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if the return value is a dictionary and keys match output variables
|
||||||
|
for output_name in output_names:
|
||||||
|
if f'"{output_name}"' not in custom_code and f"'{output_name}'" not in custom_code:
|
||||||
|
# Code may not meet specifications, use default code
|
||||||
|
custom_code = default_code
|
||||||
|
break
|
||||||
|
|
||||||
|
# Use fixed code
|
||||||
|
code = custom_code
|
||||||
|
else:
|
||||||
|
code = default_code
|
||||||
|
|
||||||
|
code_node = CodeNodeType(
|
||||||
|
title=node_desc.title,
|
||||||
|
desc=node_desc.description or "",
|
||||||
|
type=BlockEnum.code,
|
||||||
|
code_language=CodeLanguage.python3,
|
||||||
|
code=code,
|
||||||
|
variables=variables,
|
||||||
|
outputs=outputs,
|
||||||
|
)
|
||||||
|
|
||||||
|
return CompleteNode(
|
||||||
|
id=node_desc.id,
|
||||||
|
type="custom",
|
||||||
|
position={"x": 0, "y": 0}, # Temporary position, will be updated later
|
||||||
|
height=82, # Increase height to match reference file
|
||||||
|
width=244,
|
||||||
|
positionAbsolute={"x": 0, "y": 0},
|
||||||
|
selected=False,
|
||||||
|
sourcePosition="right",
|
||||||
|
targetPosition="left",
|
||||||
|
data=code_node,
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _create_template_node(node_desc: NodeDescription) -> CompleteNode:
|
||||||
|
"""Create template node"""
|
||||||
|
# Build variable list
|
||||||
|
variables = []
|
||||||
|
template_text = node_desc.template or ""
|
||||||
|
|
||||||
|
# Collect all node IDs referenced in the template
|
||||||
|
referenced_nodes = set()
|
||||||
|
for var in node_desc.variables or []:
|
||||||
|
source_node = var.source_node or ""
|
||||||
|
source_variable = var.source_variable or ""
|
||||||
|
|
||||||
|
variable = Variable(variable=var.name, value_selector=[source_node, source_variable])
|
||||||
|
variables.append(variable)
|
||||||
|
|
||||||
|
if source_node:
|
||||||
|
referenced_nodes.add(source_node)
|
||||||
|
|
||||||
|
# Modify variable reference format in the template
|
||||||
|
# Replace {{#node_id.variable#}} with {{ variable }}
|
||||||
|
if source_node and source_variable:
|
||||||
|
template_text = template_text.replace(f"{{{{#{source_node}.{source_variable}#}}}}", f"{{ {var.name} }}")
|
||||||
|
|
||||||
|
# Check if a reference to the start node needs to be added
|
||||||
|
# If the template contains a reference to the start node but the variable list does not have a corresponding variable # noqa: E501
|
||||||
|
import re
|
||||||
|
|
||||||
|
start_node_refs = re.findall(r"{{#(\d+)\.([^#]+)#}}", template_text)
|
||||||
|
|
||||||
|
for node_id, var_name in start_node_refs:
|
||||||
|
# Check if there is already a reference to this variable
|
||||||
|
if not any(v.variable == var_name for v in variables):
|
||||||
|
# Add reference to start node variable
|
||||||
|
variable = Variable(variable=var_name, value_selector=[node_id, var_name])
|
||||||
|
variables.append(variable)
|
||||||
|
|
||||||
|
# Modify variable reference format in the template
|
||||||
|
template_text = template_text.replace(f"{{{{#{node_id}.{var_name}#}}}}", f"{{ {var_name} }}")
|
||||||
|
|
||||||
|
# Get all variable names
|
||||||
|
var_names = [var.variable for var in variables]
|
||||||
|
|
||||||
|
# Simple and crude method: directly replace all possible variable reference formats
|
||||||
|
for var_name in var_names:
|
||||||
|
# Replace {var_name} with {{ var_name }}
|
||||||
|
template_text = template_text.replace("{" + var_name + "}", "{{ " + var_name + " }}")
|
||||||
|
# Replace { var_name } with {{ var_name }}
|
||||||
|
template_text = template_text.replace("{ " + var_name + " }", "{{ " + var_name + " }}")
|
||||||
|
# Replace {var_name } with {{ var_name }}
|
||||||
|
template_text = template_text.replace("{" + var_name + " }", "{{ " + var_name + " }}")
|
||||||
|
# Replace { var_name} with {{ var_name }}
|
||||||
|
template_text = template_text.replace("{ " + var_name + "}", "{{ " + var_name + " }}")
|
||||||
|
# Replace {{{ var_name }}} with {{ var_name }}
|
||||||
|
template_text = template_text.replace("{{{ " + var_name + " }}}", "{{ " + var_name + " }}")
|
||||||
|
# Replace {{{var_name}}} with {{ var_name }}
|
||||||
|
template_text = template_text.replace("{{{" + var_name + "}}}", "{{ " + var_name + " }}")
|
||||||
|
|
||||||
|
# Use regular expression to replace all triple curly braces with double curly braces
|
||||||
|
template_text = re.sub(r"{{{([^}]+)}}}", r"{{ \1 }}", template_text)
|
||||||
|
|
||||||
|
template_node = TemplateTransformNodeType(
|
||||||
|
title=node_desc.title,
|
||||||
|
desc=node_desc.description or "",
|
||||||
|
type=BlockEnum.template_transform,
|
||||||
|
template=template_text,
|
||||||
|
variables=variables,
|
||||||
|
)
|
||||||
|
|
||||||
|
return CompleteNode(
|
||||||
|
id=node_desc.id,
|
||||||
|
type="custom",
|
||||||
|
position={"x": 0, "y": 0}, # Temporary position, will be updated later
|
||||||
|
height=82, # Increase height to match reference file
|
||||||
|
width=244,
|
||||||
|
positionAbsolute={"x": 0, "y": 0},
|
||||||
|
selected=False,
|
||||||
|
sourcePosition="right",
|
||||||
|
targetPosition="left",
|
||||||
|
data=template_node,
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _create_end_node(node_desc: NodeDescription) -> CompleteNode:
|
||||||
|
"""Create end node"""
|
||||||
|
# Build output variable list
|
||||||
|
outputs = []
|
||||||
|
for output in node_desc.outputs or []:
|
||||||
|
variable = Variable(
|
||||||
|
variable=output.name, value_selector=[output.source_node or "", output.source_variable or ""]
|
||||||
|
)
|
||||||
|
outputs.append(variable)
|
||||||
|
|
||||||
|
end_node = EndNodeType(
|
||||||
|
title=node_desc.title, desc=node_desc.description or "", type=BlockEnum.end, outputs=outputs
|
||||||
|
)
|
||||||
|
|
||||||
|
return CompleteNode(
|
||||||
|
id=node_desc.id,
|
||||||
|
type="custom",
|
||||||
|
position={"x": 0, "y": 0}, # Temporary position, will be updated later
|
||||||
|
height=90,
|
||||||
|
width=244,
|
||||||
|
positionAbsolute={"x": 0, "y": 0},
|
||||||
|
selected=False,
|
||||||
|
sourcePosition="right",
|
||||||
|
targetPosition="left",
|
||||||
|
data=end_node,
|
||||||
|
)
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
"""
|
||||||
|
模型包
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .workflow_description import ConnectionDescription, NodeDescription, WorkflowDescription
|
||||||
|
|
||||||
|
__all__ = ["ConnectionDescription", "NodeDescription", "WorkflowDescription"]
|
||||||
@ -0,0 +1,80 @@
|
|||||||
|
"""
|
||||||
|
Workflow Description Model
|
||||||
|
Used to represent the simplified workflow description generated by large language models
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
|
class VariableDescription(BaseModel):
|
||||||
|
"""Variable description"""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
type: str
|
||||||
|
description: Optional[str] = None
|
||||||
|
required: bool = True
|
||||||
|
source_node: Optional[str] = None
|
||||||
|
source_variable: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class OutputDescription(BaseModel):
|
||||||
|
"""Output description"""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
type: str = "string"
|
||||||
|
description: Optional[str] = None
|
||||||
|
source_node: Optional[str] = None
|
||||||
|
source_variable: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class NodeDescription(BaseModel):
|
||||||
|
"""Node description"""
|
||||||
|
|
||||||
|
id: str
|
||||||
|
type: str
|
||||||
|
title: str
|
||||||
|
description: Optional[str] = ""
|
||||||
|
variables: Optional[list[VariableDescription]] = Field(default_factory=list)
|
||||||
|
outputs: Optional[list[OutputDescription]] = Field(default_factory=list)
|
||||||
|
|
||||||
|
# LLM node specific fields
|
||||||
|
system_prompt: Optional[str] = None
|
||||||
|
user_prompt: Optional[str] = None
|
||||||
|
provider: Optional[str] = "zhipuai"
|
||||||
|
model: Optional[str] = "glm-4-flash"
|
||||||
|
|
||||||
|
# Code node specific fields
|
||||||
|
code: Optional[str] = None
|
||||||
|
|
||||||
|
# Template node specific fields
|
||||||
|
template: Optional[str] = None
|
||||||
|
|
||||||
|
# Reference to workflow description, used for node relationship analysis
|
||||||
|
workflow_description: Optional["WorkflowDescription"] = Field(default=None, exclude=True)
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
exclude = {"workflow_description"}
|
||||||
|
|
||||||
|
|
||||||
|
class ConnectionDescription(BaseModel):
|
||||||
|
"""Connection description"""
|
||||||
|
|
||||||
|
source: str
|
||||||
|
target: str
|
||||||
|
|
||||||
|
|
||||||
|
class WorkflowDescription(BaseModel):
|
||||||
|
"""Workflow description"""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
description: Optional[str] = ""
|
||||||
|
nodes: list[NodeDescription]
|
||||||
|
connections: list[ConnectionDescription]
|
||||||
|
|
||||||
|
def __init__(self, **data):
|
||||||
|
super().__init__(**data)
|
||||||
|
# Add workflow description reference to each node
|
||||||
|
for node in self.nodes:
|
||||||
|
node.workflow_description = self
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
"""
|
||||||
|
工具函数包
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .llm_client import LLMClient
|
||||||
|
from .prompts import DEFAULT_MODEL_CONFIG, DEFAULT_SYSTEM_PROMPT, build_workflow_prompt
|
||||||
|
from .type_mapper import map_string_to_var_type, map_var_type_to_input_type
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"DEFAULT_MODEL_CONFIG",
|
||||||
|
"DEFAULT_SYSTEM_PROMPT",
|
||||||
|
"LLMClient",
|
||||||
|
"build_workflow_prompt",
|
||||||
|
"map_string_to_var_type",
|
||||||
|
"map_var_type_to_input_type",
|
||||||
|
]
|
||||||
@ -0,0 +1,142 @@
|
|||||||
|
"""
|
||||||
|
Configuration Manager
|
||||||
|
Used to manage configurations and prompts
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigManager:
|
||||||
|
"""Configuration manager for managing configurations"""
|
||||||
|
|
||||||
|
def __init__(self, config_dir: str = "config"):
|
||||||
|
"""
|
||||||
|
Initialize configuration manager
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config_dir: Configuration directory path
|
||||||
|
"""
|
||||||
|
self.config_dir = Path(config_dir)
|
||||||
|
self.config: dict[str, Any] = {}
|
||||||
|
self.last_load_time: float = 0
|
||||||
|
self.reload_interval: float = 60 # Check every 60 seconds
|
||||||
|
self._load_config()
|
||||||
|
|
||||||
|
def _should_reload(self) -> bool:
|
||||||
|
"""Check if configuration needs to be reloaded"""
|
||||||
|
return time.time() - self.last_load_time > self.reload_interval
|
||||||
|
|
||||||
|
def _load_config(self) -> dict[str, Any]:
|
||||||
|
"""Load configuration files"""
|
||||||
|
default_config = self._load_yaml(self.config_dir / "default.yaml")
|
||||||
|
custom_config = self._load_yaml(self.config_dir / "custom.yaml")
|
||||||
|
self.config = self._deep_merge(default_config, custom_config)
|
||||||
|
self.last_load_time = time.time()
|
||||||
|
return self.config
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _load_yaml(path: Path) -> dict[str, Any]:
|
||||||
|
"""Load YAML file"""
|
||||||
|
try:
|
||||||
|
with open(path, encoding="utf-8") as f:
|
||||||
|
return yaml.safe_load(f) or {}
|
||||||
|
except FileNotFoundError:
|
||||||
|
print(f"Warning: Config file not found at {path}")
|
||||||
|
return {}
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading config file {path}: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _deep_merge(dict1: dict, dict2: dict) -> dict:
|
||||||
|
"""Recursively merge two dictionaries"""
|
||||||
|
result = dict1.copy()
|
||||||
|
for key, value in dict2.items():
|
||||||
|
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
|
||||||
|
result[key] = ConfigManager._deep_merge(result[key], value)
|
||||||
|
else:
|
||||||
|
result[key] = value
|
||||||
|
return result
|
||||||
|
|
||||||
|
def get(self, *keys: str, default: Any = None) -> Any:
|
||||||
|
"""
|
||||||
|
Get configuration value
|
||||||
|
|
||||||
|
Args:
|
||||||
|
*keys: Configuration key path
|
||||||
|
default: Default value
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Configuration value or default value
|
||||||
|
"""
|
||||||
|
if self._should_reload():
|
||||||
|
self._load_config()
|
||||||
|
|
||||||
|
current = self.config
|
||||||
|
for key in keys:
|
||||||
|
if isinstance(current, dict) and key in current:
|
||||||
|
current = current[key]
|
||||||
|
else:
|
||||||
|
return default
|
||||||
|
return current
|
||||||
|
|
||||||
|
@property
|
||||||
|
def workflow_generator(self) -> dict[str, Any]:
|
||||||
|
"""Get workflow generator configuration"""
|
||||||
|
return self.get("workflow_generator", default={})
|
||||||
|
|
||||||
|
@property
|
||||||
|
def workflow_nodes(self) -> dict[str, Any]:
|
||||||
|
"""Get workflow nodes configuration"""
|
||||||
|
return self.get("workflow_nodes", default={})
|
||||||
|
|
||||||
|
@property
|
||||||
|
def output(self) -> dict[str, Any]:
|
||||||
|
"""Get output configuration"""
|
||||||
|
return self.get("output", default={})
|
||||||
|
|
||||||
|
def get_output_path(self, filename: Optional[str] = None) -> str:
|
||||||
|
"""
|
||||||
|
Get output file path
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filename: Optional filename, uses default filename from config if not specified
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Complete output file path
|
||||||
|
"""
|
||||||
|
output_config = self.output
|
||||||
|
output_dir = output_config.get("dir", "output/")
|
||||||
|
output_filename = filename or output_config.get("filename", "generated_workflow.yml")
|
||||||
|
return os.path.join(output_dir, output_filename)
|
||||||
|
|
||||||
|
def get_workflow_model(self, model_name: Optional[str] = None) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Get workflow generation model configuration
|
||||||
|
|
||||||
|
Args:
|
||||||
|
model_name: Model name, uses default model if not specified
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Model configuration
|
||||||
|
"""
|
||||||
|
models = self.workflow_generator.get("models", {})
|
||||||
|
|
||||||
|
if not model_name:
|
||||||
|
model_name = models.get("default")
|
||||||
|
|
||||||
|
return models.get("available", {}).get(model_name, {})
|
||||||
|
|
||||||
|
def get_llm_node_config(self) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Get LLM node configuration
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
LLM node configuration
|
||||||
|
"""
|
||||||
|
return self.workflow_nodes.get("llm", {})
|
||||||
@ -0,0 +1,151 @@
|
|||||||
|
"""
|
||||||
|
Debug Manager
|
||||||
|
Used to manage debug information saving
|
||||||
|
"""
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import uuid
|
||||||
|
from typing import Any, Optional, Union
|
||||||
|
|
||||||
|
|
||||||
|
class DebugManager:
|
||||||
|
"""Debug manager for managing debug information saving"""
|
||||||
|
|
||||||
|
_instance = None
|
||||||
|
|
||||||
|
def __new__(cls, *args, **kwargs):
|
||||||
|
"""Singleton pattern"""
|
||||||
|
if cls._instance is None:
|
||||||
|
cls._instance = super().__new__(cls)
|
||||||
|
cls._instance._initialized = False
|
||||||
|
return cls._instance
|
||||||
|
|
||||||
|
def __init__(self, config: dict[str, Any] = {}, debug_enabled: bool = False):
|
||||||
|
"""
|
||||||
|
Initialize debug manager
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config: Debug configuration
|
||||||
|
debug_enabled: Whether to enable debug mode
|
||||||
|
"""
|
||||||
|
# Avoid repeated initialization
|
||||||
|
if self._initialized:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._initialized = True
|
||||||
|
self.config = config or {}
|
||||||
|
self.debug_enabled = debug_enabled or self.config.get("enabled", False)
|
||||||
|
self.debug_dir = self.config.get("dir", "debug/")
|
||||||
|
self.save_options = self.config.get(
|
||||||
|
"save_options", {"prompt": True, "response": True, "json": True, "workflow": True}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Generate run ID
|
||||||
|
self.case_id = self._generate_case_id()
|
||||||
|
self.case_dir = os.path.join(self.debug_dir, self.case_id)
|
||||||
|
|
||||||
|
# If debug is enabled, create debug directory
|
||||||
|
if self.debug_enabled:
|
||||||
|
os.makedirs(self.case_dir, exist_ok=True)
|
||||||
|
print(f"Debug mode enabled, debug information will be saved to: {self.case_dir}")
|
||||||
|
|
||||||
|
def _generate_case_id(self) -> str:
|
||||||
|
"""
|
||||||
|
Generate run ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Run ID
|
||||||
|
"""
|
||||||
|
# Use format from configuration to generate run ID
|
||||||
|
format_str = self.config.get("case_id_format", "%Y%m%d_%H%M%S_%f")
|
||||||
|
timestamp = datetime.datetime.now().strftime(format_str)
|
||||||
|
|
||||||
|
# Add random string
|
||||||
|
random_str = str(uuid.uuid4())[:8]
|
||||||
|
|
||||||
|
return f"{timestamp}_{random_str}"
|
||||||
|
|
||||||
|
def save_text(self, content: str, filename: str, subdir: Optional[str] = None) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Save text content to file
|
||||||
|
|
||||||
|
Args:
|
||||||
|
content: Text content
|
||||||
|
filename: File name
|
||||||
|
subdir: Subdirectory name
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Saved file path, returns None if debug is not enabled
|
||||||
|
"""
|
||||||
|
if not self.debug_enabled:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Determine save path
|
||||||
|
save_dir = self.case_dir
|
||||||
|
if subdir:
|
||||||
|
save_dir = os.path.join(save_dir, subdir)
|
||||||
|
os.makedirs(save_dir, exist_ok=True)
|
||||||
|
|
||||||
|
file_path = os.path.join(save_dir, filename)
|
||||||
|
|
||||||
|
# Save content
|
||||||
|
with open(file_path, "w", encoding="utf-8") as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
print(f"Debug information saved to: {file_path}")
|
||||||
|
return file_path
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Failed to save debug information: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def save_json(self, data: Union[dict, list], filename: str, subdir: Optional[str] = None) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Save JSON data to file
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data: JSON data
|
||||||
|
filename: File name
|
||||||
|
subdir: Subdirectory name
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Saved file path, returns None if debug is not enabled
|
||||||
|
"""
|
||||||
|
if not self.debug_enabled:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Determine save path
|
||||||
|
save_dir = self.case_dir
|
||||||
|
if subdir:
|
||||||
|
save_dir = os.path.join(save_dir, subdir)
|
||||||
|
os.makedirs(save_dir, exist_ok=True)
|
||||||
|
|
||||||
|
file_path = os.path.join(save_dir, filename)
|
||||||
|
|
||||||
|
# Save content
|
||||||
|
with open(file_path, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
print(f"Debug information saved to: {file_path}")
|
||||||
|
return file_path
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Failed to save debug information: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def should_save(self, option: str) -> bool:
|
||||||
|
"""
|
||||||
|
Check if specified type of debug information should be saved
|
||||||
|
|
||||||
|
Args:
|
||||||
|
option: Debug information type
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Whether it should be saved
|
||||||
|
"""
|
||||||
|
if not self.debug_enabled:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return self.save_options.get(option, False)
|
||||||
@ -0,0 +1,438 @@
|
|||||||
|
"""
|
||||||
|
LLM Client
|
||||||
|
Used to call LLM API
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from core.auto.workflow_generator.utils.debug_manager import DebugManager
|
||||||
|
from core.auto.workflow_generator.utils.prompts import DEFAULT_SYSTEM_PROMPT
|
||||||
|
from core.model_manager import ModelInstance
|
||||||
|
from core.model_runtime.entities.message_entities import SystemPromptMessage, UserPromptMessage
|
||||||
|
|
||||||
|
|
||||||
|
class LLMClient:
|
||||||
|
"""LLM Client"""
|
||||||
|
|
||||||
|
def __init__(self, model_instance: ModelInstance, debug_manager: DebugManager):
|
||||||
|
"""
|
||||||
|
Initialize LLM client
|
||||||
|
|
||||||
|
Args:
|
||||||
|
api_key: API key
|
||||||
|
model: Model name
|
||||||
|
api_base: API base URL
|
||||||
|
max_tokens: Maximum number of tokens to generate
|
||||||
|
debug_manager: Debug manager
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.debug_manager = debug_manager or DebugManager()
|
||||||
|
self.model_instance = model_instance
|
||||||
|
|
||||||
|
def generate(self, prompt: str) -> str:
|
||||||
|
"""
|
||||||
|
Generate text
|
||||||
|
|
||||||
|
Args:
|
||||||
|
prompt: Prompt text
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Generated text
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Save prompt
|
||||||
|
if self.debug_manager.should_save("prompt"):
|
||||||
|
self.debug_manager.save_text(prompt, "prompt.txt", "llm")
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = self.model_instance.invoke_llm(
|
||||||
|
prompt_messages=[
|
||||||
|
SystemPromptMessage(content=DEFAULT_SYSTEM_PROMPT),
|
||||||
|
UserPromptMessage(content=prompt),
|
||||||
|
],
|
||||||
|
model_parameters={"temperature": 0.7, "max_tokens": 4900},
|
||||||
|
)
|
||||||
|
content = ""
|
||||||
|
for chunk in response:
|
||||||
|
content += chunk.delta.message.content
|
||||||
|
print(f"Generation complete, text length: {len(content)} characters")
|
||||||
|
|
||||||
|
# Save response
|
||||||
|
if self.debug_manager.should_save("response"):
|
||||||
|
self.debug_manager.save_text(content, "response.txt", "llm")
|
||||||
|
|
||||||
|
return content
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error generating text: {e}")
|
||||||
|
raise e
|
||||||
|
|
||||||
|
def extract_json(self, text: str) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Extract JSON from text
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: Text containing JSON
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Extracted JSON object
|
||||||
|
"""
|
||||||
|
print("Starting JSON extraction from text...")
|
||||||
|
|
||||||
|
# Save original text
|
||||||
|
if self.debug_manager.should_save("json"):
|
||||||
|
self.debug_manager.save_text(text, "original_text.txt", "json")
|
||||||
|
|
||||||
|
# Use regex to extract JSON part
|
||||||
|
json_match = re.search(r"```json\n(.*?)\n```", text, re.DOTALL)
|
||||||
|
if json_match:
|
||||||
|
json_str = json_match.group(1)
|
||||||
|
print("Successfully extracted JSON from code block")
|
||||||
|
else:
|
||||||
|
# Try to match code block without language identifier
|
||||||
|
json_match = re.search(r"```\n(.*?)\n```", text, re.DOTALL)
|
||||||
|
if json_match:
|
||||||
|
json_str = json_match.group(1)
|
||||||
|
print("Successfully extracted JSON from code block without language identifier")
|
||||||
|
else:
|
||||||
|
# Try to match JSON surrounded by curly braces
|
||||||
|
json_match = re.search(r"(\{.*\})", text, re.DOTALL)
|
||||||
|
if json_match:
|
||||||
|
json_str = json_match.group(1)
|
||||||
|
print("Successfully extracted JSON from curly braces")
|
||||||
|
else:
|
||||||
|
# Try to parse entire text
|
||||||
|
json_str = text
|
||||||
|
print("No JSON code block found, attempting to parse entire text")
|
||||||
|
|
||||||
|
# Save extracted JSON string
|
||||||
|
if self.debug_manager.should_save("json"):
|
||||||
|
self.debug_manager.save_text(json_str, "extracted_json.txt", "json")
|
||||||
|
|
||||||
|
# Try multiple methods to parse JSON
|
||||||
|
try:
|
||||||
|
# Try direct parsing
|
||||||
|
result = json.loads(json_str)
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
print(f"Direct JSON parsing failed: {e}, attempting basic cleaning")
|
||||||
|
try:
|
||||||
|
# Try basic cleaning
|
||||||
|
cleaned_text = self._clean_text(json_str)
|
||||||
|
if self.debug_manager.should_save("json"):
|
||||||
|
self.debug_manager.save_text(cleaned_text, "cleaned_json_1.txt", "json")
|
||||||
|
result = json.loads(cleaned_text)
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
print(f"JSON parsing after basic cleaning failed: {e}, attempting to fix common errors")
|
||||||
|
try:
|
||||||
|
# Try fixing common errors
|
||||||
|
fixed_text = self._fix_json_errors(json_str)
|
||||||
|
if self.debug_manager.should_save("json"):
|
||||||
|
self.debug_manager.save_text(fixed_text, "cleaned_json_2.txt", "json")
|
||||||
|
result = json.loads(fixed_text)
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
print(f"JSON parsing after fixing common errors failed: {e}, attempting aggressive cleaning")
|
||||||
|
try:
|
||||||
|
# Try aggressive cleaning
|
||||||
|
aggressive_cleaned = self._aggressive_clean(json_str)
|
||||||
|
if self.debug_manager.should_save("json"):
|
||||||
|
self.debug_manager.save_text(aggressive_cleaned, "cleaned_json_3.txt", "json")
|
||||||
|
result = json.loads(aggressive_cleaned)
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
print(f"JSON parsing after aggressive cleaning failed: {e}, attempting manual JSON extraction")
|
||||||
|
# Try manual JSON structure extraction
|
||||||
|
result = self._manual_json_extraction(json_str)
|
||||||
|
if self.debug_manager.should_save("json"):
|
||||||
|
self.debug_manager.save_json(result, "manual_json.json", "json")
|
||||||
|
|
||||||
|
# Check for nested workflow structure
|
||||||
|
if "workflow" in result and isinstance(result["workflow"], dict):
|
||||||
|
print("Detected nested workflow structure, extracting top-level data")
|
||||||
|
# Extract workflow name and description
|
||||||
|
name = result.get("name", "Text Analysis Workflow")
|
||||||
|
description = result.get("description", "")
|
||||||
|
|
||||||
|
# Extract nodes and connections
|
||||||
|
nodes = result["workflow"].get("nodes", [])
|
||||||
|
connections = []
|
||||||
|
|
||||||
|
# If there are connections, extract them
|
||||||
|
if "connections" in result["workflow"]:
|
||||||
|
connections = result["workflow"]["connections"]
|
||||||
|
|
||||||
|
# Build standard format workflow description
|
||||||
|
result = {"name": name, "description": description, "nodes": nodes, "connections": connections}
|
||||||
|
|
||||||
|
# Save final parsed JSON
|
||||||
|
if self.debug_manager.should_save("json"):
|
||||||
|
self.debug_manager.save_json(result, "final_json.json", "json")
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"JSON parsing successful, contains {len(result.get('nodes', []))} nodes and {len(result.get('connections', []))} connections" # noqa: E501
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _clean_text(self, text: str) -> str:
|
||||||
|
"""
|
||||||
|
Clean text by removing non-JSON characters
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: Text to clean
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Cleaned text
|
||||||
|
"""
|
||||||
|
print("Starting text cleaning...")
|
||||||
|
# Remove characters that might cause JSON parsing to fail
|
||||||
|
lines = text.split("\n")
|
||||||
|
cleaned_lines = []
|
||||||
|
|
||||||
|
in_json = False
|
||||||
|
for line in lines:
|
||||||
|
if line.strip().startswith("{") or line.strip().startswith("["):
|
||||||
|
in_json = True
|
||||||
|
|
||||||
|
if in_json:
|
||||||
|
cleaned_lines.append(line)
|
||||||
|
|
||||||
|
if line.strip().endswith("}") or line.strip().endswith("]"):
|
||||||
|
in_json = False
|
||||||
|
|
||||||
|
cleaned_text = "\n".join(cleaned_lines)
|
||||||
|
print(f"Text cleaning complete, length before: {len(text)}, length after: {len(cleaned_text)}")
|
||||||
|
return cleaned_text
|
||||||
|
|
||||||
|
def _fix_json_errors(self, text: str) -> str:
|
||||||
|
"""
|
||||||
|
Fix common JSON errors
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: Text to fix
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Fixed text
|
||||||
|
"""
|
||||||
|
print("Attempting to fix JSON errors...")
|
||||||
|
|
||||||
|
# Replace single quotes with double quotes
|
||||||
|
text = re.sub(r"'([^']*)'", r'"\1"', text)
|
||||||
|
|
||||||
|
# Fix missing commas
|
||||||
|
text = re.sub(r"}\s*{", "},{", text)
|
||||||
|
text = re.sub(r"]\s*{", "],{", text)
|
||||||
|
text = re.sub(r"}\s*\[", r"},\[", text)
|
||||||
|
text = re.sub(r"]\s*\[", r"],\[", text)
|
||||||
|
|
||||||
|
# Fix extra commas
|
||||||
|
text = re.sub(r",\s*}", "}", text)
|
||||||
|
text = re.sub(r",\s*]", "]", text)
|
||||||
|
|
||||||
|
# Ensure property names have double quotes
|
||||||
|
text = re.sub(r"([{,]\s*)(\w+)(\s*:)", r'\1"\2"\3', text)
|
||||||
|
|
||||||
|
return text
|
||||||
|
|
||||||
|
def _aggressive_clean(self, text: str) -> str:
|
||||||
|
"""
|
||||||
|
More aggressive text cleaning
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: Text to clean
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Cleaned text
|
||||||
|
"""
|
||||||
|
print("Using aggressive cleaning method...")
|
||||||
|
|
||||||
|
# Try to find outermost curly braces
|
||||||
|
start_idx = text.find("{")
|
||||||
|
end_idx = text.rfind("}")
|
||||||
|
|
||||||
|
if start_idx != -1 and end_idx != -1 and start_idx < end_idx:
|
||||||
|
text = text[start_idx : end_idx + 1]
|
||||||
|
|
||||||
|
# Remove comments
|
||||||
|
text = re.sub(r"//.*?\n", "\n", text)
|
||||||
|
text = re.sub(r"/\*.*?\*/", "", text, flags=re.DOTALL)
|
||||||
|
|
||||||
|
# Fix JSON format
|
||||||
|
text = self._fix_json_errors(text)
|
||||||
|
|
||||||
|
# Remove escape characters
|
||||||
|
text = text.replace("\\n", "\n").replace("\\t", "\t").replace('\\"', '"')
|
||||||
|
|
||||||
|
# Fix potential Unicode escape issues
|
||||||
|
text = re.sub(r"\\u([0-9a-fA-F]{4})", lambda m: chr(int(m.group(1), 16)), text)
|
||||||
|
|
||||||
|
return text
|
||||||
|
|
||||||
|
def _manual_json_extraction(self, text: str) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Manual JSON structure extraction
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: Text to extract from
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Extracted JSON object
|
||||||
|
"""
|
||||||
|
print("Attempting manual JSON structure extraction...")
|
||||||
|
|
||||||
|
# Extract workflow name
|
||||||
|
name_match = re.search(r'"name"\s*:\s*"([^"]*)"', text)
|
||||||
|
name = name_match.group(1) if name_match else "Simple Workflow"
|
||||||
|
|
||||||
|
# Extract workflow description
|
||||||
|
desc_match = re.search(r'"description"\s*:\s*"([^"]*)"', text)
|
||||||
|
description = desc_match.group(1) if desc_match else "Automatically generated workflow"
|
||||||
|
|
||||||
|
# Extract nodes
|
||||||
|
nodes = []
|
||||||
|
node_matches = re.finditer(r'\{\s*"id"\s*:\s*"([^"]*)"\s*,\s*"type"\s*:\s*"([^"]*)"', text)
|
||||||
|
|
||||||
|
for match in node_matches:
|
||||||
|
node_id = match.group(1)
|
||||||
|
node_type = match.group(2)
|
||||||
|
|
||||||
|
# Extract node title
|
||||||
|
title_match = re.search(rf'"id"\s*:\s*"{node_id}".*?"title"\s*:\s*"([^"]*)"', text, re.DOTALL)
|
||||||
|
title = title_match.group(1) if title_match else f"{node_type.capitalize()} Node"
|
||||||
|
|
||||||
|
# Extract node description
|
||||||
|
desc_match = re.search(rf'"id"\s*:\s*"{node_id}".*?"description"\s*:\s*"([^"]*)"', text, re.DOTALL)
|
||||||
|
desc = desc_match.group(1) if desc_match else ""
|
||||||
|
|
||||||
|
# Create basic node based on node type
|
||||||
|
if node_type == "start":
|
||||||
|
# Extract variables
|
||||||
|
variables = []
|
||||||
|
var_section_match = re.search(rf'"id"\s*:\s*"{node_id}".*?"variables"\s*:\s*\[(.*?)\]', text, re.DOTALL)
|
||||||
|
|
||||||
|
if var_section_match:
|
||||||
|
var_section = var_section_match.group(1)
|
||||||
|
var_matches = re.finditer(r'\{\s*"name"\s*:\s*"([^"]*)"\s*,\s*"type"\s*:\s*"([^"]*)"', var_section)
|
||||||
|
|
||||||
|
for var_match in var_matches:
|
||||||
|
var_name = var_match.group(1)
|
||||||
|
var_type = var_match.group(2)
|
||||||
|
|
||||||
|
# Extract variable description
|
||||||
|
var_desc_match = re.search(
|
||||||
|
rf'"name"\s*:\s*"{var_name}".*?"description"\s*:\s*"([^"]*)"', var_section, re.DOTALL
|
||||||
|
)
|
||||||
|
var_desc = var_desc_match.group(1) if var_desc_match else ""
|
||||||
|
|
||||||
|
# Extract required status
|
||||||
|
var_required_match = re.search(
|
||||||
|
rf'"name"\s*:\s*"{var_name}".*?"required"\s*:\s*(true|false)', var_section, re.DOTALL
|
||||||
|
)
|
||||||
|
var_required = var_required_match.group(1).lower() == "true" if var_required_match else True
|
||||||
|
|
||||||
|
variables.append(
|
||||||
|
{"name": var_name, "type": var_type, "description": var_desc, "required": var_required}
|
||||||
|
)
|
||||||
|
|
||||||
|
# If no variables found but this is a greeting workflow, add default user_name variable
|
||||||
|
if not variables and ("greeting" in name.lower()):
|
||||||
|
variables.append(
|
||||||
|
{"name": "user_name", "type": "string", "description": "User's name", "required": True}
|
||||||
|
)
|
||||||
|
|
||||||
|
nodes.append({"id": node_id, "type": "start", "title": title, "desc": desc, "variables": variables})
|
||||||
|
elif node_type == "llm":
|
||||||
|
# Extract system prompt
|
||||||
|
system_prompt_match = re.search(
|
||||||
|
rf'"id"\s*:\s*"{node_id}".*?"system_prompt"\s*:\s*"([^"]*)"', text, re.DOTALL
|
||||||
|
)
|
||||||
|
system_prompt = system_prompt_match.group(1) if system_prompt_match else "You are a helpful assistant"
|
||||||
|
|
||||||
|
# Extract user prompt
|
||||||
|
user_prompt_match = re.search(
|
||||||
|
rf'"id"\s*:\s*"{node_id}".*?"user_prompt"\s*:\s*"([^"]*)"', text, re.DOTALL
|
||||||
|
)
|
||||||
|
user_prompt = user_prompt_match.group(1) if user_prompt_match else "Please answer the user's question"
|
||||||
|
|
||||||
|
nodes.append(
|
||||||
|
{
|
||||||
|
"id": node_id,
|
||||||
|
"type": "llm",
|
||||||
|
"title": title,
|
||||||
|
"desc": desc,
|
||||||
|
"provider": "zhipuai",
|
||||||
|
"model": "glm-4-flash",
|
||||||
|
"system_prompt": system_prompt,
|
||||||
|
"user_prompt": user_prompt,
|
||||||
|
"variables": [],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
elif node_type in ("template", "template-transform"):
|
||||||
|
# Extract template content
|
||||||
|
template_match = re.search(rf'"id"\s*:\s*"{node_id}".*?"template"\s*:\s*"([^"]*)"', text, re.DOTALL)
|
||||||
|
template = template_match.group(1) if template_match else ""
|
||||||
|
|
||||||
|
# Fix triple curly brace issue in template, replace {{{ with {{ and }}} with }}
|
||||||
|
template = template.replace("{{{", "{{").replace("}}}", "}}")
|
||||||
|
|
||||||
|
nodes.append(
|
||||||
|
{
|
||||||
|
"id": node_id,
|
||||||
|
"type": "template-transform",
|
||||||
|
"title": title,
|
||||||
|
"desc": desc,
|
||||||
|
"template": template,
|
||||||
|
"variables": [],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
elif node_type == "end":
|
||||||
|
# Extract outputs
|
||||||
|
outputs = []
|
||||||
|
output_section_match = re.search(
|
||||||
|
rf'"id"\s*:\s*"{node_id}".*?"outputs"\s*:\s*\[(.*?)\]', text, re.DOTALL
|
||||||
|
)
|
||||||
|
|
||||||
|
if output_section_match:
|
||||||
|
output_section = output_section_match.group(1)
|
||||||
|
output_matches = re.finditer(
|
||||||
|
r'\{\s*"name"\s*:\s*"([^"]*)"\s*,\s*"type"\s*:\s*"([^"]*)"', output_section
|
||||||
|
)
|
||||||
|
|
||||||
|
for output_match in output_matches:
|
||||||
|
output_name = output_match.group(1)
|
||||||
|
output_type = output_match.group(2)
|
||||||
|
|
||||||
|
# Extract source node
|
||||||
|
source_node_match = re.search(
|
||||||
|
rf'"name"\s*:\s*"{output_name}".*?"source_node"\s*:\s*"([^"]*)"', output_section, re.DOTALL
|
||||||
|
)
|
||||||
|
source_node = source_node_match.group(1) if source_node_match else ""
|
||||||
|
|
||||||
|
# Extract source variable
|
||||||
|
source_var_match = re.search(
|
||||||
|
rf'"name"\s*:\s*"{output_name}".*?"source_variable"\s*:\s*"([^"]*)"',
|
||||||
|
output_section,
|
||||||
|
re.DOTALL,
|
||||||
|
)
|
||||||
|
source_var = source_var_match.group(1) if source_var_match else ""
|
||||||
|
|
||||||
|
outputs.append(
|
||||||
|
{
|
||||||
|
"name": output_name,
|
||||||
|
"type": output_type,
|
||||||
|
"source_node": source_node,
|
||||||
|
"source_variable": source_var,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
nodes.append({"id": node_id, "type": "end", "title": title, "desc": desc, "outputs": outputs})
|
||||||
|
else:
|
||||||
|
# Other node types
|
||||||
|
nodes.append({"id": node_id, "type": node_type, "title": title, "desc": desc})
|
||||||
|
|
||||||
|
# Extract connections
|
||||||
|
connections = []
|
||||||
|
conn_matches = re.finditer(r'\{\s*"source"\s*:\s*"([^"]*)"\s*,\s*"target"\s*:\s*"([^"]*)"', text)
|
||||||
|
|
||||||
|
for match in conn_matches:
|
||||||
|
connections.append({"source": match.group(1), "target": match.group(2)})
|
||||||
|
|
||||||
|
return {"name": name, "description": description, "nodes": nodes, "connections": connections}
|
||||||
@ -0,0 +1,50 @@
|
|||||||
|
"""
|
||||||
|
Type Mapping Utility
|
||||||
|
Used to map string types to Dify types
|
||||||
|
"""
|
||||||
|
|
||||||
|
from core.auto.node_types.common import InputVarType, VarType
|
||||||
|
|
||||||
|
|
||||||
|
def map_var_type_to_input_type(var_type: str) -> InputVarType:
|
||||||
|
"""
|
||||||
|
Map variable type to input variable type
|
||||||
|
|
||||||
|
Args:
|
||||||
|
var_type: Variable type string
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Input variable type
|
||||||
|
"""
|
||||||
|
type_map = {
|
||||||
|
"string": InputVarType.text_input,
|
||||||
|
"number": InputVarType.number,
|
||||||
|
"boolean": InputVarType.select,
|
||||||
|
"object": InputVarType.json,
|
||||||
|
"array": InputVarType.json,
|
||||||
|
"file": InputVarType.file,
|
||||||
|
}
|
||||||
|
|
||||||
|
return type_map.get(var_type.lower(), InputVarType.text_input)
|
||||||
|
|
||||||
|
|
||||||
|
def map_string_to_var_type(type_str: str) -> VarType:
|
||||||
|
"""
|
||||||
|
Map string to variable type
|
||||||
|
|
||||||
|
Args:
|
||||||
|
type_str: Type string
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Variable type
|
||||||
|
"""
|
||||||
|
type_map = {
|
||||||
|
"string": VarType.string,
|
||||||
|
"number": VarType.number,
|
||||||
|
"boolean": VarType.boolean,
|
||||||
|
"object": VarType.object,
|
||||||
|
"array": VarType.array,
|
||||||
|
"file": VarType.file,
|
||||||
|
}
|
||||||
|
|
||||||
|
return type_map.get(type_str.lower(), VarType.string)
|
||||||
@ -0,0 +1,134 @@
|
|||||||
|
import json
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
from core.auto.node_types.common import CompleteEdge, CompleteNode
|
||||||
|
|
||||||
|
|
||||||
|
class Workflow:
|
||||||
|
"""
|
||||||
|
Workflow class
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, name: str, nodes: list[CompleteNode], edges: list[CompleteEdge]):
|
||||||
|
"""
|
||||||
|
Initialize workflow
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: Workflow name
|
||||||
|
nodes: List of nodes
|
||||||
|
edges: List of edges
|
||||||
|
"""
|
||||||
|
self.name = name
|
||||||
|
self.nodes = nodes
|
||||||
|
self.edges = edges
|
||||||
|
|
||||||
|
def to_dict(self) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Convert workflow to dictionary
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Workflow dictionary
|
||||||
|
"""
|
||||||
|
# Apply basic information (fixed template)
|
||||||
|
app_info = {
|
||||||
|
"description": "",
|
||||||
|
"icon": "🤖",
|
||||||
|
"icon_background": "#FFEAD5",
|
||||||
|
"mode": "workflow",
|
||||||
|
"name": self.name,
|
||||||
|
"use_icon_as_answer_icon": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Feature configuration (fixed template)
|
||||||
|
features = {
|
||||||
|
"file_upload": {
|
||||||
|
"allowed_file_extensions": [".JPG", ".JPEG", ".PNG", ".GIF", ".WEBP", ".SVG"],
|
||||||
|
"allowed_file_types": ["image"],
|
||||||
|
"allowed_file_upload_methods": ["local_file", "remote_url"],
|
||||||
|
"enabled": False,
|
||||||
|
"fileUploadConfig": {
|
||||||
|
"audio_file_size_limit": 50,
|
||||||
|
"batch_count_limit": 5,
|
||||||
|
"file_size_limit": 15,
|
||||||
|
"image_file_size_limit": 10,
|
||||||
|
"video_file_size_limit": 100,
|
||||||
|
},
|
||||||
|
"image": {"enabled": False, "number_limits": 3, "transfer_methods": ["local_file", "remote_url"]},
|
||||||
|
"number_limits": 3,
|
||||||
|
},
|
||||||
|
"opening_statement": "",
|
||||||
|
"retriever_resource": {"enabled": True},
|
||||||
|
"sensitive_word_avoidance": {"enabled": False},
|
||||||
|
"speech_to_text": {"enabled": False},
|
||||||
|
"suggested_questions": [],
|
||||||
|
"suggested_questions_after_answer": {"enabled": False},
|
||||||
|
"text_to_speech": {"enabled": False, "language": "", "voice": ""},
|
||||||
|
}
|
||||||
|
|
||||||
|
# View configuration (fixed template)
|
||||||
|
viewport = {"x": 92.96659905656679, "y": 79.13437154762897, "zoom": 0.9002006986311041}
|
||||||
|
|
||||||
|
# Nodes and edges
|
||||||
|
nodes_data = []
|
||||||
|
for node in self.nodes:
|
||||||
|
node_data = node.to_json()
|
||||||
|
nodes_data.append(node_data)
|
||||||
|
|
||||||
|
edges_data = []
|
||||||
|
for edge in self.edges:
|
||||||
|
edge_data = edge.to_json()
|
||||||
|
edges_data.append(edge_data)
|
||||||
|
|
||||||
|
# Build a complete workflow dictionary
|
||||||
|
workflow_dict = {
|
||||||
|
"app": app_info,
|
||||||
|
"kind": "app",
|
||||||
|
"version": "0.1.2",
|
||||||
|
"workflow": {
|
||||||
|
"conversation_variables": [],
|
||||||
|
"environment_variables": [],
|
||||||
|
"features": features,
|
||||||
|
"graph": {"edges": edges_data, "nodes": nodes_data, "viewport": viewport},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return workflow_dict
|
||||||
|
|
||||||
|
def save_to_yaml(self, file_path: str):
|
||||||
|
"""
|
||||||
|
Save workflow to YAML file
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_path: File path
|
||||||
|
"""
|
||||||
|
workflow_dict = self.to_dict()
|
||||||
|
|
||||||
|
with open(file_path, "w", encoding="utf-8") as f:
|
||||||
|
yaml.dump(workflow_dict, f, allow_unicode=True, sort_keys=False)
|
||||||
|
|
||||||
|
print(f"Workflow saved to: {file_path}")
|
||||||
|
|
||||||
|
def save_to_json(self, file_path: str):
|
||||||
|
"""
|
||||||
|
Save workflow to JSON file
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_path: File path
|
||||||
|
"""
|
||||||
|
workflow_dict = self.to_dict()
|
||||||
|
|
||||||
|
with open(file_path, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(workflow_dict, f, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
print(f"Workflow saved to: {file_path}")
|
||||||
|
|
||||||
|
def to_yaml(self) -> str:
|
||||||
|
"""
|
||||||
|
Convert workflow to YAML string
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
YAML string
|
||||||
|
"""
|
||||||
|
return yaml.dump(self.to_dict(), allow_unicode=True, sort_keys=False)
|
||||||
@ -0,0 +1,159 @@
|
|||||||
|
"""
|
||||||
|
Workflow Generator
|
||||||
|
Used to generate Dify workflows based on user requirements
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pydantic import ValidationError
|
||||||
|
|
||||||
|
from core.auto.workflow_generator.generators.edge_generator import EdgeGenerator
|
||||||
|
from core.auto.workflow_generator.generators.layout_engine import LayoutEngine
|
||||||
|
from core.auto.workflow_generator.generators.node_generator import NodeGenerator
|
||||||
|
from core.auto.workflow_generator.models.workflow_description import WorkflowDescription
|
||||||
|
from core.auto.workflow_generator.utils.config_manager import ConfigManager
|
||||||
|
from core.auto.workflow_generator.utils.debug_manager import DebugManager
|
||||||
|
from core.auto.workflow_generator.utils.llm_client import LLMClient
|
||||||
|
from core.auto.workflow_generator.utils.prompts import build_workflow_prompt
|
||||||
|
from core.auto.workflow_generator.workflow import Workflow
|
||||||
|
from core.model_manager import ModelInstance
|
||||||
|
|
||||||
|
|
||||||
|
class WorkflowGenerator:
|
||||||
|
"""Workflow generator for creating Dify workflows based on user requirements"""
|
||||||
|
|
||||||
|
def __init__(self, model_instance: ModelInstance, config_dir: str = "config", debug_enabled: bool = False):
|
||||||
|
"""
|
||||||
|
Initialize workflow generator
|
||||||
|
|
||||||
|
Args:
|
||||||
|
api_key: LLM API key
|
||||||
|
config_dir: Configuration directory path
|
||||||
|
model_name: Specified model name, uses default model if not specified
|
||||||
|
debug_enabled: Whether to enable debug mode
|
||||||
|
"""
|
||||||
|
# Load configuration
|
||||||
|
self.config = ConfigManager(config_dir)
|
||||||
|
|
||||||
|
# Initialize debug manager
|
||||||
|
self.debug_manager = DebugManager(config=self.config.get("debug", default={}), debug_enabled=debug_enabled)
|
||||||
|
|
||||||
|
# Get model configuration
|
||||||
|
|
||||||
|
# Initialize LLM client
|
||||||
|
self.llm_client = LLMClient(model_instance=model_instance, debug_manager=self.debug_manager)
|
||||||
|
|
||||||
|
def generate_workflow(self, user_requirement: str) -> str:
|
||||||
|
"""
|
||||||
|
Generate workflow based on user requirements
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_requirement: User requirement description
|
||||||
|
output_path: Output file path, uses default path from config if None
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Generated workflow YAML file path
|
||||||
|
"""
|
||||||
|
print("\n===== Starting Workflow Generation =====")
|
||||||
|
print(f"User requirement: {user_requirement}")
|
||||||
|
|
||||||
|
# Save user requirement
|
||||||
|
if self.debug_manager.should_save("workflow"):
|
||||||
|
self.debug_manager.save_text(user_requirement, "user_requirement.txt", "workflow")
|
||||||
|
|
||||||
|
# Use default path from config if output path not specified
|
||||||
|
|
||||||
|
# Step 1: Generate simple workflow description
|
||||||
|
print("\n----- Step 1: Generating Simple Workflow Description -----")
|
||||||
|
workflow_description = self._generate_workflow_description(user_requirement)
|
||||||
|
print(f"Workflow name: {workflow_description.name}")
|
||||||
|
print(f"Workflow description: {workflow_description.description}")
|
||||||
|
print(f"Number of nodes: {len(workflow_description.nodes)}")
|
||||||
|
print(f"Number of connections: {len(workflow_description.connections)}")
|
||||||
|
|
||||||
|
# Save workflow description
|
||||||
|
if self.debug_manager.should_save("workflow"):
|
||||||
|
self.debug_manager.save_json(workflow_description.dict(), "workflow_description.json", "workflow")
|
||||||
|
|
||||||
|
# Step 2: Parse description and generate nodes
|
||||||
|
print("\n----- Step 2: Parsing Description, Generating Nodes -----")
|
||||||
|
nodes = NodeGenerator.create_nodes(workflow_description.nodes)
|
||||||
|
print(f"Generated nodes: {len(nodes)}")
|
||||||
|
for i, node in enumerate(nodes):
|
||||||
|
print(f"Node {i + 1}: ID={node.id}, Type={node.data.type.value}, Title={node.data.title}")
|
||||||
|
|
||||||
|
# Save node information
|
||||||
|
if self.debug_manager.should_save("workflow"):
|
||||||
|
nodes_data = [node.dict() for node in nodes]
|
||||||
|
self.debug_manager.save_json(nodes_data, "nodes.json", "workflow")
|
||||||
|
|
||||||
|
# Step 3: Generate edges
|
||||||
|
print("\n----- Step 3: Generating Edges -----")
|
||||||
|
edges = EdgeGenerator.create_edges(nodes, workflow_description.connections)
|
||||||
|
print(f"Generated edges: {len(edges)}")
|
||||||
|
for i, edge in enumerate(edges):
|
||||||
|
print(f"Edge {i + 1}: ID={edge.id}, Source={edge.source}, Target={edge.target}")
|
||||||
|
|
||||||
|
# Save edge information
|
||||||
|
if self.debug_manager.should_save("workflow"):
|
||||||
|
edges_data = [edge.dict() for edge in edges]
|
||||||
|
self.debug_manager.save_json(edges_data, "edges.json", "workflow")
|
||||||
|
|
||||||
|
# Step 4: Apply layout
|
||||||
|
print("\n----- Step 4: Applying Layout -----")
|
||||||
|
LayoutEngine.apply_topological_layout(nodes, edges)
|
||||||
|
print("Applied topological sort layout")
|
||||||
|
|
||||||
|
# Save nodes with layout
|
||||||
|
if self.debug_manager.should_save("workflow"):
|
||||||
|
nodes_with_layout = [node.dict() for node in nodes]
|
||||||
|
self.debug_manager.save_json(nodes_with_layout, "nodes_with_layout.json", "workflow")
|
||||||
|
|
||||||
|
# Step 5: Generate YAML
|
||||||
|
print("\n----- Step 5: Generating YAML -----")
|
||||||
|
workflow = Workflow(name=workflow_description.name, nodes=nodes, edges=edges)
|
||||||
|
|
||||||
|
# Ensure output directory exists
|
||||||
|
|
||||||
|
# Save as YAML
|
||||||
|
|
||||||
|
# Save final YAML
|
||||||
|
print("\n===== Workflow Generation Complete =====")
|
||||||
|
return workflow.to_yaml()
|
||||||
|
|
||||||
|
def _generate_workflow_description(self, user_requirement: str) -> WorkflowDescription:
|
||||||
|
"""
|
||||||
|
Generate simple workflow description using LLM
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_requirement: User requirement description
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Simple workflow description
|
||||||
|
"""
|
||||||
|
# Build prompt
|
||||||
|
print("Building prompt...")
|
||||||
|
prompt = build_workflow_prompt(user_requirement)
|
||||||
|
|
||||||
|
# Call LLM
|
||||||
|
print("Calling LLM to generate workflow description...")
|
||||||
|
response_text = self.llm_client.generate(prompt)
|
||||||
|
|
||||||
|
# Parse LLM response
|
||||||
|
print("Parsing LLM response...")
|
||||||
|
workflow_description_dict = self.llm_client.extract_json(response_text)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Parse into WorkflowDescription object
|
||||||
|
print("Converting JSON to WorkflowDescription object...")
|
||||||
|
workflow_description = WorkflowDescription.parse_obj(workflow_description_dict)
|
||||||
|
return workflow_description
|
||||||
|
except ValidationError as e:
|
||||||
|
# If parsing fails, print error and raise exception
|
||||||
|
error_msg = f"Failed to parse workflow description: {e}"
|
||||||
|
print(error_msg)
|
||||||
|
|
||||||
|
# Save error information
|
||||||
|
if self.debug_manager.should_save("workflow"):
|
||||||
|
self.debug_manager.save_text(str(e), "validation_error.txt", "workflow")
|
||||||
|
self.debug_manager.save_json(workflow_description_dict, "invalid_workflow_description.json", "workflow")
|
||||||
|
|
||||||
|
raise ValueError(error_msg)
|
||||||
@ -0,0 +1,203 @@
|
|||||||
|
import type { FC } from 'react'
|
||||||
|
import React from 'react'
|
||||||
|
import cn from 'classnames'
|
||||||
|
import useBoolean from 'ahooks/lib/useBoolean'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { generateWorkflow } from '@/service/debug'
|
||||||
|
import { type Model, ModelModeType } from '@/types/app'
|
||||||
|
import Modal from '@/app/components/base/modal'
|
||||||
|
import Button from '@/app/components/base/button'
|
||||||
|
import { useContext } from 'use-context-selector'
|
||||||
|
|
||||||
|
import Loading from '@/app/components/base/loading'
|
||||||
|
import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||||
|
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||||
|
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 { importDSL } from '@/service/apps'
|
||||||
|
import { DSLImportMode, DSLImportStatus } from '@/models/app'
|
||||||
|
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
||||||
|
import { getRedirection } from '@/utils/app-redirection'
|
||||||
|
import { useAppContext } from '@/context/app-context'
|
||||||
|
import { useRouter } from 'next/navigation'
|
||||||
|
import { ToastContext } from '../../base/toast'
|
||||||
|
import Generator from '../../base/icons/src/vender/other/Generator'
|
||||||
|
export type IGetCodeGeneratorResProps = {
|
||||||
|
isShow: boolean
|
||||||
|
onClose: () => void
|
||||||
|
onSuccess?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AutoGenerateModal: FC<IGetCodeGeneratorResProps> = (
|
||||||
|
{
|
||||||
|
isShow,
|
||||||
|
onClose,
|
||||||
|
onSuccess,
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
const { notify } = useContext(ToastContext)
|
||||||
|
|
||||||
|
const {
|
||||||
|
currentProvider,
|
||||||
|
currentModel,
|
||||||
|
} = useModelListAndDefaultModelAndCurrentProviderAndModel(ModelTypeEnum.textGeneration)
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { push } = useRouter()
|
||||||
|
|
||||||
|
const [instruction, setInstruction] = React.useState<string>('')
|
||||||
|
const [isLoading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = useBoolean(false)
|
||||||
|
const { isCurrentWorkspaceEditor } = useAppContext()
|
||||||
|
const [res, setRes] = React.useState<string | null>(null)
|
||||||
|
const isValid = () => {
|
||||||
|
if (instruction.trim() === '') {
|
||||||
|
notify({
|
||||||
|
type: 'error',
|
||||||
|
message: t('common.errorMsg.fieldRequired', {
|
||||||
|
field: t('appDebug.code.instruction'),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
const model: Model = {
|
||||||
|
provider: currentProvider?.provider || '',
|
||||||
|
name: currentModel?.model || '',
|
||||||
|
mode: ModelModeType.chat,
|
||||||
|
// This is a fixed parameter
|
||||||
|
completion_params: {
|
||||||
|
temperature: 0.7,
|
||||||
|
max_tokens: 0,
|
||||||
|
top_p: 0,
|
||||||
|
echo: false,
|
||||||
|
stop: [],
|
||||||
|
presence_penalty: 0,
|
||||||
|
frequency_penalty: 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const isInLLMNode = true
|
||||||
|
const onGenerate = async () => {
|
||||||
|
if (!isValid())
|
||||||
|
return
|
||||||
|
if (isLoading)
|
||||||
|
return
|
||||||
|
setLoadingTrue()
|
||||||
|
try {
|
||||||
|
const res = await generateWorkflow({
|
||||||
|
instruction,
|
||||||
|
model_config: model,
|
||||||
|
})
|
||||||
|
setRes(res)
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
setLoadingFalse()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderLoading = (
|
||||||
|
<div className='w-0 grow flex flex-col items-center justify-center h-full space-y-3'>
|
||||||
|
<Loading />
|
||||||
|
<div className='text-[13px] text-gray-400'>{t('appDebug.autoGenerate.loading')}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
const renderNoData = (
|
||||||
|
<div className='w-0 grow flex flex-col items-center px-8 justify-center h-full space-y-3'>
|
||||||
|
<Generator className='w-14 h-14 text-gray-300' />
|
||||||
|
<div className='leading-5 text-center text-[13px] font-normal text-gray-400'>
|
||||||
|
<div>{t('appDebug.autoGenerate.noDataLine1')}</div>
|
||||||
|
<div>{t('appDebug.autoGenerate.noDataLine2')}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isShow={isShow}
|
||||||
|
onClose={onClose}
|
||||||
|
className='!p-0 min-w-[1140px]'
|
||||||
|
closable
|
||||||
|
>
|
||||||
|
<div className='relative flex h-[680px] flex-wrap'>
|
||||||
|
<div className='w-[570px] shrink-0 p-8 h-full overflow-y-auto border-r border-gray-100'>
|
||||||
|
<div className='mb-8'>
|
||||||
|
<div className={'leading-[28px] text-lg font-bold'}>{t('appDebug.autoGenerate.title')}</div>
|
||||||
|
<div className='mt-1 text-[13px] font-normal text-gray-500'>{t('appDebug.autoGenerate.description')}</div>
|
||||||
|
</div>
|
||||||
|
<div className='flex items-center'>
|
||||||
|
<ModelIcon
|
||||||
|
className='shrink-0 mr-1.5'
|
||||||
|
provider={currentProvider}
|
||||||
|
modelName={currentModel?.model}
|
||||||
|
/>
|
||||||
|
<ModelName
|
||||||
|
className='grow'
|
||||||
|
modelItem={currentModel!}
|
||||||
|
showMode
|
||||||
|
showFeatures
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='mt-6'>
|
||||||
|
<div className='text-[0px]'>
|
||||||
|
<div className='mb-2 leading-5 text-sm font-medium text-gray-900'>{t('appDebug.autoGenerate.instruction')}</div>
|
||||||
|
<textarea
|
||||||
|
className="w-full h-[200px] overflow-y-auto px-3 py-2 text-sm bg-gray-50 rounded-lg"
|
||||||
|
placeholder={t('appDebug.autoGenerate.instructionPlaceholder') || ''}
|
||||||
|
value={instruction}
|
||||||
|
onChange={e => setInstruction(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-5 flex justify-end'>
|
||||||
|
<Button
|
||||||
|
className='flex space-x-1'
|
||||||
|
variant='primary'
|
||||||
|
onClick={onGenerate}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
<Generator className='w-4 h-4 text-white' />
|
||||||
|
<span className='text-xs font-semibold text-white'>{t('appDebug.autoGenerate.generate')}</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{isLoading && renderLoading}
|
||||||
|
{!isLoading && !res && renderNoData}
|
||||||
|
{(!isLoading && res) && (
|
||||||
|
<div className='w-0 grow p-6 pb-0 h-full'>
|
||||||
|
<div className='shrink-0 mb-3 leading-[160%] text-base font-semibold text-gray-800'>{t('appDebug.autoGenerate.resTitle')}</div>
|
||||||
|
<div className={cn('max-h-[555px] overflow-y-auto', !isInLLMNode && 'pb-2')}>
|
||||||
|
{res}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex justify-end py-4 bg-white'>
|
||||||
|
<Button onClick={onClose}>{t('common.operation.cancel')}</Button>
|
||||||
|
<Button variant='primary' className='ml-2' onClick={async () => {
|
||||||
|
const response = await importDSL({
|
||||||
|
mode: DSLImportMode.YAML_CONTENT,
|
||||||
|
yaml_content: res || '',
|
||||||
|
})
|
||||||
|
if (!response)
|
||||||
|
return
|
||||||
|
|
||||||
|
const { status, app_id } = response
|
||||||
|
if (status === DSLImportStatus.COMPLETED || status === DSLImportStatus.COMPLETED_WITH_WARNINGS) {
|
||||||
|
if (onSuccess)
|
||||||
|
onSuccess()
|
||||||
|
}
|
||||||
|
notify({
|
||||||
|
type: status === DSLImportStatus.COMPLETED ? 'success' : 'warning',
|
||||||
|
message: t(status === DSLImportStatus.COMPLETED ? 'app.newApp.appCreated' : 'app.newApp.caution'),
|
||||||
|
children: status === DSLImportStatus.COMPLETED_WITH_WARNINGS && t('app.newApp.appCreateDSLWarning'),
|
||||||
|
})
|
||||||
|
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
|
||||||
|
getRedirection(isCurrentWorkspaceEditor, { id: app_id }, push)
|
||||||
|
}}>{t('appDebug.autoGenerate.apply')}</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default React.memo(AutoGenerateModal)
|
||||||
Loading…
Reference in New Issue