Merge branch 'main' into fix/chore-fix
commit
86291c13e4
@ -0,0 +1,96 @@
|
|||||||
|
exclude = [
|
||||||
|
"migrations/*",
|
||||||
|
]
|
||||||
|
line-length = 120
|
||||||
|
|
||||||
|
[format]
|
||||||
|
quote-style = "double"
|
||||||
|
|
||||||
|
[lint]
|
||||||
|
preview = true
|
||||||
|
select = [
|
||||||
|
"B", # flake8-bugbear rules
|
||||||
|
"C4", # flake8-comprehensions
|
||||||
|
"E", # pycodestyle E rules
|
||||||
|
"F", # pyflakes rules
|
||||||
|
"FURB", # refurb rules
|
||||||
|
"I", # isort rules
|
||||||
|
"N", # pep8-naming
|
||||||
|
"PT", # flake8-pytest-style rules
|
||||||
|
"PLC0208", # iteration-over-set
|
||||||
|
"PLC2801", # unnecessary-dunder-call
|
||||||
|
"PLC0414", # useless-import-alias
|
||||||
|
"PLE0604", # invalid-all-object
|
||||||
|
"PLE0605", # invalid-all-format
|
||||||
|
"PLR0402", # manual-from-import
|
||||||
|
"PLR1711", # useless-return
|
||||||
|
"PLR1714", # repeated-equality-comparison
|
||||||
|
"RUF013", # implicit-optional
|
||||||
|
"RUF019", # unnecessary-key-check
|
||||||
|
"RUF100", # unused-noqa
|
||||||
|
"RUF101", # redirected-noqa
|
||||||
|
"RUF200", # invalid-pyproject-toml
|
||||||
|
"RUF022", # unsorted-dunder-all
|
||||||
|
"S506", # unsafe-yaml-load
|
||||||
|
"SIM", # flake8-simplify rules
|
||||||
|
"TRY400", # error-instead-of-exception
|
||||||
|
"TRY401", # verbose-log-message
|
||||||
|
"UP", # pyupgrade rules
|
||||||
|
"W191", # tab-indentation
|
||||||
|
"W605", # invalid-escape-sequence
|
||||||
|
]
|
||||||
|
|
||||||
|
ignore = [
|
||||||
|
"E402", # module-import-not-at-top-of-file
|
||||||
|
"E711", # none-comparison
|
||||||
|
"E712", # true-false-comparison
|
||||||
|
"E721", # type-comparison
|
||||||
|
"E722", # bare-except
|
||||||
|
"E731", # lambda-assignment
|
||||||
|
"F821", # undefined-name
|
||||||
|
"F841", # unused-variable
|
||||||
|
"FURB113", # repeated-append
|
||||||
|
"FURB152", # math-constant
|
||||||
|
"UP007", # non-pep604-annotation
|
||||||
|
"UP032", # f-string
|
||||||
|
"B005", # strip-with-multi-characters
|
||||||
|
"B006", # mutable-argument-default
|
||||||
|
"B007", # unused-loop-control-variable
|
||||||
|
"B026", # star-arg-unpacking-after-keyword-arg
|
||||||
|
"B904", # raise-without-from-inside-except
|
||||||
|
"B905", # zip-without-explicit-strict
|
||||||
|
"N806", # non-lowercase-variable-in-function
|
||||||
|
"N815", # mixed-case-variable-in-class-scope
|
||||||
|
"PT011", # pytest-raises-too-broad
|
||||||
|
"SIM102", # collapsible-if
|
||||||
|
"SIM103", # needless-bool
|
||||||
|
"SIM105", # suppressible-exception
|
||||||
|
"SIM107", # return-in-try-except-finally
|
||||||
|
"SIM108", # if-else-block-instead-of-if-exp
|
||||||
|
"SIM113", # eumerate-for-loop
|
||||||
|
"SIM117", # multiple-with-statements
|
||||||
|
"SIM210", # if-expr-with-true-false
|
||||||
|
"SIM300", # yoda-conditions,
|
||||||
|
]
|
||||||
|
|
||||||
|
[lint.per-file-ignores]
|
||||||
|
"__init__.py" = [
|
||||||
|
"F401", # unused-import
|
||||||
|
"F811", # redefined-while-unused
|
||||||
|
]
|
||||||
|
"configs/*" = [
|
||||||
|
"N802", # invalid-function-name
|
||||||
|
]
|
||||||
|
"libs/gmpy2_pkcs10aep_cipher.py" = [
|
||||||
|
"N803", # invalid-argument-name
|
||||||
|
]
|
||||||
|
"tests/*" = [
|
||||||
|
"F811", # redefined-while-unused
|
||||||
|
"F401", # unused-import
|
||||||
|
]
|
||||||
|
|
||||||
|
[lint.pyflakes]
|
||||||
|
extend-generics = [
|
||||||
|
"_pytest.monkeypatch",
|
||||||
|
"tests.integration_tests",
|
||||||
|
]
|
||||||
@ -1,113 +1,13 @@
|
|||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
python_version = sys.version_info
|
|
||||||
if not ((3, 11) <= python_version < (3, 13)):
|
|
||||||
print(f"Python 3.11 or 3.12 is required, current version is {python_version.major}.{python_version.minor}")
|
|
||||||
raise SystemExit(1)
|
|
||||||
|
|
||||||
from configs import dify_config
|
|
||||||
|
|
||||||
if not dify_config.DEBUG:
|
|
||||||
from gevent import monkey
|
|
||||||
|
|
||||||
monkey.patch_all()
|
|
||||||
|
|
||||||
import grpc.experimental.gevent
|
|
||||||
|
|
||||||
grpc.experimental.gevent.init_gevent()
|
|
||||||
|
|
||||||
import json
|
|
||||||
import threading
|
|
||||||
import time
|
|
||||||
import warnings
|
|
||||||
|
|
||||||
from flask import Response
|
|
||||||
|
|
||||||
from app_factory import create_app
|
from app_factory import create_app
|
||||||
|
from libs import threadings_utils, version_utils
|
||||||
|
|
||||||
# DO NOT REMOVE BELOW
|
# preparation before creating app
|
||||||
from events import event_handlers # noqa: F401
|
version_utils.check_supported_python_version()
|
||||||
from extensions.ext_database import db
|
threadings_utils.apply_gevent_threading_patch()
|
||||||
|
|
||||||
# TODO: Find a way to avoid importing models here
|
|
||||||
from models import account, dataset, model, source, task, tool, tools, web # noqa: F401
|
|
||||||
|
|
||||||
# DO NOT REMOVE ABOVE
|
|
||||||
|
|
||||||
|
|
||||||
warnings.simplefilter("ignore", ResourceWarning)
|
|
||||||
|
|
||||||
os.environ["TZ"] = "UTC"
|
|
||||||
# windows platform not support tzset
|
|
||||||
if hasattr(time, "tzset"):
|
|
||||||
time.tzset()
|
|
||||||
|
|
||||||
|
|
||||||
# create app
|
# create app
|
||||||
app = create_app()
|
app = create_app()
|
||||||
celery = app.extensions["celery"]
|
celery = app.extensions["celery"]
|
||||||
|
|
||||||
if dify_config.TESTING:
|
|
||||||
print("App is running in TESTING mode")
|
|
||||||
|
|
||||||
|
|
||||||
@app.after_request
|
|
||||||
def after_request(response):
|
|
||||||
"""Add Version headers to the response."""
|
|
||||||
response.headers.add("X-Version", dify_config.CURRENT_VERSION)
|
|
||||||
response.headers.add("X-Env", dify_config.DEPLOY_ENV)
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/health")
|
|
||||||
def health():
|
|
||||||
return Response(
|
|
||||||
json.dumps({"pid": os.getpid(), "status": "ok", "version": dify_config.CURRENT_VERSION}),
|
|
||||||
status=200,
|
|
||||||
content_type="application/json",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/threads")
|
|
||||||
def threads():
|
|
||||||
num_threads = threading.active_count()
|
|
||||||
threads = threading.enumerate()
|
|
||||||
|
|
||||||
thread_list = []
|
|
||||||
for thread in threads:
|
|
||||||
thread_name = thread.name
|
|
||||||
thread_id = thread.ident
|
|
||||||
is_alive = thread.is_alive()
|
|
||||||
|
|
||||||
thread_list.append(
|
|
||||||
{
|
|
||||||
"name": thread_name,
|
|
||||||
"id": thread_id,
|
|
||||||
"is_alive": is_alive,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"pid": os.getpid(),
|
|
||||||
"thread_num": num_threads,
|
|
||||||
"threads": thread_list,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/db-pool-stat")
|
|
||||||
def pool_stat():
|
|
||||||
engine = db.engine
|
|
||||||
return {
|
|
||||||
"pid": os.getpid(),
|
|
||||||
"pool_size": engine.pool.size(),
|
|
||||||
"checked_in_connections": engine.pool.checkedin(),
|
|
||||||
"checked_out_connections": engine.pool.checkedout(),
|
|
||||||
"overflow_connections": engine.pool.overflow(),
|
|
||||||
"connection_timeout": engine.pool.timeout(),
|
|
||||||
"recycle_time": db.engine.pool._recycle,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
app.run(host="0.0.0.0", port=5001)
|
app.run(host="0.0.0.0", port=5001)
|
||||||
|
|||||||
@ -0,0 +1,32 @@
|
|||||||
|
from core.prompt.utils.extract_thread_messages import extract_thread_messages
|
||||||
|
from extensions.ext_database import db
|
||||||
|
from models.model import Message
|
||||||
|
|
||||||
|
|
||||||
|
def get_thread_messages_length(conversation_id: str) -> int:
|
||||||
|
"""
|
||||||
|
Get the number of thread messages based on the parent message id.
|
||||||
|
"""
|
||||||
|
# Fetch all messages related to the conversation
|
||||||
|
query = (
|
||||||
|
db.session.query(
|
||||||
|
Message.id,
|
||||||
|
Message.parent_message_id,
|
||||||
|
Message.answer,
|
||||||
|
)
|
||||||
|
.filter(
|
||||||
|
Message.conversation_id == conversation_id,
|
||||||
|
)
|
||||||
|
.order_by(Message.created_at.desc())
|
||||||
|
)
|
||||||
|
|
||||||
|
messages = query.all()
|
||||||
|
|
||||||
|
# Extract thread messages
|
||||||
|
thread_messages = extract_thread_messages(messages)
|
||||||
|
|
||||||
|
# Exclude the newly created message with an empty answer
|
||||||
|
if thread_messages and not thread_messages[0].answer:
|
||||||
|
thread_messages.pop(0)
|
||||||
|
|
||||||
|
return len(thread_messages)
|
||||||
@ -1,4 +1,4 @@
|
|||||||
from .answer_node import AnswerNode
|
from .answer_node import AnswerNode
|
||||||
from .entities import AnswerStreamGenerateRoute
|
from .entities import AnswerStreamGenerateRoute
|
||||||
|
|
||||||
__all__ = ["AnswerStreamGenerateRoute", "AnswerNode"]
|
__all__ = ["AnswerNode", "AnswerStreamGenerateRoute"]
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
from .entities import BaseIterationNodeData, BaseIterationState, BaseNodeData
|
from .entities import BaseIterationNodeData, BaseIterationState, BaseNodeData
|
||||||
from .node import BaseNode
|
from .node import BaseNode
|
||||||
|
|
||||||
__all__ = ["BaseNode", "BaseNodeData", "BaseIterationNodeData", "BaseIterationState"]
|
__all__ = ["BaseIterationNodeData", "BaseIterationState", "BaseNode", "BaseNodeData"]
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
from .end_node import EndNode
|
from .end_node import EndNode
|
||||||
from .entities import EndStreamParam
|
from .entities import EndStreamParam
|
||||||
|
|
||||||
__all__ = ["EndStreamParam", "EndNode"]
|
__all__ = ["EndNode", "EndStreamParam"]
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
from .entities import BodyData, HttpRequestNodeAuthorization, HttpRequestNodeBody, HttpRequestNodeData
|
from .entities import BodyData, HttpRequestNodeAuthorization, HttpRequestNodeBody, HttpRequestNodeData
|
||||||
from .node import HttpRequestNode
|
from .node import HttpRequestNode
|
||||||
|
|
||||||
__all__ = ["HttpRequestNodeData", "HttpRequestNodeAuthorization", "HttpRequestNodeBody", "BodyData", "HttpRequestNode"]
|
__all__ = ["BodyData", "HttpRequestNode", "HttpRequestNodeAuthorization", "HttpRequestNodeBody", "HttpRequestNodeData"]
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
from .entities import QuestionClassifierNodeData
|
from .entities import QuestionClassifierNodeData
|
||||||
from .question_classifier_node import QuestionClassifierNode
|
from .question_classifier_node import QuestionClassifierNode
|
||||||
|
|
||||||
__all__ = ["QuestionClassifierNodeData", "QuestionClassifierNode"]
|
__all__ = ["QuestionClassifierNode", "QuestionClassifierNodeData"]
|
||||||
|
|||||||
@ -1,8 +0,0 @@
|
|||||||
from .node import VariableAssignerNode
|
|
||||||
from .node_data import VariableAssignerData, WriteMode
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
"VariableAssignerNode",
|
|
||||||
"VariableAssignerData",
|
|
||||||
"WriteMode",
|
|
||||||
]
|
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
class VariableOperatorNodeError(Exception):
|
||||||
|
"""Base error type, don't use directly."""
|
||||||
|
|
||||||
|
pass
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from core.variables import Variable
|
||||||
|
from core.workflow.nodes.variable_assigner.common.exc import VariableOperatorNodeError
|
||||||
|
from extensions.ext_database import db
|
||||||
|
from models import ConversationVariable
|
||||||
|
|
||||||
|
|
||||||
|
def update_conversation_variable(conversation_id: str, variable: Variable):
|
||||||
|
stmt = select(ConversationVariable).where(
|
||||||
|
ConversationVariable.id == variable.id, ConversationVariable.conversation_id == conversation_id
|
||||||
|
)
|
||||||
|
with Session(db.engine) as session:
|
||||||
|
row = session.scalar(stmt)
|
||||||
|
if not row:
|
||||||
|
raise VariableOperatorNodeError("conversation variable not found in the database")
|
||||||
|
row.data = variable.model_dump_json()
|
||||||
|
session.commit()
|
||||||
@ -1,2 +0,0 @@
|
|||||||
class VariableAssignerNodeError(Exception):
|
|
||||||
pass
|
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
from .node import VariableAssignerNode
|
||||||
|
|
||||||
|
__all__ = ["VariableAssignerNode"]
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
from .node import VariableAssignerNode
|
||||||
|
|
||||||
|
__all__ = ["VariableAssignerNode"]
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
from core.variables import SegmentType
|
||||||
|
|
||||||
|
EMPTY_VALUE_MAPPING = {
|
||||||
|
SegmentType.STRING: "",
|
||||||
|
SegmentType.NUMBER: 0,
|
||||||
|
SegmentType.OBJECT: {},
|
||||||
|
SegmentType.ARRAY_ANY: [],
|
||||||
|
SegmentType.ARRAY_STRING: [],
|
||||||
|
SegmentType.ARRAY_NUMBER: [],
|
||||||
|
SegmentType.ARRAY_OBJECT: [],
|
||||||
|
}
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
from collections.abc import Sequence
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from core.workflow.nodes.base import BaseNodeData
|
||||||
|
|
||||||
|
from .enums import InputType, Operation
|
||||||
|
|
||||||
|
|
||||||
|
class VariableOperationItem(BaseModel):
|
||||||
|
variable_selector: Sequence[str]
|
||||||
|
input_type: InputType
|
||||||
|
operation: Operation
|
||||||
|
value: Any | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class VariableAssignerNodeData(BaseNodeData):
|
||||||
|
version: str = "2"
|
||||||
|
items: Sequence[VariableOperationItem]
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
from enum import StrEnum
|
||||||
|
|
||||||
|
|
||||||
|
class Operation(StrEnum):
|
||||||
|
OVER_WRITE = "over-write"
|
||||||
|
CLEAR = "clear"
|
||||||
|
APPEND = "append"
|
||||||
|
EXTEND = "extend"
|
||||||
|
SET = "set"
|
||||||
|
ADD = "+="
|
||||||
|
SUBTRACT = "-="
|
||||||
|
MULTIPLY = "*="
|
||||||
|
DIVIDE = "/="
|
||||||
|
|
||||||
|
|
||||||
|
class InputType(StrEnum):
|
||||||
|
VARIABLE = "variable"
|
||||||
|
CONSTANT = "constant"
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
from collections.abc import Sequence
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from core.workflow.nodes.variable_assigner.common.exc import VariableOperatorNodeError
|
||||||
|
|
||||||
|
from .enums import InputType, Operation
|
||||||
|
|
||||||
|
|
||||||
|
class OperationNotSupportedError(VariableOperatorNodeError):
|
||||||
|
def __init__(self, *, operation: Operation, varialbe_type: str):
|
||||||
|
super().__init__(f"Operation {operation} is not supported for type {varialbe_type}")
|
||||||
|
|
||||||
|
|
||||||
|
class InputTypeNotSupportedError(VariableOperatorNodeError):
|
||||||
|
def __init__(self, *, input_type: InputType, operation: Operation):
|
||||||
|
super().__init__(f"Input type {input_type} is not supported for operation {operation}")
|
||||||
|
|
||||||
|
|
||||||
|
class VariableNotFoundError(VariableOperatorNodeError):
|
||||||
|
def __init__(self, *, variable_selector: Sequence[str]):
|
||||||
|
super().__init__(f"Variable {variable_selector} not found")
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidInputValueError(VariableOperatorNodeError):
|
||||||
|
def __init__(self, *, value: Any):
|
||||||
|
super().__init__(f"Invalid input value {value}")
|
||||||
|
|
||||||
|
|
||||||
|
class ConversationIDNotFoundError(VariableOperatorNodeError):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__("conversation_id not found")
|
||||||
@ -0,0 +1,91 @@
|
|||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from core.variables import SegmentType
|
||||||
|
|
||||||
|
from .enums import Operation
|
||||||
|
|
||||||
|
|
||||||
|
def is_operation_supported(*, variable_type: SegmentType, operation: Operation):
|
||||||
|
match operation:
|
||||||
|
case Operation.OVER_WRITE | Operation.CLEAR:
|
||||||
|
return True
|
||||||
|
case Operation.SET:
|
||||||
|
return variable_type in {SegmentType.OBJECT, SegmentType.STRING, SegmentType.NUMBER}
|
||||||
|
case Operation.ADD | Operation.SUBTRACT | Operation.MULTIPLY | Operation.DIVIDE:
|
||||||
|
# Only number variable can be added, subtracted, multiplied or divided
|
||||||
|
return variable_type == SegmentType.NUMBER
|
||||||
|
case Operation.APPEND | Operation.EXTEND:
|
||||||
|
# Only array variable can be appended or extended
|
||||||
|
return variable_type in {
|
||||||
|
SegmentType.ARRAY_ANY,
|
||||||
|
SegmentType.ARRAY_OBJECT,
|
||||||
|
SegmentType.ARRAY_STRING,
|
||||||
|
SegmentType.ARRAY_NUMBER,
|
||||||
|
SegmentType.ARRAY_FILE,
|
||||||
|
}
|
||||||
|
case _:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def is_variable_input_supported(*, operation: Operation):
|
||||||
|
if operation in {Operation.SET, Operation.ADD, Operation.SUBTRACT, Operation.MULTIPLY, Operation.DIVIDE}:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def is_constant_input_supported(*, variable_type: SegmentType, operation: Operation):
|
||||||
|
match variable_type:
|
||||||
|
case SegmentType.STRING | SegmentType.OBJECT:
|
||||||
|
return operation in {Operation.OVER_WRITE, Operation.SET}
|
||||||
|
case SegmentType.NUMBER:
|
||||||
|
return operation in {
|
||||||
|
Operation.OVER_WRITE,
|
||||||
|
Operation.SET,
|
||||||
|
Operation.ADD,
|
||||||
|
Operation.SUBTRACT,
|
||||||
|
Operation.MULTIPLY,
|
||||||
|
Operation.DIVIDE,
|
||||||
|
}
|
||||||
|
case _:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def is_input_value_valid(*, variable_type: SegmentType, operation: Operation, value: Any):
|
||||||
|
if operation == Operation.CLEAR:
|
||||||
|
return True
|
||||||
|
match variable_type:
|
||||||
|
case SegmentType.STRING:
|
||||||
|
return isinstance(value, str)
|
||||||
|
|
||||||
|
case SegmentType.NUMBER:
|
||||||
|
if not isinstance(value, int | float):
|
||||||
|
return False
|
||||||
|
if operation == Operation.DIVIDE and value == 0:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
case SegmentType.OBJECT:
|
||||||
|
return isinstance(value, dict)
|
||||||
|
|
||||||
|
# Array & Append
|
||||||
|
case SegmentType.ARRAY_ANY if operation == Operation.APPEND:
|
||||||
|
return isinstance(value, str | float | int | dict)
|
||||||
|
case SegmentType.ARRAY_STRING if operation == Operation.APPEND:
|
||||||
|
return isinstance(value, str)
|
||||||
|
case SegmentType.ARRAY_NUMBER if operation == Operation.APPEND:
|
||||||
|
return isinstance(value, int | float)
|
||||||
|
case SegmentType.ARRAY_OBJECT if operation == Operation.APPEND:
|
||||||
|
return isinstance(value, dict)
|
||||||
|
|
||||||
|
# Array & Extend / Overwrite
|
||||||
|
case SegmentType.ARRAY_ANY if operation in {Operation.EXTEND, Operation.OVER_WRITE}:
|
||||||
|
return isinstance(value, list) and all(isinstance(item, str | float | int | dict) for item in value)
|
||||||
|
case SegmentType.ARRAY_STRING if operation in {Operation.EXTEND, Operation.OVER_WRITE}:
|
||||||
|
return isinstance(value, list) and all(isinstance(item, str) for item in value)
|
||||||
|
case SegmentType.ARRAY_NUMBER if operation in {Operation.EXTEND, Operation.OVER_WRITE}:
|
||||||
|
return isinstance(value, list) and all(isinstance(item, int | float) for item in value)
|
||||||
|
case SegmentType.ARRAY_OBJECT if operation in {Operation.EXTEND, Operation.OVER_WRITE}:
|
||||||
|
return isinstance(value, list) and all(isinstance(item, dict) for item in value)
|
||||||
|
|
||||||
|
case _:
|
||||||
|
return False
|
||||||
@ -0,0 +1,159 @@
|
|||||||
|
import json
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from core.variables import SegmentType, Variable
|
||||||
|
from core.workflow.constants import CONVERSATION_VARIABLE_NODE_ID
|
||||||
|
from core.workflow.entities.node_entities import NodeRunResult
|
||||||
|
from core.workflow.nodes.base import BaseNode
|
||||||
|
from core.workflow.nodes.enums import NodeType
|
||||||
|
from core.workflow.nodes.variable_assigner.common import helpers as common_helpers
|
||||||
|
from core.workflow.nodes.variable_assigner.common.exc import VariableOperatorNodeError
|
||||||
|
from models.workflow import WorkflowNodeExecutionStatus
|
||||||
|
|
||||||
|
from . import helpers
|
||||||
|
from .constants import EMPTY_VALUE_MAPPING
|
||||||
|
from .entities import VariableAssignerNodeData
|
||||||
|
from .enums import InputType, Operation
|
||||||
|
from .exc import (
|
||||||
|
ConversationIDNotFoundError,
|
||||||
|
InputTypeNotSupportedError,
|
||||||
|
InvalidInputValueError,
|
||||||
|
OperationNotSupportedError,
|
||||||
|
VariableNotFoundError,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class VariableAssignerNode(BaseNode[VariableAssignerNodeData]):
|
||||||
|
_node_data_cls = VariableAssignerNodeData
|
||||||
|
_node_type = NodeType.VARIABLE_ASSIGNER
|
||||||
|
|
||||||
|
def _run(self) -> NodeRunResult:
|
||||||
|
inputs = self.node_data.model_dump()
|
||||||
|
process_data = {}
|
||||||
|
# NOTE: This node has no outputs
|
||||||
|
updated_variables: list[Variable] = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
for item in self.node_data.items:
|
||||||
|
variable = self.graph_runtime_state.variable_pool.get(item.variable_selector)
|
||||||
|
|
||||||
|
# ==================== Validation Part
|
||||||
|
|
||||||
|
# Check if variable exists
|
||||||
|
if not isinstance(variable, Variable):
|
||||||
|
raise VariableNotFoundError(variable_selector=item.variable_selector)
|
||||||
|
|
||||||
|
# Check if operation is supported
|
||||||
|
if not helpers.is_operation_supported(variable_type=variable.value_type, operation=item.operation):
|
||||||
|
raise OperationNotSupportedError(operation=item.operation, varialbe_type=variable.value_type)
|
||||||
|
|
||||||
|
# Check if variable input is supported
|
||||||
|
if item.input_type == InputType.VARIABLE and not helpers.is_variable_input_supported(
|
||||||
|
operation=item.operation
|
||||||
|
):
|
||||||
|
raise InputTypeNotSupportedError(input_type=InputType.VARIABLE, operation=item.operation)
|
||||||
|
|
||||||
|
# Check if constant input is supported
|
||||||
|
if item.input_type == InputType.CONSTANT and not helpers.is_constant_input_supported(
|
||||||
|
variable_type=variable.value_type, operation=item.operation
|
||||||
|
):
|
||||||
|
raise InputTypeNotSupportedError(input_type=InputType.CONSTANT, operation=item.operation)
|
||||||
|
|
||||||
|
# Get value from variable pool
|
||||||
|
if (
|
||||||
|
item.input_type == InputType.VARIABLE
|
||||||
|
and item.operation != Operation.CLEAR
|
||||||
|
and item.value is not None
|
||||||
|
):
|
||||||
|
value = self.graph_runtime_state.variable_pool.get(item.value)
|
||||||
|
if value is None:
|
||||||
|
raise VariableNotFoundError(variable_selector=item.value)
|
||||||
|
# Skip if value is NoneSegment
|
||||||
|
if value.value_type == SegmentType.NONE:
|
||||||
|
continue
|
||||||
|
item.value = value.value
|
||||||
|
|
||||||
|
# If set string / bytes / bytearray to object, try convert string to object.
|
||||||
|
if (
|
||||||
|
item.operation == Operation.SET
|
||||||
|
and variable.value_type == SegmentType.OBJECT
|
||||||
|
and isinstance(item.value, str | bytes | bytearray)
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
item.value = json.loads(item.value)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
raise InvalidInputValueError(value=item.value)
|
||||||
|
|
||||||
|
# Check if input value is valid
|
||||||
|
if not helpers.is_input_value_valid(
|
||||||
|
variable_type=variable.value_type, operation=item.operation, value=item.value
|
||||||
|
):
|
||||||
|
raise InvalidInputValueError(value=item.value)
|
||||||
|
|
||||||
|
# ==================== Execution Part
|
||||||
|
|
||||||
|
updated_value = self._handle_item(
|
||||||
|
variable=variable,
|
||||||
|
operation=item.operation,
|
||||||
|
value=item.value,
|
||||||
|
)
|
||||||
|
variable = variable.model_copy(update={"value": updated_value})
|
||||||
|
updated_variables.append(variable)
|
||||||
|
except VariableOperatorNodeError as e:
|
||||||
|
return NodeRunResult(
|
||||||
|
status=WorkflowNodeExecutionStatus.FAILED,
|
||||||
|
inputs=inputs,
|
||||||
|
process_data=process_data,
|
||||||
|
error=str(e),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update variables
|
||||||
|
for variable in updated_variables:
|
||||||
|
self.graph_runtime_state.variable_pool.add(variable.selector, variable)
|
||||||
|
process_data[variable.name] = variable.value
|
||||||
|
|
||||||
|
if variable.selector[0] == CONVERSATION_VARIABLE_NODE_ID:
|
||||||
|
conversation_id = self.graph_runtime_state.variable_pool.get(["sys", "conversation_id"])
|
||||||
|
if not conversation_id:
|
||||||
|
raise ConversationIDNotFoundError
|
||||||
|
else:
|
||||||
|
conversation_id = conversation_id.value
|
||||||
|
common_helpers.update_conversation_variable(
|
||||||
|
conversation_id=conversation_id,
|
||||||
|
variable=variable,
|
||||||
|
)
|
||||||
|
|
||||||
|
return NodeRunResult(
|
||||||
|
status=WorkflowNodeExecutionStatus.SUCCEEDED,
|
||||||
|
inputs=inputs,
|
||||||
|
process_data=process_data,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _handle_item(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
variable: Variable,
|
||||||
|
operation: Operation,
|
||||||
|
value: Any,
|
||||||
|
):
|
||||||
|
match operation:
|
||||||
|
case Operation.OVER_WRITE:
|
||||||
|
return value
|
||||||
|
case Operation.CLEAR:
|
||||||
|
return EMPTY_VALUE_MAPPING[variable.value_type]
|
||||||
|
case Operation.APPEND:
|
||||||
|
return variable.value + [value]
|
||||||
|
case Operation.EXTEND:
|
||||||
|
return variable.value + value
|
||||||
|
case Operation.SET:
|
||||||
|
return value
|
||||||
|
case Operation.ADD:
|
||||||
|
return variable.value + value
|
||||||
|
case Operation.SUBTRACT:
|
||||||
|
return variable.value - value
|
||||||
|
case Operation.MULTIPLY:
|
||||||
|
return variable.value * value
|
||||||
|
case Operation.DIVIDE:
|
||||||
|
return variable.value / value
|
||||||
|
case _:
|
||||||
|
raise OperationNotSupportedError(operation=operation, varialbe_type=variable.value_type)
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
from flask import Flask
|
||||||
|
|
||||||
|
|
||||||
|
class DifyApp(Flask):
|
||||||
|
pass
|
||||||
@ -0,0 +1,65 @@
|
|||||||
|
import json
|
||||||
|
import os
|
||||||
|
import threading
|
||||||
|
|
||||||
|
from flask import Response
|
||||||
|
|
||||||
|
from configs import dify_config
|
||||||
|
from dify_app import DifyApp
|
||||||
|
|
||||||
|
|
||||||
|
def init_app(app: DifyApp):
|
||||||
|
@app.after_request
|
||||||
|
def after_request(response):
|
||||||
|
"""Add Version headers to the response."""
|
||||||
|
response.headers.add("X-Version", dify_config.CURRENT_VERSION)
|
||||||
|
response.headers.add("X-Env", dify_config.DEPLOY_ENV)
|
||||||
|
return response
|
||||||
|
|
||||||
|
@app.route("/health")
|
||||||
|
def health():
|
||||||
|
return Response(
|
||||||
|
json.dumps({"pid": os.getpid(), "status": "ok", "version": dify_config.CURRENT_VERSION}),
|
||||||
|
status=200,
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.route("/threads")
|
||||||
|
def threads():
|
||||||
|
num_threads = threading.active_count()
|
||||||
|
threads = threading.enumerate()
|
||||||
|
|
||||||
|
thread_list = []
|
||||||
|
for thread in threads:
|
||||||
|
thread_name = thread.name
|
||||||
|
thread_id = thread.ident
|
||||||
|
is_alive = thread.is_alive()
|
||||||
|
|
||||||
|
thread_list.append(
|
||||||
|
{
|
||||||
|
"name": thread_name,
|
||||||
|
"id": thread_id,
|
||||||
|
"is_alive": is_alive,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"pid": os.getpid(),
|
||||||
|
"thread_num": num_threads,
|
||||||
|
"threads": thread_list,
|
||||||
|
}
|
||||||
|
|
||||||
|
@app.route("/db-pool-stat")
|
||||||
|
def pool_stat():
|
||||||
|
from extensions.ext_database import db
|
||||||
|
|
||||||
|
engine = db.engine
|
||||||
|
return {
|
||||||
|
"pid": os.getpid(),
|
||||||
|
"pool_size": engine.pool.size(),
|
||||||
|
"checked_in_connections": engine.pool.checkedin(),
|
||||||
|
"checked_out_connections": engine.pool.checkedout(),
|
||||||
|
"overflow_connections": engine.pool.overflow(),
|
||||||
|
"connection_timeout": engine.pool.timeout(),
|
||||||
|
"recycle_time": db.engine.pool._recycle,
|
||||||
|
}
|
||||||
@ -0,0 +1,48 @@
|
|||||||
|
from configs import dify_config
|
||||||
|
from dify_app import DifyApp
|
||||||
|
|
||||||
|
|
||||||
|
def init_app(app: DifyApp):
|
||||||
|
# register blueprint routers
|
||||||
|
|
||||||
|
from flask_cors import CORS
|
||||||
|
|
||||||
|
from controllers.console import bp as console_app_bp
|
||||||
|
from controllers.files import bp as files_bp
|
||||||
|
from controllers.inner_api import bp as inner_api_bp
|
||||||
|
from controllers.service_api import bp as service_api_bp
|
||||||
|
from controllers.web import bp as web_bp
|
||||||
|
|
||||||
|
CORS(
|
||||||
|
service_api_bp,
|
||||||
|
allow_headers=["Content-Type", "Authorization", "X-App-Code"],
|
||||||
|
methods=["GET", "PUT", "POST", "DELETE", "OPTIONS", "PATCH"],
|
||||||
|
)
|
||||||
|
app.register_blueprint(service_api_bp)
|
||||||
|
|
||||||
|
CORS(
|
||||||
|
web_bp,
|
||||||
|
resources={r"/*": {"origins": dify_config.WEB_API_CORS_ALLOW_ORIGINS}},
|
||||||
|
supports_credentials=True,
|
||||||
|
allow_headers=["Content-Type", "Authorization", "X-App-Code"],
|
||||||
|
methods=["GET", "PUT", "POST", "DELETE", "OPTIONS", "PATCH"],
|
||||||
|
expose_headers=["X-Version", "X-Env"],
|
||||||
|
)
|
||||||
|
|
||||||
|
app.register_blueprint(web_bp)
|
||||||
|
|
||||||
|
CORS(
|
||||||
|
console_app_bp,
|
||||||
|
resources={r"/*": {"origins": dify_config.CONSOLE_CORS_ALLOW_ORIGINS}},
|
||||||
|
supports_credentials=True,
|
||||||
|
allow_headers=["Content-Type", "Authorization"],
|
||||||
|
methods=["GET", "PUT", "POST", "DELETE", "OPTIONS", "PATCH"],
|
||||||
|
expose_headers=["X-Version", "X-Env"],
|
||||||
|
)
|
||||||
|
|
||||||
|
app.register_blueprint(console_app_bp)
|
||||||
|
|
||||||
|
CORS(files_bp, allow_headers=["Content-Type"], methods=["GET", "PUT", "POST", "DELETE", "OPTIONS", "PATCH"])
|
||||||
|
app.register_blueprint(files_bp)
|
||||||
|
|
||||||
|
app.register_blueprint(inner_api_bp)
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
from dify_app import DifyApp
|
||||||
|
|
||||||
|
|
||||||
|
def init_app(app: DifyApp):
|
||||||
|
from commands import (
|
||||||
|
add_qdrant_doc_id_index,
|
||||||
|
convert_to_agent_apps,
|
||||||
|
create_tenant,
|
||||||
|
fix_app_site_missing,
|
||||||
|
reset_email,
|
||||||
|
reset_encrypt_key_pair,
|
||||||
|
reset_password,
|
||||||
|
upgrade_db,
|
||||||
|
vdb_migrate,
|
||||||
|
)
|
||||||
|
|
||||||
|
cmds_to_register = [
|
||||||
|
reset_password,
|
||||||
|
reset_email,
|
||||||
|
reset_encrypt_key_pair,
|
||||||
|
vdb_migrate,
|
||||||
|
convert_to_agent_apps,
|
||||||
|
add_qdrant_doc_id_index,
|
||||||
|
create_tenant,
|
||||||
|
upgrade_db,
|
||||||
|
fix_app_site_missing,
|
||||||
|
]
|
||||||
|
for cmd in cmds_to_register:
|
||||||
|
app.cli.add_command(cmd)
|
||||||
@ -1,17 +1,13 @@
|
|||||||
from flask import Flask
|
|
||||||
|
|
||||||
from configs import dify_config
|
from configs import dify_config
|
||||||
|
from dify_app import DifyApp
|
||||||
|
|
||||||
|
|
||||||
|
def is_enabled() -> bool:
|
||||||
|
return dify_config.API_COMPRESSION_ENABLED
|
||||||
|
|
||||||
def init_app(app: Flask):
|
|
||||||
if dify_config.API_COMPRESSION_ENABLED:
|
|
||||||
from flask_compress import Compress
|
|
||||||
|
|
||||||
app.config["COMPRESS_MIMETYPES"] = [
|
def init_app(app: DifyApp):
|
||||||
"application/json",
|
from flask_compress import Compress
|
||||||
"image/svg+xml",
|
|
||||||
"text/html",
|
|
||||||
]
|
|
||||||
|
|
||||||
compress = Compress()
|
compress = Compress()
|
||||||
compress.init_app(app)
|
compress.init_app(app)
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
from flask import Flask
|
|
||||||
|
|
||||||
from core.hosting_configuration import HostingConfiguration
|
from core.hosting_configuration import HostingConfiguration
|
||||||
|
|
||||||
hosting_configuration = HostingConfiguration()
|
hosting_configuration = HostingConfiguration()
|
||||||
|
|
||||||
|
|
||||||
def init_app(app: Flask):
|
from dify_app import DifyApp
|
||||||
|
|
||||||
|
|
||||||
|
def init_app(app: DifyApp):
|
||||||
hosting_configuration.init_app(app)
|
hosting_configuration.init_app(app)
|
||||||
|
|||||||
@ -0,0 +1,6 @@
|
|||||||
|
from dify_app import DifyApp
|
||||||
|
|
||||||
|
|
||||||
|
def init_app(app: DifyApp):
|
||||||
|
from events import event_handlers # noqa: F401
|
||||||
|
from models import account, dataset, model, source, task, tool, tools, web # noqa: F401
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue