Compare commits
103
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac80c04bd3 | ||
|
|
fa9b767bf2 | ||
|
|
abeaea4f79 | ||
|
|
b65f2eb55f | ||
|
|
7d620ffd5e | ||
|
|
6f6ba2f025 | ||
|
|
33ba7e659b | ||
|
|
750ec55646 | ||
|
|
86d3fff666 | ||
|
|
e91531fc23 | ||
|
|
2524f16525 | ||
|
|
cefec44070 | ||
|
|
20376ca951 | ||
|
|
475b8d731e | ||
|
|
963b6f628a | ||
|
|
63ea6f1ecf | ||
|
|
947c9f70fb | ||
|
|
5e52d4d6b3 | ||
|
|
939dcb4c0a | ||
|
|
223ab5a38f | ||
|
|
db7a37a111 | ||
|
|
fe0d932f50 | ||
|
|
69fb0a4a28 | ||
|
|
04a0ae3aa9 | ||
|
|
e5d6047fb4 | ||
|
|
9e782d4c1e | ||
|
|
98a4b3e78b | ||
|
|
2b4d1cf1db | ||
|
|
fe76dfe1f8 | ||
|
|
c3774bef7e | ||
|
|
695a7400a9 | ||
|
|
e6a8800f66 | ||
|
|
cee8731393 | ||
|
|
4ae94dc027 | ||
|
|
3a69a6a452 | ||
|
|
f8f21ef7c0 | ||
|
|
0587eb4956 | ||
|
|
433374abea | ||
|
|
23ed3a520b | ||
|
|
5646442931 | ||
|
|
1a6298b6ea | ||
|
|
bf9b572bc3 | ||
|
|
cf72e53a10 | ||
|
|
98bd79f548 | ||
|
|
84a866028a | ||
|
|
10bd03611c | ||
|
|
7c27d4b202 | ||
|
|
8165d0b469 | ||
|
|
e796937d02 | ||
|
|
49c952a631 | ||
|
|
5f9d236d22 | ||
|
|
59f5a82261 | ||
|
|
f22a1adb8b | ||
|
|
a8e8c37fdd | ||
|
|
37486a9cc6 | ||
|
|
efebbffe96 | ||
|
|
5e035a4209 | ||
|
|
12fa517297 | ||
|
|
36ae0e5476 | ||
|
|
74f66d3119 | ||
|
|
adfaee7ab5 | ||
|
|
d37490adc3 | ||
|
|
087bb60b31 | ||
|
|
5019547d33 | ||
|
|
58f012f3de | ||
|
|
b938c9b7f6 | ||
|
|
2b1facc7a6 | ||
|
|
1d5ea80a2b | ||
|
|
0415cc209d | ||
|
|
545e5cbcd6 | ||
|
|
1fab02c25a | ||
|
|
258736f505 | ||
|
|
0bc4da38fc | ||
|
|
037f200527 | ||
|
|
b541792465 | ||
|
|
eb9b256ee8 | ||
|
|
5d8b32a249 | ||
|
|
c960b364c9 | ||
|
|
b817036343 | ||
|
|
46036e6ce6 | ||
|
|
1ffda0dd34 | ||
|
|
da01b460fe | ||
|
|
90a1508b87 | ||
|
|
b07016113c | ||
|
|
d8317fcf81 | ||
|
|
a6bc642721 | ||
|
|
b730f243dc | ||
|
|
71a57275ab | ||
|
|
41bf8d925f | ||
|
|
6d172498d1 | ||
|
|
cad58658c2 | ||
|
|
a58b990855 | ||
|
|
b6b1903a37 | ||
|
|
ed5596a8f4 | ||
|
|
49d0acd188 | ||
|
|
58a74fe1fb | ||
|
|
a1ab4aec3d | ||
|
|
f77f7e1437 | ||
|
|
adda049265 | ||
|
|
9b2a9260ef | ||
|
|
c8cc31af88 | ||
|
|
d333de274f | ||
|
|
9e220d5d30 |
@@ -0,0 +1,13 @@
|
||||
name: "👾 Tracker"
|
||||
description: For inner usages, please donot use this template.
|
||||
title: "[Tracker] "
|
||||
labels:
|
||||
- tracker
|
||||
body:
|
||||
- type: textarea
|
||||
id: content
|
||||
attributes:
|
||||
label: Blockers
|
||||
placeholder: "- [ ] ..."
|
||||
validations:
|
||||
required: true
|
||||
@@ -5,6 +5,7 @@ on:
|
||||
branches:
|
||||
- "main"
|
||||
- "deploy/dev"
|
||||
- "deploy/enterprise"
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
name: Deploy Enterprise
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Build and Push API & Web"]
|
||||
branches:
|
||||
- "deploy/enterprise"
|
||||
types:
|
||||
- completed
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
github.event.workflow_run.conclusion == 'success' &&
|
||||
github.event.workflow_run.head_branch == 'deploy/enterprise'
|
||||
|
||||
steps:
|
||||
- name: Deploy to server
|
||||
uses: appleboy/ssh-action@v0.1.8
|
||||
with:
|
||||
host: ${{ secrets.ENTERPRISE_SSH_HOST }}
|
||||
username: ${{ secrets.ENTERPRISE_SSH_USER }}
|
||||
password: ${{ secrets.ENTERPRISE_SSH_PASSWORD }}
|
||||
script: |
|
||||
${{ vars.ENTERPRISE_SSH_SCRIPT || secrets.ENTERPRISE_SSH_SCRIPT }}
|
||||
@@ -10,5 +10,6 @@ yq eval '.services["elasticsearch"].ports += ["9200:9200"]' -i docker/docker-com
|
||||
yq eval '.services.couchbase-server.ports += ["8091-8096:8091-8096"]' -i docker/docker-compose.yaml
|
||||
yq eval '.services.couchbase-server.ports += ["11210:11210"]' -i docker/docker-compose.yaml
|
||||
yq eval '.services.tidb.ports += ["4000:4000"]' -i docker/tidb/docker-compose.yaml
|
||||
yq eval '.services.opengauss.ports += ["6600:6600"]' -i docker/docker-compose.yaml
|
||||
|
||||
echo "Ports exposed for sandbox, weaviate, tidb, qdrant, chroma, milvus, pgvector, pgvecto-rs, elasticsearch, couchbase"
|
||||
echo "Ports exposed for sandbox, weaviate, tidb, qdrant, chroma, milvus, pgvector, pgvecto-rs, elasticsearch, couchbase, opengauss"
|
||||
|
||||
@@ -76,6 +76,7 @@ jobs:
|
||||
milvus-standalone
|
||||
pgvecto-rs
|
||||
pgvector
|
||||
opengauss
|
||||
chroma
|
||||
elasticsearch
|
||||
|
||||
|
||||
@@ -202,3 +202,6 @@ api/.vscode
|
||||
|
||||
# plugin migrate
|
||||
plugins.jsonl
|
||||
|
||||
# mise
|
||||
mise.toml
|
||||
|
||||
+2
-2
@@ -26,7 +26,7 @@
|
||||
| [@jyong](https://github.com/JohnJyong) | RAG 流水线设计 |
|
||||
| [@GarfieldDai](https://github.com/GarfieldDai) | 构建 workflow 编排 |
|
||||
| [@iamjoel](https://github.com/iamjoel) & [@zxhlyh](https://github.com/zxhlyh) | 让我们的前端更易用 |
|
||||
| [@guchenhe](https://github.com/guchenhe) & [@crazywoola](https://github.com/crazywoola) | 开发人员体验, 综合事项联系人 |
|
||||
| [@guchenhe](https://github.com/guchenhe) & [@crazywoola](https://github.com/crazywoola) | 开发人员体验,综合事项联系人 |
|
||||
| [@takatost](https://github.com/takatost) | 产品整体方向和架构 |
|
||||
|
||||
事项优先级:
|
||||
@@ -47,7 +47,7 @@
|
||||
| ------------------------------------------------------------ | --------------- |
|
||||
| 核心功能的 Bugs(例如无法登录、应用无法工作、安全漏洞) | 紧急 |
|
||||
| 非紧急 bugs, 性能提升 | 中等优先级 |
|
||||
| 小幅修复(错别字, 能正常工作但存在误导的 UI) | 低优先级 |
|
||||
| 小幅修复 (错别字,能正常工作但存在误导的 UI) | 低优先级 |
|
||||
|
||||
## 安装
|
||||
|
||||
|
||||
+3
-3
@@ -79,7 +79,7 @@ Dify 是一个开源的 LLM 应用开发平台。其直观的界面结合了 AI
|
||||
广泛的 RAG 功能,涵盖从文档摄入到检索的所有内容,支持从 PDF、PPT 和其他常见文档格式中提取文本的开箱即用的支持。
|
||||
|
||||
**5. Agent 智能体**:
|
||||
您可以基于 LLM 函数调用或 ReAct 定义 Agent,并为 Agent 添加预构建或自定义工具。Dify 为 AI Agent 提供了50多种内置工具,如谷歌搜索、DALL·E、Stable Diffusion 和 WolframAlpha 等。
|
||||
您可以基于 LLM 函数调用或 ReAct 定义 Agent,并为 Agent 添加预构建或自定义工具。Dify 为 AI Agent 提供了 50 多种内置工具,如谷歌搜索、DALL·E、Stable Diffusion 和 WolframAlpha 等。
|
||||
|
||||
**6. LLMOps**:
|
||||
随时间监视和分析应用程序日志和性能。您可以根据生产数据和标注持续改进提示、数据集和模型。
|
||||
@@ -112,7 +112,7 @@ Dify 是一个开源的 LLM 应用开发平台。其直观的界面结合了 AI
|
||||
<td align="center">仅限 OpenAI</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">RAG引擎</td>
|
||||
<td align="center">RAG 引擎</td>
|
||||
<td align="center">✅</td>
|
||||
<td align="center">✅</td>
|
||||
<td align="center">✅</td>
|
||||
@@ -234,7 +234,7 @@ docker compose up -d
|
||||
对于那些想要贡献代码的人,请参阅我们的[贡献指南](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md)。
|
||||
同时,请考虑通过社交媒体、活动和会议来支持 Dify 的分享。
|
||||
|
||||
> 我们正在寻找贡献者来帮助将Dify翻译成除了中文和英文之外的其他语言。如果您有兴趣帮助,请参阅我们的[i18n README](https://github.com/langgenius/dify/blob/main/web/i18n/README.md)获取更多信息,并在我们的[Discord社区服务器](https://discord.gg/8Tpq4AcN9c)的`global-users`频道中留言。
|
||||
> 我们正在寻找贡献者来帮助将 Dify 翻译成除了中文和英文之外的其他语言。如果您有兴趣帮助,请参阅我们的[i18n README](https://github.com/langgenius/dify/blob/main/web/i18n/README.md)获取更多信息,并在我们的[Discord 社区服务器](https://discord.gg/8Tpq4AcN9c)的`global-users`频道中留言。
|
||||
|
||||
**Contributors**
|
||||
|
||||
|
||||
+11
-2
@@ -137,7 +137,7 @@ WEB_API_CORS_ALLOW_ORIGINS=http://127.0.0.1:3000,*
|
||||
CONSOLE_CORS_ALLOW_ORIGINS=http://127.0.0.1:3000,*
|
||||
|
||||
# Vector database configuration
|
||||
# support: weaviate, qdrant, milvus, myscale, relyt, pgvecto_rs, pgvector, pgvector, chroma, opensearch, tidb_vector, couchbase, vikingdb, upstash, lindorm, oceanbase
|
||||
# support: weaviate, qdrant, milvus, myscale, relyt, pgvecto_rs, pgvector, pgvector, chroma, opensearch, tidb_vector, couchbase, vikingdb, upstash, lindorm, oceanbase, opengauss
|
||||
VECTOR_STORE=weaviate
|
||||
|
||||
# Weaviate configuration
|
||||
@@ -298,6 +298,14 @@ OCEANBASE_VECTOR_PASSWORD=difyai123456
|
||||
OCEANBASE_VECTOR_DATABASE=test
|
||||
OCEANBASE_MEMORY_LIMIT=6G
|
||||
|
||||
# openGauss configuration
|
||||
OPENGAUSS_HOST=127.0.0.1
|
||||
OPENGAUSS_PORT=6600
|
||||
OPENGAUSS_USER=postgres
|
||||
OPENGAUSS_PASSWORD=Dify@123
|
||||
OPENGAUSS_DATABASE=dify
|
||||
OPENGAUSS_MIN_CONNECTION=1
|
||||
OPENGAUSS_MAX_CONNECTION=5
|
||||
|
||||
# Upload configuration
|
||||
UPLOAD_FILE_SIZE_LIMIT=15
|
||||
@@ -378,6 +386,7 @@ HTTP_REQUEST_MAX_READ_TIMEOUT=600
|
||||
HTTP_REQUEST_MAX_WRITE_TIMEOUT=600
|
||||
HTTP_REQUEST_NODE_MAX_BINARY_SIZE=10485760
|
||||
HTTP_REQUEST_NODE_MAX_TEXT_SIZE=1048576
|
||||
HTTP_REQUEST_NODE_SSL_VERIFY=True
|
||||
|
||||
# Respect X-* headers to redirect clients
|
||||
RESPECT_XFORWARD_HEADERS_ENABLED=false
|
||||
@@ -444,4 +453,4 @@ CREATE_TIDB_SERVICE_JOB_ENABLED=false
|
||||
# Maximum number of submitted thread count in a ThreadPool for parallel node execution
|
||||
MAX_SUBMIT_COUNT=100
|
||||
# Lockout duration in seconds
|
||||
LOGIN_LOCKOUT_DURATION=86400
|
||||
LOGIN_LOCKOUT_DURATION=86400
|
||||
|
||||
@@ -56,8 +56,6 @@ RUN \
|
||||
curl nodejs libgmp-dev libmpfr-dev libmpc-dev \
|
||||
# For Security
|
||||
expat libldap-2.5-0 perl libsqlite3-0 zlib1g \
|
||||
# install a chinese font to support the use of tools like matplotlib
|
||||
fonts-noto-cjk \
|
||||
# install a package to improve the accuracy of guessing mime type and file extension
|
||||
media-types \
|
||||
# install libmagic to support the use of python-magic guess MIMETYPE
|
||||
|
||||
+9
-2
@@ -160,11 +160,17 @@ def migrate_annotation_vector_database():
|
||||
while True:
|
||||
try:
|
||||
# get apps info
|
||||
per_page = 50
|
||||
apps = (
|
||||
App.query.filter(App.status == "normal")
|
||||
db.session.query(App)
|
||||
.filter(App.status == "normal")
|
||||
.order_by(App.created_at.desc())
|
||||
.paginate(page=page, per_page=50)
|
||||
.limit(per_page)
|
||||
.offset((page - 1) * per_page)
|
||||
.all()
|
||||
)
|
||||
if not apps:
|
||||
break
|
||||
except NotFound:
|
||||
break
|
||||
|
||||
@@ -267,6 +273,7 @@ def migrate_knowledge_vector_database():
|
||||
VectorType.WEAVIATE,
|
||||
VectorType.ORACLE,
|
||||
VectorType.ELASTICSEARCH,
|
||||
VectorType.OPENGAUSS,
|
||||
}
|
||||
lower_collection_vector_types = {
|
||||
VectorType.ANALYTICDB,
|
||||
|
||||
@@ -332,6 +332,11 @@ class HttpConfig(BaseSettings):
|
||||
default=1 * 1024 * 1024,
|
||||
)
|
||||
|
||||
HTTP_REQUEST_NODE_SSL_VERIFY: bool = Field(
|
||||
description="Enable or disable SSL verification for HTTP requests",
|
||||
default=True,
|
||||
)
|
||||
|
||||
SSRF_DEFAULT_MAX_RETRIES: PositiveInt = Field(
|
||||
description="Maximum number of retries for network requests (SSRF)",
|
||||
default=3,
|
||||
|
||||
@@ -26,6 +26,7 @@ from .vdb.lindorm_config import LindormConfig
|
||||
from .vdb.milvus_config import MilvusConfig
|
||||
from .vdb.myscale_config import MyScaleConfig
|
||||
from .vdb.oceanbase_config import OceanBaseVectorConfig
|
||||
from .vdb.opengauss_config import OpenGaussConfig
|
||||
from .vdb.opensearch_config import OpenSearchConfig
|
||||
from .vdb.oracle_config import OracleConfig
|
||||
from .vdb.pgvector_config import PGVectorConfig
|
||||
@@ -281,5 +282,6 @@ class MiddlewareConfig(
|
||||
LindormConfig,
|
||||
OceanBaseVectorConfig,
|
||||
BaiduVectorDBConfig,
|
||||
OpenGaussConfig,
|
||||
):
|
||||
pass
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import Field, PositiveInt
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
|
||||
class OpenGaussConfig(BaseSettings):
|
||||
"""
|
||||
Configuration settings for OpenGauss
|
||||
"""
|
||||
|
||||
OPENGAUSS_HOST: Optional[str] = Field(
|
||||
description="Hostname or IP address of the OpenGauss server(e.g., 'localhost')",
|
||||
default=None,
|
||||
)
|
||||
|
||||
OPENGAUSS_PORT: PositiveInt = Field(
|
||||
description="Port number on which the OpenGauss server is listening (default is 6600)",
|
||||
default=6600,
|
||||
)
|
||||
|
||||
OPENGAUSS_USER: Optional[str] = Field(
|
||||
description="Username for authenticating with the OpenGauss database",
|
||||
default=None,
|
||||
)
|
||||
|
||||
OPENGAUSS_PASSWORD: Optional[str] = Field(
|
||||
description="Password for authenticating with the OpenGauss database",
|
||||
default=None,
|
||||
)
|
||||
|
||||
OPENGAUSS_DATABASE: Optional[str] = Field(
|
||||
description="Name of the OpenGauss database to connect to",
|
||||
default=None,
|
||||
)
|
||||
|
||||
OPENGAUSS_MIN_CONNECTION: PositiveInt = Field(
|
||||
description="Min connection of the OpenGauss database",
|
||||
default=1,
|
||||
)
|
||||
|
||||
OPENGAUSS_MAX_CONNECTION: PositiveInt = Field(
|
||||
description="Max connection of the OpenGauss database",
|
||||
default=5,
|
||||
)
|
||||
@@ -43,3 +43,8 @@ class PGVectorConfig(BaseSettings):
|
||||
description="Max connection of the PostgreSQL database",
|
||||
default=5,
|
||||
)
|
||||
|
||||
PGVECTOR_PG_BIGM: bool = Field(
|
||||
description="Whether to use pg_bigm module for full text search",
|
||||
default=False,
|
||||
)
|
||||
|
||||
@@ -9,7 +9,7 @@ class PackagingInfo(BaseSettings):
|
||||
|
||||
CURRENT_VERSION: str = Field(
|
||||
description="Dify version",
|
||||
default="1.0.0",
|
||||
default="1.1.0",
|
||||
)
|
||||
|
||||
COMMIT_SHA: str = Field(
|
||||
|
||||
@@ -81,6 +81,7 @@ from .datasets import (
|
||||
datasets_segments,
|
||||
external,
|
||||
hit_testing,
|
||||
metadata,
|
||||
website,
|
||||
)
|
||||
|
||||
|
||||
@@ -316,7 +316,7 @@ class AppTraceApi(Resource):
|
||||
@account_initialization_required
|
||||
def post(self, app_id):
|
||||
# add app trace
|
||||
if not current_user.is_admin_or_owner:
|
||||
if not current_user.is_editor:
|
||||
raise Forbidden()
|
||||
parser = reqparse.RequestParser()
|
||||
parser.add_argument("enabled", type=bool, required=True, location="json")
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import os
|
||||
from typing import cast
|
||||
|
||||
from flask_login import current_user # type: ignore
|
||||
from flask_restful import Resource, reqparse # type: ignore
|
||||
@@ -12,11 +11,8 @@ from controllers.console.app.error import (
|
||||
ProviderQuotaExceededError,
|
||||
)
|
||||
from controllers.console.wraps import account_initialization_required, setup_required
|
||||
from core.auto.workflow_generator.workflow_generator import WorkflowGenerator
|
||||
from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError
|
||||
from core.llm_generator.llm_generator import LLMGenerator
|
||||
from core.model_manager import ModelManager
|
||||
from core.model_runtime.entities.model_entities import ModelType
|
||||
from core.model_runtime.errors.invoke import InvokeError
|
||||
from libs.login import login_required
|
||||
|
||||
@@ -89,45 +85,5 @@ class RuleCodeGenerateApi(Resource):
|
||||
return code_result
|
||||
|
||||
|
||||
class AutoGenerateWorkflowApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
def post(self):
|
||||
"""
|
||||
Auto generate workflow
|
||||
"""
|
||||
|
||||
parser = reqparse.RequestParser()
|
||||
parser.add_argument("instruction", type=str, required=True, location="json")
|
||||
parser.add_argument("model_config", type=dict, required=True, location="json")
|
||||
tenant_id = cast(str, current_user.current_tenant_id)
|
||||
args = parser.parse_args()
|
||||
instruction = args.get("instruction")
|
||||
if not instruction:
|
||||
raise ValueError("Instruction is required")
|
||||
if not args.get("model_config"):
|
||||
raise ValueError("Model config is required")
|
||||
model_config = cast(dict, args.get("model_config"))
|
||||
model_manager = ModelManager()
|
||||
model_instance = model_manager.get_model_instance(
|
||||
tenant_id=tenant_id,
|
||||
model_type=ModelType.LLM,
|
||||
provider=model_config.get("provider", ""),
|
||||
model=model_config.get("name", ""),
|
||||
)
|
||||
workflow_generator = WorkflowGenerator(
|
||||
model_instance=model_instance,
|
||||
)
|
||||
workflow_yaml = workflow_generator.generate_workflow(
|
||||
user_requirement=instruction,
|
||||
)
|
||||
return workflow_yaml
|
||||
|
||||
|
||||
api.add_resource(RuleGenerateApi, "/rule-generate")
|
||||
api.add_resource(RuleCodeGenerateApi, "/rule-code-generate")
|
||||
api.add_resource(
|
||||
AutoGenerateWorkflowApi,
|
||||
"/auto-generate",
|
||||
)
|
||||
|
||||
@@ -122,7 +122,7 @@ class DataSourceNotionListApi(Resource):
|
||||
if dataset.data_source_type != "notion_import":
|
||||
raise ValueError("Dataset is not notion type.")
|
||||
|
||||
documents = session.execute(
|
||||
documents = session.scalars(
|
||||
select(Document).filter_by(
|
||||
dataset_id=dataset_id,
|
||||
tenant_id=current_user.current_tenant_id,
|
||||
|
||||
@@ -10,7 +10,12 @@ from controllers.console import api
|
||||
from controllers.console.apikey import api_key_fields, api_key_list
|
||||
from controllers.console.app.error import ProviderNotInitializeError
|
||||
from controllers.console.datasets.error import DatasetInUseError, DatasetNameDuplicateError, IndexingEstimateError
|
||||
from controllers.console.wraps import account_initialization_required, enterprise_license_required, setup_required
|
||||
from controllers.console.wraps import (
|
||||
account_initialization_required,
|
||||
cloud_edition_billing_rate_limit_check,
|
||||
enterprise_license_required,
|
||||
setup_required,
|
||||
)
|
||||
from core.errors.error import LLMBadRequestError, ProviderTokenNotInitError
|
||||
from core.indexing_runner import IndexingRunner
|
||||
from core.model_runtime.entities.model_entities import ModelType
|
||||
@@ -96,6 +101,7 @@ class DatasetListApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@cloud_edition_billing_rate_limit_check("knowledge")
|
||||
def post(self):
|
||||
parser = reqparse.RequestParser()
|
||||
parser.add_argument(
|
||||
@@ -178,6 +184,10 @@ class DatasetApi(Resource):
|
||||
except services.errors.account.NoPermissionError as e:
|
||||
raise Forbidden(str(e))
|
||||
data = marshal(dataset, dataset_detail_fields)
|
||||
if dataset.indexing_technique == "high_quality":
|
||||
if dataset.embedding_model_provider:
|
||||
provider_id = ModelProviderID(dataset.embedding_model_provider)
|
||||
data["embedding_model_provider"] = str(provider_id)
|
||||
if data.get("permission") == "partial_members":
|
||||
part_users_list = DatasetPermissionService.get_dataset_partial_member_list(dataset_id_str)
|
||||
data.update({"partial_member_list": part_users_list})
|
||||
@@ -210,6 +220,7 @@ class DatasetApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@cloud_edition_billing_rate_limit_check("knowledge")
|
||||
def patch(self, dataset_id):
|
||||
dataset_id_str = str(dataset_id)
|
||||
dataset = DatasetService.get_dataset(dataset_id_str)
|
||||
@@ -276,7 +287,11 @@ class DatasetApi(Resource):
|
||||
data = request.get_json()
|
||||
|
||||
# check embedding model setting
|
||||
if data.get("indexing_technique") == "high_quality":
|
||||
if (
|
||||
data.get("indexing_technique") == "high_quality"
|
||||
and data.get("embedding_model_provider") is not None
|
||||
and data.get("embedding_model") is not None
|
||||
):
|
||||
DatasetService.check_embedding_model_setting(
|
||||
dataset.tenant_id, data.get("embedding_model_provider"), data.get("embedding_model")
|
||||
)
|
||||
@@ -313,6 +328,7 @@ class DatasetApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@cloud_edition_billing_rate_limit_check("knowledge")
|
||||
def delete(self, dataset_id):
|
||||
dataset_id_str = str(dataset_id)
|
||||
|
||||
@@ -647,6 +663,7 @@ class DatasetRetrievalSettingApi(Resource):
|
||||
| VectorType.LINDORM
|
||||
| VectorType.COUCHBASE
|
||||
| VectorType.MILVUS
|
||||
| VectorType.OPENGAUSS
|
||||
):
|
||||
return {
|
||||
"retrieval_method": [
|
||||
@@ -690,6 +707,7 @@ class DatasetRetrievalSettingMockApi(Resource):
|
||||
| VectorType.COUCHBASE
|
||||
| VectorType.PGVECTOR
|
||||
| VectorType.LINDORM
|
||||
| VectorType.OPENGAUSS
|
||||
):
|
||||
return {
|
||||
"retrieval_method": [
|
||||
|
||||
@@ -26,6 +26,7 @@ from controllers.console.datasets.error import (
|
||||
)
|
||||
from controllers.console.wraps import (
|
||||
account_initialization_required,
|
||||
cloud_edition_billing_rate_limit_check,
|
||||
cloud_edition_billing_resource_check,
|
||||
setup_required,
|
||||
)
|
||||
@@ -242,6 +243,7 @@ class DatasetDocumentListApi(Resource):
|
||||
@account_initialization_required
|
||||
@marshal_with(documents_and_batch_fields)
|
||||
@cloud_edition_billing_resource_check("vector_space")
|
||||
@cloud_edition_billing_rate_limit_check("knowledge")
|
||||
def post(self, dataset_id):
|
||||
dataset_id = str(dataset_id)
|
||||
|
||||
@@ -297,6 +299,7 @@ class DatasetDocumentListApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@cloud_edition_billing_rate_limit_check("knowledge")
|
||||
def delete(self, dataset_id):
|
||||
dataset_id = str(dataset_id)
|
||||
dataset = DatasetService.get_dataset(dataset_id)
|
||||
@@ -320,9 +323,10 @@ class DatasetInitApi(Resource):
|
||||
@account_initialization_required
|
||||
@marshal_with(dataset_and_document_fields)
|
||||
@cloud_edition_billing_resource_check("vector_space")
|
||||
@cloud_edition_billing_rate_limit_check("knowledge")
|
||||
def post(self):
|
||||
# The role of the current user in the ta table must be admin, owner, or editor
|
||||
if not current_user.is_editor:
|
||||
# The role of the current user in the ta table must be admin, owner, dataset_operator, or editor
|
||||
if not current_user.is_dataset_editor:
|
||||
raise Forbidden()
|
||||
|
||||
parser = reqparse.RequestParser()
|
||||
@@ -617,7 +621,7 @@ class DocumentDetailApi(DocumentResource):
|
||||
raise InvalidMetadataError(f"Invalid metadata value: {metadata}")
|
||||
|
||||
if metadata == "only":
|
||||
response = {"id": document.id, "doc_type": document.doc_type, "doc_metadata": document.doc_metadata}
|
||||
response = {"id": document.id, "doc_type": document.doc_type, "doc_metadata": document.doc_metadata_details}
|
||||
elif metadata == "without":
|
||||
dataset_process_rules = DatasetService.get_process_rules(dataset_id)
|
||||
document_process_rules = document.dataset_process_rule.to_dict()
|
||||
@@ -678,7 +682,7 @@ class DocumentDetailApi(DocumentResource):
|
||||
"disabled_by": document.disabled_by,
|
||||
"archived": document.archived,
|
||||
"doc_type": document.doc_type,
|
||||
"doc_metadata": document.doc_metadata,
|
||||
"doc_metadata": document.doc_metadata_details,
|
||||
"segment_count": document.segment_count,
|
||||
"average_segment_length": document.average_segment_length,
|
||||
"hit_count": document.hit_count,
|
||||
@@ -694,13 +698,14 @@ class DocumentProcessingApi(DocumentResource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@cloud_edition_billing_rate_limit_check("knowledge")
|
||||
def patch(self, dataset_id, document_id, action):
|
||||
dataset_id = str(dataset_id)
|
||||
document_id = str(document_id)
|
||||
document = self.get_document(dataset_id, document_id)
|
||||
|
||||
# The role of the current user in the ta table must be admin, owner, or editor
|
||||
if not current_user.is_editor:
|
||||
# The role of the current user in the ta table must be admin, owner, dataset_operator, or editor
|
||||
if not current_user.is_dataset_editor:
|
||||
raise Forbidden()
|
||||
|
||||
if action == "pause":
|
||||
@@ -730,6 +735,7 @@ class DocumentDeleteApi(DocumentResource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@cloud_edition_billing_rate_limit_check("knowledge")
|
||||
def delete(self, dataset_id, document_id):
|
||||
dataset_id = str(dataset_id)
|
||||
document_id = str(document_id)
|
||||
@@ -763,8 +769,8 @@ class DocumentMetadataApi(DocumentResource):
|
||||
doc_type = req_data.get("doc_type")
|
||||
doc_metadata = req_data.get("doc_metadata")
|
||||
|
||||
# The role of the current user in the ta table must be admin, owner, or editor
|
||||
if not current_user.is_editor:
|
||||
# The role of the current user in the ta table must be admin, owner, dataset_operator, or editor
|
||||
if not current_user.is_dataset_editor:
|
||||
raise Forbidden()
|
||||
|
||||
if doc_type is None or doc_metadata is None:
|
||||
@@ -798,6 +804,7 @@ class DocumentStatusApi(DocumentResource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@cloud_edition_billing_resource_check("vector_space")
|
||||
@cloud_edition_billing_rate_limit_check("knowledge")
|
||||
def patch(self, dataset_id, action):
|
||||
dataset_id = str(dataset_id)
|
||||
dataset = DatasetService.get_dataset(dataset_id)
|
||||
@@ -893,6 +900,7 @@ class DocumentPauseApi(DocumentResource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@cloud_edition_billing_rate_limit_check("knowledge")
|
||||
def patch(self, dataset_id, document_id):
|
||||
"""pause document."""
|
||||
dataset_id = str(dataset_id)
|
||||
@@ -925,6 +933,7 @@ class DocumentRecoverApi(DocumentResource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@cloud_edition_billing_rate_limit_check("knowledge")
|
||||
def patch(self, dataset_id, document_id):
|
||||
"""recover document."""
|
||||
dataset_id = str(dataset_id)
|
||||
@@ -954,6 +963,7 @@ class DocumentRetryApi(DocumentResource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@cloud_edition_billing_rate_limit_check("knowledge")
|
||||
def post(self, dataset_id):
|
||||
"""retry document."""
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ from controllers.console.datasets.error import (
|
||||
from controllers.console.wraps import (
|
||||
account_initialization_required,
|
||||
cloud_edition_billing_knowledge_limit_check,
|
||||
cloud_edition_billing_rate_limit_check,
|
||||
cloud_edition_billing_resource_check,
|
||||
setup_required,
|
||||
)
|
||||
@@ -106,6 +107,7 @@ class DatasetDocumentSegmentListApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@cloud_edition_billing_rate_limit_check("knowledge")
|
||||
def delete(self, dataset_id, document_id):
|
||||
# check dataset
|
||||
dataset_id = str(dataset_id)
|
||||
@@ -121,8 +123,8 @@ class DatasetDocumentSegmentListApi(Resource):
|
||||
raise NotFound("Document not found.")
|
||||
segment_ids = request.args.getlist("segment_id")
|
||||
|
||||
# The role of the current user in the ta table must be admin or owner
|
||||
if not current_user.is_editor:
|
||||
# The role of the current user in the ta table must be admin, owner, dataset_operator, or editor
|
||||
if not current_user.is_dataset_editor:
|
||||
raise Forbidden()
|
||||
try:
|
||||
DatasetService.check_dataset_permission(dataset, current_user)
|
||||
@@ -137,6 +139,7 @@ class DatasetDocumentSegmentApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@cloud_edition_billing_resource_check("vector_space")
|
||||
@cloud_edition_billing_rate_limit_check("knowledge")
|
||||
def patch(self, dataset_id, document_id, action):
|
||||
dataset_id = str(dataset_id)
|
||||
dataset = DatasetService.get_dataset(dataset_id)
|
||||
@@ -148,8 +151,8 @@ class DatasetDocumentSegmentApi(Resource):
|
||||
raise NotFound("Document not found.")
|
||||
# check user's model setting
|
||||
DatasetService.check_dataset_model_setting(dataset)
|
||||
# The role of the current user in the ta table must be admin, owner, or editor
|
||||
if not current_user.is_editor:
|
||||
# The role of the current user in the ta table must be admin, owner, dataset_operator, or editor
|
||||
if not current_user.is_dataset_editor:
|
||||
raise Forbidden()
|
||||
|
||||
try:
|
||||
@@ -191,6 +194,7 @@ class DatasetDocumentSegmentAddApi(Resource):
|
||||
@account_initialization_required
|
||||
@cloud_edition_billing_resource_check("vector_space")
|
||||
@cloud_edition_billing_knowledge_limit_check("add_segment")
|
||||
@cloud_edition_billing_rate_limit_check("knowledge")
|
||||
def post(self, dataset_id, document_id):
|
||||
# check dataset
|
||||
dataset_id = str(dataset_id)
|
||||
@@ -202,7 +206,7 @@ class DatasetDocumentSegmentAddApi(Resource):
|
||||
document = DocumentService.get_document(dataset_id, document_id)
|
||||
if not document:
|
||||
raise NotFound("Document not found.")
|
||||
if not current_user.is_editor:
|
||||
if not current_user.is_dataset_editor:
|
||||
raise Forbidden()
|
||||
# check embedding model setting
|
||||
if dataset.indexing_technique == "high_quality":
|
||||
@@ -240,6 +244,7 @@ class DatasetDocumentSegmentUpdateApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@cloud_edition_billing_resource_check("vector_space")
|
||||
@cloud_edition_billing_rate_limit_check("knowledge")
|
||||
def patch(self, dataset_id, document_id, segment_id):
|
||||
# check dataset
|
||||
dataset_id = str(dataset_id)
|
||||
@@ -276,8 +281,8 @@ class DatasetDocumentSegmentUpdateApi(Resource):
|
||||
).first()
|
||||
if not segment:
|
||||
raise NotFound("Segment not found.")
|
||||
# The role of the current user in the ta table must be admin, owner, or editor
|
||||
if not current_user.is_editor:
|
||||
# The role of the current user in the ta table must be admin, owner, dataset_operator, or editor
|
||||
if not current_user.is_dataset_editor:
|
||||
raise Forbidden()
|
||||
try:
|
||||
DatasetService.check_dataset_permission(dataset, current_user)
|
||||
@@ -299,6 +304,7 @@ class DatasetDocumentSegmentUpdateApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@cloud_edition_billing_rate_limit_check("knowledge")
|
||||
def delete(self, dataset_id, document_id, segment_id):
|
||||
# check dataset
|
||||
dataset_id = str(dataset_id)
|
||||
@@ -319,8 +325,8 @@ class DatasetDocumentSegmentUpdateApi(Resource):
|
||||
).first()
|
||||
if not segment:
|
||||
raise NotFound("Segment not found.")
|
||||
# The role of the current user in the ta table must be admin or owner
|
||||
if not current_user.is_editor:
|
||||
# The role of the current user in the ta table must be admin, owner, dataset_operator, or editor
|
||||
if not current_user.is_dataset_editor:
|
||||
raise Forbidden()
|
||||
try:
|
||||
DatasetService.check_dataset_permission(dataset, current_user)
|
||||
@@ -336,6 +342,7 @@ class DatasetDocumentSegmentBatchImportApi(Resource):
|
||||
@account_initialization_required
|
||||
@cloud_edition_billing_resource_check("vector_space")
|
||||
@cloud_edition_billing_knowledge_limit_check("add_segment")
|
||||
@cloud_edition_billing_rate_limit_check("knowledge")
|
||||
def post(self, dataset_id, document_id):
|
||||
# check dataset
|
||||
dataset_id = str(dataset_id)
|
||||
@@ -402,6 +409,7 @@ class ChildChunkAddApi(Resource):
|
||||
@account_initialization_required
|
||||
@cloud_edition_billing_resource_check("vector_space")
|
||||
@cloud_edition_billing_knowledge_limit_check("add_segment")
|
||||
@cloud_edition_billing_rate_limit_check("knowledge")
|
||||
def post(self, dataset_id, document_id, segment_id):
|
||||
# check dataset
|
||||
dataset_id = str(dataset_id)
|
||||
@@ -420,7 +428,7 @@ class ChildChunkAddApi(Resource):
|
||||
).first()
|
||||
if not segment:
|
||||
raise NotFound("Segment not found.")
|
||||
if not current_user.is_editor:
|
||||
if not current_user.is_dataset_editor:
|
||||
raise Forbidden()
|
||||
# check embedding model setting
|
||||
if dataset.indexing_technique == "high_quality":
|
||||
@@ -499,6 +507,7 @@ class ChildChunkAddApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@cloud_edition_billing_resource_check("vector_space")
|
||||
@cloud_edition_billing_rate_limit_check("knowledge")
|
||||
def patch(self, dataset_id, document_id, segment_id):
|
||||
# check dataset
|
||||
dataset_id = str(dataset_id)
|
||||
@@ -519,8 +528,8 @@ class ChildChunkAddApi(Resource):
|
||||
).first()
|
||||
if not segment:
|
||||
raise NotFound("Segment not found.")
|
||||
# The role of the current user in the ta table must be admin, owner, or editor
|
||||
if not current_user.is_editor:
|
||||
# The role of the current user in the ta table must be admin, owner, dataset_operator, or editor
|
||||
if not current_user.is_dataset_editor:
|
||||
raise Forbidden()
|
||||
try:
|
||||
DatasetService.check_dataset_permission(dataset, current_user)
|
||||
@@ -542,6 +551,7 @@ class ChildChunkUpdateApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@cloud_edition_billing_rate_limit_check("knowledge")
|
||||
def delete(self, dataset_id, document_id, segment_id, child_chunk_id):
|
||||
# check dataset
|
||||
dataset_id = str(dataset_id)
|
||||
@@ -569,8 +579,8 @@ class ChildChunkUpdateApi(Resource):
|
||||
).first()
|
||||
if not child_chunk:
|
||||
raise NotFound("Child chunk not found.")
|
||||
# The role of the current user in the ta table must be admin or owner
|
||||
if not current_user.is_editor:
|
||||
# The role of the current user in the ta table must be admin, owner, dataset_operator, or editor
|
||||
if not current_user.is_dataset_editor:
|
||||
raise Forbidden()
|
||||
try:
|
||||
DatasetService.check_dataset_permission(dataset, current_user)
|
||||
@@ -586,6 +596,7 @@ class ChildChunkUpdateApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@cloud_edition_billing_resource_check("vector_space")
|
||||
@cloud_edition_billing_rate_limit_check("knowledge")
|
||||
def patch(self, dataset_id, document_id, segment_id, child_chunk_id):
|
||||
# check dataset
|
||||
dataset_id = str(dataset_id)
|
||||
@@ -613,8 +624,8 @@ class ChildChunkUpdateApi(Resource):
|
||||
).first()
|
||||
if not child_chunk:
|
||||
raise NotFound("Child chunk not found.")
|
||||
# The role of the current user in the ta table must be admin or owner
|
||||
if not current_user.is_editor:
|
||||
# The role of the current user in the ta table must be admin, owner, dataset_operator, or editor
|
||||
if not current_user.is_dataset_editor:
|
||||
raise Forbidden()
|
||||
try:
|
||||
DatasetService.check_dataset_permission(dataset, current_user)
|
||||
|
||||
@@ -2,7 +2,11 @@ from flask_restful import Resource # type: ignore
|
||||
|
||||
from controllers.console import api
|
||||
from controllers.console.datasets.hit_testing_base import DatasetsHitTestingBase
|
||||
from controllers.console.wraps import account_initialization_required, setup_required
|
||||
from controllers.console.wraps import (
|
||||
account_initialization_required,
|
||||
cloud_edition_billing_rate_limit_check,
|
||||
setup_required,
|
||||
)
|
||||
from libs.login import login_required
|
||||
|
||||
|
||||
@@ -10,6 +14,7 @@ class HitTestingApi(Resource, DatasetsHitTestingBase):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@cloud_edition_billing_rate_limit_check("knowledge")
|
||||
def post(self, dataset_id):
|
||||
dataset_id_str = str(dataset_id)
|
||||
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
from flask_login import current_user # type: ignore # type: ignore
|
||||
from flask_restful import Resource, marshal_with, reqparse # type: ignore
|
||||
from werkzeug.exceptions import NotFound
|
||||
|
||||
from controllers.console import api
|
||||
from controllers.console.wraps import account_initialization_required, enterprise_license_required, setup_required
|
||||
from fields.dataset_fields import dataset_metadata_fields
|
||||
from libs.login import login_required
|
||||
from services.dataset_service import DatasetService
|
||||
from services.entities.knowledge_entities.knowledge_entities import (
|
||||
MetadataArgs,
|
||||
MetadataOperationData,
|
||||
)
|
||||
from services.metadata_service import MetadataService
|
||||
|
||||
|
||||
def _validate_name(name):
|
||||
if not name or len(name) < 1 or len(name) > 40:
|
||||
raise ValueError("Name must be between 1 to 40 characters.")
|
||||
return name
|
||||
|
||||
|
||||
def _validate_description_length(description):
|
||||
if len(description) > 400:
|
||||
raise ValueError("Description cannot exceed 400 characters.")
|
||||
return description
|
||||
|
||||
|
||||
class DatasetMetadataCreateApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@enterprise_license_required
|
||||
@marshal_with(dataset_metadata_fields)
|
||||
def post(self, dataset_id):
|
||||
parser = reqparse.RequestParser()
|
||||
parser.add_argument("type", type=str, required=True, nullable=True, location="json")
|
||||
parser.add_argument("name", type=str, required=True, nullable=True, location="json")
|
||||
args = parser.parse_args()
|
||||
metadata_args = MetadataArgs(**args)
|
||||
|
||||
dataset_id_str = str(dataset_id)
|
||||
dataset = DatasetService.get_dataset(dataset_id_str)
|
||||
if dataset is None:
|
||||
raise NotFound("Dataset not found.")
|
||||
DatasetService.check_dataset_permission(dataset, current_user)
|
||||
|
||||
metadata = MetadataService.create_metadata(dataset_id_str, metadata_args)
|
||||
return metadata, 201
|
||||
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@enterprise_license_required
|
||||
def get(self, dataset_id):
|
||||
dataset_id_str = str(dataset_id)
|
||||
dataset = DatasetService.get_dataset(dataset_id_str)
|
||||
if dataset is None:
|
||||
raise NotFound("Dataset not found.")
|
||||
return MetadataService.get_dataset_metadatas(dataset), 200
|
||||
|
||||
|
||||
class DatasetMetadataApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@enterprise_license_required
|
||||
@marshal_with(dataset_metadata_fields)
|
||||
def patch(self, dataset_id, metadata_id):
|
||||
parser = reqparse.RequestParser()
|
||||
parser.add_argument("name", type=str, required=True, nullable=True, location="json")
|
||||
args = parser.parse_args()
|
||||
|
||||
dataset_id_str = str(dataset_id)
|
||||
metadata_id_str = str(metadata_id)
|
||||
dataset = DatasetService.get_dataset(dataset_id_str)
|
||||
if dataset is None:
|
||||
raise NotFound("Dataset not found.")
|
||||
DatasetService.check_dataset_permission(dataset, current_user)
|
||||
|
||||
metadata = MetadataService.update_metadata_name(dataset_id_str, metadata_id_str, args.get("name"))
|
||||
return metadata, 200
|
||||
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@enterprise_license_required
|
||||
def delete(self, dataset_id, metadata_id):
|
||||
dataset_id_str = str(dataset_id)
|
||||
metadata_id_str = str(metadata_id)
|
||||
dataset = DatasetService.get_dataset(dataset_id_str)
|
||||
if dataset is None:
|
||||
raise NotFound("Dataset not found.")
|
||||
DatasetService.check_dataset_permission(dataset, current_user)
|
||||
|
||||
MetadataService.delete_metadata(dataset_id_str, metadata_id_str)
|
||||
return 200
|
||||
|
||||
|
||||
class DatasetMetadataBuiltInFieldApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@enterprise_license_required
|
||||
def get(self):
|
||||
built_in_fields = MetadataService.get_built_in_fields()
|
||||
return {"fields": built_in_fields}, 200
|
||||
|
||||
|
||||
class DatasetMetadataBuiltInFieldActionApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@enterprise_license_required
|
||||
def post(self, dataset_id, action):
|
||||
dataset_id_str = str(dataset_id)
|
||||
dataset = DatasetService.get_dataset(dataset_id_str)
|
||||
if dataset is None:
|
||||
raise NotFound("Dataset not found.")
|
||||
DatasetService.check_dataset_permission(dataset, current_user)
|
||||
|
||||
if action == "enable":
|
||||
MetadataService.enable_built_in_field(dataset)
|
||||
elif action == "disable":
|
||||
MetadataService.disable_built_in_field(dataset)
|
||||
return 200
|
||||
|
||||
|
||||
class DocumentMetadataEditApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@enterprise_license_required
|
||||
def post(self, dataset_id):
|
||||
dataset_id_str = str(dataset_id)
|
||||
dataset = DatasetService.get_dataset(dataset_id_str)
|
||||
if dataset is None:
|
||||
raise NotFound("Dataset not found.")
|
||||
DatasetService.check_dataset_permission(dataset, current_user)
|
||||
|
||||
parser = reqparse.RequestParser()
|
||||
parser.add_argument("operation_data", type=list, required=True, nullable=True, location="json")
|
||||
args = parser.parse_args()
|
||||
metadata_args = MetadataOperationData(**args)
|
||||
|
||||
MetadataService.update_documents_metadata(dataset, metadata_args)
|
||||
|
||||
return 200
|
||||
|
||||
|
||||
api.add_resource(DatasetMetadataCreateApi, "/datasets/<uuid:dataset_id>/metadata")
|
||||
api.add_resource(DatasetMetadataApi, "/datasets/<uuid:dataset_id>/metadata/<uuid:metadata_id>")
|
||||
api.add_resource(DatasetMetadataBuiltInFieldApi, "/datasets/metadata/built-in")
|
||||
api.add_resource(DatasetMetadataBuiltInFieldActionApi, "/datasets/<uuid:dataset_id>/metadata/built-in/<string:action>")
|
||||
api.add_resource(DocumentMetadataEditApi, "/datasets/<uuid:dataset_id>/documents/metadata")
|
||||
@@ -26,6 +26,7 @@ from libs.helper import TimestampField
|
||||
from libs.login import login_required
|
||||
from models.account import Tenant, TenantStatus
|
||||
from services.account_service import TenantService
|
||||
from services.feature_service import FeatureService
|
||||
from services.file_service import FileService
|
||||
from services.workspace_service import WorkspaceService
|
||||
|
||||
@@ -68,6 +69,11 @@ class TenantListApi(Resource):
|
||||
tenants = TenantService.get_join_tenants(current_user)
|
||||
|
||||
for tenant in tenants:
|
||||
features = FeatureService.get_features(tenant.id)
|
||||
if features.billing.enabled:
|
||||
tenant.plan = features.billing.subscription.plan
|
||||
else:
|
||||
tenant.plan = "sandbox"
|
||||
if tenant.id == current_user.current_tenant_id:
|
||||
tenant.current = True # Set current=True for current tenant
|
||||
return {"workspaces": marshal(tenants, tenants_fields)}, 200
|
||||
@@ -82,28 +88,20 @@ class WorkspaceListApi(Resource):
|
||||
parser.add_argument("limit", type=inputs.int_range(1, 100), required=False, default=20, location="args")
|
||||
args = parser.parse_args()
|
||||
|
||||
tenants = Tenant.query.order_by(Tenant.created_at.desc()).paginate(page=args["page"], per_page=args["limit"])
|
||||
|
||||
tenants = Tenant.query.order_by(Tenant.created_at.desc()).paginate(
|
||||
page=args["page"], per_page=args["limit"], error_out=False
|
||||
)
|
||||
has_more = False
|
||||
if len(tenants.items) == args["limit"]:
|
||||
current_page_first_tenant = tenants[-1]
|
||||
rest_count = (
|
||||
db.session.query(Tenant)
|
||||
.filter(
|
||||
Tenant.created_at < current_page_first_tenant.created_at, Tenant.id != current_page_first_tenant.id
|
||||
)
|
||||
.count()
|
||||
)
|
||||
|
||||
if rest_count > 0:
|
||||
has_more = True
|
||||
total = db.session.query(Tenant).count()
|
||||
if tenants.has_next:
|
||||
has_more = True
|
||||
|
||||
return {
|
||||
"data": marshal(tenants.items, workspace_fields),
|
||||
"has_more": has_more,
|
||||
"limit": args["limit"],
|
||||
"page": args["page"],
|
||||
"total": total,
|
||||
"total": tenants.total,
|
||||
}, 200
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from functools import wraps
|
||||
|
||||
from flask import abort, request
|
||||
@@ -8,6 +9,8 @@ from flask_login import current_user # type: ignore
|
||||
from configs import dify_config
|
||||
from controllers.console.workspace.error import AccountNotInitializedError
|
||||
from extensions.ext_database import db
|
||||
from extensions.ext_redis import redis_client
|
||||
from models.dataset import RateLimitLog
|
||||
from models.model import DifySetup
|
||||
from services.feature_service import FeatureService, LicenseStatus
|
||||
from services.operation_service import OperationService
|
||||
@@ -67,7 +70,9 @@ def cloud_edition_billing_resource_check(resource: str):
|
||||
elif resource == "apps" and 0 < apps.limit <= apps.size:
|
||||
abort(403, "The number of apps has reached the limit of your subscription.")
|
||||
elif resource == "vector_space" and 0 < vector_space.limit <= vector_space.size:
|
||||
abort(403, "The capacity of the vector space has reached the limit of your subscription.")
|
||||
abort(
|
||||
403, "The capacity of the knowledge storage space has reached the limit of your subscription."
|
||||
)
|
||||
elif resource == "documents" and 0 < documents_upload_quota.limit <= documents_upload_quota.size:
|
||||
# The api of file upload is used in the multiple places,
|
||||
# so we need to check the source of the request from datasets
|
||||
@@ -112,6 +117,41 @@ def cloud_edition_billing_knowledge_limit_check(resource: str):
|
||||
return interceptor
|
||||
|
||||
|
||||
def cloud_edition_billing_rate_limit_check(resource: str):
|
||||
def interceptor(view):
|
||||
@wraps(view)
|
||||
def decorated(*args, **kwargs):
|
||||
if resource == "knowledge":
|
||||
knowledge_rate_limit = FeatureService.get_knowledge_rate_limit(current_user.current_tenant_id)
|
||||
if knowledge_rate_limit.enabled:
|
||||
current_time = int(time.time() * 1000)
|
||||
key = f"rate_limit_{current_user.current_tenant_id}"
|
||||
|
||||
redis_client.zadd(key, {current_time: current_time})
|
||||
|
||||
redis_client.zremrangebyscore(key, 0, current_time - 60000)
|
||||
|
||||
request_count = redis_client.zcard(key)
|
||||
|
||||
if request_count > knowledge_rate_limit.limit:
|
||||
# add ratelimit record
|
||||
rate_limit_log = RateLimitLog(
|
||||
tenant_id=current_user.current_tenant_id,
|
||||
subscription_plan=knowledge_rate_limit.subscription_plan,
|
||||
operation="knowledge",
|
||||
)
|
||||
db.session.add(rate_limit_log)
|
||||
db.session.commit()
|
||||
abort(
|
||||
403, "Sorry, you have reached the knowledge base request rate limit of your subscription."
|
||||
)
|
||||
return view(*args, **kwargs)
|
||||
|
||||
return decorated
|
||||
|
||||
return interceptor
|
||||
|
||||
|
||||
def cloud_utm_record(view):
|
||||
@wraps(view)
|
||||
def decorated(*args, **kwargs):
|
||||
|
||||
@@ -10,7 +10,7 @@ from controllers.service_api.app.error import NotChatAppError
|
||||
from controllers.service_api.wraps import FetchUserArg, WhereisUserArg, validate_app_token
|
||||
from core.app.entities.app_invoke_entities import InvokeFrom
|
||||
from fields.conversation_fields import message_file_fields
|
||||
from fields.message_fields import feedback_fields, retriever_resource_fields
|
||||
from fields.message_fields import agent_thought_fields, feedback_fields, retriever_resource_fields
|
||||
from fields.raws import FilesContainedField
|
||||
from libs.helper import TimestampField, uuid_value
|
||||
from models.model import App, AppMode, EndUser
|
||||
@@ -19,20 +19,6 @@ from services.message_service import MessageService
|
||||
|
||||
|
||||
class MessageListApi(Resource):
|
||||
agent_thought_fields = {
|
||||
"id": fields.String,
|
||||
"chain_id": fields.String,
|
||||
"message_id": fields.String,
|
||||
"position": fields.Integer,
|
||||
"thought": fields.String,
|
||||
"tool": fields.String,
|
||||
"tool_labels": fields.Raw,
|
||||
"tool_input": fields.String,
|
||||
"created_at": TimestampField,
|
||||
"observation": fields.String,
|
||||
"message_files": fields.List(fields.Nested(message_file_fields)),
|
||||
}
|
||||
|
||||
message_fields = {
|
||||
"id": fields.String,
|
||||
"conversation_id": fields.String,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import time
|
||||
from collections.abc import Callable
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from enum import Enum
|
||||
@@ -13,8 +14,10 @@ from sqlalchemy.orm import Session
|
||||
from werkzeug.exceptions import Forbidden, Unauthorized
|
||||
|
||||
from extensions.ext_database import db
|
||||
from extensions.ext_redis import redis_client
|
||||
from libs.login import _get_user
|
||||
from models.account import Account, Tenant, TenantAccountJoin, TenantStatus
|
||||
from models.dataset import RateLimitLog
|
||||
from models.model import ApiToken, App, EndUser
|
||||
from services.feature_service import FeatureService
|
||||
|
||||
@@ -139,6 +142,43 @@ def cloud_edition_billing_knowledge_limit_check(resource: str, api_token_type: s
|
||||
return interceptor
|
||||
|
||||
|
||||
def cloud_edition_billing_rate_limit_check(resource: str, api_token_type: str):
|
||||
def interceptor(view):
|
||||
@wraps(view)
|
||||
def decorated(*args, **kwargs):
|
||||
api_token = validate_and_get_api_token(api_token_type)
|
||||
|
||||
if resource == "knowledge":
|
||||
knowledge_rate_limit = FeatureService.get_knowledge_rate_limit(api_token.tenant_id)
|
||||
if knowledge_rate_limit.enabled:
|
||||
current_time = int(time.time() * 1000)
|
||||
key = f"rate_limit_{api_token.tenant_id}"
|
||||
|
||||
redis_client.zadd(key, {current_time: current_time})
|
||||
|
||||
redis_client.zremrangebyscore(key, 0, current_time - 60000)
|
||||
|
||||
request_count = redis_client.zcard(key)
|
||||
|
||||
if request_count > knowledge_rate_limit.limit:
|
||||
# add ratelimit record
|
||||
rate_limit_log = RateLimitLog(
|
||||
tenant_id=api_token.tenant_id,
|
||||
subscription_plan=knowledge_rate_limit.subscription_plan,
|
||||
operation="knowledge",
|
||||
)
|
||||
db.session.add(rate_limit_log)
|
||||
db.session.commit()
|
||||
raise Forbidden(
|
||||
"Sorry, you have reached the knowledge base request rate limit of your subscription."
|
||||
)
|
||||
return view(*args, **kwargs)
|
||||
|
||||
return decorated
|
||||
|
||||
return interceptor
|
||||
|
||||
|
||||
def validate_dataset_token(view=None):
|
||||
def decorator(view):
|
||||
@wraps(view)
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import uuid
|
||||
from typing import Optional
|
||||
|
||||
from core.app.app_config.entities import DatasetEntity, DatasetRetrieveConfigEntity
|
||||
from core.app.app_config.entities import (
|
||||
DatasetEntity,
|
||||
DatasetRetrieveConfigEntity,
|
||||
MetadataFilteringCondition,
|
||||
ModelConfig,
|
||||
)
|
||||
from core.entities.agent_entities import PlanningStrategy
|
||||
from models.model import AppMode
|
||||
from services.dataset_service import DatasetService
|
||||
@@ -78,6 +83,15 @@ class DatasetConfigManager:
|
||||
retrieve_strategy=DatasetRetrieveConfigEntity.RetrieveStrategy.value_of(
|
||||
dataset_configs["retrieval_model"]
|
||||
),
|
||||
metadata_filtering_mode=dataset_configs.get("metadata_filtering_mode", "disabled"),
|
||||
metadata_model_config=ModelConfig(**dataset_configs.get("metadata_model_config"))
|
||||
if dataset_configs.get("metadata_model_config")
|
||||
else None,
|
||||
metadata_filtering_conditions=MetadataFilteringCondition(
|
||||
**dataset_configs.get("metadata_filtering_conditions", {})
|
||||
)
|
||||
if dataset_configs.get("metadata_filtering_conditions")
|
||||
else None,
|
||||
),
|
||||
)
|
||||
else:
|
||||
@@ -89,11 +103,22 @@ class DatasetConfigManager:
|
||||
dataset_configs["retrieval_model"]
|
||||
),
|
||||
top_k=dataset_configs.get("top_k", 4),
|
||||
score_threshold=dataset_configs.get("score_threshold"),
|
||||
score_threshold=dataset_configs.get("score_threshold")
|
||||
if dataset_configs.get("score_threshold_enabled", False)
|
||||
else None,
|
||||
reranking_model=dataset_configs.get("reranking_model"),
|
||||
weights=dataset_configs.get("weights"),
|
||||
reranking_enabled=dataset_configs.get("reranking_enabled", True),
|
||||
rerank_mode=dataset_configs.get("reranking_mode", "reranking_model"),
|
||||
metadata_filtering_mode=dataset_configs.get("metadata_filtering_mode", "disabled"),
|
||||
metadata_model_config=ModelConfig(**dataset_configs.get("metadata_model_config"))
|
||||
if dataset_configs.get("metadata_model_config")
|
||||
else None,
|
||||
metadata_filtering_conditions=MetadataFilteringCondition(
|
||||
**dataset_configs.get("metadata_filtering_conditions", {})
|
||||
)
|
||||
if dataset_configs.get("metadata_filtering_conditions")
|
||||
else None,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
from collections.abc import Sequence
|
||||
from enum import Enum, StrEnum
|
||||
from typing import Any, Optional
|
||||
from typing import Any, Literal, Optional
|
||||
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
|
||||
from core.file import FileTransferMethod, FileType, FileUploadConfig
|
||||
from core.model_runtime.entities.llm_entities import LLMMode
|
||||
from core.model_runtime.entities.message_entities import PromptMessageRole
|
||||
from models.model import AppMode
|
||||
|
||||
@@ -135,6 +136,55 @@ class ExternalDataVariableEntity(BaseModel):
|
||||
config: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
SupportedComparisonOperator = Literal[
|
||||
# for string or array
|
||||
"contains",
|
||||
"not contains",
|
||||
"start with",
|
||||
"end with",
|
||||
"is",
|
||||
"is not",
|
||||
"empty",
|
||||
"not empty",
|
||||
# for number
|
||||
"=",
|
||||
"≠",
|
||||
">",
|
||||
"<",
|
||||
"≥",
|
||||
"≤",
|
||||
# for time
|
||||
"before",
|
||||
"after",
|
||||
]
|
||||
|
||||
|
||||
class ModelConfig(BaseModel):
|
||||
provider: str
|
||||
name: str
|
||||
mode: LLMMode
|
||||
completion_params: dict[str, Any] = {}
|
||||
|
||||
|
||||
class Condition(BaseModel):
|
||||
"""
|
||||
Conditon detail
|
||||
"""
|
||||
|
||||
name: str
|
||||
comparison_operator: SupportedComparisonOperator
|
||||
value: str | Sequence[str] | None | int | float = None
|
||||
|
||||
|
||||
class MetadataFilteringCondition(BaseModel):
|
||||
"""
|
||||
Metadata Filtering Condition.
|
||||
"""
|
||||
|
||||
logical_operator: Optional[Literal["and", "or"]] = "and"
|
||||
conditions: Optional[list[Condition]] = Field(default=None, deprecated=True)
|
||||
|
||||
|
||||
class DatasetRetrieveConfigEntity(BaseModel):
|
||||
"""
|
||||
Dataset Retrieve Config Entity.
|
||||
@@ -171,6 +221,9 @@ class DatasetRetrieveConfigEntity(BaseModel):
|
||||
reranking_model: Optional[dict] = None
|
||||
weights: Optional[dict] = None
|
||||
reranking_enabled: Optional[bool] = True
|
||||
metadata_filtering_mode: Optional[Literal["disabled", "automatic", "manual"]] = "disabled"
|
||||
metadata_model_config: Optional[ModelConfig] = None
|
||||
metadata_filtering_conditions: Optional[MetadataFilteringCondition] = None
|
||||
|
||||
|
||||
class DatasetEntity(BaseModel):
|
||||
|
||||
@@ -17,17 +17,15 @@ class FileUploadConfigManager:
|
||||
if file_upload_dict:
|
||||
if file_upload_dict.get("enabled"):
|
||||
transform_methods = file_upload_dict.get("allowed_file_upload_methods", [])
|
||||
data = {
|
||||
"image_config": {
|
||||
"number_limits": file_upload_dict["number_limits"],
|
||||
"transfer_methods": transform_methods,
|
||||
}
|
||||
file_upload_dict["image_config"] = {
|
||||
"number_limits": file_upload_dict.get("number_limits", 1),
|
||||
"transfer_methods": transform_methods,
|
||||
}
|
||||
|
||||
if is_vision:
|
||||
data["image_config"]["detail"] = file_upload_dict.get("image", {}).get("detail", "low")
|
||||
file_upload_dict["image_config"]["detail"] = file_upload_dict.get("image", {}).get("detail", "high")
|
||||
|
||||
return FileUploadConfig.model_validate(data)
|
||||
return FileUploadConfig.model_validate(file_upload_dict)
|
||||
|
||||
@classmethod
|
||||
def validate_and_set_defaults(cls, config: dict) -> tuple[dict, list[str]]:
|
||||
|
||||
@@ -151,7 +151,7 @@ class BaseAppGenerator:
|
||||
|
||||
def gen():
|
||||
for message in generator:
|
||||
if isinstance(message, (Mapping, dict)):
|
||||
if isinstance(message, Mapping | dict):
|
||||
yield f"data: {json.dumps(message)}\n\n"
|
||||
else:
|
||||
yield f"event: {message}\n\n"
|
||||
|
||||
@@ -17,7 +17,11 @@ from core.external_data_tool.external_data_fetch import ExternalDataFetch
|
||||
from core.memory.token_buffer_memory import TokenBufferMemory
|
||||
from core.model_manager import ModelInstance
|
||||
from core.model_runtime.entities.llm_entities import LLMResult, LLMResultChunk, LLMResultChunkDelta, LLMUsage
|
||||
from core.model_runtime.entities.message_entities import AssistantPromptMessage, PromptMessage
|
||||
from core.model_runtime.entities.message_entities import (
|
||||
AssistantPromptMessage,
|
||||
ImagePromptMessageContent,
|
||||
PromptMessage,
|
||||
)
|
||||
from core.model_runtime.entities.model_entities import ModelPropertyKey
|
||||
from core.model_runtime.errors.invoke import InvokeBadRequestError
|
||||
from core.moderation.input_moderation import InputModeration
|
||||
@@ -141,6 +145,7 @@ class AppRunner:
|
||||
query: Optional[str] = None,
|
||||
context: Optional[str] = None,
|
||||
memory: Optional[TokenBufferMemory] = None,
|
||||
image_detail_config: Optional[ImagePromptMessageContent.DETAIL] = None,
|
||||
) -> tuple[list[PromptMessage], Optional[list[str]]]:
|
||||
"""
|
||||
Organize prompt messages
|
||||
@@ -167,6 +172,7 @@ class AppRunner:
|
||||
context=context,
|
||||
memory=memory,
|
||||
model_config=model_config,
|
||||
image_detail_config=image_detail_config,
|
||||
)
|
||||
else:
|
||||
memory_config = MemoryConfig(window=MemoryConfig.WindowConfig(enabled=False))
|
||||
@@ -201,6 +207,7 @@ class AppRunner:
|
||||
memory_config=memory_config,
|
||||
memory=memory,
|
||||
model_config=model_config,
|
||||
image_detail_config=image_detail_config,
|
||||
)
|
||||
stop = model_config.stop
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ from core.app.entities.queue_entities import QueueAnnotationReplyEvent
|
||||
from core.callback_handler.index_tool_callback_handler import DatasetIndexToolCallbackHandler
|
||||
from core.memory.token_buffer_memory import TokenBufferMemory
|
||||
from core.model_manager import ModelInstance
|
||||
from core.model_runtime.entities.message_entities import ImagePromptMessageContent
|
||||
from core.moderation.base import ModerationError
|
||||
from core.rag.retrieval.dataset_retrieval import DatasetRetrieval
|
||||
from extensions.ext_database import db
|
||||
@@ -50,6 +51,16 @@ class ChatAppRunner(AppRunner):
|
||||
query = application_generate_entity.query
|
||||
files = application_generate_entity.files
|
||||
|
||||
image_detail_config = (
|
||||
application_generate_entity.file_upload_config.image_config.detail
|
||||
if (
|
||||
application_generate_entity.file_upload_config
|
||||
and application_generate_entity.file_upload_config.image_config
|
||||
)
|
||||
else None
|
||||
)
|
||||
image_detail_config = image_detail_config or ImagePromptMessageContent.DETAIL.LOW
|
||||
|
||||
# Pre-calculate the number of tokens of the prompt messages,
|
||||
# and return the rest number of tokens by model context token size limit and max token size limit.
|
||||
# If the rest number of tokens is not enough, raise exception.
|
||||
@@ -85,6 +96,7 @@ class ChatAppRunner(AppRunner):
|
||||
files=files,
|
||||
query=query,
|
||||
memory=memory,
|
||||
image_detail_config=image_detail_config,
|
||||
)
|
||||
|
||||
# moderation
|
||||
@@ -168,6 +180,7 @@ class ChatAppRunner(AppRunner):
|
||||
hit_callback=hit_callback,
|
||||
memory=memory,
|
||||
message_id=message.id,
|
||||
inputs=inputs,
|
||||
)
|
||||
|
||||
# reorganize all inputs and template to prompt messages
|
||||
@@ -182,6 +195,7 @@ class ChatAppRunner(AppRunner):
|
||||
query=query,
|
||||
context=context,
|
||||
memory=memory,
|
||||
image_detail_config=image_detail_config,
|
||||
)
|
||||
|
||||
# check hosting moderation
|
||||
|
||||
@@ -9,6 +9,7 @@ from core.app.entities.app_invoke_entities import (
|
||||
)
|
||||
from core.callback_handler.index_tool_callback_handler import DatasetIndexToolCallbackHandler
|
||||
from core.model_manager import ModelInstance
|
||||
from core.model_runtime.entities.message_entities import ImagePromptMessageContent
|
||||
from core.moderation.base import ModerationError
|
||||
from core.rag.retrieval.dataset_retrieval import DatasetRetrieval
|
||||
from extensions.ext_database import db
|
||||
@@ -43,6 +44,16 @@ class CompletionAppRunner(AppRunner):
|
||||
query = application_generate_entity.query
|
||||
files = application_generate_entity.files
|
||||
|
||||
image_detail_config = (
|
||||
application_generate_entity.file_upload_config.image_config.detail
|
||||
if (
|
||||
application_generate_entity.file_upload_config
|
||||
and application_generate_entity.file_upload_config.image_config
|
||||
)
|
||||
else None
|
||||
)
|
||||
image_detail_config = image_detail_config or ImagePromptMessageContent.DETAIL.LOW
|
||||
|
||||
# Pre-calculate the number of tokens of the prompt messages,
|
||||
# and return the rest number of tokens by model context token size limit and max token size limit.
|
||||
# If the rest number of tokens is not enough, raise exception.
|
||||
@@ -66,6 +77,7 @@ class CompletionAppRunner(AppRunner):
|
||||
inputs=inputs,
|
||||
files=files,
|
||||
query=query,
|
||||
image_detail_config=image_detail_config,
|
||||
)
|
||||
|
||||
# moderation
|
||||
@@ -127,6 +139,7 @@ class CompletionAppRunner(AppRunner):
|
||||
show_retrieve_source=app_config.additional_features.show_retrieve_source,
|
||||
hit_callback=hit_callback,
|
||||
message_id=message.id,
|
||||
inputs=inputs,
|
||||
)
|
||||
|
||||
# reorganize all inputs and template to prompt messages
|
||||
@@ -140,6 +153,7 @@ class CompletionAppRunner(AppRunner):
|
||||
files=files,
|
||||
query=query,
|
||||
context=context,
|
||||
image_detail_config=image_detail_config,
|
||||
)
|
||||
|
||||
# check hosting moderation
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
# 自定义配置文件
|
||||
workflow_generator:
|
||||
# 用于生成工作流的模型配置
|
||||
models:
|
||||
default: my-gpt-4o-mini # 默认使用的模型
|
||||
available: # 可用的模型列表
|
||||
my-gpt-4o-mini:
|
||||
model_name: gpt-4o-mini
|
||||
base_url: https://api.pandalla.ai/v1
|
||||
key_path: ./openai_key
|
||||
max_tokens: 4096
|
||||
my-gpt-4o:
|
||||
model_name: gpt-4o
|
||||
base_url: https://api.pandalla.ai/v1
|
||||
key_path: ./openai_key
|
||||
max_tokens: 4096
|
||||
|
||||
# 调试配置
|
||||
debug:
|
||||
enabled: false # 默认不启用调试模式,可通过命令行参数 --debug 启用
|
||||
dir: debug/ # 调试信息保存目录
|
||||
save_options: # 调试信息保存选项
|
||||
prompt: true # 保存提示词
|
||||
response: true # 保存大模型响应
|
||||
json: true # 保存JSON解析过程
|
||||
workflow: true # 保存工作流生成过程
|
||||
case_id_format: "%Y%m%d_%H%M%S_%f" # 运行ID格式,使用datetime.strftime格式
|
||||
@@ -1,33 +0,0 @@
|
||||
# 默认配置文件
|
||||
|
||||
# 工作流生成器配置
|
||||
workflow_generator:
|
||||
# 用于生成工作流的模型配置
|
||||
models:
|
||||
default: gpt-4 # 默认使用的模型
|
||||
available: # 可用的模型列表
|
||||
gpt-4:
|
||||
model_name: gpt-4
|
||||
base_url: https://api.openai.com/v1
|
||||
key_path: ./openai_key
|
||||
max_tokens: 8192
|
||||
gpt-4-turbo:
|
||||
model_name: gpt-4-1106-preview
|
||||
base_url: https://api.openai.com/v1
|
||||
key_path: ./openai_key
|
||||
max_tokens: 4096
|
||||
|
||||
# 工作流节点配置
|
||||
workflow_nodes:
|
||||
# LLM节点默认配置(使用 Dify 平台配置的模型)
|
||||
llm:
|
||||
provider: zhipuai
|
||||
model: glm-4-flash
|
||||
max_tokens: 16384
|
||||
temperature: 0.7
|
||||
mode: chat
|
||||
|
||||
# 输出配置
|
||||
output:
|
||||
dir: output/
|
||||
filename: generated_workflow.yml
|
||||
@@ -1,78 +0,0 @@
|
||||
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",
|
||||
]
|
||||
@@ -1,34 +0,0 @@
|
||||
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)
|
||||
@@ -1,21 +0,0 @@
|
||||
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)
|
||||
@@ -1,62 +0,0 @@
|
||||
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)
|
||||
@@ -1,56 +0,0 @@
|
||||
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())
|
||||
@@ -1,690 +0,0 @@
|
||||
from enum import Enum
|
||||
from typing import Any, Optional, Union
|
||||
|
||||
import yaml
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
# BlockEnum 枚举
|
||||
class BlockEnum(str, Enum):
|
||||
start = "start"
|
||||
end = "end"
|
||||
answer = "answer"
|
||||
llm = "llm"
|
||||
knowledge_retrieval = "knowledge-retrieval"
|
||||
question_classifier = "question-classifier"
|
||||
if_else = "if-else"
|
||||
code = "code"
|
||||
template_transform = "template-transform"
|
||||
http_request = "http-request"
|
||||
variable_assigner = "variable-assigner"
|
||||
variable_aggregator = "variable-aggregator"
|
||||
tool = "tool"
|
||||
parameter_extractor = "parameter-extractor"
|
||||
iteration = "iteration"
|
||||
document_extractor = "document-extractor"
|
||||
list_operator = "list-operator"
|
||||
iteration_start = "iteration-start"
|
||||
assigner = "assigner" # is now named as VariableAssigner
|
||||
agent = "agent"
|
||||
|
||||
|
||||
# Error枚举
|
||||
class ErrorHandleMode(str, Enum):
|
||||
terminated = "terminated"
|
||||
continue_on_error = "continue-on-error"
|
||||
remove_abnormal_output = "remove-abnormal-output"
|
||||
|
||||
|
||||
class ErrorHandleTypeEnum(str, Enum):
|
||||
none = ("none",)
|
||||
failBranch = ("fail-branch",)
|
||||
defaultValue = ("default-value",)
|
||||
|
||||
|
||||
# Branch 类型
|
||||
class Branch(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
|
||||
|
||||
# NodeRunningStatus 枚举
|
||||
class NodeRunningStatus(str, Enum):
|
||||
not_start = "not-start"
|
||||
waiting = "waiting"
|
||||
running = "running"
|
||||
succeeded = "succeeded"
|
||||
failed = "failed"
|
||||
exception = "exception"
|
||||
retry = "retry"
|
||||
|
||||
|
||||
# 创建一个基类来统一CommonNodeType和CommonEdgeType的序列化逻辑
|
||||
class BaseType(BaseModel):
|
||||
"""基类,用于统一CommonNodeType和CommonEdgeType的序列化逻辑"""
|
||||
|
||||
def to_json(self) -> dict[str, Any]:
|
||||
"""
|
||||
将对象转换为JSON格式的字典,通过循环模型字段来构建JSON数据
|
||||
"""
|
||||
json_data = {}
|
||||
|
||||
# 获取模型的所有字段
|
||||
for field_name, field_value in self.__dict__.items():
|
||||
if field_value is not None:
|
||||
# 特殊处理Branch类型的列表
|
||||
if field_name == "_targetBranches" and field_value is not None:
|
||||
json_data[field_name] = [branch.dict(exclude_none=True) for branch in field_value]
|
||||
# 处理枚举类型
|
||||
elif isinstance(field_value, Enum):
|
||||
json_data[field_name] = field_value.value
|
||||
# 处理嵌套的Pydantic模型
|
||||
elif hasattr(field_value, "dict") and callable(field_value.dict):
|
||||
json_data[field_name] = field_value.dict(exclude_none=True)
|
||||
# 处理列表中的Pydantic模型
|
||||
elif isinstance(field_value, list):
|
||||
processed_list = []
|
||||
for item in field_value:
|
||||
if hasattr(item, "dict") and callable(item.dict):
|
||||
processed_list.append(item.dict(exclude_none=True))
|
||||
else:
|
||||
processed_list.append(item)
|
||||
json_data[field_name] = processed_list
|
||||
# 处理字典中的Pydantic模型
|
||||
elif isinstance(field_value, dict):
|
||||
processed_dict = {}
|
||||
for key, value in field_value.items():
|
||||
if hasattr(value, "dict") and callable(value.dict):
|
||||
processed_dict[key] = value.dict(exclude_none=True)
|
||||
else:
|
||||
processed_dict[key] = value
|
||||
json_data[field_name] = processed_dict
|
||||
# 其他字段直接添加
|
||||
else:
|
||||
json_data[field_name] = field_value
|
||||
|
||||
return json_data
|
||||
|
||||
|
||||
# CommonNodeType 类型
|
||||
class CommonNodeType(BaseType):
|
||||
_connectedSourceHandleIds: Optional[list[str]] = None
|
||||
_connectedTargetHandleIds: Optional[list[str]] = None
|
||||
_targetBranches: Optional[list[Branch]] = None
|
||||
_isSingleRun: Optional[bool] = None
|
||||
_runningStatus: Optional[NodeRunningStatus] = None
|
||||
_singleRunningStatus: Optional[NodeRunningStatus] = None
|
||||
_isCandidate: Optional[bool] = None
|
||||
_isBundled: Optional[bool] = None
|
||||
_children: Optional[list[str]] = None
|
||||
_isEntering: Optional[bool] = None
|
||||
_showAddVariablePopup: Optional[bool] = None
|
||||
_holdAddVariablePopup: Optional[bool] = None
|
||||
_iterationLength: Optional[int] = None
|
||||
_iterationIndex: Optional[int] = None
|
||||
_inParallelHovering: Optional[bool] = None
|
||||
isInIteration: Optional[bool] = None
|
||||
iteration_id: Optional[str] = None
|
||||
selected: Optional[bool] = None
|
||||
title: str
|
||||
desc: str
|
||||
type: BlockEnum
|
||||
width: Optional[float] = None
|
||||
height: Optional[float] = None
|
||||
|
||||
@classmethod
|
||||
def get_all_required_fields(cls) -> dict[str, str]:
|
||||
"""
|
||||
获取所有必选字段,包括从父类继承的字段
|
||||
这是一个类方法,可以通过类直接调用
|
||||
"""
|
||||
all_required_fields = {}
|
||||
|
||||
# 获取所有父类(除了 object 和 BaseModel)
|
||||
mro = [c for c in cls.__mro__ if c not in (object, BaseModel, BaseType)]
|
||||
|
||||
# 从父类到子类的顺序处理,这样子类的字段会覆盖父类的同名字段
|
||||
for class_type in reversed(mro):
|
||||
if hasattr(class_type, "__annotations__"):
|
||||
for field_name, field_info in class_type.__annotations__.items():
|
||||
# 检查字段是否有默认值
|
||||
has_default = hasattr(class_type, field_name)
|
||||
# 检查字段是否为可选类型
|
||||
is_optional = "Optional" in str(field_info)
|
||||
|
||||
# 如果字段没有默认值且不是Optional类型,则为必选字段
|
||||
if not has_default and not is_optional:
|
||||
all_required_fields[field_name] = str(field_info)
|
||||
|
||||
return all_required_fields
|
||||
|
||||
|
||||
# CommonEdgeType 类型
|
||||
class CommonEdgeType(BaseType):
|
||||
_hovering: Optional[bool] = None
|
||||
_connectedNodeIsHovering: Optional[bool] = None
|
||||
_connectedNodeIsSelected: Optional[bool] = None
|
||||
_run: Optional[bool] = None
|
||||
_isBundled: Optional[bool] = None
|
||||
isInIteration: Optional[bool] = None
|
||||
iteration_id: Optional[str] = None
|
||||
sourceType: BlockEnum
|
||||
targetType: BlockEnum
|
||||
|
||||
|
||||
class ValueSelector(BaseModel):
|
||||
"""Value selector for selecting values from other nodes."""
|
||||
|
||||
value: list[str] = Field(default_factory=list)
|
||||
|
||||
def dict(self, *args, **kwargs):
|
||||
"""自定义序列化方法,直接返回 value 列表"""
|
||||
return self.value
|
||||
|
||||
|
||||
# Add Context class for LLM node
|
||||
class Context(BaseModel):
|
||||
"""Context configuration for LLM node."""
|
||||
|
||||
enabled: bool = False
|
||||
variable_selector: Optional[ValueSelector] = None
|
||||
|
||||
def dict(self, *args, **kwargs):
|
||||
"""自定义序列化方法,确保 variable_selector 字段正确序列化"""
|
||||
result = {"enabled": self.enabled}
|
||||
|
||||
if self.variable_selector:
|
||||
result["variable_selector"] = self.variable_selector.dict()
|
||||
else:
|
||||
result["variable_selector"] = []
|
||||
|
||||
return result
|
||||
|
||||
|
||||
# Variable 类型
|
||||
class Variable(BaseModel):
|
||||
"""
|
||||
变量类型,用于定义节点的输入/输出变量
|
||||
与Dify中的Variable类型保持一致
|
||||
"""
|
||||
|
||||
variable: str # 变量名
|
||||
label: Optional[Union[str, dict[str, str]]] = None # 变量标签,可以是字符串或对象
|
||||
value_selector: list[str] # 变量值选择器,格式为[nodeId, key]
|
||||
variable_type: Optional[str] = None # 变量类型,对应Dify中的VarType枚举
|
||||
value: Optional[str] = None # 变量值(常量值)
|
||||
options: Optional[list[str]] = None # 选项列表(用于select类型)
|
||||
required: Optional[bool] = None # 是否必填
|
||||
isParagraph: Optional[bool] = None # 是否为段落
|
||||
max_length: Optional[int] = None # 最大长度
|
||||
|
||||
def dict(self, *args, **kwargs):
|
||||
"""自定义序列化方法,确保正确序列化"""
|
||||
result = {"variable": self.variable}
|
||||
|
||||
if self.label is not None:
|
||||
result["label"] = self.label
|
||||
|
||||
if self.value_selector:
|
||||
result["value_selector"] = self.value_selector
|
||||
|
||||
if self.variable_type is not None:
|
||||
result["type"] = self.variable_type # 使用type而不是variable_type,与Dify保持一致
|
||||
|
||||
if self.value is not None:
|
||||
result["value"] = self.value
|
||||
|
||||
if self.options is not None:
|
||||
result["options"] = self.options
|
||||
|
||||
if self.required is not None:
|
||||
result["required"] = self.required
|
||||
|
||||
if self.isParagraph is not None:
|
||||
result["isParagraph"] = self.isParagraph
|
||||
|
||||
if self.max_length is not None:
|
||||
result["max_length"] = self.max_length
|
||||
|
||||
return result
|
||||
|
||||
|
||||
# EnvironmentVariable 类型
|
||||
class EnvironmentVariable(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
value: Any
|
||||
value_type: str # Expecting to be either 'string', 'number', or 'secret'
|
||||
|
||||
|
||||
# ConversationVariable 类型
|
||||
class ConversationVariable(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
value_type: str
|
||||
value: Any
|
||||
description: str
|
||||
|
||||
|
||||
# GlobalVariable 类型
|
||||
class GlobalVariable(BaseModel):
|
||||
name: str
|
||||
value_type: str # Expecting to be either 'string' or 'number'
|
||||
description: str
|
||||
|
||||
|
||||
# VariableWithValue 类型
|
||||
class VariableWithValue(BaseModel):
|
||||
key: str
|
||||
value: str
|
||||
|
||||
|
||||
# InputVarType 枚举
|
||||
class InputVarType(str, Enum):
|
||||
text_input = "text-input"
|
||||
paragraph = "paragraph"
|
||||
select = "select"
|
||||
number = "number"
|
||||
url = "url"
|
||||
files = "files"
|
||||
json = "json"
|
||||
contexts = "contexts"
|
||||
iterator = "iterator"
|
||||
file = "file"
|
||||
file_list = "file-list"
|
||||
|
||||
|
||||
# InputVar 类型
|
||||
class InputVar(BaseModel):
|
||||
type: InputVarType
|
||||
label: Union[str, dict[str, Any]] # 可以是字符串或对象
|
||||
variable: str
|
||||
max_length: Optional[int] = None
|
||||
default: Optional[str] = None
|
||||
required: bool
|
||||
hint: Optional[str] = None
|
||||
options: Optional[list[str]] = None
|
||||
value_selector: Optional[list[str]] = None
|
||||
|
||||
def dict(self, *args, **kwargs):
|
||||
"""自定义序列化方法,确保正确序列化"""
|
||||
result = {
|
||||
"type": self.type.value if isinstance(self.type, Enum) else self.type,
|
||||
"label": self.label,
|
||||
"variable": self.variable,
|
||||
"required": self.required,
|
||||
}
|
||||
|
||||
if self.max_length is not None:
|
||||
result["max_length"] = self.max_length
|
||||
|
||||
if self.default is not None:
|
||||
result["default"] = self.default
|
||||
|
||||
if self.hint is not None:
|
||||
result["hint"] = self.hint
|
||||
|
||||
if self.options is not None:
|
||||
result["options"] = self.options
|
||||
|
||||
if self.value_selector is not None:
|
||||
result["value_selector"] = self.value_selector
|
||||
|
||||
return result
|
||||
|
||||
|
||||
# ModelConfig 类型
|
||||
class ModelConfig(BaseModel):
|
||||
provider: str
|
||||
name: str
|
||||
mode: str
|
||||
completion_params: dict[str, Any]
|
||||
|
||||
|
||||
# PromptRole 枚举
|
||||
class PromptRole(str, Enum):
|
||||
system = "system"
|
||||
user = "user"
|
||||
assistant = "assistant"
|
||||
|
||||
|
||||
# EditionType 枚举
|
||||
class EditionType(str, Enum):
|
||||
basic = "basic"
|
||||
jinja2 = "jinja2"
|
||||
|
||||
|
||||
# PromptItem 类型
|
||||
class PromptItem(BaseModel):
|
||||
id: Optional[str] = None
|
||||
role: Optional[PromptRole] = None
|
||||
text: str
|
||||
edition_type: Optional[EditionType] = None
|
||||
jinja2_text: Optional[str] = None
|
||||
|
||||
def dict(self, *args, **kwargs):
|
||||
"""自定义序列化方法,确保 role 字段正确序列化"""
|
||||
result = {"id": self.id, "text": self.text}
|
||||
|
||||
if self.role:
|
||||
result["role"] = self.role.value
|
||||
|
||||
if self.edition_type:
|
||||
result["edition_type"] = self.edition_type.value
|
||||
|
||||
if self.jinja2_text:
|
||||
result["jinja2_text"] = self.jinja2_text
|
||||
|
||||
return result
|
||||
|
||||
|
||||
# MemoryRole 枚举
|
||||
class MemoryRole(str, Enum):
|
||||
user = "user"
|
||||
assistant = "assistant"
|
||||
|
||||
|
||||
# RolePrefix 类型
|
||||
class RolePrefix(BaseModel):
|
||||
user: str
|
||||
assistant: str
|
||||
|
||||
|
||||
# Memory 类型
|
||||
class Memory(BaseModel):
|
||||
role_prefix: Optional[RolePrefix] = None
|
||||
window: dict[str, Any] # Expecting to have 'enabled' and 'size'
|
||||
query_prompt_template: str
|
||||
|
||||
|
||||
# VarType 枚举
|
||||
class VarType(str, Enum):
|
||||
string = "string"
|
||||
number = "number"
|
||||
secret = "secret"
|
||||
boolean = "boolean"
|
||||
object = "object"
|
||||
file = "file"
|
||||
array = "array"
|
||||
arrayString = "array[string]"
|
||||
arrayNumber = "array[number]"
|
||||
arrayObject = "array[object]"
|
||||
arrayFile = "array[file]"
|
||||
any = "any"
|
||||
|
||||
|
||||
# Var 类型
|
||||
class Var(BaseModel):
|
||||
variable: str
|
||||
type: VarType
|
||||
children: Optional[list["Var"]] = None # Self-reference
|
||||
isParagraph: Optional[bool] = None
|
||||
isSelect: Optional[bool] = None
|
||||
options: Optional[list[str]] = None
|
||||
required: Optional[bool] = None
|
||||
des: Optional[str] = None
|
||||
isException: Optional[bool] = None
|
||||
|
||||
def dict(self, *args, **kwargs):
|
||||
"""自定义序列化方法,确保type字段正确序列化"""
|
||||
result = {"variable": self.variable, "type": self.type.value if isinstance(self.type, Enum) else self.type}
|
||||
|
||||
if self.children is not None:
|
||||
result["children"] = [child.dict() for child in self.children]
|
||||
|
||||
if self.isParagraph is not None:
|
||||
result["isParagraph"] = self.isParagraph
|
||||
|
||||
if self.isSelect is not None:
|
||||
result["isSelect"] = self.isSelect
|
||||
|
||||
if self.options is not None:
|
||||
result["options"] = self.options
|
||||
|
||||
if self.required is not None:
|
||||
result["required"] = self.required
|
||||
|
||||
if self.des is not None:
|
||||
result["des"] = self.des
|
||||
|
||||
if self.isException is not None:
|
||||
result["isException"] = self.isException
|
||||
|
||||
return result
|
||||
|
||||
|
||||
# NodeOutPutVar 类型
|
||||
class NodeOutPutVar(BaseModel):
|
||||
nodeId: str
|
||||
title: str
|
||||
vars: list[Var]
|
||||
isStartNode: Optional[bool] = None
|
||||
|
||||
|
||||
# Block 类型
|
||||
class Block(BaseModel):
|
||||
classification: Optional[str] = None
|
||||
type: BlockEnum
|
||||
title: str
|
||||
description: Optional[str] = None
|
||||
|
||||
|
||||
# NodeDefault 类型
|
||||
class NodeDefault(BaseModel):
|
||||
defaultValue: dict[str, Any]
|
||||
getAvailablePrevNodes: Any # Placeholder for function reference
|
||||
getAvailableNextNodes: Any # Placeholder for function reference
|
||||
checkValid: Any # Placeholder for function reference
|
||||
|
||||
|
||||
# OnSelectBlock 类型
|
||||
class OnSelectBlock(BaseModel):
|
||||
nodeType: BlockEnum
|
||||
additional_data: Optional[dict[str, Any]] = None
|
||||
|
||||
|
||||
# WorkflowRunningStatus 枚举
|
||||
class WorkflowRunningStatus(str, Enum):
|
||||
waiting = "waiting"
|
||||
running = "running"
|
||||
succeeded = "succeeded"
|
||||
failed = "failed"
|
||||
stopped = "stopped"
|
||||
|
||||
|
||||
# WorkflowVersion 枚举
|
||||
class WorkflowVersion(str, Enum):
|
||||
draft = "draft"
|
||||
latest = "latest"
|
||||
|
||||
|
||||
# OnNodeAdd 类型
|
||||
class OnNodeAdd(BaseModel):
|
||||
nodeType: BlockEnum
|
||||
sourceHandle: Optional[str] = None
|
||||
targetHandle: Optional[str] = None
|
||||
toolDefaultValue: Optional[dict[str, Any]] = None
|
||||
|
||||
|
||||
# CheckValidRes 类型
|
||||
class CheckValidRes(BaseModel):
|
||||
isValid: bool
|
||||
errorMessage: Optional[str] = None
|
||||
|
||||
|
||||
# RunFile 类型
|
||||
class RunFile(BaseModel):
|
||||
type: str
|
||||
transfer_method: list[str]
|
||||
url: Optional[str] = None
|
||||
upload_file_id: Optional[str] = None
|
||||
|
||||
|
||||
# WorkflowRunningData 类型
|
||||
class WorkflowRunningData(BaseModel):
|
||||
task_id: Optional[str] = None
|
||||
message_id: Optional[str] = None
|
||||
conversation_id: Optional[str] = None
|
||||
result: dict[str, Any] # Expecting a structured object
|
||||
tracing: Optional[list[dict[str, Any]]] = None # Placeholder for NodeTracing
|
||||
|
||||
|
||||
# HistoryWorkflowData 类型
|
||||
class HistoryWorkflowData(BaseModel):
|
||||
id: str
|
||||
sequence_number: int
|
||||
status: str
|
||||
conversation_id: Optional[str] = None
|
||||
|
||||
|
||||
# ChangeType 枚举
|
||||
class ChangeType(str, Enum):
|
||||
changeVarName = "changeVarName"
|
||||
remove = "remove"
|
||||
|
||||
|
||||
# MoreInfo 类型
|
||||
class MoreInfo(BaseModel):
|
||||
type: ChangeType
|
||||
payload: Optional[dict[str, Any]] = None
|
||||
|
||||
|
||||
# ToolWithProvider 类型
|
||||
class ToolWithProvider(BaseModel):
|
||||
tools: list[dict[str, Any]] # Placeholder for Tool type
|
||||
|
||||
|
||||
# SupportUploadFileTypes 枚举
|
||||
class SupportUploadFileTypes(str, Enum):
|
||||
image = "image"
|
||||
document = "document"
|
||||
audio = "audio"
|
||||
video = "video"
|
||||
custom = "custom"
|
||||
|
||||
|
||||
# UploadFileSetting 类型
|
||||
class UploadFileSetting(BaseModel):
|
||||
allowed_file_upload_methods: list[str]
|
||||
allowed_file_types: list[SupportUploadFileTypes]
|
||||
allowed_file_extensions: Optional[list[str]] = None
|
||||
max_length: int
|
||||
number_limits: Optional[int] = None
|
||||
|
||||
|
||||
# VisionSetting 类型
|
||||
class VisionSetting(BaseModel):
|
||||
variable_selector: list[str]
|
||||
detail: dict[str, Any] # Placeholder for Resolution type
|
||||
|
||||
|
||||
# 创建一个基类来统一序列化逻辑
|
||||
class CompleteBase(BaseModel):
|
||||
"""基类,用于统一CompleteNode和CompleteEdge的序列化逻辑"""
|
||||
|
||||
def to_json(self):
|
||||
"""将对象转换为JSON格式的字典"""
|
||||
json_data = {}
|
||||
|
||||
# 获取模型的所有字段
|
||||
for field_name, field_value in self.__dict__.items():
|
||||
if field_value is not None:
|
||||
# 处理嵌套的数据对象
|
||||
if field_name == "data" and hasattr(field_value, "to_json"):
|
||||
json_data[field_name] = field_value.to_json()
|
||||
# 处理枚举类型
|
||||
elif isinstance(field_value, Enum):
|
||||
json_data[field_name] = field_value.value
|
||||
# 处理嵌套的Pydantic模型
|
||||
elif hasattr(field_value, "dict") and callable(field_value.dict):
|
||||
json_data[field_name] = field_value.dict(exclude_none=True)
|
||||
# 处理列表中的Pydantic模型
|
||||
elif isinstance(field_value, list):
|
||||
processed_list = []
|
||||
for item in field_value:
|
||||
if hasattr(item, "dict") and callable(item.dict):
|
||||
processed_list.append(item.dict(exclude_none=True))
|
||||
else:
|
||||
processed_list.append(item)
|
||||
json_data[field_name] = processed_list
|
||||
# 处理字典中的Pydantic模型
|
||||
elif isinstance(field_value, dict):
|
||||
processed_dict = {}
|
||||
for key, value in field_value.items():
|
||||
if hasattr(value, "dict") and callable(value.dict):
|
||||
processed_dict[key] = value.dict(exclude_none=True)
|
||||
else:
|
||||
processed_dict[key] = value
|
||||
json_data[field_name] = processed_dict
|
||||
# 其他字段直接添加
|
||||
else:
|
||||
json_data[field_name] = field_value
|
||||
|
||||
return json_data
|
||||
|
||||
def to_yaml(self):
|
||||
"""将对象转换为YAML格式的字符串"""
|
||||
return yaml.dump(self.to_json(), allow_unicode=True)
|
||||
|
||||
|
||||
class CompleteNode(CompleteBase):
|
||||
id: str
|
||||
position: dict
|
||||
height: int
|
||||
width: float
|
||||
positionAbsolute: dict
|
||||
selected: bool
|
||||
sourcePosition: Union[dict, str]
|
||||
targetPosition: Union[dict, str]
|
||||
type: str
|
||||
data: Optional[Union[CommonNodeType, None]] = None # Flexible field to store CommonNodeType or None
|
||||
|
||||
def add_data(self, data: Union[CommonNodeType, None]):
|
||||
self.data = data
|
||||
|
||||
def to_json(self):
|
||||
json_data = super().to_json()
|
||||
|
||||
# 特殊处理sourcePosition和targetPosition
|
||||
json_data["sourcePosition"] = "right" # 直接输出为字符串"right"
|
||||
json_data["targetPosition"] = "left" # 直接输出为字符串"left"
|
||||
|
||||
# 确保 width 是整数而不是浮点数
|
||||
if isinstance(json_data["width"], float):
|
||||
json_data["width"] = int(json_data["width"])
|
||||
|
||||
return json_data
|
||||
|
||||
|
||||
class CompleteEdge(CompleteBase):
|
||||
id: str
|
||||
source: str
|
||||
sourceHandle: str
|
||||
target: str
|
||||
targetHandle: str
|
||||
type: str
|
||||
zIndex: int
|
||||
data: Optional[Union[CommonEdgeType, None]] = None # Flexible field to store CommonEdgeType or None
|
||||
|
||||
def add_data(self, data: Union[CommonEdgeType, None]):
|
||||
self.data = data
|
||||
|
||||
|
||||
# 示例用法
|
||||
if __name__ == "__main__":
|
||||
# 这里可以添加示例数据进行验证
|
||||
common_node = CompleteNode(
|
||||
id="1740019130520",
|
||||
position={"x": 80, "y": 282},
|
||||
height=100,
|
||||
width=100,
|
||||
positionAbsolute={"x": 80, "y": 282},
|
||||
selected=True,
|
||||
sourcePosition={"x": 80, "y": 282},
|
||||
targetPosition={"x": 80, "y": 282},
|
||||
type="custom",
|
||||
)
|
||||
common_data = CommonNodeType(title="示例节点", desc="这是一个示例节点", type="")
|
||||
print(CommonNodeType.get_all_required_fields())
|
||||
common_node.add_data(common_data)
|
||||
# print(common_node)
|
||||
@@ -1,22 +0,0 @@
|
||||
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)
|
||||
@@ -1,127 +0,0 @@
|
||||
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)
|
||||
@@ -1,99 +0,0 @@
|
||||
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)
|
||||
@@ -1,45 +0,0 @@
|
||||
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)
|
||||
@@ -1,25 +0,0 @@
|
||||
from .common import BlockEnum, CommonNodeType
|
||||
|
||||
# 引入之前定义的 CommonNodeType
|
||||
# 假设它们在同一模块中定义
|
||||
|
||||
|
||||
class IterationStartNodeType(CommonNodeType):
|
||||
"""
|
||||
Iteration Start node type implementation.
|
||||
|
||||
This node type is used as the starting point within an iteration block.
|
||||
It inherits all properties from CommonNodeType without adding any additional fields.
|
||||
"""
|
||||
|
||||
pass # 仅仅继承 CommonNodeType,无其他字段
|
||||
|
||||
|
||||
# 示例用法
|
||||
if __name__ == "__main__":
|
||||
example_node = IterationStartNodeType(
|
||||
title="Example Iteration Start Node",
|
||||
desc="An iteration start node example",
|
||||
type=BlockEnum.iteration_start,
|
||||
)
|
||||
print(example_node)
|
||||
@@ -1,115 +0,0 @@
|
||||
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)
|
||||
@@ -1,73 +0,0 @@
|
||||
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)
|
||||
@@ -1,66 +0,0 @@
|
||||
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)
|
||||
@@ -1,38 +0,0 @@
|
||||
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)
|
||||
@@ -1,85 +0,0 @@
|
||||
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)
|
||||
@@ -1,51 +0,0 @@
|
||||
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)
|
||||
@@ -1,22 +0,0 @@
|
||||
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)
|
||||
@@ -1,26 +0,0 @@
|
||||
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)
|
||||
@@ -1,54 +0,0 @@
|
||||
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
|
||||
@@ -1,56 +0,0 @@
|
||||
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
|
||||
@@ -1,239 +0,0 @@
|
||||
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: 1740019130521-source-1740019130522-target
|
||||
source: '1740019130521'
|
||||
sourceHandle: source
|
||||
target: '1740019130522'
|
||||
targetHandle: target
|
||||
type: custom
|
||||
zIndex: 0
|
||||
data:
|
||||
isInIteration: false
|
||||
sourceType: llm
|
||||
targetType: code
|
||||
- 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: input_text
|
||||
variable: input_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: LLM节点分析文本情绪,识别出积极、消极或中性情绪。
|
||||
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: 分析此文本的情绪:{{input_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: 代码节点将根据LLM分析的结果处理情绪类型。
|
||||
type: code
|
||||
variables:
|
||||
- variable: emotion
|
||||
value_selector:
|
||||
- '1740019130521'
|
||||
- emotion
|
||||
code_language: python3
|
||||
code: "def analyze_sentiment(emotion):\n if emotion == 'positive':\n \
|
||||
\ return '积极'\n elif emotion == 'negative':\n return '消极'\n\
|
||||
\ else:\n return '中性'\n\nemotion = '{{emotion}}'\nresult = analyze_sentiment(emotion)\n\
|
||||
return {'result': result}"
|
||||
outputs:
|
||||
sentiment_result:
|
||||
type: string
|
||||
- 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:
|
||||
- '1740019130522'
|
||||
- sentiment_result
|
||||
template: 文本的情绪分析结果为:{{sentiment_result}}
|
||||
- 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: output
|
||||
value_selector:
|
||||
- '1740019130523'
|
||||
- output
|
||||
viewport:
|
||||
x: 92.96659905656679
|
||||
y: 79.13437154762897
|
||||
zoom: 0.9002006986311041
|
||||
@@ -1,247 +0,0 @@
|
||||
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: 1740019130521-source-1740019130522-target
|
||||
source: '1740019130521'
|
||||
sourceHandle: source
|
||||
target: '1740019130522'
|
||||
targetHandle: target
|
||||
type: custom
|
||||
zIndex: 0
|
||||
data:
|
||||
isInIteration: false
|
||||
sourceType: llm
|
||||
targetType: code
|
||||
- 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: number
|
||||
label: num1
|
||||
variable: num1
|
||||
required: true
|
||||
max_length: 48
|
||||
options: []
|
||||
- type: number
|
||||
label: num2
|
||||
variable: num2
|
||||
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: LLM节点,根据输入的两个数字生成计算它们之和的Python函数。
|
||||
type: llm
|
||||
model:
|
||||
provider: openai
|
||||
name: gpt-4
|
||||
mode: chat
|
||||
completion_params:
|
||||
temperature: 0.7
|
||||
prompt_template:
|
||||
- id: 1740019130521-system
|
||||
text: 你是一个Python开发助手,请根据以下输入生成一个计算两数之和的Python函数。
|
||||
role: system
|
||||
- id: 1740019130521-user
|
||||
text: 请为两个数字{{num1}}和{{num2}}生成一个Python函数,计算它们的和。
|
||||
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: 代码节点,执行LLM生成的Python函数,并计算结果。
|
||||
type: code
|
||||
variables:
|
||||
- variable: num1
|
||||
value_selector:
|
||||
- '1740019130520'
|
||||
- num1
|
||||
- variable: num2
|
||||
value_selector:
|
||||
- '1740019130520'
|
||||
- num2
|
||||
code_language: python3
|
||||
code: "def calculate_sum(num1, num2):\n return num1 + num2\n\nresult =\
|
||||
\ calculate_sum({{num1}}, {{num2}})\nreturn result"
|
||||
outputs:
|
||||
result:
|
||||
type: number
|
||||
- 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: result
|
||||
value_selector:
|
||||
- '1740019130522'
|
||||
- result
|
||||
template: '计算结果为: {{result}}'
|
||||
- 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: output
|
||||
value_selector:
|
||||
- '1740019130523'
|
||||
- output
|
||||
viewport:
|
||||
x: 92.96659905656679
|
||||
y: 79.13437154762897
|
||||
zoom: 0.9002006986311041
|
||||
@@ -1,262 +0,0 @@
|
||||
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
|
||||
@@ -1,8 +0,0 @@
|
||||
"""
|
||||
工作流生成器包
|
||||
用于根据用户需求生成Dify工作流
|
||||
"""
|
||||
|
||||
from .workflow_generator import WorkflowGenerator
|
||||
|
||||
__all__ = ["WorkflowGenerator"]
|
||||
@@ -1,9 +0,0 @@
|
||||
"""
|
||||
节点和边生成器包
|
||||
"""
|
||||
|
||||
from .edge_generator import EdgeGenerator
|
||||
from .layout_engine import LayoutEngine
|
||||
from .node_generator import NodeGenerator
|
||||
|
||||
__all__ = ["EdgeGenerator", "LayoutEngine", "NodeGenerator"]
|
||||
@@ -1,101 +0,0 @@
|
||||
"""
|
||||
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
|
||||
@@ -1,77 +0,0 @@
|
||||
"""
|
||||
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
|
||||
@@ -1,446 +0,0 @@
|
||||
"""
|
||||
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,
|
||||
)
|
||||
@@ -1,7 +0,0 @@
|
||||
"""
|
||||
模型包
|
||||
"""
|
||||
|
||||
from .workflow_description import ConnectionDescription, NodeDescription, WorkflowDescription
|
||||
|
||||
__all__ = ["ConnectionDescription", "NodeDescription", "WorkflowDescription"]
|
||||
@@ -1,80 +0,0 @@
|
||||
"""
|
||||
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
|
||||
@@ -1,16 +0,0 @@
|
||||
"""
|
||||
工具函数包
|
||||
"""
|
||||
|
||||
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",
|
||||
]
|
||||
@@ -1,142 +0,0 @@
|
||||
"""
|
||||
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", {})
|
||||
@@ -1,151 +0,0 @@
|
||||
"""
|
||||
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)
|
||||
@@ -1,438 +0,0 @@
|
||||
"""
|
||||
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}
|
||||
@@ -1,171 +0,0 @@
|
||||
"""
|
||||
Prompt Template Collection
|
||||
Contains all prompt templates used for generating workflows
|
||||
"""
|
||||
|
||||
# Default model configuration
|
||||
DEFAULT_MODEL_CONFIG = {
|
||||
"provider": "zhipuai",
|
||||
"model": "glm-4-flash",
|
||||
"mode": "chat",
|
||||
"completion_params": {"temperature": 0.7},
|
||||
}
|
||||
|
||||
|
||||
# Default system prompt
|
||||
DEFAULT_SYSTEM_PROMPT = "You are a workflow design expert who can design Dify workflows based on user requirements."
|
||||
|
||||
|
||||
# Code node template
|
||||
CODE_NODE_TEMPLATE = """def main(input_var):
|
||||
# Process input variable
|
||||
result = input_var
|
||||
|
||||
# Return a dictionary; keys must exactly match variable names defined in outputs
|
||||
return {"output_var_name": result}"""
|
||||
|
||||
|
||||
def build_workflow_prompt(user_requirement: str) -> str:
|
||||
"""
|
||||
Build workflow generation prompt
|
||||
|
||||
Args:
|
||||
user_requirement: User requirement description
|
||||
|
||||
Returns:
|
||||
Prompt string
|
||||
"""
|
||||
# String concatenation to avoid brace escaping
|
||||
prompt_part1 = (
|
||||
"""
|
||||
Please design a Dify workflow based on the following user requirement:
|
||||
|
||||
User requirement: """
|
||||
+ user_requirement
|
||||
+ """
|
||||
|
||||
The description's language should align consistently with the user's requirements.
|
||||
|
||||
Generate a concise workflow description containing the following node types:
|
||||
- Start: Start node, defines workflow input parameters
|
||||
- LLM: Large Language Model node for text generation
|
||||
- Code: Code node to execute Python code
|
||||
- Template: Template node for formatting outputs
|
||||
- End: End node, defines workflow output
|
||||
|
||||
【Important Guidelines】:
|
||||
1. When referencing variables in LLM nodes, use the format {{#nodeID.variable_name#}}, e.g., {{#1740019130520.user_question#}}, where 1740019130520 is the source node ID. Otherwise, in most cases, the user prompt should define a template to guide the LLM’s response.
|
||||
2. Code nodes must define a `main` function that directly receives variables from upstream nodes as parameters; do not use template syntax inside the function.
|
||||
3. Dictionary keys returned by Code nodes must exactly match the variable names defined in outputs.
|
||||
4. Variables in Template nodes must strictly use double curly braces format "{{ variable_name }}"; note exactly two curly braces, neither one nor three. For example, "User question is: {{ user_question }}, answer: {{ answer }}". Triple curly braces such as "{{{ variable_name }}}" are strictly forbidden.
|
||||
5. IMPORTANT: In Code nodes, the function parameter names MUST EXACTLY MATCH the variable names defined in that Code node. For example, if a Code node defines a variable with name "input_text" that receives data from an upstream node, the function parameter must also be named "input_text" (e.g., def main(input_text): ...).
|
||||
6. CRITICAL: LLM nodes ALWAYS output their result in a variable named "text". When a Code node receives data from an LLM node, the source_variable MUST be "text". For example, if a Code node has a variable named "llm_output" that receives data from an LLM node, the source_variable should be "text", not "input_text" or any other name.
|
||||
|
||||
Return the workflow description in JSON format as follows:
|
||||
```json
|
||||
{
|
||||
"name": "Workflow Name",
|
||||
"description": "Workflow description",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "node1",
|
||||
"type": "start",
|
||||
"title": "Start Node",
|
||||
"description": "Description of the start node",
|
||||
"variables": [
|
||||
{
|
||||
"name": "variable_name",
|
||||
"type": "string|number",
|
||||
"description": "Variable description",
|
||||
"required": true|false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "node2",
|
||||
"type": "llm",
|
||||
"title": "LLM Node",
|
||||
"description": "Description of LLM node",
|
||||
"system_prompt": "System prompt",
|
||||
"user_prompt": "User prompt, variables referenced using {{#nodeID.variable_name#}}, e.g., {{#node1.variable_name#}}",
|
||||
"provider": "zhipuai",
|
||||
"model": "glm-4-flash",
|
||||
"variables": [
|
||||
{
|
||||
"name": "variable_name",
|
||||
"type": "string|number",
|
||||
"source_node": "node1",
|
||||
"source_variable": "variable_name"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "node3",
|
||||
"type": "code",
|
||||
"title": "Code Node",
|
||||
"description": "Description of the code node",
|
||||
"code": "def main(input_var):\n import re\n match = re.search(r'Result[::](.*?)(?=[.]|$)', input_var)\n result = match.group(1).strip() if match else 'Not found'\n return {'output': result}",
|
||||
"variables": [
|
||||
{
|
||||
"name": "input_var",
|
||||
"type": "string|number",
|
||||
"source_node": "node2",
|
||||
"source_variable": "text"
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "output_var_name",
|
||||
"type": "string|number|object"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "node4",
|
||||
"type": "template",
|
||||
"title": "Template Node",
|
||||
"description": "Description of the template node",
|
||||
"template": "Template content using double curly braces, e.g.: The result is: {{ result }}",
|
||||
"variables": [
|
||||
{
|
||||
"name": "variable_name",
|
||||
"type": "string|number",
|
||||
"source_node": "node3",
|
||||
"source_variable": "output_var_name"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "node5",
|
||||
"type": "end",
|
||||
"title": "End Node",
|
||||
"description": "Description of the end node",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "output_variable_name",
|
||||
"type": "string|number",
|
||||
"source_node": "node4",
|
||||
"source_variable": "output"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"connections": [
|
||||
{"source": "node1", "target": "node2"},
|
||||
{"source": "node2", "target": "node3"},
|
||||
{"source": "node3", "target": "node4"},
|
||||
{"source": "node4", "target": "node5"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Ensure the workflow logic is coherent, node connections are correct, and variable passing is logical.
|
||||
Generate unique numeric IDs for each node, e.g., 1740019130520.
|
||||
Generate appropriate unique names for each variable across the workflow.
|
||||
Ensure all LLM nodes use provider "zhipuai" and model "glm-4-flash".
|
||||
|
||||
Note: LLM nodes usually return a long text; Code nodes typically require regex to extract relevant information.
|
||||
""" # noqa: E501
|
||||
)
|
||||
|
||||
return prompt_part1
|
||||
@@ -1,50 +0,0 @@
|
||||
"""
|
||||
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)
|
||||
@@ -1,134 +0,0 @@
|
||||
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)
|
||||
@@ -1,159 +0,0 @@
|
||||
"""
|
||||
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)
|
||||
@@ -7,7 +7,6 @@ from json import JSONDecodeError
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from sqlalchemy import or_
|
||||
|
||||
from constants import HIDDEN_VALUE
|
||||
from core.entities.model_entities import ModelStatus, ModelWithProviderEntity, SimpleModelProviderEntity
|
||||
@@ -180,37 +179,35 @@ class ProviderConfiguration(BaseModel):
|
||||
else [],
|
||||
)
|
||||
|
||||
def _get_custom_provider_credentials(self) -> Provider | None:
|
||||
"""
|
||||
Get custom provider credentials.
|
||||
"""
|
||||
# get provider
|
||||
model_provider_id = ModelProviderID(self.provider.provider)
|
||||
provider_names = [self.provider.provider]
|
||||
if model_provider_id.is_langgenius():
|
||||
provider_names.append(model_provider_id.provider_name)
|
||||
|
||||
provider_record = (
|
||||
db.session.query(Provider)
|
||||
.filter(
|
||||
Provider.tenant_id == self.tenant_id,
|
||||
Provider.provider_type == ProviderType.CUSTOM.value,
|
||||
Provider.provider_name.in_(provider_names),
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
return provider_record
|
||||
|
||||
def custom_credentials_validate(self, credentials: dict) -> tuple[Provider | None, dict]:
|
||||
"""
|
||||
Validate custom credentials.
|
||||
:param credentials: provider credentials
|
||||
:return:
|
||||
"""
|
||||
# get provider
|
||||
model_provider_id = ModelProviderID(self.provider.provider)
|
||||
if model_provider_id.is_langgenius():
|
||||
provider_record = (
|
||||
db.session.query(Provider)
|
||||
.filter(
|
||||
Provider.tenant_id == self.tenant_id,
|
||||
Provider.provider_type == ProviderType.CUSTOM.value,
|
||||
or_(
|
||||
Provider.provider_name == model_provider_id.provider_name,
|
||||
Provider.provider_name == self.provider.provider,
|
||||
),
|
||||
)
|
||||
.first()
|
||||
)
|
||||
else:
|
||||
provider_record = (
|
||||
db.session.query(Provider)
|
||||
.filter(
|
||||
Provider.tenant_id == self.tenant_id,
|
||||
Provider.provider_type == ProviderType.CUSTOM.value,
|
||||
Provider.provider_name == self.provider.provider,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
provider_record = self._get_custom_provider_credentials()
|
||||
|
||||
# Get provider credential secret variables
|
||||
provider_credential_secret_variables = self.extract_secret_variables(
|
||||
@@ -291,18 +288,7 @@ class ProviderConfiguration(BaseModel):
|
||||
:return:
|
||||
"""
|
||||
# get provider
|
||||
provider_record = (
|
||||
db.session.query(Provider)
|
||||
.filter(
|
||||
Provider.tenant_id == self.tenant_id,
|
||||
or_(
|
||||
Provider.provider_name == ModelProviderID(self.provider.provider).plugin_name,
|
||||
Provider.provider_name == self.provider.provider,
|
||||
),
|
||||
Provider.provider_type == ProviderType.CUSTOM.value,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
provider_record = self._get_custom_provider_credentials()
|
||||
|
||||
# delete provider
|
||||
if provider_record:
|
||||
@@ -349,6 +335,33 @@ class ProviderConfiguration(BaseModel):
|
||||
|
||||
return None
|
||||
|
||||
def _get_custom_model_credentials(
|
||||
self,
|
||||
model_type: ModelType,
|
||||
model: str,
|
||||
) -> ProviderModel | None:
|
||||
"""
|
||||
Get custom model credentials.
|
||||
"""
|
||||
# get provider model
|
||||
model_provider_id = ModelProviderID(self.provider.provider)
|
||||
provider_names = [self.provider.provider]
|
||||
if model_provider_id.is_langgenius():
|
||||
provider_names.append(model_provider_id.provider_name)
|
||||
|
||||
provider_model_record = (
|
||||
db.session.query(ProviderModel)
|
||||
.filter(
|
||||
ProviderModel.tenant_id == self.tenant_id,
|
||||
ProviderModel.provider_name.in_(provider_names),
|
||||
ProviderModel.model_name == model,
|
||||
ProviderModel.model_type == model_type.to_origin_model_type(),
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
return provider_model_record
|
||||
|
||||
def custom_model_credentials_validate(
|
||||
self, model_type: ModelType, model: str, credentials: dict
|
||||
) -> tuple[ProviderModel | None, dict]:
|
||||
@@ -361,16 +374,7 @@ class ProviderConfiguration(BaseModel):
|
||||
:return:
|
||||
"""
|
||||
# get provider model
|
||||
provider_model_record = (
|
||||
db.session.query(ProviderModel)
|
||||
.filter(
|
||||
ProviderModel.tenant_id == self.tenant_id,
|
||||
ProviderModel.provider_name == self.provider.provider,
|
||||
ProviderModel.model_name == model,
|
||||
ProviderModel.model_type == model_type.to_origin_model_type(),
|
||||
)
|
||||
.first()
|
||||
)
|
||||
provider_model_record = self._get_custom_model_credentials(model_type, model)
|
||||
|
||||
# Get provider credential secret variables
|
||||
provider_credential_secret_variables = self.extract_secret_variables(
|
||||
@@ -451,16 +455,7 @@ class ProviderConfiguration(BaseModel):
|
||||
:return:
|
||||
"""
|
||||
# get provider model
|
||||
provider_model_record = (
|
||||
db.session.query(ProviderModel)
|
||||
.filter(
|
||||
ProviderModel.tenant_id == self.tenant_id,
|
||||
ProviderModel.provider_name == self.provider.provider,
|
||||
ProviderModel.model_name == model,
|
||||
ProviderModel.model_type == model_type.to_origin_model_type(),
|
||||
)
|
||||
.first()
|
||||
)
|
||||
provider_model_record = self._get_custom_model_credentials(model_type, model)
|
||||
|
||||
# delete provider model
|
||||
if provider_model_record:
|
||||
@@ -475,6 +470,26 @@ class ProviderConfiguration(BaseModel):
|
||||
|
||||
provider_model_credentials_cache.delete()
|
||||
|
||||
def _get_provider_model_setting(self, model_type: ModelType, model: str) -> ProviderModelSetting | None:
|
||||
"""
|
||||
Get provider model setting.
|
||||
"""
|
||||
model_provider_id = ModelProviderID(self.provider.provider)
|
||||
provider_names = [self.provider.provider]
|
||||
if model_provider_id.is_langgenius():
|
||||
provider_names.append(model_provider_id.provider_name)
|
||||
|
||||
return (
|
||||
db.session.query(ProviderModelSetting)
|
||||
.filter(
|
||||
ProviderModelSetting.tenant_id == self.tenant_id,
|
||||
ProviderModelSetting.provider_name.in_(provider_names),
|
||||
ProviderModelSetting.model_type == model_type.to_origin_model_type(),
|
||||
ProviderModelSetting.model_name == model,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
def enable_model(self, model_type: ModelType, model: str) -> ProviderModelSetting:
|
||||
"""
|
||||
Enable model.
|
||||
@@ -482,16 +497,7 @@ class ProviderConfiguration(BaseModel):
|
||||
:param model: model name
|
||||
:return:
|
||||
"""
|
||||
model_setting = (
|
||||
db.session.query(ProviderModelSetting)
|
||||
.filter(
|
||||
ProviderModelSetting.tenant_id == self.tenant_id,
|
||||
ProviderModelSetting.provider_name == self.provider.provider,
|
||||
ProviderModelSetting.model_type == model_type.to_origin_model_type(),
|
||||
ProviderModelSetting.model_name == model,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
model_setting = self._get_provider_model_setting(model_type, model)
|
||||
|
||||
if model_setting:
|
||||
model_setting.enabled = True
|
||||
@@ -516,16 +522,7 @@ class ProviderConfiguration(BaseModel):
|
||||
:param model: model name
|
||||
:return:
|
||||
"""
|
||||
model_setting = (
|
||||
db.session.query(ProviderModelSetting)
|
||||
.filter(
|
||||
ProviderModelSetting.tenant_id == self.tenant_id,
|
||||
ProviderModelSetting.provider_name == self.provider.provider,
|
||||
ProviderModelSetting.model_type == model_type.to_origin_model_type(),
|
||||
ProviderModelSetting.model_name == model,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
model_setting = self._get_provider_model_setting(model_type, model)
|
||||
|
||||
if model_setting:
|
||||
model_setting.enabled = False
|
||||
@@ -550,13 +547,24 @@ class ProviderConfiguration(BaseModel):
|
||||
:param model: model name
|
||||
:return:
|
||||
"""
|
||||
return self._get_provider_model_setting(model_type, model)
|
||||
|
||||
def _get_load_balancing_config(self, model_type: ModelType, model: str) -> Optional[LoadBalancingModelConfig]:
|
||||
"""
|
||||
Get load balancing config.
|
||||
"""
|
||||
model_provider_id = ModelProviderID(self.provider.provider)
|
||||
provider_names = [self.provider.provider]
|
||||
if model_provider_id.is_langgenius():
|
||||
provider_names.append(model_provider_id.provider_name)
|
||||
|
||||
return (
|
||||
db.session.query(ProviderModelSetting)
|
||||
db.session.query(LoadBalancingModelConfig)
|
||||
.filter(
|
||||
ProviderModelSetting.tenant_id == self.tenant_id,
|
||||
ProviderModelSetting.provider_name == self.provider.provider,
|
||||
ProviderModelSetting.model_type == model_type.to_origin_model_type(),
|
||||
ProviderModelSetting.model_name == model,
|
||||
LoadBalancingModelConfig.tenant_id == self.tenant_id,
|
||||
LoadBalancingModelConfig.provider_name.in_(provider_names),
|
||||
LoadBalancingModelConfig.model_type == model_type.to_origin_model_type(),
|
||||
LoadBalancingModelConfig.model_name == model,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
@@ -568,11 +576,16 @@ class ProviderConfiguration(BaseModel):
|
||||
:param model: model name
|
||||
:return:
|
||||
"""
|
||||
model_provider_id = ModelProviderID(self.provider.provider)
|
||||
provider_names = [self.provider.provider]
|
||||
if model_provider_id.is_langgenius():
|
||||
provider_names.append(model_provider_id.provider_name)
|
||||
|
||||
load_balancing_config_count = (
|
||||
db.session.query(LoadBalancingModelConfig)
|
||||
.filter(
|
||||
LoadBalancingModelConfig.tenant_id == self.tenant_id,
|
||||
LoadBalancingModelConfig.provider_name == self.provider.provider,
|
||||
LoadBalancingModelConfig.provider_name.in_(provider_names),
|
||||
LoadBalancingModelConfig.model_type == model_type.to_origin_model_type(),
|
||||
LoadBalancingModelConfig.model_name == model,
|
||||
)
|
||||
@@ -582,16 +595,7 @@ class ProviderConfiguration(BaseModel):
|
||||
if load_balancing_config_count <= 1:
|
||||
raise ValueError("Model load balancing configuration must be more than 1.")
|
||||
|
||||
model_setting = (
|
||||
db.session.query(ProviderModelSetting)
|
||||
.filter(
|
||||
ProviderModelSetting.tenant_id == self.tenant_id,
|
||||
ProviderModelSetting.provider_name == self.provider.provider,
|
||||
ProviderModelSetting.model_type == model_type.to_origin_model_type(),
|
||||
ProviderModelSetting.model_name == model,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
model_setting = self._get_provider_model_setting(model_type, model)
|
||||
|
||||
if model_setting:
|
||||
model_setting.load_balancing_enabled = True
|
||||
@@ -616,11 +620,16 @@ class ProviderConfiguration(BaseModel):
|
||||
:param model: model name
|
||||
:return:
|
||||
"""
|
||||
model_provider_id = ModelProviderID(self.provider.provider)
|
||||
provider_names = [self.provider.provider]
|
||||
if model_provider_id.is_langgenius():
|
||||
provider_names.append(model_provider_id.provider_name)
|
||||
|
||||
model_setting = (
|
||||
db.session.query(ProviderModelSetting)
|
||||
.filter(
|
||||
ProviderModelSetting.tenant_id == self.tenant_id,
|
||||
ProviderModelSetting.provider_name == self.provider.provider,
|
||||
ProviderModelSetting.provider_name.in_(provider_names),
|
||||
ProviderModelSetting.model_type == model_type.to_origin_model_type(),
|
||||
ProviderModelSetting.model_name == model,
|
||||
)
|
||||
@@ -677,11 +686,16 @@ class ProviderConfiguration(BaseModel):
|
||||
return
|
||||
|
||||
# get preferred provider
|
||||
model_provider_id = ModelProviderID(self.provider.provider)
|
||||
provider_names = [self.provider.provider]
|
||||
if model_provider_id.is_langgenius():
|
||||
provider_names.append(model_provider_id.provider_name)
|
||||
|
||||
preferred_model_provider = (
|
||||
db.session.query(TenantPreferredModelProvider)
|
||||
.filter(
|
||||
TenantPreferredModelProvider.tenant_id == self.tenant_id,
|
||||
TenantPreferredModelProvider.provider_name == self.provider.provider,
|
||||
TenantPreferredModelProvider.provider_name.in_(provider_names),
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
@@ -63,7 +63,9 @@ class File(BaseModel):
|
||||
extension: Optional[str] = None,
|
||||
mime_type: Optional[str] = None,
|
||||
size: int = -1,
|
||||
storage_key: str,
|
||||
storage_key: Optional[str] = None,
|
||||
dify_model_identity: Optional[str] = FILE_MODEL_IDENTITY,
|
||||
url: Optional[str] = None,
|
||||
):
|
||||
super().__init__(
|
||||
id=id,
|
||||
@@ -76,8 +78,10 @@ class File(BaseModel):
|
||||
extension=extension,
|
||||
mime_type=mime_type,
|
||||
size=size,
|
||||
dify_model_identity=dify_model_identity,
|
||||
url=url,
|
||||
)
|
||||
self._storage_key = storage_key
|
||||
self._storage_key = str(storage_key)
|
||||
|
||||
def to_dict(self) -> Mapping[str, str | int | None]:
|
||||
data = self.model_dump(mode="json")
|
||||
|
||||
@@ -11,6 +11,19 @@ from configs import dify_config
|
||||
|
||||
SSRF_DEFAULT_MAX_RETRIES = dify_config.SSRF_DEFAULT_MAX_RETRIES
|
||||
|
||||
HTTP_REQUEST_NODE_SSL_VERIFY = True # Default value for HTTP_REQUEST_NODE_SSL_VERIFY is True
|
||||
try:
|
||||
HTTP_REQUEST_NODE_SSL_VERIFY = dify_config.HTTP_REQUEST_NODE_SSL_VERIFY
|
||||
http_request_node_ssl_verify_lower = str(HTTP_REQUEST_NODE_SSL_VERIFY).lower()
|
||||
if http_request_node_ssl_verify_lower == "true":
|
||||
HTTP_REQUEST_NODE_SSL_VERIFY = True
|
||||
elif http_request_node_ssl_verify_lower == "false":
|
||||
HTTP_REQUEST_NODE_SSL_VERIFY = False
|
||||
else:
|
||||
raise ValueError("Invalid value. HTTP_REQUEST_NODE_SSL_VERIFY should be 'True' or 'False'")
|
||||
except NameError:
|
||||
HTTP_REQUEST_NODE_SSL_VERIFY = True
|
||||
|
||||
BACKOFF_FACTOR = 0.5
|
||||
STATUS_FORCELIST = [429, 500, 502, 503, 504]
|
||||
|
||||
@@ -39,17 +52,17 @@ def make_request(method, url, max_retries=SSRF_DEFAULT_MAX_RETRIES, **kwargs):
|
||||
while retries <= max_retries:
|
||||
try:
|
||||
if dify_config.SSRF_PROXY_ALL_URL:
|
||||
with httpx.Client(proxy=dify_config.SSRF_PROXY_ALL_URL) as client:
|
||||
with httpx.Client(proxy=dify_config.SSRF_PROXY_ALL_URL, verify=HTTP_REQUEST_NODE_SSL_VERIFY) as client:
|
||||
response = client.request(method=method, url=url, **kwargs)
|
||||
elif dify_config.SSRF_PROXY_HTTP_URL and dify_config.SSRF_PROXY_HTTPS_URL:
|
||||
proxy_mounts = {
|
||||
"http://": httpx.HTTPTransport(proxy=dify_config.SSRF_PROXY_HTTP_URL),
|
||||
"https://": httpx.HTTPTransport(proxy=dify_config.SSRF_PROXY_HTTPS_URL),
|
||||
}
|
||||
with httpx.Client(mounts=proxy_mounts) as client:
|
||||
with httpx.Client(mounts=proxy_mounts, verify=HTTP_REQUEST_NODE_SSL_VERIFY) as client:
|
||||
response = client.request(method=method, url=url, **kwargs)
|
||||
else:
|
||||
with httpx.Client() as client:
|
||||
with httpx.Client(verify=HTTP_REQUEST_NODE_SSL_VERIFY) as client:
|
||||
response = client.request(method=method, url=url, **kwargs)
|
||||
|
||||
if response.status_code not in STATUS_FORCELIST:
|
||||
|
||||
@@ -493,7 +493,7 @@ If inputting a combination of text and images, the images need to be constructed
|
||||
The base class for all Role message bodies, used only for parameter declaration and cannot be initialized.
|
||||
|
||||
```python
|
||||
class PromptMessage(ABC, BaseModel):
|
||||
class PromptMessage(BaseModel):
|
||||
"""
|
||||
Model class for prompt message.
|
||||
"""
|
||||
|
||||
@@ -533,7 +533,7 @@ class ImagePromptMessageContent(PromptMessageContent):
|
||||
所有 Role 消息体的基类,仅作为参数声明用,不可初始化。
|
||||
|
||||
```python
|
||||
class PromptMessage(ABC, BaseModel):
|
||||
class PromptMessage(BaseModel):
|
||||
"""
|
||||
Model class for prompt message.
|
||||
"""
|
||||
|
||||
@@ -31,11 +31,9 @@ __all__ = [
|
||||
"ModelPropertyKey",
|
||||
"MultiModalPromptMessageContent",
|
||||
"PromptMessage",
|
||||
"PromptMessage",
|
||||
"PromptMessageContent",
|
||||
"PromptMessageContentType",
|
||||
"PromptMessageRole",
|
||||
"PromptMessageRole",
|
||||
"PromptMessageTool",
|
||||
"SystemPromptMessage",
|
||||
"TextPromptMessageContent",
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
from abc import ABC
|
||||
from collections.abc import Sequence
|
||||
from enum import Enum, StrEnum
|
||||
from typing import Optional
|
||||
@@ -119,7 +118,7 @@ class DocumentPromptMessageContent(MultiModalPromptMessageContent):
|
||||
type: PromptMessageContentType = PromptMessageContentType.DOCUMENT
|
||||
|
||||
|
||||
class PromptMessage(ABC, BaseModel):
|
||||
class PromptMessage(BaseModel):
|
||||
"""
|
||||
Model class for prompt message.
|
||||
"""
|
||||
|
||||
@@ -80,7 +80,7 @@ class AIModel(BaseModel):
|
||||
)
|
||||
)
|
||||
elif isinstance(invoke_error, InvokeError):
|
||||
return invoke_error(description=f"[{self.provider_name}] {invoke_error.description}, {str(error)}")
|
||||
return InvokeError(description=f"[{self.provider_name}] {invoke_error.description}, {str(error)}")
|
||||
else:
|
||||
return error
|
||||
|
||||
|
||||
@@ -214,6 +214,8 @@ class OpsTraceManager:
|
||||
provider_config_map[tracing_provider]["trace_instance"],
|
||||
provider_config_map[tracing_provider]["config_class"],
|
||||
)
|
||||
if not decrypt_trace_config:
|
||||
return None
|
||||
tracing_instance = trace_instance(config_class(**decrypt_trace_config))
|
||||
return tracing_instance
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ from binascii import hexlify, unhexlify
|
||||
from collections.abc import Generator
|
||||
|
||||
from core.model_manager import ModelManager
|
||||
from core.model_runtime.entities.llm_entities import LLMResult, LLMResultChunk
|
||||
from core.model_runtime.entities.llm_entities import LLMResult, LLMResultChunk, LLMResultChunkDelta
|
||||
from core.model_runtime.entities.message_entities import (
|
||||
PromptMessage,
|
||||
SystemPromptMessage,
|
||||
@@ -46,7 +46,7 @@ class PluginModelBackwardsInvocation(BaseBackwardsInvocation):
|
||||
model_parameters=payload.completion_params,
|
||||
tools=payload.tools,
|
||||
stop=payload.stop,
|
||||
stream=payload.stream or True,
|
||||
stream=True if payload.stream is None else payload.stream,
|
||||
user=user_id,
|
||||
)
|
||||
|
||||
@@ -64,7 +64,21 @@ class PluginModelBackwardsInvocation(BaseBackwardsInvocation):
|
||||
else:
|
||||
if response.usage:
|
||||
LLMNode.deduct_llm_quota(tenant_id=tenant.id, model_instance=model_instance, usage=response.usage)
|
||||
return response
|
||||
|
||||
def handle_non_streaming(response: LLMResult) -> Generator[LLMResultChunk, None, None]:
|
||||
yield LLMResultChunk(
|
||||
model=response.model,
|
||||
prompt_messages=response.prompt_messages,
|
||||
system_fingerprint=response.system_fingerprint,
|
||||
delta=LLMResultChunkDelta(
|
||||
index=0,
|
||||
message=response.message,
|
||||
usage=response.usage,
|
||||
finish_reason="",
|
||||
),
|
||||
)
|
||||
|
||||
return handle_non_streaming(response)
|
||||
|
||||
@classmethod
|
||||
def invoke_text_embedding(cls, user_id: str, tenant: Tenant, payload: RequestInvokeTextEmbedding):
|
||||
|
||||
@@ -147,7 +147,7 @@ def init_frontend_parameter(rule: PluginParameter, type: enum.StrEnum, value: An
|
||||
init frontend parameter by rule
|
||||
"""
|
||||
parameter_value = value
|
||||
if not parameter_value and parameter_value != 0:
|
||||
if not parameter_value and parameter_value != 0 and type != PluginParameterType.TOOLS_SELECTOR:
|
||||
# get default value
|
||||
parameter_value = rule.default
|
||||
if not parameter_value and rule.required:
|
||||
|
||||
@@ -46,6 +46,7 @@ class AdvancedPromptTransform(PromptTransform):
|
||||
memory_config: Optional[MemoryConfig],
|
||||
memory: Optional[TokenBufferMemory],
|
||||
model_config: ModelConfigWithCredentialsEntity,
|
||||
image_detail_config: Optional[ImagePromptMessageContent.DETAIL] = None,
|
||||
) -> list[PromptMessage]:
|
||||
prompt_messages = []
|
||||
|
||||
@@ -59,6 +60,7 @@ class AdvancedPromptTransform(PromptTransform):
|
||||
memory_config=memory_config,
|
||||
memory=memory,
|
||||
model_config=model_config,
|
||||
image_detail_config=image_detail_config,
|
||||
)
|
||||
elif isinstance(prompt_template, list) and all(isinstance(item, ChatModelMessage) for item in prompt_template):
|
||||
prompt_messages = self._get_chat_model_prompt_messages(
|
||||
@@ -70,6 +72,7 @@ class AdvancedPromptTransform(PromptTransform):
|
||||
memory_config=memory_config,
|
||||
memory=memory,
|
||||
model_config=model_config,
|
||||
image_detail_config=image_detail_config,
|
||||
)
|
||||
|
||||
return prompt_messages
|
||||
@@ -84,6 +87,7 @@ class AdvancedPromptTransform(PromptTransform):
|
||||
memory_config: Optional[MemoryConfig],
|
||||
memory: Optional[TokenBufferMemory],
|
||||
model_config: ModelConfigWithCredentialsEntity,
|
||||
image_detail_config: Optional[ImagePromptMessageContent.DETAIL] = None,
|
||||
) -> list[PromptMessage]:
|
||||
"""
|
||||
Get completion model prompt messages.
|
||||
@@ -124,7 +128,9 @@ class AdvancedPromptTransform(PromptTransform):
|
||||
prompt_message_contents: list[PromptMessageContent] = []
|
||||
prompt_message_contents.append(TextPromptMessageContent(data=prompt))
|
||||
for file in files:
|
||||
prompt_message_contents.append(file_manager.to_prompt_message_content(file))
|
||||
prompt_message_contents.append(
|
||||
file_manager.to_prompt_message_content(file, image_detail_config=image_detail_config)
|
||||
)
|
||||
|
||||
prompt_messages.append(UserPromptMessage(content=prompt_message_contents))
|
||||
else:
|
||||
@@ -142,6 +148,7 @@ class AdvancedPromptTransform(PromptTransform):
|
||||
memory_config: Optional[MemoryConfig],
|
||||
memory: Optional[TokenBufferMemory],
|
||||
model_config: ModelConfigWithCredentialsEntity,
|
||||
image_detail_config: Optional[ImagePromptMessageContent.DETAIL] = None,
|
||||
) -> list[PromptMessage]:
|
||||
"""
|
||||
Get chat model prompt messages.
|
||||
@@ -197,7 +204,9 @@ class AdvancedPromptTransform(PromptTransform):
|
||||
prompt_message_contents: list[PromptMessageContent] = []
|
||||
prompt_message_contents.append(TextPromptMessageContent(data=query))
|
||||
for file in files:
|
||||
prompt_message_contents.append(file_manager.to_prompt_message_content(file))
|
||||
prompt_message_contents.append(
|
||||
file_manager.to_prompt_message_content(file, image_detail_config=image_detail_config)
|
||||
)
|
||||
prompt_messages.append(UserPromptMessage(content=prompt_message_contents))
|
||||
else:
|
||||
prompt_messages.append(UserPromptMessage(content=query))
|
||||
@@ -209,19 +218,25 @@ class AdvancedPromptTransform(PromptTransform):
|
||||
# get last user message content and add files
|
||||
prompt_message_contents = [TextPromptMessageContent(data=cast(str, last_message.content))]
|
||||
for file in files:
|
||||
prompt_message_contents.append(file_manager.to_prompt_message_content(file))
|
||||
prompt_message_contents.append(
|
||||
file_manager.to_prompt_message_content(file, image_detail_config=image_detail_config)
|
||||
)
|
||||
|
||||
last_message.content = prompt_message_contents
|
||||
else:
|
||||
prompt_message_contents = [TextPromptMessageContent(data="")] # not for query
|
||||
for file in files:
|
||||
prompt_message_contents.append(file_manager.to_prompt_message_content(file))
|
||||
prompt_message_contents.append(
|
||||
file_manager.to_prompt_message_content(file, image_detail_config=image_detail_config)
|
||||
)
|
||||
|
||||
prompt_messages.append(UserPromptMessage(content=prompt_message_contents))
|
||||
else:
|
||||
prompt_message_contents = [TextPromptMessageContent(data=query)]
|
||||
for file in files:
|
||||
prompt_message_contents.append(file_manager.to_prompt_message_content(file))
|
||||
prompt_message_contents.append(
|
||||
file_manager.to_prompt_message_content(file, image_detail_config=image_detail_config)
|
||||
)
|
||||
|
||||
prompt_messages.append(UserPromptMessage(content=prompt_message_contents))
|
||||
elif query:
|
||||
|
||||
@@ -9,6 +9,7 @@ from core.app.entities.app_invoke_entities import ModelConfigWithCredentialsEnti
|
||||
from core.file import file_manager
|
||||
from core.memory.token_buffer_memory import TokenBufferMemory
|
||||
from core.model_runtime.entities.message_entities import (
|
||||
ImagePromptMessageContent,
|
||||
PromptMessage,
|
||||
PromptMessageContent,
|
||||
SystemPromptMessage,
|
||||
@@ -60,6 +61,7 @@ class SimplePromptTransform(PromptTransform):
|
||||
context: Optional[str],
|
||||
memory: Optional[TokenBufferMemory],
|
||||
model_config: ModelConfigWithCredentialsEntity,
|
||||
image_detail_config: Optional[ImagePromptMessageContent.DETAIL] = None,
|
||||
) -> tuple[list[PromptMessage], Optional[list[str]]]:
|
||||
inputs = {key: str(value) for key, value in inputs.items()}
|
||||
|
||||
@@ -74,6 +76,7 @@ class SimplePromptTransform(PromptTransform):
|
||||
context=context,
|
||||
memory=memory,
|
||||
model_config=model_config,
|
||||
image_detail_config=image_detail_config,
|
||||
)
|
||||
else:
|
||||
prompt_messages, stops = self._get_completion_model_prompt_messages(
|
||||
@@ -85,6 +88,7 @@ class SimplePromptTransform(PromptTransform):
|
||||
context=context,
|
||||
memory=memory,
|
||||
model_config=model_config,
|
||||
image_detail_config=image_detail_config,
|
||||
)
|
||||
|
||||
return prompt_messages, stops
|
||||
@@ -175,6 +179,7 @@ class SimplePromptTransform(PromptTransform):
|
||||
files: Sequence["File"],
|
||||
memory: Optional[TokenBufferMemory],
|
||||
model_config: ModelConfigWithCredentialsEntity,
|
||||
image_detail_config: Optional[ImagePromptMessageContent.DETAIL] = None,
|
||||
) -> tuple[list[PromptMessage], Optional[list[str]]]:
|
||||
prompt_messages: list[PromptMessage] = []
|
||||
|
||||
@@ -204,9 +209,9 @@ class SimplePromptTransform(PromptTransform):
|
||||
)
|
||||
|
||||
if query:
|
||||
prompt_messages.append(self.get_last_user_message(query, files))
|
||||
prompt_messages.append(self.get_last_user_message(query, files, image_detail_config))
|
||||
else:
|
||||
prompt_messages.append(self.get_last_user_message(prompt, files))
|
||||
prompt_messages.append(self.get_last_user_message(prompt, files, image_detail_config))
|
||||
|
||||
return prompt_messages, None
|
||||
|
||||
@@ -220,6 +225,7 @@ class SimplePromptTransform(PromptTransform):
|
||||
files: Sequence["File"],
|
||||
memory: Optional[TokenBufferMemory],
|
||||
model_config: ModelConfigWithCredentialsEntity,
|
||||
image_detail_config: Optional[ImagePromptMessageContent.DETAIL] = None,
|
||||
) -> tuple[list[PromptMessage], Optional[list[str]]]:
|
||||
# get prompt
|
||||
prompt, prompt_rules = self.get_prompt_str_and_rules(
|
||||
@@ -262,14 +268,21 @@ class SimplePromptTransform(PromptTransform):
|
||||
if stops is not None and len(stops) == 0:
|
||||
stops = None
|
||||
|
||||
return [self.get_last_user_message(prompt, files)], stops
|
||||
return [self.get_last_user_message(prompt, files, image_detail_config)], stops
|
||||
|
||||
def get_last_user_message(self, prompt: str, files: Sequence["File"]) -> UserPromptMessage:
|
||||
def get_last_user_message(
|
||||
self,
|
||||
prompt: str,
|
||||
files: Sequence["File"],
|
||||
image_detail_config: Optional[ImagePromptMessageContent.DETAIL] = None,
|
||||
) -> UserPromptMessage:
|
||||
if files:
|
||||
prompt_message_contents: list[PromptMessageContent] = []
|
||||
prompt_message_contents.append(TextPromptMessageContent(data=prompt))
|
||||
for file in files:
|
||||
prompt_message_contents.append(file_manager.to_prompt_message_content(file))
|
||||
prompt_message_contents.append(
|
||||
file_manager.to_prompt_message_content(file, image_detail_config=image_detail_config)
|
||||
)
|
||||
|
||||
prompt_message = UserPromptMessage(content=prompt_message_contents)
|
||||
else:
|
||||
|
||||
@@ -149,6 +149,11 @@ class ProviderManager:
|
||||
provider_name = provider_entity.provider
|
||||
provider_records = provider_name_to_provider_records_dict.get(provider_entity.provider, [])
|
||||
provider_model_records = provider_name_to_provider_model_records_dict.get(provider_entity.provider, [])
|
||||
provider_id_entity = ModelProviderID(provider_name)
|
||||
if provider_id_entity.is_langgenius():
|
||||
provider_model_records.extend(
|
||||
provider_name_to_provider_model_records_dict.get(provider_id_entity.provider_name, [])
|
||||
)
|
||||
|
||||
# Convert to custom configuration
|
||||
custom_configuration = self._to_custom_configuration(
|
||||
@@ -190,6 +195,20 @@ class ProviderManager:
|
||||
provider_name
|
||||
)
|
||||
|
||||
provider_id_entity = ModelProviderID(provider_name)
|
||||
|
||||
if provider_id_entity.is_langgenius():
|
||||
if provider_model_settings is not None:
|
||||
provider_model_settings.extend(
|
||||
provider_name_to_provider_model_settings_dict.get(provider_id_entity.provider_name, [])
|
||||
)
|
||||
if provider_load_balancing_configs is not None:
|
||||
provider_load_balancing_configs.extend(
|
||||
provider_name_to_provider_load_balancing_model_configs_dict.get(
|
||||
provider_id_entity.provider_name, []
|
||||
)
|
||||
)
|
||||
|
||||
# Convert to model settings
|
||||
model_settings = self._to_model_settings(
|
||||
provider_entity=provider_entity,
|
||||
@@ -207,7 +226,7 @@ class ProviderManager:
|
||||
model_settings=model_settings,
|
||||
)
|
||||
|
||||
provider_configurations[str(ModelProviderID(provider_name))] = provider_configuration
|
||||
provider_configurations[str(provider_id_entity)] = provider_configuration
|
||||
|
||||
# Return the encapsulated object
|
||||
return provider_configurations
|
||||
|
||||
@@ -88,16 +88,17 @@ class Jieba(BaseKeyword):
|
||||
keyword_table = self._get_dataset_keyword_table()
|
||||
|
||||
k = kwargs.get("top_k", 4)
|
||||
|
||||
document_ids_filter = kwargs.get("document_ids_filter")
|
||||
sorted_chunk_indices = self._retrieve_ids_by_query(keyword_table or {}, query, k)
|
||||
|
||||
documents = []
|
||||
for chunk_index in sorted_chunk_indices:
|
||||
segment = (
|
||||
db.session.query(DocumentSegment)
|
||||
.filter(DocumentSegment.dataset_id == self.dataset.id, DocumentSegment.index_node_id == chunk_index)
|
||||
.first()
|
||||
segment_query = db.session.query(DocumentSegment).filter(
|
||||
DocumentSegment.dataset_id == self.dataset.id, DocumentSegment.index_node_id == chunk_index
|
||||
)
|
||||
if document_ids_filter:
|
||||
segment_query = segment_query.filter(DocumentSegment.document_id.in_(document_ids_filter))
|
||||
segment = segment_query.first()
|
||||
|
||||
if segment:
|
||||
documents.append(
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import concurrent.futures
|
||||
import json
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from typing import Optional
|
||||
|
||||
@@ -42,6 +41,7 @@ class RetrievalService:
|
||||
reranking_model: Optional[dict] = None,
|
||||
reranking_mode: str = "reranking_model",
|
||||
weights: Optional[dict] = None,
|
||||
document_ids_filter: Optional[list[str]] = None,
|
||||
):
|
||||
if not query:
|
||||
return []
|
||||
@@ -65,6 +65,7 @@ class RetrievalService:
|
||||
top_k=top_k,
|
||||
all_documents=all_documents,
|
||||
exceptions=exceptions,
|
||||
document_ids_filter=document_ids_filter,
|
||||
)
|
||||
)
|
||||
if RetrievalMethod.is_support_semantic_search(retrieval_method):
|
||||
@@ -80,6 +81,7 @@ class RetrievalService:
|
||||
all_documents=all_documents,
|
||||
retrieval_method=retrieval_method,
|
||||
exceptions=exceptions,
|
||||
document_ids_filter=document_ids_filter,
|
||||
)
|
||||
)
|
||||
if RetrievalMethod.is_support_fulltext_search(retrieval_method):
|
||||
@@ -131,7 +133,14 @@ class RetrievalService:
|
||||
|
||||
@classmethod
|
||||
def keyword_search(
|
||||
cls, flask_app: Flask, dataset_id: str, query: str, top_k: int, all_documents: list, exceptions: list
|
||||
cls,
|
||||
flask_app: Flask,
|
||||
dataset_id: str,
|
||||
query: str,
|
||||
top_k: int,
|
||||
all_documents: list,
|
||||
exceptions: list,
|
||||
document_ids_filter: Optional[list[str]] = None,
|
||||
):
|
||||
with flask_app.app_context():
|
||||
try:
|
||||
@@ -140,7 +149,10 @@ class RetrievalService:
|
||||
raise ValueError("dataset not found")
|
||||
|
||||
keyword = Keyword(dataset=dataset)
|
||||
documents = keyword.search(cls.escape_query_for_search(query), top_k=top_k)
|
||||
|
||||
documents = keyword.search(
|
||||
cls.escape_query_for_search(query), top_k=top_k, document_ids_filter=document_ids_filter
|
||||
)
|
||||
all_documents.extend(documents)
|
||||
except Exception as e:
|
||||
exceptions.append(str(e))
|
||||
@@ -157,6 +169,7 @@ class RetrievalService:
|
||||
all_documents: list,
|
||||
retrieval_method: str,
|
||||
exceptions: list,
|
||||
document_ids_filter: Optional[list[str]] = None,
|
||||
):
|
||||
with flask_app.app_context():
|
||||
try:
|
||||
@@ -171,6 +184,7 @@ class RetrievalService:
|
||||
top_k=top_k,
|
||||
score_threshold=score_threshold,
|
||||
filter={"group_id": [dataset.id]},
|
||||
document_ids_filter=document_ids_filter,
|
||||
)
|
||||
|
||||
if documents:
|
||||
@@ -243,7 +257,7 @@ class RetrievalService:
|
||||
|
||||
@staticmethod
|
||||
def escape_query_for_search(query: str) -> str:
|
||||
return json.dumps(query).strip('"')
|
||||
return query.replace('"', '\\"')
|
||||
|
||||
@classmethod
|
||||
def format_retrieval_documents(cls, documents: list[Document]) -> list[RetrievalSegments]:
|
||||
@@ -277,6 +291,8 @@ class RetrievalService:
|
||||
continue
|
||||
|
||||
dataset_document = dataset_documents[document_id]
|
||||
if not dataset_document:
|
||||
continue
|
||||
|
||||
if dataset_document.doc_form == IndexType.PARENT_CHILD_INDEX:
|
||||
# Handle parent-child documents
|
||||
|
||||
@@ -53,7 +53,7 @@ class AnalyticdbVector(BaseVector):
|
||||
self.analyticdb_vector.delete_by_metadata_field(key, value)
|
||||
|
||||
def search_by_vector(self, query_vector: list[float], **kwargs: Any) -> list[Document]:
|
||||
return self.analyticdb_vector.search_by_vector(query_vector)
|
||||
return self.analyticdb_vector.search_by_vector(query_vector, **kwargs)
|
||||
|
||||
def search_by_full_text(self, query: str, **kwargs: Any) -> list[Document]:
|
||||
return self.analyticdb_vector.search_by_full_text(query, **kwargs)
|
||||
|
||||
@@ -194,6 +194,13 @@ class AnalyticdbVectorBySql:
|
||||
|
||||
def search_by_vector(self, query_vector: list[float], **kwargs: Any) -> list[Document]:
|
||||
top_k = kwargs.get("top_k", 4)
|
||||
if not isinstance(top_k, int) or top_k <= 0:
|
||||
raise ValueError("top_k must be a positive integer")
|
||||
document_ids_filter = kwargs.get("document_ids_filter")
|
||||
where_clause = "WHERE 1=1"
|
||||
if document_ids_filter:
|
||||
document_ids = ", ".join(f"'{id}'" for id in document_ids_filter)
|
||||
where_clause += f"AND metadata_->>'document_id' IN ({document_ids})"
|
||||
score_threshold = float(kwargs.get("score_threshold") or 0.0)
|
||||
with self._get_cursor() as cur:
|
||||
query_vector_str = json.dumps(query_vector)
|
||||
@@ -202,7 +209,7 @@ class AnalyticdbVectorBySql:
|
||||
f"SELECT t.id AS id, t.vector AS vector, (1.0 - t.score) AS score, "
|
||||
f"t.page_content as page_content, t.metadata_ AS metadata_ "
|
||||
f"FROM (SELECT id, vector, page_content, metadata_, vector <=> %s AS score "
|
||||
f"FROM {self.table_name} ORDER BY score LIMIT {top_k} ) t",
|
||||
f"FROM {self.table_name} {where_clause} ORDER BY score LIMIT {top_k} ) t",
|
||||
(query_vector_str,),
|
||||
)
|
||||
documents = []
|
||||
@@ -220,12 +227,19 @@ class AnalyticdbVectorBySql:
|
||||
|
||||
def search_by_full_text(self, query: str, **kwargs: Any) -> list[Document]:
|
||||
top_k = kwargs.get("top_k", 4)
|
||||
if not isinstance(top_k, int) or top_k <= 0:
|
||||
raise ValueError("top_k must be a positive integer")
|
||||
document_ids_filter = kwargs.get("document_ids_filter")
|
||||
where_clause = ""
|
||||
if document_ids_filter:
|
||||
document_ids = ", ".join(f"'{id}'" for id in document_ids_filter)
|
||||
where_clause += f"AND metadata_->>'document_id' IN ({document_ids})"
|
||||
with self._get_cursor() as cur:
|
||||
cur.execute(
|
||||
f"""SELECT id, vector, page_content, metadata_,
|
||||
ts_rank(to_tsvector, to_tsquery_from_text(%s, 'zh_cn'), 32) AS score
|
||||
FROM {self.table_name}
|
||||
WHERE to_tsvector@@to_tsquery_from_text(%s, 'zh_cn')
|
||||
WHERE to_tsvector@@to_tsquery_from_text(%s, 'zh_cn') {where_clause}
|
||||
ORDER BY score DESC
|
||||
LIMIT {top_k}""",
|
||||
(f"'{query}'", f"'{query}'"),
|
||||
|
||||
@@ -123,11 +123,21 @@ class BaiduVector(BaseVector):
|
||||
|
||||
def search_by_vector(self, query_vector: list[float], **kwargs: Any) -> list[Document]:
|
||||
query_vector = [float(val) if isinstance(val, np.float64) else val for val in query_vector]
|
||||
anns = AnnSearch(
|
||||
vector_field=self.field_vector,
|
||||
vector_floats=query_vector,
|
||||
params=HNSWSearchParams(ef=kwargs.get("ef", 10), limit=kwargs.get("top_k", 4)),
|
||||
)
|
||||
document_ids_filter = kwargs.get("document_ids_filter")
|
||||
if document_ids_filter:
|
||||
document_ids = ", ".join(f"'{id}'" for id in document_ids_filter)
|
||||
anns = AnnSearch(
|
||||
vector_field=self.field_vector,
|
||||
vector_floats=query_vector,
|
||||
params=HNSWSearchParams(ef=kwargs.get("ef", 10), limit=kwargs.get("top_k", 4)),
|
||||
filter=f"document_id IN ({document_ids})",
|
||||
)
|
||||
else:
|
||||
anns = AnnSearch(
|
||||
vector_field=self.field_vector,
|
||||
vector_floats=query_vector,
|
||||
params=HNSWSearchParams(ef=kwargs.get("ef", 10), limit=kwargs.get("top_k", 4)),
|
||||
)
|
||||
res = self._db.table(self._collection_name).search(
|
||||
anns=anns,
|
||||
projections=[self.field_id, self.field_text, self.field_metadata],
|
||||
|
||||
@@ -95,7 +95,15 @@ class ChromaVector(BaseVector):
|
||||
|
||||
def search_by_vector(self, query_vector: list[float], **kwargs: Any) -> list[Document]:
|
||||
collection = self._client.get_or_create_collection(self._collection_name)
|
||||
results: QueryResult = collection.query(query_embeddings=query_vector, n_results=kwargs.get("top_k", 4))
|
||||
document_ids_filter = kwargs.get("document_ids_filter")
|
||||
if document_ids_filter:
|
||||
results: QueryResult = collection.query(
|
||||
query_embeddings=query_vector,
|
||||
n_results=kwargs.get("top_k", 4),
|
||||
where={"document_id": {"$in": document_ids_filter}}, # type: ignore
|
||||
)
|
||||
else:
|
||||
results: QueryResult = collection.query(query_embeddings=query_vector, n_results=kwargs.get("top_k", 4)) # type: ignore
|
||||
score_threshold = float(kwargs.get("score_threshold") or 0.0)
|
||||
|
||||
# Check if results contain data
|
||||
|
||||
@@ -117,6 +117,9 @@ class ElasticSearchVector(BaseVector):
|
||||
top_k = kwargs.get("top_k", 4)
|
||||
num_candidates = math.ceil(top_k * 1.5)
|
||||
knn = {"field": Field.VECTOR.value, "query_vector": query_vector, "k": top_k, "num_candidates": num_candidates}
|
||||
document_ids_filter = kwargs.get("document_ids_filter")
|
||||
if document_ids_filter:
|
||||
knn["filter"] = {"terms": {"metadata.document_id": document_ids_filter}}
|
||||
|
||||
results = self._client.search(index=self._collection_name, knn=knn, size=top_k)
|
||||
|
||||
@@ -145,6 +148,9 @@ class ElasticSearchVector(BaseVector):
|
||||
|
||||
def search_by_full_text(self, query: str, **kwargs: Any) -> list[Document]:
|
||||
query_str = {"match": {Field.CONTENT_KEY.value: query}}
|
||||
document_ids_filter = kwargs.get("document_ids_filter")
|
||||
if document_ids_filter:
|
||||
query_str["filter"] = {"terms": {"metadata.document_id": document_ids_filter}} # type: ignore
|
||||
results = self._client.search(index=self._collection_name, query=query_str, size=kwargs.get("top_k", 4))
|
||||
docs = []
|
||||
for hit in results["hits"]["hits"]:
|
||||
|
||||
@@ -168,7 +168,12 @@ class LindormVectorStore(BaseVector):
|
||||
raise ValueError("All elements in query_vector should be floats")
|
||||
|
||||
top_k = kwargs.get("top_k", 10)
|
||||
query = default_vector_search_query(query_vector=query_vector, k=top_k, **kwargs)
|
||||
document_ids_filter = kwargs.get("document_ids_filter")
|
||||
filters = []
|
||||
if document_ids_filter:
|
||||
filters.append({"terms": {"metadata.document_id": document_ids_filter}})
|
||||
query = default_vector_search_query(query_vector=query_vector, k=top_k, filters=filters, **kwargs)
|
||||
|
||||
try:
|
||||
params = {}
|
||||
if self._using_ugc:
|
||||
@@ -206,7 +211,10 @@ class LindormVectorStore(BaseVector):
|
||||
should = kwargs.get("should")
|
||||
minimum_should_match = kwargs.get("minimum_should_match", 0)
|
||||
top_k = kwargs.get("top_k", 10)
|
||||
filters = kwargs.get("filter")
|
||||
filters = kwargs.get("filter", [])
|
||||
document_ids_filter = kwargs.get("document_ids_filter")
|
||||
if document_ids_filter:
|
||||
filters.append({"terms": {"metadata.document_id": document_ids_filter}})
|
||||
routing = self._routing
|
||||
full_text_query = default_text_search_query(
|
||||
query_text=query,
|
||||
|
||||
@@ -228,12 +228,18 @@ class MilvusVector(BaseVector):
|
||||
"""
|
||||
Search for documents by vector similarity.
|
||||
"""
|
||||
document_ids_filter = kwargs.get("document_ids_filter")
|
||||
filter = ""
|
||||
if document_ids_filter:
|
||||
document_ids = ", ".join(f"'{id}'" for id in document_ids_filter)
|
||||
filter = f'metadata["document_id"] in ({document_ids})'
|
||||
results = self._client.search(
|
||||
collection_name=self._collection_name,
|
||||
data=[query_vector],
|
||||
anns_field=Field.VECTOR.value,
|
||||
limit=kwargs.get("top_k", 4),
|
||||
output_fields=[Field.CONTENT_KEY.value, Field.METADATA_KEY.value],
|
||||
filter=filter,
|
||||
)
|
||||
|
||||
return self._process_search_results(
|
||||
@@ -249,6 +255,11 @@ class MilvusVector(BaseVector):
|
||||
if not self._hybrid_search_enabled or not self.field_exists(Field.SPARSE_VECTOR.value):
|
||||
logger.warning("Full-text search is not supported in current Milvus version (requires >= 2.5.0)")
|
||||
return []
|
||||
document_ids_filter = kwargs.get("document_ids_filter")
|
||||
filter = ""
|
||||
if document_ids_filter:
|
||||
document_ids = ", ".join(f"'{id}'" for id in document_ids_filter)
|
||||
filter = f'metadata["document_id"] in ({document_ids})'
|
||||
|
||||
results = self._client.search(
|
||||
collection_name=self._collection_name,
|
||||
@@ -256,6 +267,7 @@ class MilvusVector(BaseVector):
|
||||
anns_field=Field.SPARSE_VECTOR.value,
|
||||
limit=kwargs.get("top_k", 4),
|
||||
output_fields=[Field.CONTENT_KEY.value, Field.METADATA_KEY.value],
|
||||
filter=filter,
|
||||
)
|
||||
|
||||
return self._process_search_results(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user