mirror of https://github.com/langgenius/dify.git
Merge branch 'main' into feat/model-auth
This commit is contained in:
commit
12083de2ab
|
|
@ -9,6 +9,7 @@ permissions:
|
|||
|
||||
jobs:
|
||||
autofix:
|
||||
if: github.repository == 'langgenius/dify'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
|
|
|||
|
|
@ -235,6 +235,10 @@ Quickly deploy Dify to Alibaba cloud with [Alibaba Cloud Computing Nest](https:/
|
|||
|
||||
One-Click deploy Dify to Alibaba Cloud with [Alibaba Cloud Data Management](https://www.alibabacloud.com/help/en/dms/dify-in-invitational-preview/)
|
||||
|
||||
#### Deploy to AKS with Azure Devops Pipeline
|
||||
|
||||
One-Click deploy Dify to AKS with [Azure Devops Pipeline Helm Chart by @LeoZhang](https://github.com/Ruiruiz30/Dify-helm-chart-AKS)
|
||||
|
||||
|
||||
## Contributing
|
||||
|
||||
|
|
|
|||
|
|
@ -217,6 +217,10 @@ docker compose up -d
|
|||
|
||||
انشر Dify على علي بابا كلاود بنقرة واحدة باستخدام [Alibaba Cloud Data Management](https://www.alibabacloud.com/help/en/dms/dify-in-invitational-preview/)
|
||||
|
||||
#### استخدام Azure Devops Pipeline للنشر على AKS
|
||||
|
||||
انشر Dify على AKS بنقرة واحدة باستخدام [Azure Devops Pipeline Helm Chart by @LeoZhang](https://github.com/Ruiruiz30/Dify-helm-chart-AKS)
|
||||
|
||||
|
||||
## المساهمة
|
||||
|
||||
|
|
|
|||
|
|
@ -235,6 +235,10 @@ GitHub-এ ডিফাইকে স্টার দিয়ে রাখুন
|
|||
|
||||
[Alibaba Cloud Data Management](https://www.alibabacloud.com/help/en/dms/dify-in-invitational-preview/)
|
||||
|
||||
#### AKS-এ ডিপ্লয় করার জন্য Azure Devops Pipeline ব্যবহার
|
||||
|
||||
[Azure Devops Pipeline Helm Chart by @LeoZhang](https://github.com/Ruiruiz30/Dify-helm-chart-AKS) ব্যবহার করে Dify কে AKS-এ এক ক্লিকে ডিপ্লয় করুন
|
||||
|
||||
|
||||
## Contributing
|
||||
|
||||
|
|
|
|||
|
|
@ -233,6 +233,9 @@ docker compose up -d
|
|||
|
||||
使用 [阿里云数据管理DMS](https://help.aliyun.com/zh/dms/dify-in-invitational-preview) 将 Dify 一键部署到 阿里云
|
||||
|
||||
#### 使用 Azure Devops Pipeline 部署到AKS
|
||||
|
||||
使用[Azure Devops Pipeline Helm Chart by @LeoZhang](https://github.com/Ruiruiz30/Dify-helm-chart-AKS) 将 Dify 一键部署到 AKS
|
||||
|
||||
## Star History
|
||||
|
||||
|
|
|
|||
|
|
@ -230,6 +230,10 @@ Bereitstellung von Dify auf AWS mit [CDK](https://aws.amazon.com/cdk/)
|
|||
|
||||
Ein-Klick-Bereitstellung von Dify in der Alibaba Cloud mit [Alibaba Cloud Data Management](https://www.alibabacloud.com/help/en/dms/dify-in-invitational-preview/)
|
||||
|
||||
#### Verwendung von Azure Devops Pipeline für AKS-Bereitstellung
|
||||
|
||||
Stellen Sie Dify mit einem Klick in AKS bereit, indem Sie [Azure Devops Pipeline Helm Chart by @LeoZhang](https://github.com/Ruiruiz30/Dify-helm-chart-AKS) verwenden
|
||||
|
||||
|
||||
## Contributing
|
||||
|
||||
|
|
|
|||
|
|
@ -230,6 +230,10 @@ Despliegue Dify en AWS usando [CDK](https://aws.amazon.com/cdk/)
|
|||
|
||||
Despliega Dify en Alibaba Cloud con un solo clic con [Alibaba Cloud Data Management](https://www.alibabacloud.com/help/en/dms/dify-in-invitational-preview/)
|
||||
|
||||
#### Uso de Azure Devops Pipeline para implementar en AKS
|
||||
|
||||
Implementa Dify en AKS con un clic usando [Azure Devops Pipeline Helm Chart by @LeoZhang](https://github.com/Ruiruiz30/Dify-helm-chart-AKS)
|
||||
|
||||
|
||||
## Contribuir
|
||||
|
||||
|
|
|
|||
|
|
@ -228,6 +228,10 @@ Déployez Dify sur AWS en utilisant [CDK](https://aws.amazon.com/cdk/)
|
|||
|
||||
Déployez Dify en un clic sur Alibaba Cloud avec [Alibaba Cloud Data Management](https://www.alibabacloud.com/help/en/dms/dify-in-invitational-preview/)
|
||||
|
||||
#### Utilisation d'Azure Devops Pipeline pour déployer sur AKS
|
||||
|
||||
Déployez Dify sur AKS en un clic en utilisant [Azure Devops Pipeline Helm Chart by @LeoZhang](https://github.com/Ruiruiz30/Dify-helm-chart-AKS)
|
||||
|
||||
|
||||
## Contribuer
|
||||
|
||||
|
|
|
|||
|
|
@ -227,6 +227,10 @@ docker compose up -d
|
|||
#### Alibaba Cloud Data Management
|
||||
[Alibaba Cloud Data Management](https://www.alibabacloud.com/help/en/dms/dify-in-invitational-preview/) を利用して、DifyをAlibaba Cloudへワンクリックでデプロイできます
|
||||
|
||||
#### AKSへのデプロイにAzure Devops Pipelineを使用
|
||||
|
||||
[Azure Devops Pipeline Helm Chart by @LeoZhang](https://github.com/Ruiruiz30/Dify-helm-chart-AKS)を使用してDifyをAKSにワンクリックでデプロイ
|
||||
|
||||
|
||||
## 貢献
|
||||
|
||||
|
|
|
|||
|
|
@ -228,6 +228,10 @@ wa'logh nIqHom neH ghun deployment toy'wI' [CDK](https://aws.amazon.com/cdk/) lo
|
|||
|
||||
[Alibaba Cloud Data Management](https://www.alibabacloud.com/help/en/dms/dify-in-invitational-preview/)
|
||||
|
||||
#### AKS 'e' Deploy je Azure Devops Pipeline lo'laH
|
||||
|
||||
[Azure Devops Pipeline Helm Chart by @LeoZhang](https://github.com/Ruiruiz30/Dify-helm-chart-AKS) lo'laH Dify AKS 'e' wa'DIch click 'e' Deploy
|
||||
|
||||
|
||||
## Contributing
|
||||
|
||||
|
|
|
|||
|
|
@ -222,6 +222,10 @@ Dify를 Kubernetes에 배포하고 프리미엄 스케일링 설정을 구성했
|
|||
|
||||
[Alibaba Cloud Data Management](https://www.alibabacloud.com/help/en/dms/dify-in-invitational-preview/)를 통해 원클릭으로 Dify를 Alibaba Cloud에 배포할 수 있습니다
|
||||
|
||||
#### AKS에 배포하기 위해 Azure Devops Pipeline 사용
|
||||
|
||||
[Azure Devops Pipeline Helm Chart by @LeoZhang](https://github.com/Ruiruiz30/Dify-helm-chart-AKS)을 사용하여 Dify를 AKS에 원클릭으로 배포
|
||||
|
||||
|
||||
## 기여
|
||||
|
||||
|
|
|
|||
|
|
@ -227,6 +227,10 @@ Implante o Dify na AWS usando [CDK](https://aws.amazon.com/cdk/)
|
|||
|
||||
Implante o Dify na Alibaba Cloud com um clique usando o [Alibaba Cloud Data Management](https://www.alibabacloud.com/help/en/dms/dify-in-invitational-preview/)
|
||||
|
||||
#### Usando Azure Devops Pipeline para Implantar no AKS
|
||||
|
||||
Implante o Dify no AKS com um clique usando [Azure Devops Pipeline Helm Chart by @LeoZhang](https://github.com/Ruiruiz30/Dify-helm-chart-AKS)
|
||||
|
||||
|
||||
## Contribuindo
|
||||
|
||||
|
|
|
|||
|
|
@ -228,6 +228,10 @@ Uvedite Dify v AWS z uporabo [CDK](https://aws.amazon.com/cdk/)
|
|||
|
||||
Z enim klikom namestite Dify na Alibaba Cloud z [Alibaba Cloud Data Management](https://www.alibabacloud.com/help/en/dms/dify-in-invitational-preview/)
|
||||
|
||||
#### Uporaba Azure Devops Pipeline za uvajanje v AKS
|
||||
|
||||
Z enim klikom namestite Dify v AKS z uporabo [Azure Devops Pipeline Helm Chart by @LeoZhang](https://github.com/Ruiruiz30/Dify-helm-chart-AKS)
|
||||
|
||||
|
||||
## Prispevam
|
||||
|
||||
|
|
|
|||
|
|
@ -221,6 +221,10 @@ Dify'ı bulut platformuna tek tıklamayla dağıtın [terraform](https://www.ter
|
|||
|
||||
[Alibaba Cloud Data Management](https://www.alibabacloud.com/help/en/dms/dify-in-invitational-preview/) kullanarak Dify'ı tek tıkla Alibaba Cloud'a dağıtın
|
||||
|
||||
#### AKS'ye Dağıtım için Azure Devops Pipeline Kullanımı
|
||||
|
||||
[Azure Devops Pipeline Helm Chart by @LeoZhang](https://github.com/Ruiruiz30/Dify-helm-chart-AKS) kullanarak Dify'ı tek tıkla AKS'ye dağıtın
|
||||
|
||||
|
||||
## Katkıda Bulunma
|
||||
|
||||
|
|
|
|||
|
|
@ -233,6 +233,10 @@ Dify 的所有功能都提供相應的 API,因此您可以輕鬆地將 Dify
|
|||
|
||||
透過 [阿里雲數據管理DMS](https://www.alibabacloud.com/help/en/dms/dify-in-invitational-preview/),一鍵將 Dify 部署至阿里雲
|
||||
|
||||
#### 使用 Azure Devops Pipeline 部署到AKS
|
||||
|
||||
使用[Azure Devops Pipeline Helm Chart by @LeoZhang](https://github.com/Ruiruiz30/Dify-helm-chart-AKS) 將 Dify 一鍵部署到 AKS
|
||||
|
||||
|
||||
## 貢獻
|
||||
|
||||
|
|
|
|||
|
|
@ -224,6 +224,10 @@ Triển khai Dify trên AWS bằng [CDK](https://aws.amazon.com/cdk/)
|
|||
|
||||
Triển khai Dify lên Alibaba Cloud chỉ với một cú nhấp chuột bằng [Alibaba Cloud Data Management](https://www.alibabacloud.com/help/en/dms/dify-in-invitational-preview/)
|
||||
|
||||
#### Sử dụng Azure Devops Pipeline để Triển khai lên AKS
|
||||
|
||||
Triển khai Dify lên AKS chỉ với một cú nhấp chuột bằng [Azure Devops Pipeline Helm Chart bởi @LeoZhang](https://github.com/Ruiruiz30/Dify-helm-chart-AKS)
|
||||
|
||||
|
||||
## Đóng góp
|
||||
|
||||
|
|
|
|||
|
|
@ -232,6 +232,7 @@ TABLESTORE_ENDPOINT=https://instance-name.cn-hangzhou.ots.aliyuncs.com
|
|||
TABLESTORE_INSTANCE_NAME=instance-name
|
||||
TABLESTORE_ACCESS_KEY_ID=xxx
|
||||
TABLESTORE_ACCESS_KEY_SECRET=xxx
|
||||
TABLESTORE_NORMALIZE_FULLTEXT_BM25_SCORE=false
|
||||
|
||||
# Tidb Vector configuration
|
||||
TIDB_VECTOR_HOST=xxx.eu-central-1.xxx.aws.tidbcloud.com
|
||||
|
|
|
|||
|
|
@ -215,7 +215,7 @@ class DatabaseConfig(BaseSettings):
|
|||
|
||||
class CeleryConfig(DatabaseConfig):
|
||||
CELERY_BACKEND: str = Field(
|
||||
description="Backend for Celery task results. Options: 'database', 'redis'.",
|
||||
description="Backend for Celery task results. Options: 'database', 'redis', 'rabbitmq'.",
|
||||
default="redis",
|
||||
)
|
||||
|
||||
|
|
@ -245,7 +245,12 @@ class CeleryConfig(DatabaseConfig):
|
|||
|
||||
@computed_field
|
||||
def CELERY_RESULT_BACKEND(self) -> str | None:
|
||||
return f"db+{self.SQLALCHEMY_DATABASE_URI}" if self.CELERY_BACKEND == "database" else self.CELERY_BROKER_URL
|
||||
if self.CELERY_BACKEND in ("database", "rabbitmq"):
|
||||
return f"db+{self.SQLALCHEMY_DATABASE_URI}"
|
||||
elif self.CELERY_BACKEND == "redis":
|
||||
return self.CELERY_BROKER_URL
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def BROKER_USE_SSL(self) -> bool:
|
||||
|
|
|
|||
|
|
@ -28,3 +28,8 @@ class TableStoreConfig(BaseSettings):
|
|||
description="AccessKey secret for the instance name",
|
||||
default=None,
|
||||
)
|
||||
|
||||
TABLESTORE_NORMALIZE_FULLTEXT_BM25_SCORE: bool = Field(
|
||||
description="Whether to normalize full-text search scores to [0, 1]",
|
||||
default=False,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ class AnnotationReplyActionStatusApi(Resource):
|
|||
return {"job_id": job_id, "job_status": job_status, "error_msg": error_msg}, 200
|
||||
|
||||
|
||||
class AnnotationListApi(Resource):
|
||||
class AnnotationApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
|
|
@ -123,6 +123,23 @@ class AnnotationListApi(Resource):
|
|||
}
|
||||
return response, 200
|
||||
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@cloud_edition_billing_resource_check("annotation")
|
||||
@marshal_with(annotation_fields)
|
||||
def post(self, app_id):
|
||||
if not current_user.is_editor:
|
||||
raise Forbidden()
|
||||
|
||||
app_id = str(app_id)
|
||||
parser = reqparse.RequestParser()
|
||||
parser.add_argument("question", required=True, type=str, location="json")
|
||||
parser.add_argument("answer", required=True, type=str, location="json")
|
||||
args = parser.parse_args()
|
||||
annotation = AppAnnotationService.insert_app_annotation_directly(args, app_id)
|
||||
return annotation
|
||||
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
|
|
@ -137,7 +154,8 @@ class AnnotationListApi(Resource):
|
|||
|
||||
# If annotation_ids are provided, handle batch deletion
|
||||
if annotation_ids:
|
||||
if not annotation_ids:
|
||||
# Check if any annotation_ids contain empty strings or invalid values
|
||||
if not all(annotation_id.strip() for annotation_id in annotation_ids if annotation_id):
|
||||
return {
|
||||
"code": "bad_request",
|
||||
"message": "annotation_ids are required if the parameter is provided.",
|
||||
|
|
@ -165,25 +183,6 @@ class AnnotationExportApi(Resource):
|
|||
return response, 200
|
||||
|
||||
|
||||
class AnnotationCreateApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@cloud_edition_billing_resource_check("annotation")
|
||||
@marshal_with(annotation_fields)
|
||||
def post(self, app_id):
|
||||
if not current_user.is_editor:
|
||||
raise Forbidden()
|
||||
|
||||
app_id = str(app_id)
|
||||
parser = reqparse.RequestParser()
|
||||
parser.add_argument("question", required=True, type=str, location="json")
|
||||
parser.add_argument("answer", required=True, type=str, location="json")
|
||||
args = parser.parse_args()
|
||||
annotation = AppAnnotationService.insert_app_annotation_directly(args, app_id)
|
||||
return annotation
|
||||
|
||||
|
||||
class AnnotationUpdateDeleteApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
|
|
@ -292,9 +291,8 @@ api.add_resource(AnnotationReplyActionApi, "/apps/<uuid:app_id>/annotation-reply
|
|||
api.add_resource(
|
||||
AnnotationReplyActionStatusApi, "/apps/<uuid:app_id>/annotation-reply/<string:action>/status/<uuid:job_id>"
|
||||
)
|
||||
api.add_resource(AnnotationListApi, "/apps/<uuid:app_id>/annotations")
|
||||
api.add_resource(AnnotationApi, "/apps/<uuid:app_id>/annotations")
|
||||
api.add_resource(AnnotationExportApi, "/apps/<uuid:app_id>/annotations/export")
|
||||
api.add_resource(AnnotationCreateApi, "/apps/<uuid:app_id>/annotations")
|
||||
api.add_resource(AnnotationUpdateDeleteApi, "/apps/<uuid:app_id>/annotations/<uuid:annotation_id>")
|
||||
api.add_resource(AnnotationBatchImportApi, "/apps/<uuid:app_id>/annotations/batch-import")
|
||||
api.add_resource(AnnotationBatchImportStatusApi, "/apps/<uuid:app_id>/annotations/batch-import-status/<uuid:job_id>")
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import json
|
||||
|
||||
from flask_restful import Resource, marshal_with, reqparse
|
||||
from flask_restful.inputs import int_range
|
||||
from sqlalchemy.orm import Session
|
||||
from werkzeug.exceptions import NotFound
|
||||
from werkzeug.exceptions import BadRequest, NotFound
|
||||
|
||||
import services
|
||||
from controllers.service_api import api
|
||||
|
|
@ -15,6 +17,7 @@ from fields.conversation_fields import (
|
|||
simple_conversation_fields,
|
||||
)
|
||||
from fields.conversation_variable_fields import (
|
||||
conversation_variable_fields,
|
||||
conversation_variable_infinite_scroll_pagination_fields,
|
||||
)
|
||||
from libs.helper import uuid_value
|
||||
|
|
@ -120,7 +123,41 @@ class ConversationVariablesApi(Resource):
|
|||
raise NotFound("Conversation Not Exists.")
|
||||
|
||||
|
||||
class ConversationVariableDetailApi(Resource):
|
||||
@validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.JSON))
|
||||
@marshal_with(conversation_variable_fields)
|
||||
def put(self, app_model: App, end_user: EndUser, c_id, variable_id):
|
||||
"""Update a conversation variable's value"""
|
||||
app_mode = AppMode.value_of(app_model.mode)
|
||||
if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT}:
|
||||
raise NotChatAppError()
|
||||
|
||||
conversation_id = str(c_id)
|
||||
variable_id = str(variable_id)
|
||||
|
||||
parser = reqparse.RequestParser()
|
||||
parser.add_argument("value", required=True, location="json")
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
return ConversationService.update_conversation_variable(
|
||||
app_model, conversation_id, variable_id, end_user, json.loads(args["value"])
|
||||
)
|
||||
except services.errors.conversation.ConversationNotExistsError:
|
||||
raise NotFound("Conversation Not Exists.")
|
||||
except services.errors.conversation.ConversationVariableNotExistsError:
|
||||
raise NotFound("Conversation Variable Not Exists.")
|
||||
except services.errors.conversation.ConversationVariableTypeMismatchError as e:
|
||||
raise BadRequest(str(e))
|
||||
|
||||
|
||||
api.add_resource(ConversationRenameApi, "/conversations/<uuid:c_id>/name", endpoint="conversation_name")
|
||||
api.add_resource(ConversationApi, "/conversations")
|
||||
api.add_resource(ConversationDetailApi, "/conversations/<uuid:c_id>", endpoint="conversation_detail")
|
||||
api.add_resource(ConversationVariablesApi, "/conversations/<uuid:c_id>/variables", endpoint="conversation_variables")
|
||||
api.add_resource(
|
||||
ConversationVariableDetailApi,
|
||||
"/conversations/<uuid:c_id>/variables/<uuid:variable_id>",
|
||||
endpoint="conversation_variable_detail",
|
||||
methods=["PUT"],
|
||||
)
|
||||
|
|
|
|||
|
|
@ -148,6 +148,8 @@ SupportedComparisonOperator = Literal[
|
|||
"is not",
|
||||
"empty",
|
||||
"not empty",
|
||||
"in",
|
||||
"not in",
|
||||
# for number
|
||||
"=",
|
||||
"≠",
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import logging
|
|||
import os
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any, Optional, Union, cast
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from openinference.semconv.trace import OpenInferenceSpanKindValues, SpanAttributes
|
||||
from opentelemetry import trace
|
||||
|
|
@ -40,8 +41,14 @@ def setup_tracer(arize_phoenix_config: ArizeConfig | PhoenixConfig) -> tuple[tra
|
|||
try:
|
||||
# Choose the appropriate exporter based on config type
|
||||
exporter: Union[GrpcOTLPSpanExporter, HttpOTLPSpanExporter]
|
||||
|
||||
# Inspect the provided endpoint to determine its structure
|
||||
parsed = urlparse(arize_phoenix_config.endpoint)
|
||||
base_endpoint = f"{parsed.scheme}://{parsed.netloc}"
|
||||
path = parsed.path.rstrip("/")
|
||||
|
||||
if isinstance(arize_phoenix_config, ArizeConfig):
|
||||
arize_endpoint = f"{arize_phoenix_config.endpoint}/v1"
|
||||
arize_endpoint = f"{base_endpoint}/v1"
|
||||
arize_headers = {
|
||||
"api_key": arize_phoenix_config.api_key or "",
|
||||
"space_id": arize_phoenix_config.space_id or "",
|
||||
|
|
@ -53,7 +60,7 @@ def setup_tracer(arize_phoenix_config: ArizeConfig | PhoenixConfig) -> tuple[tra
|
|||
timeout=30,
|
||||
)
|
||||
else:
|
||||
phoenix_endpoint = f"{arize_phoenix_config.endpoint}/v1/traces"
|
||||
phoenix_endpoint = f"{base_endpoint}{path}/v1/traces"
|
||||
phoenix_headers = {
|
||||
"api_key": arize_phoenix_config.api_key or "",
|
||||
"authorization": f"Bearer {arize_phoenix_config.api_key or ''}",
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ class PhoenixConfig(BaseTracingConfig):
|
|||
@field_validator("endpoint")
|
||||
@classmethod
|
||||
def endpoint_validator(cls, v, info: ValidationInfo):
|
||||
return cls.validate_endpoint_url(v, "https://app.phoenix.arize.com")
|
||||
return validate_url_with_path(v, "https://app.phoenix.arize.com")
|
||||
|
||||
|
||||
class LangfuseConfig(BaseTracingConfig):
|
||||
|
|
|
|||
|
|
@ -322,7 +322,7 @@ class OpsTraceManager:
|
|||
:return:
|
||||
"""
|
||||
# auth check
|
||||
if enabled == True:
|
||||
if enabled:
|
||||
try:
|
||||
provider_config_map[tracing_provider]
|
||||
except KeyError:
|
||||
|
|
@ -407,7 +407,6 @@ class TraceTask:
|
|||
def __init__(
|
||||
self,
|
||||
trace_type: Any,
|
||||
trace_id: Optional[str] = None,
|
||||
message_id: Optional[str] = None,
|
||||
workflow_execution: Optional[WorkflowExecution] = None,
|
||||
conversation_id: Optional[str] = None,
|
||||
|
|
@ -423,7 +422,7 @@ class TraceTask:
|
|||
self.timer = timer
|
||||
self.file_base_url = os.getenv("FILES_URL", "http://127.0.0.1:5001")
|
||||
self.app_id = None
|
||||
|
||||
self.trace_id = None
|
||||
self.kwargs = kwargs
|
||||
external_trace_id = kwargs.get("external_trace_id")
|
||||
if external_trace_id:
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import json
|
||||
import logging
|
||||
import math
|
||||
from typing import Any, Optional
|
||||
|
||||
import tablestore # type: ignore
|
||||
|
|
@ -22,6 +23,7 @@ class TableStoreConfig(BaseModel):
|
|||
access_key_secret: Optional[str] = None
|
||||
instance_name: Optional[str] = None
|
||||
endpoint: Optional[str] = None
|
||||
normalize_full_text_bm25_score: Optional[bool] = False
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
|
|
@ -47,6 +49,7 @@ class TableStoreVector(BaseVector):
|
|||
config.access_key_secret,
|
||||
config.instance_name,
|
||||
)
|
||||
self._normalize_full_text_bm25_score = config.normalize_full_text_bm25_score
|
||||
self._table_name = f"{collection_name}"
|
||||
self._index_name = f"{collection_name}_idx"
|
||||
self._tags_field = f"{Field.METADATA_KEY.value}_tags"
|
||||
|
|
@ -131,8 +134,8 @@ class TableStoreVector(BaseVector):
|
|||
filtered_list = None
|
||||
if document_ids_filter:
|
||||
filtered_list = ["document_id=" + item for item in document_ids_filter]
|
||||
|
||||
return self._search_by_full_text(query, filtered_list, top_k)
|
||||
score_threshold = float(kwargs.get("score_threshold") or 0.0)
|
||||
return self._search_by_full_text(query, filtered_list, top_k, score_threshold)
|
||||
|
||||
def delete(self) -> None:
|
||||
self._delete_table_if_exist()
|
||||
|
|
@ -318,7 +321,19 @@ class TableStoreVector(BaseVector):
|
|||
documents = sorted(documents, key=lambda x: x.metadata["score"] if x.metadata else 0, reverse=True)
|
||||
return documents
|
||||
|
||||
def _search_by_full_text(self, query: str, document_ids_filter: list[str] | None, top_k: int) -> list[Document]:
|
||||
@staticmethod
|
||||
def _normalize_score_exp_decay(score: float, k: float = 0.15) -> float:
|
||||
"""
|
||||
Args:
|
||||
score: BM25 search score.
|
||||
k: decay factor, the larger the k, the steeper the low score end
|
||||
"""
|
||||
normalized_score = 1 - math.exp(-k * score)
|
||||
return max(0.0, min(1.0, normalized_score))
|
||||
|
||||
def _search_by_full_text(
|
||||
self, query: str, document_ids_filter: list[str] | None, top_k: int, score_threshold: float
|
||||
) -> list[Document]:
|
||||
bool_query = tablestore.BoolQuery(must_queries=[], filter_queries=[], should_queries=[], must_not_queries=[])
|
||||
bool_query.must_queries.append(tablestore.MatchQuery(text=query, field_name=Field.CONTENT_KEY.value))
|
||||
|
||||
|
|
@ -339,15 +354,27 @@ class TableStoreVector(BaseVector):
|
|||
|
||||
documents = []
|
||||
for search_hit in search_response.search_hits:
|
||||
score = None
|
||||
if self._normalize_full_text_bm25_score:
|
||||
score = self._normalize_score_exp_decay(search_hit.score)
|
||||
|
||||
# skip when score is below threshold and use normalize score
|
||||
if score and score <= score_threshold:
|
||||
continue
|
||||
|
||||
ots_column_map = {}
|
||||
for col in search_hit.row[1]:
|
||||
ots_column_map[col[0]] = col[1]
|
||||
|
||||
vector_str = ots_column_map.get(Field.VECTOR.value)
|
||||
metadata_str = ots_column_map.get(Field.METADATA_KEY.value)
|
||||
vector = json.loads(vector_str) if vector_str else None
|
||||
metadata = json.loads(metadata_str) if metadata_str else {}
|
||||
|
||||
vector_str = ots_column_map.get(Field.VECTOR.value)
|
||||
vector = json.loads(vector_str) if vector_str else None
|
||||
|
||||
if score:
|
||||
metadata["score"] = score
|
||||
|
||||
documents.append(
|
||||
Document(
|
||||
page_content=ots_column_map.get(Field.CONTENT_KEY.value) or "",
|
||||
|
|
@ -355,6 +382,8 @@ class TableStoreVector(BaseVector):
|
|||
metadata=metadata,
|
||||
)
|
||||
)
|
||||
if self._normalize_full_text_bm25_score:
|
||||
documents = sorted(documents, key=lambda x: x.metadata["score"] if x.metadata else 0, reverse=True)
|
||||
return documents
|
||||
|
||||
|
||||
|
|
@ -375,5 +404,6 @@ class TableStoreVectorFactory(AbstractVectorFactory):
|
|||
instance_name=dify_config.TABLESTORE_INSTANCE_NAME,
|
||||
access_key_id=dify_config.TABLESTORE_ACCESS_KEY_ID,
|
||||
access_key_secret=dify_config.TABLESTORE_ACCESS_KEY_SECRET,
|
||||
normalize_full_text_bm25_score=dify_config.TABLESTORE_NORMALIZE_FULLTEXT_BM25_SCORE,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ SupportedComparisonOperator = Literal[
|
|||
"is not",
|
||||
"empty",
|
||||
"not empty",
|
||||
"in",
|
||||
"not in",
|
||||
# for number
|
||||
"=",
|
||||
"≠",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import json
|
||||
import logging
|
||||
import operator
|
||||
from typing import Any, Optional, cast
|
||||
|
||||
import requests
|
||||
|
|
@ -130,13 +131,15 @@ class NotionExtractor(BaseExtractor):
|
|||
data[property_name] = value
|
||||
row_dict = {k: v for k, v in data.items() if v}
|
||||
row_content = ""
|
||||
for key, value in row_dict.items():
|
||||
for key, value in sorted(row_dict.items(), key=operator.itemgetter(0)):
|
||||
if isinstance(value, dict):
|
||||
value_dict = {k: v for k, v in value.items() if v}
|
||||
value_content = "".join(f"{k}:{v} " for k, v in value_dict.items())
|
||||
row_content = row_content + f"{key}:{value_content}\n"
|
||||
else:
|
||||
row_content = row_content + f"{key}:{value}\n"
|
||||
if "url" in result:
|
||||
row_content = row_content + f"Row Page URL:{result.get('url', '')}\n"
|
||||
database_content.append(row_content)
|
||||
|
||||
has_more = response_data.get("has_more", False)
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ class TimezoneConversionTool(BuiltinTool):
|
|||
target_time = self.timezone_convert(current_time, current_timezone, target_timezone) # type: ignore
|
||||
if not target_time:
|
||||
yield self.create_text_message(
|
||||
f"Invalid datatime and timezone: {current_time},{current_timezone},{target_timezone}"
|
||||
f"Invalid datetime and timezone: {current_time},{current_timezone},{target_timezone}"
|
||||
)
|
||||
return
|
||||
|
||||
|
|
|
|||
|
|
@ -597,7 +597,7 @@ def _extract_text_from_vtt(vtt_bytes: bytes) -> str:
|
|||
|
||||
for i in range(1, len(raw_results)):
|
||||
spk, txt = raw_results[i]
|
||||
if spk == None:
|
||||
if spk is None:
|
||||
merged_results.append((None, current_text))
|
||||
continue
|
||||
|
||||
|
|
|
|||
|
|
@ -277,6 +277,22 @@ class Executor:
|
|||
elif self.auth.config.type == "custom":
|
||||
headers[authorization.config.header] = authorization.config.api_key or ""
|
||||
|
||||
# Handle Content-Type for multipart/form-data requests
|
||||
# Fix for issue #22880: Missing boundary when using multipart/form-data
|
||||
body = self.node_data.body
|
||||
if body and body.type == "form-data":
|
||||
# For multipart/form-data with files, let httpx handle the boundary automatically
|
||||
# by not setting Content-Type header when files are present
|
||||
if not self.files or all(f[0] == "__multipart_placeholder__" for f in self.files):
|
||||
# Only set Content-Type when there are no actual files
|
||||
# This ensures httpx generates the correct boundary
|
||||
if "content-type" not in (k.lower() for k in headers):
|
||||
headers["Content-Type"] = "multipart/form-data"
|
||||
elif body and body.type in BODY_TYPE_TO_CONTENT_TYPE:
|
||||
# Set Content-Type for other body types
|
||||
if "content-type" not in (k.lower() for k in headers):
|
||||
headers["Content-Type"] = BODY_TYPE_TO_CONTENT_TYPE[body.type]
|
||||
|
||||
return headers
|
||||
|
||||
def _validate_and_parse_response(self, response: httpx.Response) -> Response:
|
||||
|
|
@ -384,15 +400,24 @@ class Executor:
|
|||
# '__multipart_placeholder__' is inserted to force multipart encoding but is not a real file.
|
||||
# This prevents logging meaningless placeholder entries.
|
||||
if self.files and not all(f[0] == "__multipart_placeholder__" for f in self.files):
|
||||
for key, (filename, content, mime_type) in self.files:
|
||||
for file_entry in self.files:
|
||||
# file_entry should be (key, (filename, content, mime_type)), but handle edge cases
|
||||
if len(file_entry) != 2 or not isinstance(file_entry[1], tuple) or len(file_entry[1]) < 2:
|
||||
continue # skip malformed entries
|
||||
key = file_entry[0]
|
||||
content = file_entry[1][1]
|
||||
body_string += f"--{boundary}\r\n"
|
||||
body_string += f'Content-Disposition: form-data; name="{key}"\r\n\r\n'
|
||||
# decode content
|
||||
try:
|
||||
body_string += content.decode("utf-8")
|
||||
except UnicodeDecodeError:
|
||||
# fix: decode binary content
|
||||
pass
|
||||
# decode content safely
|
||||
if isinstance(content, bytes):
|
||||
try:
|
||||
body_string += content.decode("utf-8")
|
||||
except UnicodeDecodeError:
|
||||
body_string += content.decode("utf-8", errors="replace")
|
||||
elif isinstance(content, str):
|
||||
body_string += content
|
||||
else:
|
||||
body_string += f"[Unsupported content type: {type(content).__name__}]"
|
||||
body_string += "\r\n"
|
||||
body_string += f"--{boundary}--\r\n"
|
||||
elif self.node_data.body:
|
||||
|
|
|
|||
|
|
@ -74,6 +74,8 @@ SupportedComparisonOperator = Literal[
|
|||
"is not",
|
||||
"empty",
|
||||
"not empty",
|
||||
"in",
|
||||
"not in",
|
||||
# for number
|
||||
"=",
|
||||
"≠",
|
||||
|
|
|
|||
|
|
@ -602,6 +602,28 @@ class KnowledgeRetrievalNode(BaseNode):
|
|||
**{key: metadata_name, key_value: f"%{value}"}
|
||||
)
|
||||
)
|
||||
case "in":
|
||||
if isinstance(value, str):
|
||||
escaped_values = [v.strip().replace("'", "''") for v in str(value).split(",")]
|
||||
escaped_value_str = ",".join(escaped_values)
|
||||
else:
|
||||
escaped_value_str = str(value)
|
||||
filters.append(
|
||||
(text(f"documents.doc_metadata ->> :{key} = any(string_to_array(:{key_value},','))")).params(
|
||||
**{key: metadata_name, key_value: escaped_value_str}
|
||||
)
|
||||
)
|
||||
case "not in":
|
||||
if isinstance(value, str):
|
||||
escaped_values = [v.strip().replace("'", "''") for v in str(value).split(",")]
|
||||
escaped_value_str = ",".join(escaped_values)
|
||||
else:
|
||||
escaped_value_str = str(value)
|
||||
filters.append(
|
||||
(text(f"documents.doc_metadata ->> :{key} != all(string_to_array(:{key_value},','))")).params(
|
||||
**{key: metadata_name, key_value: escaped_value_str}
|
||||
)
|
||||
)
|
||||
case "=" | "is":
|
||||
if isinstance(value, str):
|
||||
filters.append(Document.doc_metadata[metadata_name] == f'"{value}"')
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import io
|
|||
import json
|
||||
import logging
|
||||
from collections.abc import Generator, Mapping, Sequence
|
||||
from typing import TYPE_CHECKING, Any, Optional, cast
|
||||
from typing import TYPE_CHECKING, Any, Optional
|
||||
|
||||
from core.app.entities.app_invoke_entities import ModelConfigWithCredentialsEntity
|
||||
from core.file import FileType, file_manager
|
||||
|
|
@ -33,12 +33,10 @@ from core.model_runtime.entities.message_entities import (
|
|||
UserPromptMessage,
|
||||
)
|
||||
from core.model_runtime.entities.model_entities import (
|
||||
AIModelEntity,
|
||||
ModelFeature,
|
||||
ModelPropertyKey,
|
||||
ModelType,
|
||||
)
|
||||
from core.model_runtime.model_providers.__base.large_language_model import LargeLanguageModel
|
||||
from core.model_runtime.utils.encoders import jsonable_encoder
|
||||
from core.prompt.entities.advanced_prompt_entities import CompletionModelPromptTemplate, MemoryConfig
|
||||
from core.prompt.utils.prompt_message_util import PromptMessageUtil
|
||||
|
|
@ -1006,21 +1004,6 @@ class LLMNode(BaseNode):
|
|||
)
|
||||
return saved_file
|
||||
|
||||
def _fetch_model_schema(self, provider: str) -> AIModelEntity | None:
|
||||
"""
|
||||
Fetch model schema
|
||||
"""
|
||||
model_name = self._node_data.model.name
|
||||
model_manager = ModelManager()
|
||||
model_instance = model_manager.get_model_instance(
|
||||
tenant_id=self.tenant_id, model_type=ModelType.LLM, provider=provider, model=model_name
|
||||
)
|
||||
model_type_instance = model_instance.model_type_instance
|
||||
model_type_instance = cast(LargeLanguageModel, model_type_instance)
|
||||
model_credentials = model_instance.credentials
|
||||
model_schema = model_type_instance.get_model_schema(model_name, model_credentials)
|
||||
return model_schema
|
||||
|
||||
@staticmethod
|
||||
def fetch_structured_output_schema(
|
||||
*,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import mimetypes
|
||||
import os
|
||||
import urllib.parse
|
||||
import uuid
|
||||
from collections.abc import Callable, Mapping, Sequence
|
||||
from typing import Any, cast
|
||||
|
|
@ -240,16 +242,21 @@ def _build_from_remote_url(
|
|||
|
||||
def _get_remote_file_info(url: str):
|
||||
file_size = -1
|
||||
filename = url.split("/")[-1].split("?")[0] or "unknown_file"
|
||||
mime_type = mimetypes.guess_type(filename)[0] or ""
|
||||
parsed_url = urllib.parse.urlparse(url)
|
||||
url_path = parsed_url.path
|
||||
filename = os.path.basename(url_path)
|
||||
|
||||
# Initialize mime_type from filename as fallback
|
||||
mime_type, _ = mimetypes.guess_type(filename)
|
||||
|
||||
resp = ssrf_proxy.head(url, follow_redirects=True)
|
||||
resp = cast(httpx.Response, resp)
|
||||
if resp.status_code == httpx.codes.OK:
|
||||
if content_disposition := resp.headers.get("Content-Disposition"):
|
||||
filename = str(content_disposition.split("filename=")[-1].strip('"'))
|
||||
# Re-guess mime_type from updated filename
|
||||
mime_type, _ = mimetypes.guess_type(filename)
|
||||
file_size = int(resp.headers.get("Content-Length", file_size))
|
||||
mime_type = mime_type or str(resp.headers.get("Content-Type", ""))
|
||||
|
||||
return mime_type, filename, file_size
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
"""manual dataset field update
|
||||
|
||||
Revision ID: 532b3f888abf
|
||||
Revises: 8bcc02c9bd07
|
||||
Create Date: 2025-07-24 14:50:48.779833
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import models as models
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '532b3f888abf'
|
||||
down_revision = '8bcc02c9bd07'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.execute("ALTER TABLE tidb_auth_bindings ALTER COLUMN status SET DEFAULT 'CREATING'::character varying")
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.execute("ALTER TABLE tidb_auth_bindings ALTER COLUMN status SET DEFAULT 'CREATING'")
|
||||
|
|
@ -4,7 +4,7 @@ from datetime import datetime
|
|||
from typing import Optional, cast
|
||||
|
||||
from flask_login import UserMixin # type: ignore
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy import DateTime, String, func, select
|
||||
from sqlalchemy.orm import Mapped, mapped_column, reconstructor
|
||||
|
||||
from models.base import Base
|
||||
|
|
@ -86,23 +86,21 @@ class Account(UserMixin, Base):
|
|||
__table_args__ = (db.PrimaryKeyConstraint("id", name="account_pkey"), db.Index("account_email_idx", "email"))
|
||||
|
||||
id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()"))
|
||||
name: Mapped[str] = mapped_column(db.String(255))
|
||||
email: Mapped[str] = mapped_column(db.String(255))
|
||||
password: Mapped[Optional[str]] = mapped_column(db.String(255))
|
||||
password_salt: Mapped[Optional[str]] = mapped_column(db.String(255))
|
||||
avatar: Mapped[Optional[str]] = mapped_column(db.String(255), nullable=True)
|
||||
interface_language: Mapped[Optional[str]] = mapped_column(db.String(255))
|
||||
interface_theme: Mapped[Optional[str]] = mapped_column(db.String(255), nullable=True)
|
||||
timezone: Mapped[Optional[str]] = mapped_column(db.String(255))
|
||||
last_login_at: Mapped[Optional[datetime]] = mapped_column(db.DateTime, nullable=True)
|
||||
last_login_ip: Mapped[Optional[str]] = mapped_column(db.String(255), nullable=True)
|
||||
last_active_at: Mapped[datetime] = mapped_column(
|
||||
db.DateTime, server_default=func.current_timestamp(), nullable=False
|
||||
)
|
||||
status: Mapped[str] = mapped_column(db.String(16), server_default=db.text("'active'::character varying"))
|
||||
initialized_at: Mapped[Optional[datetime]] = mapped_column(db.DateTime, nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(db.DateTime, server_default=func.current_timestamp(), nullable=False)
|
||||
updated_at: Mapped[datetime] = mapped_column(db.DateTime, server_default=func.current_timestamp(), nullable=False)
|
||||
name: Mapped[str] = mapped_column(String(255))
|
||||
email: Mapped[str] = mapped_column(String(255))
|
||||
password: Mapped[Optional[str]] = mapped_column(String(255))
|
||||
password_salt: Mapped[Optional[str]] = mapped_column(String(255))
|
||||
avatar: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
|
||||
interface_language: Mapped[Optional[str]] = mapped_column(String(255))
|
||||
interface_theme: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
|
||||
timezone: Mapped[Optional[str]] = mapped_column(String(255))
|
||||
last_login_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
|
||||
last_login_ip: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
|
||||
last_active_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.current_timestamp(), nullable=False)
|
||||
status: Mapped[str] = mapped_column(String(16), server_default=db.text("'active'::character varying"))
|
||||
initialized_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.current_timestamp(), nullable=False)
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.current_timestamp(), nullable=False)
|
||||
|
||||
@reconstructor
|
||||
def init_on_load(self):
|
||||
|
|
@ -200,13 +198,13 @@ class Tenant(Base):
|
|||
__table_args__ = (db.PrimaryKeyConstraint("id", name="tenant_pkey"),)
|
||||
|
||||
id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()"))
|
||||
name: Mapped[str] = mapped_column(db.String(255))
|
||||
name: Mapped[str] = mapped_column(String(255))
|
||||
encrypt_public_key = db.Column(db.Text)
|
||||
plan: Mapped[str] = mapped_column(db.String(255), server_default=db.text("'basic'::character varying"))
|
||||
status: Mapped[str] = mapped_column(db.String(255), server_default=db.text("'normal'::character varying"))
|
||||
plan: Mapped[str] = mapped_column(String(255), server_default=db.text("'basic'::character varying"))
|
||||
status: Mapped[str] = mapped_column(String(255), server_default=db.text("'normal'::character varying"))
|
||||
custom_config: Mapped[Optional[str]] = mapped_column(db.Text)
|
||||
created_at: Mapped[datetime] = mapped_column(db.DateTime, server_default=func.current_timestamp(), nullable=False)
|
||||
updated_at: Mapped[datetime] = mapped_column(db.DateTime, server_default=func.current_timestamp())
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.current_timestamp(), nullable=False)
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.current_timestamp())
|
||||
|
||||
def get_accounts(self) -> list[Account]:
|
||||
return (
|
||||
|
|
@ -237,10 +235,10 @@ class TenantAccountJoin(Base):
|
|||
tenant_id: Mapped[str] = mapped_column(StringUUID)
|
||||
account_id: Mapped[str] = mapped_column(StringUUID)
|
||||
current: Mapped[bool] = mapped_column(db.Boolean, server_default=db.text("false"))
|
||||
role: Mapped[str] = mapped_column(db.String(16), server_default="normal")
|
||||
role: Mapped[str] = mapped_column(String(16), server_default="normal")
|
||||
invited_by: Mapped[Optional[str]] = mapped_column(StringUUID)
|
||||
created_at: Mapped[datetime] = mapped_column(db.DateTime, server_default=func.current_timestamp())
|
||||
updated_at: Mapped[datetime] = mapped_column(db.DateTime, server_default=func.current_timestamp())
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.current_timestamp())
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.current_timestamp())
|
||||
|
||||
|
||||
class AccountIntegrate(Base):
|
||||
|
|
@ -253,11 +251,11 @@ class AccountIntegrate(Base):
|
|||
|
||||
id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()"))
|
||||
account_id: Mapped[str] = mapped_column(StringUUID)
|
||||
provider: Mapped[str] = mapped_column(db.String(16))
|
||||
open_id: Mapped[str] = mapped_column(db.String(255))
|
||||
encrypted_token: Mapped[str] = mapped_column(db.String(255))
|
||||
created_at: Mapped[datetime] = mapped_column(db.DateTime, server_default=func.current_timestamp())
|
||||
updated_at: Mapped[datetime] = mapped_column(db.DateTime, server_default=func.current_timestamp())
|
||||
provider: Mapped[str] = mapped_column(String(16))
|
||||
open_id: Mapped[str] = mapped_column(String(255))
|
||||
encrypted_token: Mapped[str] = mapped_column(String(255))
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.current_timestamp())
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.current_timestamp())
|
||||
|
||||
|
||||
class InvitationCode(Base):
|
||||
|
|
@ -269,14 +267,14 @@ class InvitationCode(Base):
|
|||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(db.Integer)
|
||||
batch: Mapped[str] = mapped_column(db.String(255))
|
||||
code: Mapped[str] = mapped_column(db.String(32))
|
||||
status: Mapped[str] = mapped_column(db.String(16), server_default=db.text("'unused'::character varying"))
|
||||
used_at: Mapped[Optional[datetime]] = mapped_column(db.DateTime)
|
||||
batch: Mapped[str] = mapped_column(String(255))
|
||||
code: Mapped[str] = mapped_column(String(32))
|
||||
status: Mapped[str] = mapped_column(String(16), server_default=db.text("'unused'::character varying"))
|
||||
used_at: Mapped[Optional[datetime]] = mapped_column(DateTime)
|
||||
used_by_tenant_id: Mapped[Optional[str]] = mapped_column(StringUUID)
|
||||
used_by_account_id: Mapped[Optional[str]] = mapped_column(StringUUID)
|
||||
deprecated_at: Mapped[Optional[datetime]] = mapped_column(db.DateTime, nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(db.DateTime, server_default=db.text("CURRENT_TIMESTAMP(0)"))
|
||||
deprecated_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=db.text("CURRENT_TIMESTAMP(0)"))
|
||||
|
||||
|
||||
class TenantPluginPermission(Base):
|
||||
|
|
@ -298,10 +296,8 @@ class TenantPluginPermission(Base):
|
|||
|
||||
id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()"))
|
||||
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
||||
install_permission: Mapped[InstallPermission] = mapped_column(
|
||||
db.String(16), nullable=False, server_default="everyone"
|
||||
)
|
||||
debug_permission: Mapped[DebugPermission] = mapped_column(db.String(16), nullable=False, server_default="noone")
|
||||
install_permission: Mapped[InstallPermission] = mapped_column(String(16), nullable=False, server_default="everyone")
|
||||
debug_permission: Mapped[DebugPermission] = mapped_column(String(16), nullable=False, server_default="noone")
|
||||
|
||||
|
||||
class TenantPluginAutoUpgradeStrategy(Base):
|
||||
|
|
@ -323,14 +319,10 @@ class TenantPluginAutoUpgradeStrategy(Base):
|
|||
|
||||
id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()"))
|
||||
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
||||
strategy_setting: Mapped[StrategySetting] = mapped_column(db.String(16), nullable=False, server_default="fix_only")
|
||||
strategy_setting: Mapped[StrategySetting] = mapped_column(String(16), nullable=False, server_default="fix_only")
|
||||
upgrade_time_of_day: Mapped[int] = mapped_column(db.Integer, nullable=False, default=0) # seconds of the day
|
||||
upgrade_mode: Mapped[UpgradeMode] = mapped_column(db.String(16), nullable=False, server_default="exclude")
|
||||
exclude_plugins: Mapped[list[str]] = mapped_column(
|
||||
db.ARRAY(db.String(255)), nullable=False
|
||||
) # plugin_id (author/name)
|
||||
include_plugins: Mapped[list[str]] = mapped_column(
|
||||
db.ARRAY(db.String(255)), nullable=False
|
||||
) # plugin_id (author/name)
|
||||
created_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
updated_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
upgrade_mode: Mapped[UpgradeMode] = mapped_column(String(16), nullable=False, server_default="exclude")
|
||||
exclude_plugins: Mapped[list[str]] = mapped_column(db.ARRAY(String(255)), nullable=False) # plugin_id (author/name)
|
||||
include_plugins: Mapped[list[str]] = mapped_column(db.ARRAY(String(255)), nullable=False) # plugin_id (author/name)
|
||||
created_at = db.Column(DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
updated_at = db.Column(DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import enum
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy.orm import mapped_column
|
||||
from sqlalchemy import DateTime, String, Text, func
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from .base import Base
|
||||
from .engine import db
|
||||
|
|
@ -24,7 +25,7 @@ class APIBasedExtension(Base):
|
|||
|
||||
id = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()"))
|
||||
tenant_id = mapped_column(StringUUID, nullable=False)
|
||||
name = mapped_column(db.String(255), nullable=False)
|
||||
api_endpoint = mapped_column(db.String(255), nullable=False)
|
||||
api_key = mapped_column(db.Text, nullable=False)
|
||||
created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
api_endpoint: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
api_key = mapped_column(Text, nullable=False)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ from datetime import datetime
|
|||
from json import JSONDecodeError
|
||||
from typing import Any, Optional, cast
|
||||
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy import DateTime, String, func, select
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
|
|
@ -48,22 +48,22 @@ class Dataset(Base):
|
|||
|
||||
id = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()"))
|
||||
tenant_id: Mapped[str] = mapped_column(StringUUID)
|
||||
name: Mapped[str] = mapped_column(db.String(255))
|
||||
name: Mapped[str] = mapped_column(String(255))
|
||||
description = mapped_column(db.Text, nullable=True)
|
||||
provider: Mapped[str] = mapped_column(db.String(255), server_default=db.text("'vendor'::character varying"))
|
||||
permission: Mapped[str] = mapped_column(db.String(255), server_default=db.text("'only_me'::character varying"))
|
||||
data_source_type = mapped_column(db.String(255))
|
||||
indexing_technique: Mapped[Optional[str]] = mapped_column(db.String(255))
|
||||
provider: Mapped[str] = mapped_column(String(255), server_default=db.text("'vendor'::character varying"))
|
||||
permission: Mapped[str] = mapped_column(String(255), server_default=db.text("'only_me'::character varying"))
|
||||
data_source_type = mapped_column(String(255))
|
||||
indexing_technique: Mapped[Optional[str]] = mapped_column(String(255))
|
||||
index_struct = mapped_column(db.Text, nullable=True)
|
||||
created_by = mapped_column(StringUUID, nullable=False)
|
||||
created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
updated_by = mapped_column(StringUUID, nullable=True)
|
||||
updated_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
embedding_model = db.Column(db.String(255), nullable=True) # TODO: mapped_column
|
||||
embedding_model_provider = db.Column(db.String(255), nullable=True) # TODO: mapped_column
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
embedding_model = db.Column(String(255), nullable=True) # TODO: mapped_column
|
||||
embedding_model_provider = db.Column(String(255), nullable=True) # TODO: mapped_column
|
||||
collection_binding_id = mapped_column(StringUUID, nullable=True)
|
||||
retrieval_model = mapped_column(JSONB, nullable=True)
|
||||
built_in_field_enabled = mapped_column(db.Boolean, nullable=False, server_default=db.text("false"))
|
||||
built_in_field_enabled: Mapped[bool] = mapped_column(db.Boolean, nullable=False, server_default=db.text("false"))
|
||||
|
||||
@property
|
||||
def dataset_keyword_table(self):
|
||||
|
|
@ -268,10 +268,10 @@ class DatasetProcessRule(Base):
|
|||
|
||||
id = mapped_column(StringUUID, nullable=False, server_default=db.text("uuid_generate_v4()"))
|
||||
dataset_id = mapped_column(StringUUID, nullable=False)
|
||||
mode = mapped_column(db.String(255), nullable=False, server_default=db.text("'automatic'::character varying"))
|
||||
mode = mapped_column(String(255), nullable=False, server_default=db.text("'automatic'::character varying"))
|
||||
rules = mapped_column(db.Text, nullable=True)
|
||||
created_by = mapped_column(StringUUID, nullable=False)
|
||||
created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
|
||||
MODES = ["automatic", "custom", "hierarchical"]
|
||||
PRE_PROCESSING_RULES = ["remove_stopwords", "remove_extra_spaces", "remove_urls_emails"]
|
||||
|
|
@ -313,61 +313,59 @@ class Document(Base):
|
|||
id = mapped_column(StringUUID, nullable=False, server_default=db.text("uuid_generate_v4()"))
|
||||
tenant_id = mapped_column(StringUUID, nullable=False)
|
||||
dataset_id = mapped_column(StringUUID, nullable=False)
|
||||
position = mapped_column(db.Integer, nullable=False)
|
||||
data_source_type = mapped_column(db.String(255), nullable=False)
|
||||
position: Mapped[int] = mapped_column(db.Integer, nullable=False)
|
||||
data_source_type: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
data_source_info = mapped_column(db.Text, nullable=True)
|
||||
dataset_process_rule_id = mapped_column(StringUUID, nullable=True)
|
||||
batch = mapped_column(db.String(255), nullable=False)
|
||||
name = mapped_column(db.String(255), nullable=False)
|
||||
created_from = mapped_column(db.String(255), nullable=False)
|
||||
batch: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
created_from: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
created_by = mapped_column(StringUUID, nullable=False)
|
||||
created_api_request_id = mapped_column(StringUUID, nullable=True)
|
||||
created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
|
||||
# start processing
|
||||
processing_started_at = mapped_column(db.DateTime, nullable=True)
|
||||
processing_started_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
|
||||
|
||||
# parsing
|
||||
file_id = mapped_column(db.Text, nullable=True)
|
||||
word_count = mapped_column(db.Integer, nullable=True)
|
||||
parsing_completed_at = mapped_column(db.DateTime, nullable=True)
|
||||
word_count: Mapped[Optional[int]] = mapped_column(db.Integer, nullable=True) # TODO: make this not nullable
|
||||
parsing_completed_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
|
||||
|
||||
# cleaning
|
||||
cleaning_completed_at = mapped_column(db.DateTime, nullable=True)
|
||||
cleaning_completed_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
|
||||
|
||||
# split
|
||||
splitting_completed_at = mapped_column(db.DateTime, nullable=True)
|
||||
splitting_completed_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
|
||||
|
||||
# indexing
|
||||
tokens = mapped_column(db.Integer, nullable=True)
|
||||
indexing_latency = mapped_column(db.Float, nullable=True)
|
||||
completed_at = mapped_column(db.DateTime, nullable=True)
|
||||
tokens: Mapped[Optional[int]] = mapped_column(db.Integer, nullable=True)
|
||||
indexing_latency: Mapped[Optional[float]] = mapped_column(db.Float, nullable=True)
|
||||
completed_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
|
||||
|
||||
# pause
|
||||
is_paused = mapped_column(db.Boolean, nullable=True, server_default=db.text("false"))
|
||||
is_paused: Mapped[Optional[bool]] = mapped_column(db.Boolean, nullable=True, server_default=db.text("false"))
|
||||
paused_by = mapped_column(StringUUID, nullable=True)
|
||||
paused_at = mapped_column(db.DateTime, nullable=True)
|
||||
paused_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
|
||||
|
||||
# error
|
||||
error = mapped_column(db.Text, nullable=True)
|
||||
stopped_at = mapped_column(db.DateTime, nullable=True)
|
||||
stopped_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
|
||||
|
||||
# basic fields
|
||||
indexing_status = mapped_column(
|
||||
db.String(255), nullable=False, server_default=db.text("'waiting'::character varying")
|
||||
)
|
||||
enabled = mapped_column(db.Boolean, nullable=False, server_default=db.text("true"))
|
||||
disabled_at = mapped_column(db.DateTime, nullable=True)
|
||||
indexing_status = mapped_column(String(255), nullable=False, server_default=db.text("'waiting'::character varying"))
|
||||
enabled: Mapped[bool] = mapped_column(db.Boolean, nullable=False, server_default=db.text("true"))
|
||||
disabled_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
|
||||
disabled_by = mapped_column(StringUUID, nullable=True)
|
||||
archived = mapped_column(db.Boolean, nullable=False, server_default=db.text("false"))
|
||||
archived_reason = mapped_column(db.String(255), nullable=True)
|
||||
archived: Mapped[bool] = mapped_column(db.Boolean, nullable=False, server_default=db.text("false"))
|
||||
archived_reason = mapped_column(String(255), nullable=True)
|
||||
archived_by = mapped_column(StringUUID, nullable=True)
|
||||
archived_at = mapped_column(db.DateTime, nullable=True)
|
||||
updated_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
doc_type = mapped_column(db.String(40), nullable=True)
|
||||
archived_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
doc_type = mapped_column(String(40), nullable=True)
|
||||
doc_metadata = mapped_column(JSONB, nullable=True)
|
||||
doc_form = mapped_column(db.String(255), nullable=False, server_default=db.text("'text_model'::character varying"))
|
||||
doc_language = mapped_column(db.String(255), nullable=True)
|
||||
doc_form = mapped_column(String(255), nullable=False, server_default=db.text("'text_model'::character varying"))
|
||||
doc_language = mapped_column(String(255), nullable=True)
|
||||
|
||||
DATA_SOURCES = ["upload_file", "notion_import", "website_crawl"]
|
||||
|
||||
|
|
@ -524,7 +522,7 @@ class Document(Base):
|
|||
"id": "built-in",
|
||||
"name": BuiltInField.upload_date,
|
||||
"type": "time",
|
||||
"value": self.created_at.timestamp(),
|
||||
"value": str(self.created_at.timestamp()),
|
||||
}
|
||||
)
|
||||
built_in_fields.append(
|
||||
|
|
@ -532,7 +530,7 @@ class Document(Base):
|
|||
"id": "built-in",
|
||||
"name": BuiltInField.last_update_date,
|
||||
"type": "time",
|
||||
"value": self.updated_at.timestamp(),
|
||||
"value": str(self.updated_at.timestamp()),
|
||||
}
|
||||
)
|
||||
built_in_fields.append(
|
||||
|
|
@ -667,23 +665,23 @@ class DocumentSegment(Base):
|
|||
|
||||
# indexing fields
|
||||
keywords = mapped_column(db.JSON, nullable=True)
|
||||
index_node_id = mapped_column(db.String(255), nullable=True)
|
||||
index_node_hash = mapped_column(db.String(255), nullable=True)
|
||||
index_node_id = mapped_column(String(255), nullable=True)
|
||||
index_node_hash = mapped_column(String(255), nullable=True)
|
||||
|
||||
# basic fields
|
||||
hit_count = mapped_column(db.Integer, nullable=False, default=0)
|
||||
enabled = mapped_column(db.Boolean, nullable=False, server_default=db.text("true"))
|
||||
disabled_at = mapped_column(db.DateTime, nullable=True)
|
||||
hit_count: Mapped[int] = mapped_column(db.Integer, nullable=False, default=0)
|
||||
enabled: Mapped[bool] = mapped_column(db.Boolean, nullable=False, server_default=db.text("true"))
|
||||
disabled_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
|
||||
disabled_by = mapped_column(StringUUID, nullable=True)
|
||||
status: Mapped[str] = mapped_column(db.String(255), server_default=db.text("'waiting'::character varying"))
|
||||
status: Mapped[str] = mapped_column(String(255), server_default=db.text("'waiting'::character varying"))
|
||||
created_by = mapped_column(StringUUID, nullable=False)
|
||||
created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
updated_by = mapped_column(StringUUID, nullable=True)
|
||||
updated_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
indexing_at = mapped_column(db.DateTime, nullable=True)
|
||||
completed_at: Mapped[Optional[datetime]] = mapped_column(db.DateTime, nullable=True)
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
indexing_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
|
||||
completed_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
|
||||
error = mapped_column(db.Text, nullable=True)
|
||||
stopped_at = mapped_column(db.DateTime, nullable=True)
|
||||
stopped_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
|
||||
|
||||
@property
|
||||
def dataset(self):
|
||||
|
|
@ -808,19 +806,23 @@ class ChildChunk(Base):
|
|||
dataset_id = mapped_column(StringUUID, nullable=False)
|
||||
document_id = mapped_column(StringUUID, nullable=False)
|
||||
segment_id = mapped_column(StringUUID, nullable=False)
|
||||
position = mapped_column(db.Integer, nullable=False)
|
||||
position: Mapped[int] = mapped_column(db.Integer, nullable=False)
|
||||
content = mapped_column(db.Text, nullable=False)
|
||||
word_count = mapped_column(db.Integer, nullable=False)
|
||||
word_count: Mapped[int] = mapped_column(db.Integer, nullable=False)
|
||||
# indexing fields
|
||||
index_node_id = mapped_column(db.String(255), nullable=True)
|
||||
index_node_hash = mapped_column(db.String(255), nullable=True)
|
||||
type = mapped_column(db.String(255), nullable=False, server_default=db.text("'automatic'::character varying"))
|
||||
index_node_id = mapped_column(String(255), nullable=True)
|
||||
index_node_hash = mapped_column(String(255), nullable=True)
|
||||
type = mapped_column(String(255), nullable=False, server_default=db.text("'automatic'::character varying"))
|
||||
created_by = mapped_column(StringUUID, nullable=False)
|
||||
created_at = mapped_column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)"))
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)")
|
||||
)
|
||||
updated_by = mapped_column(StringUUID, nullable=True)
|
||||
updated_at = mapped_column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)"))
|
||||
indexing_at = mapped_column(db.DateTime, nullable=True)
|
||||
completed_at = mapped_column(db.DateTime, nullable=True)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)")
|
||||
)
|
||||
indexing_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
|
||||
completed_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
|
||||
error = mapped_column(db.Text, nullable=True)
|
||||
|
||||
@property
|
||||
|
|
@ -846,7 +848,7 @@ class AppDatasetJoin(Base):
|
|||
id = mapped_column(StringUUID, primary_key=True, nullable=False, server_default=db.text("uuid_generate_v4()"))
|
||||
app_id = mapped_column(StringUUID, nullable=False)
|
||||
dataset_id = mapped_column(StringUUID, nullable=False)
|
||||
created_at = mapped_column(db.DateTime, nullable=False, server_default=db.func.current_timestamp())
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=db.func.current_timestamp())
|
||||
|
||||
@property
|
||||
def app(self):
|
||||
|
|
@ -863,11 +865,11 @@ class DatasetQuery(Base):
|
|||
id = mapped_column(StringUUID, primary_key=True, nullable=False, server_default=db.text("uuid_generate_v4()"))
|
||||
dataset_id = mapped_column(StringUUID, nullable=False)
|
||||
content = mapped_column(db.Text, nullable=False)
|
||||
source = mapped_column(db.String(255), nullable=False)
|
||||
source: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
source_app_id = mapped_column(StringUUID, nullable=True)
|
||||
created_by_role = mapped_column(db.String, nullable=False)
|
||||
created_by_role = mapped_column(String, nullable=False)
|
||||
created_by = mapped_column(StringUUID, nullable=False)
|
||||
created_at = mapped_column(db.DateTime, nullable=False, server_default=db.func.current_timestamp())
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=db.func.current_timestamp())
|
||||
|
||||
|
||||
class DatasetKeywordTable(Base):
|
||||
|
|
@ -881,7 +883,7 @@ class DatasetKeywordTable(Base):
|
|||
dataset_id = mapped_column(StringUUID, nullable=False, unique=True)
|
||||
keyword_table = mapped_column(db.Text, nullable=False)
|
||||
data_source_type = mapped_column(
|
||||
db.String(255), nullable=False, server_default=db.text("'database'::character varying")
|
||||
String(255), nullable=False, server_default=db.text("'database'::character varying")
|
||||
)
|
||||
|
||||
@property
|
||||
|
|
@ -925,12 +927,12 @@ class Embedding(Base):
|
|||
|
||||
id = mapped_column(StringUUID, primary_key=True, server_default=db.text("uuid_generate_v4()"))
|
||||
model_name = mapped_column(
|
||||
db.String(255), nullable=False, server_default=db.text("'text-embedding-ada-002'::character varying")
|
||||
String(255), nullable=False, server_default=db.text("'text-embedding-ada-002'::character varying")
|
||||
)
|
||||
hash = mapped_column(db.String(64), nullable=False)
|
||||
hash = mapped_column(String(64), nullable=False)
|
||||
embedding = mapped_column(db.LargeBinary, nullable=False)
|
||||
created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
provider_name = mapped_column(db.String(255), nullable=False, server_default=db.text("''::character varying"))
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
provider_name = mapped_column(String(255), nullable=False, server_default=db.text("''::character varying"))
|
||||
|
||||
def set_embedding(self, embedding_data: list[float]):
|
||||
self.embedding = pickle.dumps(embedding_data, protocol=pickle.HIGHEST_PROTOCOL)
|
||||
|
|
@ -947,11 +949,11 @@ class DatasetCollectionBinding(Base):
|
|||
)
|
||||
|
||||
id = mapped_column(StringUUID, primary_key=True, server_default=db.text("uuid_generate_v4()"))
|
||||
provider_name = mapped_column(db.String(255), nullable=False)
|
||||
model_name = mapped_column(db.String(255), nullable=False)
|
||||
type = mapped_column(db.String(40), server_default=db.text("'dataset'::character varying"), nullable=False)
|
||||
collection_name = mapped_column(db.String(64), nullable=False)
|
||||
created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
provider_name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
model_name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
type = mapped_column(String(40), server_default=db.text("'dataset'::character varying"), nullable=False)
|
||||
collection_name = mapped_column(String(64), nullable=False)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
|
||||
|
||||
class TidbAuthBinding(Base):
|
||||
|
|
@ -965,13 +967,13 @@ class TidbAuthBinding(Base):
|
|||
)
|
||||
id = mapped_column(StringUUID, primary_key=True, server_default=db.text("uuid_generate_v4()"))
|
||||
tenant_id = mapped_column(StringUUID, nullable=True)
|
||||
cluster_id = mapped_column(db.String(255), nullable=False)
|
||||
cluster_name = mapped_column(db.String(255), nullable=False)
|
||||
active = mapped_column(db.Boolean, nullable=False, server_default=db.text("false"))
|
||||
status = mapped_column(db.String(255), nullable=False, server_default=db.text("CREATING"))
|
||||
account = mapped_column(db.String(255), nullable=False)
|
||||
password = mapped_column(db.String(255), nullable=False)
|
||||
created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
cluster_id: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
cluster_name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
active: Mapped[bool] = mapped_column(db.Boolean, nullable=False, server_default=db.text("false"))
|
||||
status = mapped_column(String(255), nullable=False, server_default=db.text("'CREATING'::character varying"))
|
||||
account: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
password: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
|
||||
|
||||
class Whitelist(Base):
|
||||
|
|
@ -982,8 +984,8 @@ class Whitelist(Base):
|
|||
)
|
||||
id = mapped_column(StringUUID, primary_key=True, server_default=db.text("uuid_generate_v4()"))
|
||||
tenant_id = mapped_column(StringUUID, nullable=True)
|
||||
category = mapped_column(db.String(255), nullable=False)
|
||||
created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
category: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
|
||||
|
||||
class DatasetPermission(Base):
|
||||
|
|
@ -999,8 +1001,8 @@ class DatasetPermission(Base):
|
|||
dataset_id = mapped_column(StringUUID, nullable=False)
|
||||
account_id = mapped_column(StringUUID, nullable=False)
|
||||
tenant_id = mapped_column(StringUUID, nullable=False)
|
||||
has_permission = mapped_column(db.Boolean, nullable=False, server_default=db.text("true"))
|
||||
created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
has_permission: Mapped[bool] = mapped_column(db.Boolean, nullable=False, server_default=db.text("true"))
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
|
||||
|
||||
class ExternalKnowledgeApis(Base):
|
||||
|
|
@ -1012,14 +1014,14 @@ class ExternalKnowledgeApis(Base):
|
|||
)
|
||||
|
||||
id = mapped_column(StringUUID, nullable=False, server_default=db.text("uuid_generate_v4()"))
|
||||
name = mapped_column(db.String(255), nullable=False)
|
||||
description = mapped_column(db.String(255), nullable=False)
|
||||
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
description: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
tenant_id = mapped_column(StringUUID, nullable=False)
|
||||
settings = mapped_column(db.Text, nullable=True)
|
||||
created_by = mapped_column(StringUUID, nullable=False)
|
||||
created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
updated_by = mapped_column(StringUUID, nullable=True)
|
||||
updated_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
|
|
@ -1072,9 +1074,9 @@ class ExternalKnowledgeBindings(Base):
|
|||
dataset_id = mapped_column(StringUUID, nullable=False)
|
||||
external_knowledge_id = mapped_column(db.Text, nullable=False)
|
||||
created_by = mapped_column(StringUUID, nullable=False)
|
||||
created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
updated_by = mapped_column(StringUUID, nullable=True)
|
||||
updated_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
|
||||
|
||||
class DatasetAutoDisableLog(Base):
|
||||
|
|
@ -1090,8 +1092,10 @@ class DatasetAutoDisableLog(Base):
|
|||
tenant_id = mapped_column(StringUUID, nullable=False)
|
||||
dataset_id = mapped_column(StringUUID, nullable=False)
|
||||
document_id = mapped_column(StringUUID, nullable=False)
|
||||
notified = mapped_column(db.Boolean, nullable=False, server_default=db.text("false"))
|
||||
created_at = mapped_column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)"))
|
||||
notified: Mapped[bool] = mapped_column(db.Boolean, nullable=False, server_default=db.text("false"))
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)")
|
||||
)
|
||||
|
||||
|
||||
class RateLimitLog(Base):
|
||||
|
|
@ -1104,9 +1108,11 @@ class RateLimitLog(Base):
|
|||
|
||||
id = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()"))
|
||||
tenant_id = mapped_column(StringUUID, nullable=False)
|
||||
subscription_plan = mapped_column(db.String(255), nullable=False)
|
||||
operation = mapped_column(db.String(255), nullable=False)
|
||||
created_at = mapped_column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)"))
|
||||
subscription_plan: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
operation: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)")
|
||||
)
|
||||
|
||||
|
||||
class DatasetMetadata(Base):
|
||||
|
|
@ -1120,10 +1126,14 @@ class DatasetMetadata(Base):
|
|||
id = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()"))
|
||||
tenant_id = mapped_column(StringUUID, nullable=False)
|
||||
dataset_id = mapped_column(StringUUID, nullable=False)
|
||||
type = mapped_column(db.String(255), nullable=False)
|
||||
name = mapped_column(db.String(255), nullable=False)
|
||||
created_at = mapped_column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)"))
|
||||
updated_at = mapped_column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)"))
|
||||
type: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)")
|
||||
)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)")
|
||||
)
|
||||
created_by = mapped_column(StringUUID, nullable=False)
|
||||
updated_by = mapped_column(StringUUID, nullable=True)
|
||||
|
||||
|
|
@ -1143,5 +1153,5 @@ class DatasetMetadataBinding(Base):
|
|||
dataset_id = mapped_column(StringUUID, nullable=False)
|
||||
metadata_id = mapped_column(StringUUID, nullable=False)
|
||||
document_id = mapped_column(StringUUID, nullable=False)
|
||||
created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
created_by = mapped_column(StringUUID, nullable=False)
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ if TYPE_CHECKING:
|
|||
import sqlalchemy as sa
|
||||
from flask import request
|
||||
from flask_login import UserMixin
|
||||
from sqlalchemy import Float, Index, PrimaryKeyConstraint, func, text
|
||||
from sqlalchemy import Float, Index, PrimaryKeyConstraint, String, func, text
|
||||
from sqlalchemy.orm import Mapped, Session, mapped_column
|
||||
|
||||
from configs import dify_config
|
||||
|
|
@ -37,7 +37,7 @@ class DifySetup(Base):
|
|||
__tablename__ = "dify_setups"
|
||||
__table_args__ = (db.PrimaryKeyConstraint("version", name="dify_setup_pkey"),)
|
||||
|
||||
version = mapped_column(db.String(255), nullable=False)
|
||||
version: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
setup_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
|
||||
|
||||
|
|
@ -73,15 +73,15 @@ class App(Base):
|
|||
|
||||
id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()"))
|
||||
tenant_id: Mapped[str] = mapped_column(StringUUID)
|
||||
name: Mapped[str] = mapped_column(db.String(255))
|
||||
name: Mapped[str] = mapped_column(String(255))
|
||||
description: Mapped[str] = mapped_column(db.Text, server_default=db.text("''::character varying"))
|
||||
mode: Mapped[str] = mapped_column(db.String(255))
|
||||
icon_type: Mapped[Optional[str]] = mapped_column(db.String(255)) # image, emoji
|
||||
icon = db.Column(db.String(255))
|
||||
icon_background: Mapped[Optional[str]] = mapped_column(db.String(255))
|
||||
mode: Mapped[str] = mapped_column(String(255))
|
||||
icon_type: Mapped[Optional[str]] = mapped_column(String(255)) # image, emoji
|
||||
icon = db.Column(String(255))
|
||||
icon_background: Mapped[Optional[str]] = mapped_column(String(255))
|
||||
app_model_config_id = mapped_column(StringUUID, nullable=True)
|
||||
workflow_id = mapped_column(StringUUID, nullable=True)
|
||||
status: Mapped[str] = mapped_column(db.String(255), server_default=db.text("'normal'::character varying"))
|
||||
status: Mapped[str] = mapped_column(String(255), server_default=db.text("'normal'::character varying"))
|
||||
enable_site: Mapped[bool] = mapped_column(db.Boolean)
|
||||
enable_api: Mapped[bool] = mapped_column(db.Boolean)
|
||||
api_rpm: Mapped[int] = mapped_column(db.Integer, server_default=db.text("0"))
|
||||
|
|
@ -306,8 +306,8 @@ class AppModelConfig(Base):
|
|||
|
||||
id = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()"))
|
||||
app_id = mapped_column(StringUUID, nullable=False)
|
||||
provider = mapped_column(db.String(255), nullable=True)
|
||||
model_id = mapped_column(db.String(255), nullable=True)
|
||||
provider = mapped_column(String(255), nullable=True)
|
||||
model_id = mapped_column(String(255), nullable=True)
|
||||
configs = mapped_column(db.JSON, nullable=True)
|
||||
created_by = mapped_column(StringUUID, nullable=True)
|
||||
created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
|
|
@ -321,12 +321,12 @@ class AppModelConfig(Base):
|
|||
more_like_this = mapped_column(db.Text)
|
||||
model = mapped_column(db.Text)
|
||||
user_input_form = mapped_column(db.Text)
|
||||
dataset_query_variable = mapped_column(db.String(255))
|
||||
dataset_query_variable = mapped_column(String(255))
|
||||
pre_prompt = mapped_column(db.Text)
|
||||
agent_mode = mapped_column(db.Text)
|
||||
sensitive_word_avoidance = mapped_column(db.Text)
|
||||
retriever_resource = mapped_column(db.Text)
|
||||
prompt_type = mapped_column(db.String(255), nullable=False, server_default=db.text("'simple'::character varying"))
|
||||
prompt_type = mapped_column(String(255), nullable=False, server_default=db.text("'simple'::character varying"))
|
||||
chat_prompt_config = mapped_column(db.Text)
|
||||
completion_prompt_config = mapped_column(db.Text)
|
||||
dataset_configs = mapped_column(db.Text)
|
||||
|
|
@ -561,14 +561,14 @@ class RecommendedApp(Base):
|
|||
id = mapped_column(StringUUID, primary_key=True, server_default=db.text("uuid_generate_v4()"))
|
||||
app_id = mapped_column(StringUUID, nullable=False)
|
||||
description = mapped_column(db.JSON, nullable=False)
|
||||
copyright = mapped_column(db.String(255), nullable=False)
|
||||
privacy_policy = mapped_column(db.String(255), nullable=False)
|
||||
copyright: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
privacy_policy: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
custom_disclaimer: Mapped[str] = mapped_column(sa.TEXT, default="")
|
||||
category = mapped_column(db.String(255), nullable=False)
|
||||
position = mapped_column(db.Integer, nullable=False, default=0)
|
||||
is_listed = mapped_column(db.Boolean, nullable=False, default=True)
|
||||
install_count = mapped_column(db.Integer, nullable=False, default=0)
|
||||
language = mapped_column(db.String(255), nullable=False, server_default=db.text("'en-US'::character varying"))
|
||||
category: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
position: Mapped[int] = mapped_column(db.Integer, nullable=False, default=0)
|
||||
is_listed: Mapped[bool] = mapped_column(db.Boolean, nullable=False, default=True)
|
||||
install_count: Mapped[int] = mapped_column(db.Integer, nullable=False, default=0)
|
||||
language = mapped_column(String(255), nullable=False, server_default=db.text("'en-US'::character varying"))
|
||||
created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
updated_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
|
||||
|
|
@ -591,8 +591,8 @@ class InstalledApp(Base):
|
|||
tenant_id = mapped_column(StringUUID, nullable=False)
|
||||
app_id = mapped_column(StringUUID, nullable=False)
|
||||
app_owner_tenant_id = mapped_column(StringUUID, nullable=False)
|
||||
position = mapped_column(db.Integer, nullable=False, default=0)
|
||||
is_pinned = mapped_column(db.Boolean, nullable=False, server_default=db.text("false"))
|
||||
position: Mapped[int] = mapped_column(db.Integer, nullable=False, default=0)
|
||||
is_pinned: Mapped[bool] = mapped_column(db.Boolean, nullable=False, server_default=db.text("false"))
|
||||
last_used_at = mapped_column(db.DateTime, nullable=True)
|
||||
created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
|
||||
|
|
@ -617,26 +617,26 @@ class Conversation(Base):
|
|||
id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()"))
|
||||
app_id = mapped_column(StringUUID, nullable=False)
|
||||
app_model_config_id = mapped_column(StringUUID, nullable=True)
|
||||
model_provider = mapped_column(db.String(255), nullable=True)
|
||||
model_provider = mapped_column(String(255), nullable=True)
|
||||
override_model_configs = mapped_column(db.Text)
|
||||
model_id = mapped_column(db.String(255), nullable=True)
|
||||
mode: Mapped[str] = mapped_column(db.String(255))
|
||||
name = mapped_column(db.String(255), nullable=False)
|
||||
model_id = mapped_column(String(255), nullable=True)
|
||||
mode: Mapped[str] = mapped_column(String(255))
|
||||
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
summary = mapped_column(db.Text)
|
||||
_inputs: Mapped[dict] = mapped_column("inputs", db.JSON)
|
||||
introduction = mapped_column(db.Text)
|
||||
system_instruction = mapped_column(db.Text)
|
||||
system_instruction_tokens = mapped_column(db.Integer, nullable=False, server_default=db.text("0"))
|
||||
status = mapped_column(db.String(255), nullable=False)
|
||||
system_instruction_tokens: Mapped[int] = mapped_column(db.Integer, nullable=False, server_default=db.text("0"))
|
||||
status: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
|
||||
# The `invoke_from` records how the conversation is created.
|
||||
#
|
||||
# Its value corresponds to the members of `InvokeFrom`.
|
||||
# (api/core/app/entities/app_invoke_entities.py)
|
||||
invoke_from = mapped_column(db.String(255), nullable=True)
|
||||
invoke_from = mapped_column(String(255), nullable=True)
|
||||
|
||||
# ref: ConversationSource.
|
||||
from_source = mapped_column(db.String(255), nullable=False)
|
||||
from_source: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
from_end_user_id = mapped_column(StringUUID)
|
||||
from_account_id = mapped_column(StringUUID)
|
||||
read_at = mapped_column(db.DateTime)
|
||||
|
|
@ -650,7 +650,7 @@ class Conversation(Base):
|
|||
"MessageAnnotation", backref="conversation", lazy="select", passive_deletes="all"
|
||||
)
|
||||
|
||||
is_deleted = mapped_column(db.Boolean, nullable=False, server_default=db.text("false"))
|
||||
is_deleted: Mapped[bool] = mapped_column(db.Boolean, nullable=False, server_default=db.text("false"))
|
||||
|
||||
@property
|
||||
def inputs(self):
|
||||
|
|
@ -894,8 +894,8 @@ class Message(Base):
|
|||
|
||||
id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()"))
|
||||
app_id = mapped_column(StringUUID, nullable=False)
|
||||
model_provider = mapped_column(db.String(255), nullable=True)
|
||||
model_id = mapped_column(db.String(255), nullable=True)
|
||||
model_provider = mapped_column(String(255), nullable=True)
|
||||
model_id = mapped_column(String(255), nullable=True)
|
||||
override_model_configs = mapped_column(db.Text)
|
||||
conversation_id = mapped_column(StringUUID, db.ForeignKey("conversations.id"), nullable=False)
|
||||
_inputs: Mapped[dict] = mapped_column("inputs", db.JSON)
|
||||
|
|
@ -911,17 +911,17 @@ class Message(Base):
|
|||
parent_message_id = mapped_column(StringUUID, nullable=True)
|
||||
provider_response_latency = mapped_column(db.Float, nullable=False, server_default=db.text("0"))
|
||||
total_price = mapped_column(db.Numeric(10, 7))
|
||||
currency = mapped_column(db.String(255), nullable=False)
|
||||
status = mapped_column(db.String(255), nullable=False, server_default=db.text("'normal'::character varying"))
|
||||
currency: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
status = mapped_column(String(255), nullable=False, server_default=db.text("'normal'::character varying"))
|
||||
error = mapped_column(db.Text)
|
||||
message_metadata = mapped_column(db.Text)
|
||||
invoke_from: Mapped[Optional[str]] = mapped_column(db.String(255), nullable=True)
|
||||
from_source = mapped_column(db.String(255), nullable=False)
|
||||
invoke_from: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
|
||||
from_source: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
from_end_user_id: Mapped[Optional[str]] = mapped_column(StringUUID)
|
||||
from_account_id: Mapped[Optional[str]] = mapped_column(StringUUID)
|
||||
created_at: Mapped[datetime] = mapped_column(db.DateTime, server_default=func.current_timestamp())
|
||||
updated_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
agent_based = mapped_column(db.Boolean, nullable=False, server_default=db.text("false"))
|
||||
agent_based: Mapped[bool] = mapped_column(db.Boolean, nullable=False, server_default=db.text("false"))
|
||||
workflow_run_id: Mapped[Optional[str]] = mapped_column(StringUUID)
|
||||
|
||||
@property
|
||||
|
|
@ -1238,9 +1238,9 @@ class MessageFeedback(Base):
|
|||
app_id = mapped_column(StringUUID, nullable=False)
|
||||
conversation_id = mapped_column(StringUUID, nullable=False)
|
||||
message_id = mapped_column(StringUUID, nullable=False)
|
||||
rating = mapped_column(db.String(255), nullable=False)
|
||||
rating: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
content = mapped_column(db.Text)
|
||||
from_source = mapped_column(db.String(255), nullable=False)
|
||||
from_source: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
from_end_user_id = mapped_column(StringUUID)
|
||||
from_account_id = mapped_column(StringUUID)
|
||||
created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
|
|
@ -1298,12 +1298,12 @@ class MessageFile(Base):
|
|||
|
||||
id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()"))
|
||||
message_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
||||
type: Mapped[str] = mapped_column(db.String(255), nullable=False)
|
||||
transfer_method: Mapped[str] = mapped_column(db.String(255), nullable=False)
|
||||
type: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
transfer_method: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
url: Mapped[Optional[str]] = mapped_column(db.Text, nullable=True)
|
||||
belongs_to: Mapped[Optional[str]] = mapped_column(db.String(255), nullable=True)
|
||||
belongs_to: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
|
||||
upload_file_id: Mapped[Optional[str]] = mapped_column(StringUUID, nullable=True)
|
||||
created_by_role: Mapped[str] = mapped_column(db.String(255), nullable=False)
|
||||
created_by_role: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
created_by: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
||||
created_at: Mapped[datetime] = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
|
||||
|
|
@ -1323,7 +1323,7 @@ class MessageAnnotation(Base):
|
|||
message_id: Mapped[Optional[str]] = mapped_column(StringUUID)
|
||||
question = db.Column(db.Text, nullable=True)
|
||||
content = mapped_column(db.Text, nullable=False)
|
||||
hit_count = mapped_column(db.Integer, nullable=False, server_default=db.text("0"))
|
||||
hit_count: Mapped[int] = mapped_column(db.Integer, nullable=False, server_default=db.text("0"))
|
||||
account_id = mapped_column(StringUUID, nullable=False)
|
||||
created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
updated_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
|
|
@ -1415,10 +1415,10 @@ class OperationLog(Base):
|
|||
id = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()"))
|
||||
tenant_id = mapped_column(StringUUID, nullable=False)
|
||||
account_id = mapped_column(StringUUID, nullable=False)
|
||||
action = mapped_column(db.String(255), nullable=False)
|
||||
action: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
content = mapped_column(db.JSON)
|
||||
created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
created_ip = mapped_column(db.String(255), nullable=False)
|
||||
created_ip: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
updated_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
|
||||
|
||||
|
|
@ -1433,10 +1433,10 @@ class EndUser(Base, UserMixin):
|
|||
id = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()"))
|
||||
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
||||
app_id = mapped_column(StringUUID, nullable=True)
|
||||
type = mapped_column(db.String(255), nullable=False)
|
||||
external_user_id = mapped_column(db.String(255), nullable=True)
|
||||
name = mapped_column(db.String(255))
|
||||
is_anonymous = mapped_column(db.Boolean, nullable=False, server_default=db.text("true"))
|
||||
type: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
external_user_id = mapped_column(String(255), nullable=True)
|
||||
name = mapped_column(String(255))
|
||||
is_anonymous: Mapped[bool] = mapped_column(db.Boolean, nullable=False, server_default=db.text("true"))
|
||||
session_id: Mapped[str] = mapped_column()
|
||||
created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
updated_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
|
|
@ -1452,10 +1452,10 @@ class AppMCPServer(Base):
|
|||
id = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()"))
|
||||
tenant_id = mapped_column(StringUUID, nullable=False)
|
||||
app_id = mapped_column(StringUUID, nullable=False)
|
||||
name = mapped_column(db.String(255), nullable=False)
|
||||
description = mapped_column(db.String(255), nullable=False)
|
||||
server_code = mapped_column(db.String(255), nullable=False)
|
||||
status = mapped_column(db.String(255), nullable=False, server_default=db.text("'normal'::character varying"))
|
||||
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
description: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
server_code: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
status = mapped_column(String(255), nullable=False, server_default=db.text("'normal'::character varying"))
|
||||
parameters = mapped_column(db.Text, nullable=False)
|
||||
|
||||
created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
|
|
@ -1485,28 +1485,28 @@ class Site(Base):
|
|||
|
||||
id = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()"))
|
||||
app_id = mapped_column(StringUUID, nullable=False)
|
||||
title = mapped_column(db.String(255), nullable=False)
|
||||
icon_type = mapped_column(db.String(255), nullable=True)
|
||||
icon = mapped_column(db.String(255))
|
||||
icon_background = mapped_column(db.String(255))
|
||||
title: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
icon_type = mapped_column(String(255), nullable=True)
|
||||
icon = mapped_column(String(255))
|
||||
icon_background = mapped_column(String(255))
|
||||
description = mapped_column(db.Text)
|
||||
default_language = mapped_column(db.String(255), nullable=False)
|
||||
chat_color_theme = mapped_column(db.String(255))
|
||||
chat_color_theme_inverted = mapped_column(db.Boolean, nullable=False, server_default=db.text("false"))
|
||||
copyright = mapped_column(db.String(255))
|
||||
privacy_policy = mapped_column(db.String(255))
|
||||
show_workflow_steps = mapped_column(db.Boolean, nullable=False, server_default=db.text("true"))
|
||||
use_icon_as_answer_icon = mapped_column(db.Boolean, nullable=False, server_default=db.text("false"))
|
||||
default_language: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
chat_color_theme = mapped_column(String(255))
|
||||
chat_color_theme_inverted: Mapped[bool] = mapped_column(db.Boolean, nullable=False, server_default=db.text("false"))
|
||||
copyright = mapped_column(String(255))
|
||||
privacy_policy = mapped_column(String(255))
|
||||
show_workflow_steps: Mapped[bool] = mapped_column(db.Boolean, nullable=False, server_default=db.text("true"))
|
||||
use_icon_as_answer_icon: Mapped[bool] = mapped_column(db.Boolean, nullable=False, server_default=db.text("false"))
|
||||
_custom_disclaimer: Mapped[str] = mapped_column("custom_disclaimer", sa.TEXT, default="")
|
||||
customize_domain = mapped_column(db.String(255))
|
||||
customize_token_strategy = mapped_column(db.String(255), nullable=False)
|
||||
prompt_public = mapped_column(db.Boolean, nullable=False, server_default=db.text("false"))
|
||||
status = mapped_column(db.String(255), nullable=False, server_default=db.text("'normal'::character varying"))
|
||||
customize_domain = mapped_column(String(255))
|
||||
customize_token_strategy: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
prompt_public: Mapped[bool] = mapped_column(db.Boolean, nullable=False, server_default=db.text("false"))
|
||||
status = mapped_column(String(255), nullable=False, server_default=db.text("'normal'::character varying"))
|
||||
created_by = mapped_column(StringUUID, nullable=True)
|
||||
created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
updated_by = mapped_column(StringUUID, nullable=True)
|
||||
updated_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
code = mapped_column(db.String(255))
|
||||
code = mapped_column(String(255))
|
||||
|
||||
@property
|
||||
def custom_disclaimer(self):
|
||||
|
|
@ -1544,8 +1544,8 @@ class ApiToken(Base):
|
|||
id = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()"))
|
||||
app_id = mapped_column(StringUUID, nullable=True)
|
||||
tenant_id = mapped_column(StringUUID, nullable=True)
|
||||
type = mapped_column(db.String(16), nullable=False)
|
||||
token = mapped_column(db.String(255), nullable=False)
|
||||
type = mapped_column(String(16), nullable=False)
|
||||
token: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
last_used_at = mapped_column(db.DateTime, nullable=True)
|
||||
created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
|
||||
|
|
@ -1567,21 +1567,21 @@ class UploadFile(Base):
|
|||
|
||||
id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()"))
|
||||
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
||||
storage_type: Mapped[str] = mapped_column(db.String(255), nullable=False)
|
||||
key: Mapped[str] = mapped_column(db.String(255), nullable=False)
|
||||
name: Mapped[str] = mapped_column(db.String(255), nullable=False)
|
||||
storage_type: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
key: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
size: Mapped[int] = mapped_column(db.Integer, nullable=False)
|
||||
extension: Mapped[str] = mapped_column(db.String(255), nullable=False)
|
||||
mime_type: Mapped[str] = mapped_column(db.String(255), nullable=True)
|
||||
extension: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
mime_type: Mapped[str] = mapped_column(String(255), nullable=True)
|
||||
created_by_role: Mapped[str] = mapped_column(
|
||||
db.String(255), nullable=False, server_default=db.text("'account'::character varying")
|
||||
String(255), nullable=False, server_default=db.text("'account'::character varying")
|
||||
)
|
||||
created_by: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
||||
created_at: Mapped[datetime] = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
used: Mapped[bool] = mapped_column(db.Boolean, nullable=False, server_default=db.text("false"))
|
||||
used_by: Mapped[str | None] = mapped_column(StringUUID, nullable=True)
|
||||
used_at: Mapped[datetime | None] = mapped_column(db.DateTime, nullable=True)
|
||||
hash: Mapped[str | None] = mapped_column(db.String(255), nullable=True)
|
||||
hash: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
||||
source_url: Mapped[str] = mapped_column(sa.TEXT, default="")
|
||||
|
||||
def __init__(
|
||||
|
|
@ -1630,10 +1630,10 @@ class ApiRequest(Base):
|
|||
id = mapped_column(StringUUID, nullable=False, server_default=db.text("uuid_generate_v4()"))
|
||||
tenant_id = mapped_column(StringUUID, nullable=False)
|
||||
api_token_id = mapped_column(StringUUID, nullable=False)
|
||||
path = mapped_column(db.String(255), nullable=False)
|
||||
path: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
request = mapped_column(db.Text, nullable=True)
|
||||
response = mapped_column(db.Text, nullable=True)
|
||||
ip = mapped_column(db.String(255), nullable=False)
|
||||
ip: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
|
||||
|
||||
|
|
@ -1646,7 +1646,7 @@ class MessageChain(Base):
|
|||
|
||||
id = mapped_column(StringUUID, nullable=False, server_default=db.text("uuid_generate_v4()"))
|
||||
message_id = mapped_column(StringUUID, nullable=False)
|
||||
type = mapped_column(db.String(255), nullable=False)
|
||||
type: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
input = mapped_column(db.Text, nullable=True)
|
||||
output = mapped_column(db.Text, nullable=True)
|
||||
created_at = mapped_column(db.DateTime, nullable=False, server_default=db.func.current_timestamp())
|
||||
|
|
@ -1663,7 +1663,7 @@ class MessageAgentThought(Base):
|
|||
id = mapped_column(StringUUID, nullable=False, server_default=db.text("uuid_generate_v4()"))
|
||||
message_id = mapped_column(StringUUID, nullable=False)
|
||||
message_chain_id = mapped_column(StringUUID, nullable=True)
|
||||
position = mapped_column(db.Integer, nullable=False)
|
||||
position: Mapped[int] = mapped_column(db.Integer, nullable=False)
|
||||
thought = mapped_column(db.Text, nullable=True)
|
||||
tool = mapped_column(db.Text, nullable=True)
|
||||
tool_labels_str = mapped_column(db.Text, nullable=False, server_default=db.text("'{}'::text"))
|
||||
|
|
@ -1673,19 +1673,19 @@ class MessageAgentThought(Base):
|
|||
# plugin_id = mapped_column(StringUUID, nullable=True) ## for future design
|
||||
tool_process_data = mapped_column(db.Text, nullable=True)
|
||||
message = mapped_column(db.Text, nullable=True)
|
||||
message_token = mapped_column(db.Integer, nullable=True)
|
||||
message_token: Mapped[Optional[int]] = mapped_column(db.Integer, nullable=True)
|
||||
message_unit_price = mapped_column(db.Numeric, nullable=True)
|
||||
message_price_unit = mapped_column(db.Numeric(10, 7), nullable=False, server_default=db.text("0.001"))
|
||||
message_files = mapped_column(db.Text, nullable=True)
|
||||
answer = db.Column(db.Text, nullable=True)
|
||||
answer_token = mapped_column(db.Integer, nullable=True)
|
||||
answer_token: Mapped[Optional[int]] = mapped_column(db.Integer, nullable=True)
|
||||
answer_unit_price = mapped_column(db.Numeric, nullable=True)
|
||||
answer_price_unit = mapped_column(db.Numeric(10, 7), nullable=False, server_default=db.text("0.001"))
|
||||
tokens = mapped_column(db.Integer, nullable=True)
|
||||
tokens: Mapped[Optional[int]] = mapped_column(db.Integer, nullable=True)
|
||||
total_price = mapped_column(db.Numeric, nullable=True)
|
||||
currency = mapped_column(db.String, nullable=True)
|
||||
latency = mapped_column(db.Float, nullable=True)
|
||||
created_by_role = mapped_column(db.String, nullable=False)
|
||||
currency = mapped_column(String, nullable=True)
|
||||
latency: Mapped[Optional[float]] = mapped_column(db.Float, nullable=True)
|
||||
created_by_role = mapped_column(String, nullable=False)
|
||||
created_by = mapped_column(StringUUID, nullable=False)
|
||||
created_at = mapped_column(db.DateTime, nullable=False, server_default=db.func.current_timestamp())
|
||||
|
||||
|
|
@ -1775,18 +1775,18 @@ class DatasetRetrieverResource(Base):
|
|||
|
||||
id = mapped_column(StringUUID, nullable=False, server_default=db.text("uuid_generate_v4()"))
|
||||
message_id = mapped_column(StringUUID, nullable=False)
|
||||
position = mapped_column(db.Integer, nullable=False)
|
||||
position: Mapped[int] = mapped_column(db.Integer, nullable=False)
|
||||
dataset_id = mapped_column(StringUUID, nullable=False)
|
||||
dataset_name = mapped_column(db.Text, nullable=False)
|
||||
document_id = mapped_column(StringUUID, nullable=True)
|
||||
document_name = mapped_column(db.Text, nullable=False)
|
||||
data_source_type = mapped_column(db.Text, nullable=True)
|
||||
segment_id = mapped_column(StringUUID, nullable=True)
|
||||
score = mapped_column(db.Float, nullable=True)
|
||||
score: Mapped[Optional[float]] = mapped_column(db.Float, nullable=True)
|
||||
content = mapped_column(db.Text, nullable=False)
|
||||
hit_count = mapped_column(db.Integer, nullable=True)
|
||||
word_count = mapped_column(db.Integer, nullable=True)
|
||||
segment_position = mapped_column(db.Integer, nullable=True)
|
||||
hit_count: Mapped[Optional[int]] = mapped_column(db.Integer, nullable=True)
|
||||
word_count: Mapped[Optional[int]] = mapped_column(db.Integer, nullable=True)
|
||||
segment_position: Mapped[Optional[int]] = mapped_column(db.Integer, nullable=True)
|
||||
index_node_hash = mapped_column(db.Text, nullable=True)
|
||||
retriever_from = mapped_column(db.Text, nullable=False)
|
||||
created_by = mapped_column(StringUUID, nullable=False)
|
||||
|
|
@ -1805,8 +1805,8 @@ class Tag(Base):
|
|||
|
||||
id = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()"))
|
||||
tenant_id = mapped_column(StringUUID, nullable=True)
|
||||
type = mapped_column(db.String(16), nullable=False)
|
||||
name = mapped_column(db.String(255), nullable=False)
|
||||
type = mapped_column(String(16), nullable=False)
|
||||
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
created_by = mapped_column(StringUUID, nullable=False)
|
||||
created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
|
||||
|
|
@ -1836,13 +1836,13 @@ class TraceAppConfig(Base):
|
|||
|
||||
id = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()"))
|
||||
app_id = mapped_column(StringUUID, nullable=False)
|
||||
tracing_provider = mapped_column(db.String(255), nullable=True)
|
||||
tracing_provider = mapped_column(String(255), nullable=True)
|
||||
tracing_config = mapped_column(db.JSON, nullable=True)
|
||||
created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
updated_at = mapped_column(
|
||||
db.DateTime, nullable=False, server_default=func.current_timestamp(), onupdate=func.current_timestamp()
|
||||
)
|
||||
is_active = mapped_column(db.Boolean, nullable=False, server_default=db.text("true"))
|
||||
is_active: Mapped[bool] = mapped_column(db.Boolean, nullable=False, server_default=db.text("true"))
|
||||
|
||||
@property
|
||||
def tracing_config_dict(self):
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ from datetime import datetime
|
|||
from enum import Enum
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import func, text
|
||||
from sqlalchemy import DateTime, String, func, text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from .base import Base
|
||||
|
|
@ -56,22 +56,22 @@ class Provider(Base):
|
|||
|
||||
id: Mapped[str] = mapped_column(StringUUID, server_default=text("uuid_generate_v4()"))
|
||||
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
||||
provider_name: Mapped[str] = mapped_column(db.String(255), nullable=False)
|
||||
provider_name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
provider_type: Mapped[str] = mapped_column(
|
||||
db.String(40), nullable=False, server_default=text("'custom'::character varying")
|
||||
String(40), nullable=False, server_default=text("'custom'::character varying")
|
||||
)
|
||||
encrypted_config: Mapped[Optional[str]] = mapped_column(db.Text, nullable=True)
|
||||
is_valid: Mapped[bool] = mapped_column(db.Boolean, nullable=False, server_default=text("false"))
|
||||
last_used: Mapped[Optional[datetime]] = mapped_column(db.DateTime, nullable=True)
|
||||
last_used: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
|
||||
|
||||
quota_type: Mapped[Optional[str]] = mapped_column(
|
||||
db.String(40), nullable=True, server_default=text("''::character varying")
|
||||
String(40), nullable=True, server_default=text("''::character varying")
|
||||
)
|
||||
quota_limit: Mapped[Optional[int]] = mapped_column(db.BigInteger, nullable=True)
|
||||
quota_used: Mapped[Optional[int]] = mapped_column(db.BigInteger, default=0)
|
||||
|
||||
created_at: Mapped[datetime] = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
updated_at: Mapped[datetime] = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
|
||||
def __repr__(self):
|
||||
return (
|
||||
|
|
@ -113,13 +113,13 @@ class ProviderModel(Base):
|
|||
|
||||
id: Mapped[str] = mapped_column(StringUUID, server_default=text("uuid_generate_v4()"))
|
||||
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
||||
provider_name: Mapped[str] = mapped_column(db.String(255), nullable=False)
|
||||
model_name: Mapped[str] = mapped_column(db.String(255), nullable=False)
|
||||
model_type: Mapped[str] = mapped_column(db.String(40), nullable=False)
|
||||
provider_name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
model_name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
model_type: Mapped[str] = mapped_column(String(40), nullable=False)
|
||||
encrypted_config: Mapped[Optional[str]] = mapped_column(db.Text, nullable=True)
|
||||
is_valid: Mapped[bool] = mapped_column(db.Boolean, nullable=False, server_default=text("false"))
|
||||
created_at: Mapped[datetime] = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
updated_at: Mapped[datetime] = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
|
||||
|
||||
class TenantDefaultModel(Base):
|
||||
|
|
@ -131,11 +131,11 @@ class TenantDefaultModel(Base):
|
|||
|
||||
id: Mapped[str] = mapped_column(StringUUID, server_default=text("uuid_generate_v4()"))
|
||||
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
||||
provider_name: Mapped[str] = mapped_column(db.String(255), nullable=False)
|
||||
model_name: Mapped[str] = mapped_column(db.String(255), nullable=False)
|
||||
model_type: Mapped[str] = mapped_column(db.String(40), nullable=False)
|
||||
created_at: Mapped[datetime] = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
updated_at: Mapped[datetime] = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
provider_name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
model_name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
model_type: Mapped[str] = mapped_column(String(40), nullable=False)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
|
||||
|
||||
class TenantPreferredModelProvider(Base):
|
||||
|
|
@ -147,10 +147,10 @@ class TenantPreferredModelProvider(Base):
|
|||
|
||||
id: Mapped[str] = mapped_column(StringUUID, server_default=text("uuid_generate_v4()"))
|
||||
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
||||
provider_name: Mapped[str] = mapped_column(db.String(255), nullable=False)
|
||||
preferred_provider_type: Mapped[str] = mapped_column(db.String(40), nullable=False)
|
||||
created_at: Mapped[datetime] = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
updated_at: Mapped[datetime] = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
provider_name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
preferred_provider_type: Mapped[str] = mapped_column(String(40), nullable=False)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
|
||||
|
||||
class ProviderOrder(Base):
|
||||
|
|
@ -162,22 +162,22 @@ class ProviderOrder(Base):
|
|||
|
||||
id: Mapped[str] = mapped_column(StringUUID, server_default=text("uuid_generate_v4()"))
|
||||
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
||||
provider_name: Mapped[str] = mapped_column(db.String(255), nullable=False)
|
||||
provider_name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
account_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
||||
payment_product_id: Mapped[str] = mapped_column(db.String(191), nullable=False)
|
||||
payment_id: Mapped[Optional[str]] = mapped_column(db.String(191))
|
||||
transaction_id: Mapped[Optional[str]] = mapped_column(db.String(191))
|
||||
payment_product_id: Mapped[str] = mapped_column(String(191), nullable=False)
|
||||
payment_id: Mapped[Optional[str]] = mapped_column(String(191))
|
||||
transaction_id: Mapped[Optional[str]] = mapped_column(String(191))
|
||||
quantity: Mapped[int] = mapped_column(db.Integer, nullable=False, server_default=text("1"))
|
||||
currency: Mapped[Optional[str]] = mapped_column(db.String(40))
|
||||
currency: Mapped[Optional[str]] = mapped_column(String(40))
|
||||
total_amount: Mapped[Optional[int]] = mapped_column(db.Integer)
|
||||
payment_status: Mapped[str] = mapped_column(
|
||||
db.String(40), nullable=False, server_default=text("'wait_pay'::character varying")
|
||||
String(40), nullable=False, server_default=text("'wait_pay'::character varying")
|
||||
)
|
||||
paid_at: Mapped[Optional[datetime]] = mapped_column(db.DateTime)
|
||||
pay_failed_at: Mapped[Optional[datetime]] = mapped_column(db.DateTime)
|
||||
refunded_at: Mapped[Optional[datetime]] = mapped_column(db.DateTime)
|
||||
created_at: Mapped[datetime] = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
updated_at: Mapped[datetime] = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
paid_at: Mapped[Optional[datetime]] = mapped_column(DateTime)
|
||||
pay_failed_at: Mapped[Optional[datetime]] = mapped_column(DateTime)
|
||||
refunded_at: Mapped[Optional[datetime]] = mapped_column(DateTime)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
|
||||
|
||||
class ProviderModelSetting(Base):
|
||||
|
|
@ -193,13 +193,13 @@ class ProviderModelSetting(Base):
|
|||
|
||||
id: Mapped[str] = mapped_column(StringUUID, server_default=text("uuid_generate_v4()"))
|
||||
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
||||
provider_name: Mapped[str] = mapped_column(db.String(255), nullable=False)
|
||||
model_name: Mapped[str] = mapped_column(db.String(255), nullable=False)
|
||||
model_type: Mapped[str] = mapped_column(db.String(40), nullable=False)
|
||||
provider_name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
model_name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
model_type: Mapped[str] = mapped_column(String(40), nullable=False)
|
||||
enabled: Mapped[bool] = mapped_column(db.Boolean, nullable=False, server_default=text("true"))
|
||||
load_balancing_enabled: Mapped[bool] = mapped_column(db.Boolean, nullable=False, server_default=text("false"))
|
||||
created_at: Mapped[datetime] = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
updated_at: Mapped[datetime] = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
|
||||
|
||||
class LoadBalancingModelConfig(Base):
|
||||
|
|
@ -215,11 +215,11 @@ class LoadBalancingModelConfig(Base):
|
|||
|
||||
id: Mapped[str] = mapped_column(StringUUID, server_default=text("uuid_generate_v4()"))
|
||||
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
||||
provider_name: Mapped[str] = mapped_column(db.String(255), nullable=False)
|
||||
model_name: Mapped[str] = mapped_column(db.String(255), nullable=False)
|
||||
model_type: Mapped[str] = mapped_column(db.String(40), nullable=False)
|
||||
name: Mapped[str] = mapped_column(db.String(255), nullable=False)
|
||||
provider_name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
model_name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
model_type: Mapped[str] = mapped_column(String(40), nullable=False)
|
||||
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
encrypted_config: Mapped[Optional[str]] = mapped_column(db.Text, nullable=True)
|
||||
enabled: Mapped[bool] = mapped_column(db.Boolean, nullable=False, server_default=text("true"))
|
||||
created_at: Mapped[datetime] = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
updated_at: Mapped[datetime] = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
import json
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy import DateTime, String, func
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
from sqlalchemy.orm import mapped_column
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from models.base import Base
|
||||
|
||||
|
|
@ -20,12 +22,12 @@ class DataSourceOauthBinding(Base):
|
|||
|
||||
id = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()"))
|
||||
tenant_id = mapped_column(StringUUID, nullable=False)
|
||||
access_token = mapped_column(db.String(255), nullable=False)
|
||||
provider = mapped_column(db.String(255), nullable=False)
|
||||
access_token: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
provider: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
source_info = mapped_column(JSONB, nullable=False)
|
||||
created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
updated_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
disabled = mapped_column(db.Boolean, nullable=True, server_default=db.text("false"))
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
disabled: Mapped[Optional[bool]] = mapped_column(db.Boolean, nullable=True, server_default=db.text("false"))
|
||||
|
||||
|
||||
class DataSourceApiKeyAuthBinding(Base):
|
||||
|
|
@ -38,12 +40,12 @@ class DataSourceApiKeyAuthBinding(Base):
|
|||
|
||||
id = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()"))
|
||||
tenant_id = mapped_column(StringUUID, nullable=False)
|
||||
category = mapped_column(db.String(255), nullable=False)
|
||||
provider = mapped_column(db.String(255), nullable=False)
|
||||
category: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
provider: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
credentials = mapped_column(db.Text, nullable=True) # JSON
|
||||
created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
updated_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
disabled = mapped_column(db.Boolean, nullable=True, server_default=db.text("false"))
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
disabled: Mapped[Optional[bool]] = mapped_column(db.Boolean, nullable=True, server_default=db.text("false"))
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ from datetime import datetime
|
|||
from typing import Optional
|
||||
|
||||
from celery import states # type: ignore
|
||||
from sqlalchemy import DateTime, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from libs.datetime_utils import naive_utc_now
|
||||
|
|
@ -16,22 +17,22 @@ class CeleryTask(Base):
|
|||
__tablename__ = "celery_taskmeta"
|
||||
|
||||
id = mapped_column(db.Integer, db.Sequence("task_id_sequence"), primary_key=True, autoincrement=True)
|
||||
task_id = mapped_column(db.String(155), unique=True)
|
||||
status = mapped_column(db.String(50), default=states.PENDING)
|
||||
task_id = mapped_column(String(155), unique=True)
|
||||
status = mapped_column(String(50), default=states.PENDING)
|
||||
result = mapped_column(db.PickleType, nullable=True)
|
||||
date_done = mapped_column(
|
||||
db.DateTime,
|
||||
DateTime,
|
||||
default=lambda: naive_utc_now(),
|
||||
onupdate=lambda: naive_utc_now(),
|
||||
nullable=True,
|
||||
)
|
||||
traceback = mapped_column(db.Text, nullable=True)
|
||||
name = mapped_column(db.String(155), nullable=True)
|
||||
name = mapped_column(String(155), nullable=True)
|
||||
args = mapped_column(db.LargeBinary, nullable=True)
|
||||
kwargs = mapped_column(db.LargeBinary, nullable=True)
|
||||
worker = mapped_column(db.String(155), nullable=True)
|
||||
retries = mapped_column(db.Integer, nullable=True)
|
||||
queue = mapped_column(db.String(155), nullable=True)
|
||||
worker = mapped_column(String(155), nullable=True)
|
||||
retries: Mapped[Optional[int]] = mapped_column(db.Integer, nullable=True)
|
||||
queue = mapped_column(String(155), nullable=True)
|
||||
|
||||
|
||||
class CeleryTaskSet(Base):
|
||||
|
|
@ -42,6 +43,6 @@ class CeleryTaskSet(Base):
|
|||
id: Mapped[int] = mapped_column(
|
||||
db.Integer, db.Sequence("taskset_id_sequence"), autoincrement=True, primary_key=True
|
||||
)
|
||||
taskset_id = mapped_column(db.String(155), unique=True)
|
||||
taskset_id = mapped_column(String(155), unique=True)
|
||||
result = mapped_column(db.PickleType, nullable=True)
|
||||
date_done: Mapped[Optional[datetime]] = mapped_column(db.DateTime, default=lambda: naive_utc_now(), nullable=True)
|
||||
date_done: Mapped[Optional[datetime]] = mapped_column(DateTime, default=lambda: naive_utc_now(), nullable=True)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ from urllib.parse import urlparse
|
|||
|
||||
import sqlalchemy as sa
|
||||
from deprecated import deprecated
|
||||
from sqlalchemy import ForeignKey, func
|
||||
from sqlalchemy import ForeignKey, String, func
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from core.file import helpers as file_helpers
|
||||
|
|
@ -30,8 +30,8 @@ class ToolOAuthSystemClient(Base):
|
|||
)
|
||||
|
||||
id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()"))
|
||||
plugin_id: Mapped[str] = mapped_column(db.String(512), nullable=False)
|
||||
provider: Mapped[str] = mapped_column(db.String(255), nullable=False)
|
||||
plugin_id = mapped_column(String(512), nullable=False)
|
||||
provider: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
# oauth params of the tool provider
|
||||
encrypted_oauth_params: Mapped[str] = mapped_column(db.Text, nullable=False)
|
||||
|
||||
|
|
@ -47,8 +47,8 @@ class ToolOAuthTenantClient(Base):
|
|||
id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()"))
|
||||
# tenant id
|
||||
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
||||
plugin_id: Mapped[str] = mapped_column(db.String(512), nullable=False)
|
||||
provider: Mapped[str] = mapped_column(db.String(255), nullable=False)
|
||||
plugin_id: Mapped[str] = mapped_column(String(512), nullable=False)
|
||||
provider: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
enabled: Mapped[bool] = mapped_column(db.Boolean, nullable=False, server_default=db.text("true"))
|
||||
# oauth params of the tool provider
|
||||
encrypted_oauth_params: Mapped[str] = mapped_column(db.Text, nullable=False)
|
||||
|
|
@ -72,26 +72,26 @@ class BuiltinToolProvider(Base):
|
|||
# id of the tool provider
|
||||
id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()"))
|
||||
name: Mapped[str] = mapped_column(
|
||||
db.String(256), nullable=False, server_default=db.text("'API KEY 1'::character varying")
|
||||
String(256), nullable=False, server_default=db.text("'API KEY 1'::character varying")
|
||||
)
|
||||
# id of the tenant
|
||||
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=True)
|
||||
# who created this tool provider
|
||||
user_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
||||
# name of the tool provider
|
||||
provider: Mapped[str] = mapped_column(db.String(256), nullable=False)
|
||||
provider: Mapped[str] = mapped_column(String(256), nullable=False)
|
||||
# credential of the tool provider
|
||||
encrypted_credentials: Mapped[str] = mapped_column(db.Text, nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)")
|
||||
sa.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)")
|
||||
)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)")
|
||||
sa.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)")
|
||||
)
|
||||
is_default: Mapped[bool] = mapped_column(db.Boolean, nullable=False, server_default=db.text("false"))
|
||||
# credential type, e.g., "api-key", "oauth2"
|
||||
credential_type: Mapped[str] = mapped_column(
|
||||
db.String(32), nullable=False, server_default=db.text("'api-key'::character varying")
|
||||
String(32), nullable=False, server_default=db.text("'api-key'::character varying")
|
||||
)
|
||||
expires_at: Mapped[int] = mapped_column(db.BigInteger, nullable=False, server_default=db.text("-1"))
|
||||
|
||||
|
|
@ -113,12 +113,12 @@ class ApiToolProvider(Base):
|
|||
|
||||
id = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()"))
|
||||
# name of the api provider
|
||||
name = mapped_column(db.String(255), nullable=False, server_default=db.text("'API KEY 1'::character varying"))
|
||||
name = mapped_column(String(255), nullable=False, server_default=db.text("'API KEY 1'::character varying"))
|
||||
# icon
|
||||
icon = mapped_column(db.String(255), nullable=False)
|
||||
icon: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
# original schema
|
||||
schema = mapped_column(db.Text, nullable=False)
|
||||
schema_type_str: Mapped[str] = mapped_column(db.String(40), nullable=False)
|
||||
schema_type_str: Mapped[str] = mapped_column(String(40), nullable=False)
|
||||
# who created this tool
|
||||
user_id = mapped_column(StringUUID, nullable=False)
|
||||
# tenant id
|
||||
|
|
@ -130,12 +130,12 @@ class ApiToolProvider(Base):
|
|||
# json format credentials
|
||||
credentials_str = mapped_column(db.Text, nullable=False)
|
||||
# privacy policy
|
||||
privacy_policy = mapped_column(db.String(255), nullable=True)
|
||||
privacy_policy = mapped_column(String(255), nullable=True)
|
||||
# custom_disclaimer
|
||||
custom_disclaimer: Mapped[str] = mapped_column(sa.TEXT, default="")
|
||||
|
||||
created_at: Mapped[datetime] = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
updated_at: Mapped[datetime] = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
created_at: Mapped[datetime] = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
updated_at: Mapped[datetime] = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
|
||||
@property
|
||||
def schema_type(self) -> ApiProviderSchemaType:
|
||||
|
|
@ -173,11 +173,11 @@ class ToolLabelBinding(Base):
|
|||
|
||||
id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()"))
|
||||
# tool id
|
||||
tool_id: Mapped[str] = mapped_column(db.String(64), nullable=False)
|
||||
tool_id: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||
# tool type
|
||||
tool_type: Mapped[str] = mapped_column(db.String(40), nullable=False)
|
||||
tool_type: Mapped[str] = mapped_column(String(40), nullable=False)
|
||||
# label name
|
||||
label_name: Mapped[str] = mapped_column(db.String(40), nullable=False)
|
||||
label_name: Mapped[str] = mapped_column(String(40), nullable=False)
|
||||
|
||||
|
||||
class WorkflowToolProvider(Base):
|
||||
|
|
@ -194,15 +194,15 @@ class WorkflowToolProvider(Base):
|
|||
|
||||
id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()"))
|
||||
# name of the workflow provider
|
||||
name: Mapped[str] = mapped_column(db.String(255), nullable=False)
|
||||
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
# label of the workflow provider
|
||||
label: Mapped[str] = mapped_column(db.String(255), nullable=False, server_default="")
|
||||
label: Mapped[str] = mapped_column(String(255), nullable=False, server_default="")
|
||||
# icon
|
||||
icon: Mapped[str] = mapped_column(db.String(255), nullable=False)
|
||||
icon: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
# app id of the workflow provider
|
||||
app_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
||||
# version of the workflow provider
|
||||
version: Mapped[str] = mapped_column(db.String(255), nullable=False, server_default="")
|
||||
version: Mapped[str] = mapped_column(String(255), nullable=False, server_default="")
|
||||
# who created this tool
|
||||
user_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
||||
# tenant id
|
||||
|
|
@ -212,13 +212,13 @@ class WorkflowToolProvider(Base):
|
|||
# parameter configuration
|
||||
parameter_configuration: Mapped[str] = mapped_column(db.Text, nullable=False, server_default="[]")
|
||||
# privacy policy
|
||||
privacy_policy: Mapped[str] = mapped_column(db.String(255), nullable=True, server_default="")
|
||||
privacy_policy: Mapped[str] = mapped_column(String(255), nullable=True, server_default="")
|
||||
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)")
|
||||
sa.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)")
|
||||
)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)")
|
||||
sa.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)")
|
||||
)
|
||||
|
||||
@property
|
||||
|
|
@ -253,15 +253,15 @@ class MCPToolProvider(Base):
|
|||
|
||||
id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()"))
|
||||
# name of the mcp provider
|
||||
name: Mapped[str] = mapped_column(db.String(40), nullable=False)
|
||||
name: Mapped[str] = mapped_column(String(40), nullable=False)
|
||||
# server identifier of the mcp provider
|
||||
server_identifier: Mapped[str] = mapped_column(db.String(64), nullable=False)
|
||||
server_identifier: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||
# encrypted url of the mcp provider
|
||||
server_url: Mapped[str] = mapped_column(db.Text, nullable=False)
|
||||
# hash of server_url for uniqueness check
|
||||
server_url_hash: Mapped[str] = mapped_column(db.String(64), nullable=False)
|
||||
server_url_hash: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||
# icon of the mcp provider
|
||||
icon: Mapped[str] = mapped_column(db.String(255), nullable=True)
|
||||
icon: Mapped[str] = mapped_column(String(255), nullable=True)
|
||||
# tenant id
|
||||
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
||||
# who created this tool
|
||||
|
|
@ -273,10 +273,10 @@ class MCPToolProvider(Base):
|
|||
# tools
|
||||
tools: Mapped[str] = mapped_column(db.Text, nullable=False, default="[]")
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)")
|
||||
sa.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)")
|
||||
)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)")
|
||||
sa.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)")
|
||||
)
|
||||
|
||||
def load_user(self) -> Account | None:
|
||||
|
|
@ -355,11 +355,11 @@ class ToolModelInvoke(Base):
|
|||
# tenant id
|
||||
tenant_id = mapped_column(StringUUID, nullable=False)
|
||||
# provider
|
||||
provider = mapped_column(db.String(255), nullable=False)
|
||||
provider: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
# type
|
||||
tool_type = mapped_column(db.String(40), nullable=False)
|
||||
tool_type = mapped_column(String(40), nullable=False)
|
||||
# tool name
|
||||
tool_name = mapped_column(db.String(128), nullable=False)
|
||||
tool_name = mapped_column(String(128), nullable=False)
|
||||
# invoke parameters
|
||||
model_parameters = mapped_column(db.Text, nullable=False)
|
||||
# prompt messages
|
||||
|
|
@ -367,15 +367,15 @@ class ToolModelInvoke(Base):
|
|||
# invoke response
|
||||
model_response = mapped_column(db.Text, nullable=False)
|
||||
|
||||
prompt_tokens = mapped_column(db.Integer, nullable=False, server_default=db.text("0"))
|
||||
answer_tokens = mapped_column(db.Integer, nullable=False, server_default=db.text("0"))
|
||||
prompt_tokens: Mapped[int] = mapped_column(db.Integer, nullable=False, server_default=db.text("0"))
|
||||
answer_tokens: Mapped[int] = mapped_column(db.Integer, nullable=False, server_default=db.text("0"))
|
||||
answer_unit_price = mapped_column(db.Numeric(10, 4), nullable=False)
|
||||
answer_price_unit = mapped_column(db.Numeric(10, 7), nullable=False, server_default=db.text("0.001"))
|
||||
provider_response_latency = mapped_column(db.Float, nullable=False, server_default=db.text("0"))
|
||||
total_price = mapped_column(db.Numeric(10, 7))
|
||||
currency = mapped_column(db.String(255), nullable=False)
|
||||
created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
updated_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
currency: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
created_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
updated_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
|
||||
|
||||
@deprecated
|
||||
|
|
@ -402,8 +402,8 @@ class ToolConversationVariables(Base):
|
|||
# variables pool
|
||||
variables_str = mapped_column(db.Text, nullable=False)
|
||||
|
||||
created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
updated_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
created_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
updated_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
|
||||
@property
|
||||
def variables(self) -> Any:
|
||||
|
|
@ -429,11 +429,11 @@ class ToolFile(Base):
|
|||
# conversation id
|
||||
conversation_id: Mapped[str] = mapped_column(StringUUID, nullable=True)
|
||||
# file key
|
||||
file_key: Mapped[str] = mapped_column(db.String(255), nullable=False)
|
||||
file_key: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
# mime type
|
||||
mimetype: Mapped[str] = mapped_column(db.String(255), nullable=False)
|
||||
mimetype: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
# original url
|
||||
original_url: Mapped[str] = mapped_column(db.String(2048), nullable=True)
|
||||
original_url: Mapped[str] = mapped_column(String(2048), nullable=True)
|
||||
# name
|
||||
name: Mapped[str] = mapped_column(default="")
|
||||
# size
|
||||
|
|
@ -465,13 +465,13 @@ class DeprecatedPublishedAppTool(Base):
|
|||
# to describe this parameter to llm, we need this field
|
||||
query_description = mapped_column(db.Text, nullable=False)
|
||||
# query name, the name of the query parameter
|
||||
query_name = mapped_column(db.String(40), nullable=False)
|
||||
query_name = mapped_column(String(40), nullable=False)
|
||||
# name of the tool provider
|
||||
tool_name = mapped_column(db.String(40), nullable=False)
|
||||
tool_name = mapped_column(String(40), nullable=False)
|
||||
# author
|
||||
author = mapped_column(db.String(40), nullable=False)
|
||||
created_at = mapped_column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)"))
|
||||
updated_at = mapped_column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)"))
|
||||
author = mapped_column(String(40), nullable=False)
|
||||
created_at = mapped_column(sa.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)"))
|
||||
updated_at = mapped_column(sa.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)"))
|
||||
|
||||
@property
|
||||
def description_i18n(self) -> I18nObject:
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
from sqlalchemy import func
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import DateTime, String, func
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from models.base import Base
|
||||
|
|
@ -19,10 +21,10 @@ class SavedMessage(Base):
|
|||
app_id = mapped_column(StringUUID, nullable=False)
|
||||
message_id = mapped_column(StringUUID, nullable=False)
|
||||
created_by_role = mapped_column(
|
||||
db.String(255), nullable=False, server_default=db.text("'end_user'::character varying")
|
||||
String(255), nullable=False, server_default=db.text("'end_user'::character varying")
|
||||
)
|
||||
created_by = mapped_column(StringUUID, nullable=False)
|
||||
created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
|
||||
@property
|
||||
def message(self):
|
||||
|
|
@ -40,7 +42,7 @@ class PinnedConversation(Base):
|
|||
app_id = mapped_column(StringUUID, nullable=False)
|
||||
conversation_id: Mapped[str] = mapped_column(StringUUID)
|
||||
created_by_role = mapped_column(
|
||||
db.String(255), nullable=False, server_default=db.text("'end_user'::character varying")
|
||||
String(255), nullable=False, server_default=db.text("'end_user'::character varying")
|
||||
)
|
||||
created_by = mapped_column(StringUUID, nullable=False)
|
||||
created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ from typing import TYPE_CHECKING, Any, Optional, Union
|
|||
from uuid import uuid4
|
||||
|
||||
from flask_login import current_user
|
||||
from sqlalchemy import orm
|
||||
from sqlalchemy import DateTime, orm
|
||||
|
||||
from core.file.constants import maybe_file_object
|
||||
from core.file.models import File
|
||||
|
|
@ -25,7 +25,7 @@ if TYPE_CHECKING:
|
|||
from models.model import AppMode
|
||||
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import Index, PrimaryKeyConstraint, UniqueConstraint, func
|
||||
from sqlalchemy import Index, PrimaryKeyConstraint, String, UniqueConstraint, func
|
||||
from sqlalchemy.orm import Mapped, declared_attr, mapped_column
|
||||
|
||||
from constants import DEFAULT_FILE_NUMBER_LIMITS, HIDDEN_VALUE
|
||||
|
|
@ -124,17 +124,17 @@ class Workflow(Base):
|
|||
id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()"))
|
||||
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
||||
app_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
||||
type: Mapped[str] = mapped_column(db.String(255), nullable=False)
|
||||
version: Mapped[str] = mapped_column(db.String(255), nullable=False)
|
||||
type: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
version: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
marked_name: Mapped[str] = mapped_column(default="", server_default="")
|
||||
marked_comment: Mapped[str] = mapped_column(default="", server_default="")
|
||||
graph: Mapped[str] = mapped_column(sa.Text)
|
||||
_features: Mapped[str] = mapped_column("features", sa.TEXT)
|
||||
created_by: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
||||
created_at: Mapped[datetime] = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
updated_by: Mapped[Optional[str]] = mapped_column(StringUUID)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
db.DateTime,
|
||||
DateTime,
|
||||
nullable=False,
|
||||
default=naive_utc_now(),
|
||||
server_onupdate=func.current_timestamp(),
|
||||
|
|
@ -500,21 +500,21 @@ class WorkflowRun(Base):
|
|||
app_id: Mapped[str] = mapped_column(StringUUID)
|
||||
|
||||
workflow_id: Mapped[str] = mapped_column(StringUUID)
|
||||
type: Mapped[str] = mapped_column(db.String(255))
|
||||
triggered_from: Mapped[str] = mapped_column(db.String(255))
|
||||
version: Mapped[str] = mapped_column(db.String(255))
|
||||
type: Mapped[str] = mapped_column(String(255))
|
||||
triggered_from: Mapped[str] = mapped_column(String(255))
|
||||
version: Mapped[str] = mapped_column(String(255))
|
||||
graph: Mapped[Optional[str]] = mapped_column(db.Text)
|
||||
inputs: Mapped[Optional[str]] = mapped_column(db.Text)
|
||||
status: Mapped[str] = mapped_column(db.String(255)) # running, succeeded, failed, stopped, partial-succeeded
|
||||
status: Mapped[str] = mapped_column(String(255)) # running, succeeded, failed, stopped, partial-succeeded
|
||||
outputs: Mapped[Optional[str]] = mapped_column(sa.Text, default="{}")
|
||||
error: Mapped[Optional[str]] = mapped_column(db.Text)
|
||||
elapsed_time: Mapped[float] = mapped_column(db.Float, nullable=False, server_default=sa.text("0"))
|
||||
total_tokens: Mapped[int] = mapped_column(sa.BigInteger, server_default=sa.text("0"))
|
||||
total_steps: Mapped[int] = mapped_column(db.Integer, server_default=db.text("0"), nullable=True)
|
||||
created_by_role: Mapped[str] = mapped_column(db.String(255)) # account, end_user
|
||||
created_by_role: Mapped[str] = mapped_column(String(255)) # account, end_user
|
||||
created_by: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
||||
created_at: Mapped[datetime] = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
finished_at: Mapped[Optional[datetime]] = mapped_column(db.DateTime)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
finished_at: Mapped[Optional[datetime]] = mapped_column(DateTime)
|
||||
exceptions_count: Mapped[int] = mapped_column(db.Integer, server_default=db.text("0"), nullable=True)
|
||||
|
||||
@property
|
||||
|
|
@ -708,25 +708,25 @@ class WorkflowNodeExecutionModel(Base):
|
|||
tenant_id: Mapped[str] = mapped_column(StringUUID)
|
||||
app_id: Mapped[str] = mapped_column(StringUUID)
|
||||
workflow_id: Mapped[str] = mapped_column(StringUUID)
|
||||
triggered_from: Mapped[str] = mapped_column(db.String(255))
|
||||
triggered_from: Mapped[str] = mapped_column(String(255))
|
||||
workflow_run_id: Mapped[Optional[str]] = mapped_column(StringUUID)
|
||||
index: Mapped[int] = mapped_column(db.Integer)
|
||||
predecessor_node_id: Mapped[Optional[str]] = mapped_column(db.String(255))
|
||||
node_execution_id: Mapped[Optional[str]] = mapped_column(db.String(255))
|
||||
node_id: Mapped[str] = mapped_column(db.String(255))
|
||||
node_type: Mapped[str] = mapped_column(db.String(255))
|
||||
title: Mapped[str] = mapped_column(db.String(255))
|
||||
predecessor_node_id: Mapped[Optional[str]] = mapped_column(String(255))
|
||||
node_execution_id: Mapped[Optional[str]] = mapped_column(String(255))
|
||||
node_id: Mapped[str] = mapped_column(String(255))
|
||||
node_type: Mapped[str] = mapped_column(String(255))
|
||||
title: Mapped[str] = mapped_column(String(255))
|
||||
inputs: Mapped[Optional[str]] = mapped_column(db.Text)
|
||||
process_data: Mapped[Optional[str]] = mapped_column(db.Text)
|
||||
outputs: Mapped[Optional[str]] = mapped_column(db.Text)
|
||||
status: Mapped[str] = mapped_column(db.String(255))
|
||||
status: Mapped[str] = mapped_column(String(255))
|
||||
error: Mapped[Optional[str]] = mapped_column(db.Text)
|
||||
elapsed_time: Mapped[float] = mapped_column(db.Float, server_default=db.text("0"))
|
||||
execution_metadata: Mapped[Optional[str]] = mapped_column(db.Text)
|
||||
created_at: Mapped[datetime] = mapped_column(db.DateTime, server_default=func.current_timestamp())
|
||||
created_by_role: Mapped[str] = mapped_column(db.String(255))
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.current_timestamp())
|
||||
created_by_role: Mapped[str] = mapped_column(String(255))
|
||||
created_by: Mapped[str] = mapped_column(StringUUID)
|
||||
finished_at: Mapped[Optional[datetime]] = mapped_column(db.DateTime)
|
||||
finished_at: Mapped[Optional[datetime]] = mapped_column(DateTime)
|
||||
|
||||
@property
|
||||
def created_by_account(self):
|
||||
|
|
@ -843,10 +843,10 @@ class WorkflowAppLog(Base):
|
|||
app_id: Mapped[str] = mapped_column(StringUUID)
|
||||
workflow_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
||||
workflow_run_id: Mapped[str] = mapped_column(StringUUID)
|
||||
created_from: Mapped[str] = mapped_column(db.String(255), nullable=False)
|
||||
created_by_role: Mapped[str] = mapped_column(db.String(255), nullable=False)
|
||||
created_from: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
created_by_role: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
created_by: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
||||
created_at: Mapped[datetime] = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
|
||||
@property
|
||||
def workflow_run(self):
|
||||
|
|
@ -873,10 +873,10 @@ class ConversationVariable(Base):
|
|||
app_id: Mapped[str] = mapped_column(StringUUID, nullable=False, index=True)
|
||||
data: Mapped[str] = mapped_column(db.Text, nullable=False)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
db.DateTime, nullable=False, server_default=func.current_timestamp(), index=True
|
||||
DateTime, nullable=False, server_default=func.current_timestamp(), index=True
|
||||
)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
db.DateTime, nullable=False, server_default=func.current_timestamp(), onupdate=func.current_timestamp()
|
||||
DateTime, nullable=False, server_default=func.current_timestamp(), onupdate=func.current_timestamp()
|
||||
)
|
||||
|
||||
def __init__(self, *, id: str, app_id: str, conversation_id: str, data: str) -> None:
|
||||
|
|
@ -936,14 +936,14 @@ class WorkflowDraftVariable(Base):
|
|||
id: Mapped[str] = mapped_column(StringUUID, primary_key=True, server_default=db.text("uuid_generate_v4()"))
|
||||
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
db.DateTime,
|
||||
DateTime,
|
||||
nullable=False,
|
||||
default=_naive_utc_datetime,
|
||||
server_default=func.current_timestamp(),
|
||||
)
|
||||
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
db.DateTime,
|
||||
DateTime,
|
||||
nullable=False,
|
||||
default=_naive_utc_datetime,
|
||||
server_default=func.current_timestamp(),
|
||||
|
|
@ -958,7 +958,7 @@ class WorkflowDraftVariable(Base):
|
|||
#
|
||||
# If it's not edited after creation, its value is `None`.
|
||||
last_edited_at: Mapped[datetime | None] = mapped_column(
|
||||
db.DateTime,
|
||||
DateTime,
|
||||
nullable=True,
|
||||
default=None,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import logging
|
||||
from datetime import datetime
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import click
|
||||
from kombu.utils.url import parse_url # type: ignore
|
||||
from redis import Redis
|
||||
|
||||
import app
|
||||
|
|
@ -10,16 +10,13 @@ from configs import dify_config
|
|||
from extensions.ext_database import db
|
||||
from libs.email_i18n import EmailType, get_email_i18n_service
|
||||
|
||||
# Create a dedicated Redis connection (using the same configuration as Celery)
|
||||
celery_broker_url = dify_config.CELERY_BROKER_URL
|
||||
|
||||
parsed = urlparse(celery_broker_url)
|
||||
host = parsed.hostname or "localhost"
|
||||
port = parsed.port or 6379
|
||||
password = parsed.password or None
|
||||
redis_db = parsed.path.strip("/") or "1" # type: ignore
|
||||
|
||||
celery_redis = Redis(host=host, port=port, password=password, db=redis_db)
|
||||
redis_config = parse_url(dify_config.CELERY_BROKER_URL)
|
||||
celery_redis = Redis(
|
||||
host=redis_config.get("hostname") or "localhost",
|
||||
port=redis_config.get("port") or 6379,
|
||||
password=redis_config.get("password") or None,
|
||||
db=int(redis_config.get("virtual_host")) if redis_config.get("virtual_host") else 1,
|
||||
)
|
||||
|
||||
|
||||
@app.celery.task(queue="monitor")
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
from collections.abc import Callable, Sequence
|
||||
from typing import Optional, Union
|
||||
from typing import Any, Optional, Union
|
||||
|
||||
from sqlalchemy import asc, desc, func, or_, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from core.app.entities.app_invoke_entities import InvokeFrom
|
||||
from core.llm_generator.llm_generator import LLMGenerator
|
||||
from core.variables.types import SegmentType
|
||||
from core.workflow.nodes.variable_assigner.common.impl import conversation_variable_updater_factory
|
||||
from extensions.ext_database import db
|
||||
from factories import variable_factory
|
||||
from libs.datetime_utils import naive_utc_now
|
||||
from libs.infinite_scroll_pagination import InfiniteScrollPagination
|
||||
from models import ConversationVariable
|
||||
|
|
@ -15,6 +18,7 @@ from models.model import App, Conversation, EndUser, Message
|
|||
from services.errors.conversation import (
|
||||
ConversationNotExistsError,
|
||||
ConversationVariableNotExistsError,
|
||||
ConversationVariableTypeMismatchError,
|
||||
LastConversationNotExistsError,
|
||||
)
|
||||
from services.errors.message import MessageNotExistsError
|
||||
|
|
@ -220,3 +224,82 @@ class ConversationService:
|
|||
]
|
||||
|
||||
return InfiniteScrollPagination(variables, limit, has_more)
|
||||
|
||||
@classmethod
|
||||
def update_conversation_variable(
|
||||
cls,
|
||||
app_model: App,
|
||||
conversation_id: str,
|
||||
variable_id: str,
|
||||
user: Optional[Union[Account, EndUser]],
|
||||
new_value: Any,
|
||||
) -> dict:
|
||||
"""
|
||||
Update a conversation variable's value.
|
||||
|
||||
Args:
|
||||
app_model: The app model
|
||||
conversation_id: The conversation ID
|
||||
variable_id: The variable ID to update
|
||||
user: The user (Account or EndUser)
|
||||
new_value: The new value for the variable
|
||||
|
||||
Returns:
|
||||
Dictionary containing the updated variable information
|
||||
|
||||
Raises:
|
||||
ConversationNotExistsError: If the conversation doesn't exist
|
||||
ConversationVariableNotExistsError: If the variable doesn't exist
|
||||
ConversationVariableTypeMismatchError: If the new value type doesn't match the variable's expected type
|
||||
"""
|
||||
# Verify conversation exists and user has access
|
||||
conversation = cls.get_conversation(app_model, conversation_id, user)
|
||||
|
||||
# Get the existing conversation variable
|
||||
stmt = (
|
||||
select(ConversationVariable)
|
||||
.where(ConversationVariable.app_id == app_model.id)
|
||||
.where(ConversationVariable.conversation_id == conversation.id)
|
||||
.where(ConversationVariable.id == variable_id)
|
||||
)
|
||||
|
||||
with Session(db.engine) as session:
|
||||
existing_variable = session.scalar(stmt)
|
||||
if not existing_variable:
|
||||
raise ConversationVariableNotExistsError()
|
||||
|
||||
# Convert existing variable to Variable object
|
||||
current_variable = existing_variable.to_variable()
|
||||
|
||||
# Validate that the new value type matches the expected variable type
|
||||
expected_type = SegmentType(current_variable.value_type)
|
||||
if not expected_type.is_valid(new_value):
|
||||
inferred_type = SegmentType.infer_segment_type(new_value)
|
||||
raise ConversationVariableTypeMismatchError(
|
||||
f"Type mismatch: variable '{current_variable.name}' expects {expected_type.value}, "
|
||||
f"but got {inferred_type.value if inferred_type else 'unknown'} type"
|
||||
)
|
||||
|
||||
# Create updated variable with new value only, preserving everything else
|
||||
updated_variable_dict = {
|
||||
"id": current_variable.id,
|
||||
"name": current_variable.name,
|
||||
"description": current_variable.description,
|
||||
"value_type": current_variable.value_type,
|
||||
"value": new_value,
|
||||
"selector": current_variable.selector,
|
||||
}
|
||||
|
||||
updated_variable = variable_factory.build_conversation_variable_from_mapping(updated_variable_dict)
|
||||
|
||||
# Use the conversation variable updater to persist the changes
|
||||
updater = conversation_variable_updater_factory()
|
||||
updater.update(conversation_id, updated_variable)
|
||||
updater.flush()
|
||||
|
||||
# Return the updated variable data
|
||||
return {
|
||||
"created_at": existing_variable.created_at,
|
||||
"updated_at": naive_utc_now(), # Update timestamp
|
||||
**updated_variable.model_dump(),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2040,6 +2040,7 @@ class SegmentService:
|
|||
|
||||
db.session.add(segment_document)
|
||||
# update document word count
|
||||
assert document.word_count is not None
|
||||
document.word_count += segment_document.word_count
|
||||
db.session.add(document)
|
||||
db.session.commit()
|
||||
|
|
@ -2124,6 +2125,7 @@ class SegmentService:
|
|||
else:
|
||||
keywords_list.append(None)
|
||||
# update document word count
|
||||
assert document.word_count is not None
|
||||
document.word_count += increment_word_count
|
||||
db.session.add(document)
|
||||
try:
|
||||
|
|
@ -2185,6 +2187,7 @@ class SegmentService:
|
|||
db.session.commit()
|
||||
# update document word count
|
||||
if word_count_change != 0:
|
||||
assert document.word_count is not None
|
||||
document.word_count = max(0, document.word_count + word_count_change)
|
||||
db.session.add(document)
|
||||
# update segment index task
|
||||
|
|
@ -2260,6 +2263,7 @@ class SegmentService:
|
|||
word_count_change = segment.word_count - word_count_change
|
||||
# update document word count
|
||||
if word_count_change != 0:
|
||||
assert document.word_count is not None
|
||||
document.word_count = max(0, document.word_count + word_count_change)
|
||||
db.session.add(document)
|
||||
db.session.add(segment)
|
||||
|
|
@ -2323,6 +2327,7 @@ class SegmentService:
|
|||
delete_segment_from_index_task.delay([segment.index_node_id], dataset.id, document.id)
|
||||
db.session.delete(segment)
|
||||
# update document word count
|
||||
assert document.word_count is not None
|
||||
document.word_count -= segment.word_count
|
||||
db.session.add(document)
|
||||
db.session.commit()
|
||||
|
|
|
|||
|
|
@ -15,3 +15,7 @@ class ConversationCompletedError(Exception):
|
|||
|
||||
class ConversationVariableNotExistsError(BaseServiceError):
|
||||
pass
|
||||
|
||||
|
||||
class ConversationVariableTypeMismatchError(BaseServiceError):
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -508,10 +508,10 @@ class BuiltinToolManageService:
|
|||
oauth_params = encrypter.decrypt(user_client.oauth_params)
|
||||
return oauth_params
|
||||
|
||||
# only verified provider can use custom oauth client
|
||||
is_verified = not isinstance(provider, PluginToolProviderController) or PluginService.is_plugin_verified(
|
||||
tenant_id, provider.plugin_unique_identifier
|
||||
)
|
||||
# only verified provider can use official oauth client
|
||||
is_verified = not isinstance(
|
||||
provider_controller, PluginToolProviderController
|
||||
) or PluginService.is_plugin_verified(tenant_id, provider_controller.plugin_unique_identifier)
|
||||
if not is_verified:
|
||||
return oauth_params
|
||||
|
||||
|
|
|
|||
|
|
@ -134,6 +134,7 @@ def batch_create_segment_to_index_task(
|
|||
db.session.add(segment_document)
|
||||
document_segments.append(segment_document)
|
||||
# update document word count
|
||||
assert dataset_document.word_count is not None
|
||||
dataset_document.word_count += word_count_change
|
||||
db.session.add(dataset_document)
|
||||
# add index to db
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import os
|
|||
import uuid
|
||||
|
||||
import tablestore
|
||||
from _pytest.python_api import approx
|
||||
|
||||
from core.rag.datasource.vdb.tablestore.tablestore_vector import (
|
||||
TableStoreConfig,
|
||||
|
|
@ -16,7 +17,7 @@ from tests.integration_tests.vdb.test_vector_store import (
|
|||
|
||||
|
||||
class TableStoreVectorTest(AbstractVectorTest):
|
||||
def __init__(self):
|
||||
def __init__(self, normalize_full_text_score: bool = False):
|
||||
super().__init__()
|
||||
self.vector = TableStoreVector(
|
||||
collection_name=self.collection_name,
|
||||
|
|
@ -25,6 +26,7 @@ class TableStoreVectorTest(AbstractVectorTest):
|
|||
instance_name=os.getenv("TABLESTORE_INSTANCE_NAME"),
|
||||
access_key_id=os.getenv("TABLESTORE_ACCESS_KEY_ID"),
|
||||
access_key_secret=os.getenv("TABLESTORE_ACCESS_KEY_SECRET"),
|
||||
normalize_full_text_bm25_score=normalize_full_text_score,
|
||||
),
|
||||
)
|
||||
|
||||
|
|
@ -64,7 +66,21 @@ class TableStoreVectorTest(AbstractVectorTest):
|
|||
docs = self.vector.search_by_full_text(get_example_text(), document_ids_filter=[self.example_doc_id])
|
||||
assert len(docs) == 1
|
||||
assert docs[0].metadata["doc_id"] == self.example_doc_id
|
||||
assert not hasattr(docs[0], "score")
|
||||
if self.vector._config.normalize_full_text_bm25_score:
|
||||
assert docs[0].metadata["score"] == approx(0.1214, abs=1e-3)
|
||||
else:
|
||||
assert docs[0].metadata.get("score") is None
|
||||
|
||||
# return none if normalize_full_text_score=true and score_threshold > 0
|
||||
docs = self.vector.search_by_full_text(
|
||||
get_example_text(), document_ids_filter=[self.example_doc_id], score_threshold=0.5
|
||||
)
|
||||
if self.vector._config.normalize_full_text_bm25_score:
|
||||
assert len(docs) == 0
|
||||
else:
|
||||
assert len(docs) == 1
|
||||
assert docs[0].metadata["doc_id"] == self.example_doc_id
|
||||
assert docs[0].metadata.get("score") is None
|
||||
|
||||
docs = self.vector.search_by_full_text(get_example_text(), document_ids_filter=[str(uuid.uuid4())])
|
||||
assert len(docs) == 0
|
||||
|
|
@ -80,3 +96,5 @@ class TableStoreVectorTest(AbstractVectorTest):
|
|||
|
||||
def test_tablestore_vector(setup_mock_redis):
|
||||
TableStoreVectorTest().run_all_tests()
|
||||
TableStoreVectorTest(normalize_full_text_score=True).run_all_tests()
|
||||
TableStoreVectorTest(normalize_full_text_score=False).run_all_tests()
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import os
|
||||
|
||||
import pytest
|
||||
from flask import Flask
|
||||
from packaging.version import Version
|
||||
from yarl import URL
|
||||
|
|
@ -137,3 +138,61 @@ def test_db_extras_options_merging(monkeypatch):
|
|||
options = engine_options["connect_args"]["options"]
|
||||
assert "search_path=myschema" in options
|
||||
assert "timezone=UTC" in options
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("broker_url", "expected_host", "expected_port", "expected_username", "expected_password", "expected_db"),
|
||||
[
|
||||
("redis://localhost:6379/1", "localhost", 6379, None, None, "1"),
|
||||
("redis://:password@localhost:6379/1", "localhost", 6379, None, "password", "1"),
|
||||
("redis://:mypass%23123@localhost:6379/1", "localhost", 6379, None, "mypass#123", "1"),
|
||||
("redis://user:pass%40word@redis-host:6380/2", "redis-host", 6380, "user", "pass@word", "2"),
|
||||
("redis://admin:complex%23pass%40word@127.0.0.1:6379/0", "127.0.0.1", 6379, "admin", "complex#pass@word", "0"),
|
||||
(
|
||||
"redis://user%40domain:secret%23123@redis.example.com:6380/3",
|
||||
"redis.example.com",
|
||||
6380,
|
||||
"user@domain",
|
||||
"secret#123",
|
||||
"3",
|
||||
),
|
||||
# Password containing %23 substring (double encoding scenario)
|
||||
("redis://:mypass%2523@localhost:6379/1", "localhost", 6379, None, "mypass%23", "1"),
|
||||
# Username and password both containing encoded characters
|
||||
("redis://user%2525%40:pass%2523@localhost:6379/1", "localhost", 6379, "user%25@", "pass%23", "1"),
|
||||
],
|
||||
)
|
||||
def test_celery_broker_url_with_special_chars_password(
|
||||
monkeypatch, broker_url, expected_host, expected_port, expected_username, expected_password, expected_db
|
||||
):
|
||||
"""Test that CELERY_BROKER_URL with various formats are handled correctly."""
|
||||
from kombu.utils.url import parse_url
|
||||
|
||||
# clear system environment variables
|
||||
os.environ.clear()
|
||||
|
||||
# Set up basic required environment variables (following existing pattern)
|
||||
monkeypatch.setenv("CONSOLE_API_URL", "https://example.com")
|
||||
monkeypatch.setenv("CONSOLE_WEB_URL", "https://example.com")
|
||||
monkeypatch.setenv("DB_USERNAME", "postgres")
|
||||
monkeypatch.setenv("DB_PASSWORD", "postgres")
|
||||
monkeypatch.setenv("DB_HOST", "localhost")
|
||||
monkeypatch.setenv("DB_PORT", "5432")
|
||||
monkeypatch.setenv("DB_DATABASE", "dify")
|
||||
|
||||
# Set the CELERY_BROKER_URL to test
|
||||
monkeypatch.setenv("CELERY_BROKER_URL", broker_url)
|
||||
|
||||
# Create config and verify the URL is stored correctly
|
||||
config = DifyConfig()
|
||||
assert broker_url == config.CELERY_BROKER_URL
|
||||
|
||||
# Test actual parsing behavior using kombu's parse_url (same as production)
|
||||
redis_config = parse_url(config.CELERY_BROKER_URL)
|
||||
|
||||
# Verify the parsing results match expectations (using kombu's field names)
|
||||
assert redis_config["hostname"] == expected_host
|
||||
assert redis_config["port"] == expected_port
|
||||
assert redis_config["userid"] == expected_username # kombu uses 'userid' not 'username'
|
||||
assert redis_config["password"] == expected_password
|
||||
assert redis_config["virtual_host"] == expected_db # kombu uses 'virtual_host' not 'db'
|
||||
|
|
|
|||
|
|
@ -0,0 +1,278 @@
|
|||
import io
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from werkzeug.exceptions import Forbidden
|
||||
|
||||
from controllers.common.errors import FilenameNotExistsError
|
||||
from controllers.console.error import (
|
||||
FileTooLargeError,
|
||||
NoFileUploadedError,
|
||||
TooManyFilesError,
|
||||
UnsupportedFileTypeError,
|
||||
)
|
||||
from services.errors.file import FileTooLargeError as ServiceFileTooLargeError
|
||||
from services.errors.file import UnsupportedFileTypeError as ServiceUnsupportedFileTypeError
|
||||
|
||||
|
||||
class TestFileUploadSecurity:
|
||||
"""Test file upload security logic without complex framework setup"""
|
||||
|
||||
# Test 1: Basic file validation
|
||||
def test_should_validate_file_presence(self):
|
||||
"""Test that missing file is detected"""
|
||||
from flask import Flask, request
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
with app.test_request_context(method="POST", data={}):
|
||||
# Simulate the check in FileApi.post()
|
||||
if "file" not in request.files:
|
||||
with pytest.raises(NoFileUploadedError):
|
||||
raise NoFileUploadedError()
|
||||
|
||||
def test_should_validate_multiple_files(self):
|
||||
"""Test that multiple files are rejected"""
|
||||
from flask import Flask, request
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
file_data = {
|
||||
"file": (io.BytesIO(b"content1"), "file1.txt", "text/plain"),
|
||||
"file2": (io.BytesIO(b"content2"), "file2.txt", "text/plain"),
|
||||
}
|
||||
|
||||
with app.test_request_context(method="POST", data=file_data, content_type="multipart/form-data"):
|
||||
# Simulate the check in FileApi.post()
|
||||
if len(request.files) > 1:
|
||||
with pytest.raises(TooManyFilesError):
|
||||
raise TooManyFilesError()
|
||||
|
||||
def test_should_validate_empty_filename(self):
|
||||
"""Test that empty filename is rejected"""
|
||||
from flask import Flask, request
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
file_data = {"file": (io.BytesIO(b"content"), "", "text/plain")}
|
||||
|
||||
with app.test_request_context(method="POST", data=file_data, content_type="multipart/form-data"):
|
||||
file = request.files["file"]
|
||||
if not file.filename:
|
||||
with pytest.raises(FilenameNotExistsError):
|
||||
raise FilenameNotExistsError
|
||||
|
||||
# Test 2: Security - Filename sanitization
|
||||
def test_should_detect_path_traversal_in_filename(self):
|
||||
"""Test protection against directory traversal attacks"""
|
||||
dangerous_filenames = [
|
||||
"../../../etc/passwd",
|
||||
"..\\..\\windows\\system32\\config\\sam",
|
||||
"../../../../etc/shadow",
|
||||
"./../../../sensitive.txt",
|
||||
]
|
||||
|
||||
for filename in dangerous_filenames:
|
||||
# Any filename containing .. should be considered dangerous
|
||||
assert ".." in filename, f"Filename {filename} should be detected as path traversal"
|
||||
|
||||
def test_should_detect_null_byte_injection(self):
|
||||
"""Test protection against null byte injection"""
|
||||
dangerous_filenames = [
|
||||
"file.jpg\x00.php",
|
||||
"document.pdf\x00.exe",
|
||||
"image.png\x00.sh",
|
||||
]
|
||||
|
||||
for filename in dangerous_filenames:
|
||||
# Null bytes should be detected
|
||||
assert "\x00" in filename, f"Filename {filename} should be detected as null byte injection"
|
||||
|
||||
def test_should_sanitize_special_characters(self):
|
||||
"""Test that special characters in filenames are handled safely"""
|
||||
# Characters that could be problematic in various contexts
|
||||
dangerous_chars = ["/", "\\", ":", "*", "?", '"', "<", ">", "|", "\x00"]
|
||||
|
||||
for char in dangerous_chars:
|
||||
filename = f"file{char}name.txt"
|
||||
# These characters should be detected or sanitized
|
||||
assert any(c in filename for c in dangerous_chars)
|
||||
|
||||
# Test 3: Permission validation
|
||||
def test_should_validate_dataset_permissions(self):
|
||||
"""Test dataset upload permission logic"""
|
||||
|
||||
class MockUser:
|
||||
is_dataset_editor = False
|
||||
|
||||
user = MockUser()
|
||||
source = "datasets"
|
||||
|
||||
# Simulate the permission check in FileApi.post()
|
||||
if source == "datasets" and not user.is_dataset_editor:
|
||||
with pytest.raises(Forbidden):
|
||||
raise Forbidden()
|
||||
|
||||
def test_should_allow_general_upload_without_permission(self):
|
||||
"""Test general upload doesn't require dataset permission"""
|
||||
|
||||
class MockUser:
|
||||
is_dataset_editor = False
|
||||
|
||||
user = MockUser()
|
||||
source = None # General upload
|
||||
|
||||
# This should not raise an exception
|
||||
if source == "datasets" and not user.is_dataset_editor:
|
||||
raise Forbidden()
|
||||
# Test passes if no exception is raised
|
||||
|
||||
# Test 4: Service error handling
|
||||
@patch("services.file_service.FileService.upload_file")
|
||||
def test_should_handle_file_too_large_error(self, mock_upload):
|
||||
"""Test that service FileTooLargeError is properly converted"""
|
||||
mock_upload.side_effect = ServiceFileTooLargeError("File too large")
|
||||
|
||||
try:
|
||||
mock_upload(filename="test.txt", content=b"data", mimetype="text/plain", user=None, source=None)
|
||||
except ServiceFileTooLargeError as e:
|
||||
# Simulate the error conversion in FileApi.post()
|
||||
with pytest.raises(FileTooLargeError):
|
||||
raise FileTooLargeError(e.description)
|
||||
|
||||
@patch("services.file_service.FileService.upload_file")
|
||||
def test_should_handle_unsupported_file_type_error(self, mock_upload):
|
||||
"""Test that service UnsupportedFileTypeError is properly converted"""
|
||||
mock_upload.side_effect = ServiceUnsupportedFileTypeError()
|
||||
|
||||
try:
|
||||
mock_upload(
|
||||
filename="test.exe", content=b"data", mimetype="application/octet-stream", user=None, source=None
|
||||
)
|
||||
except ServiceUnsupportedFileTypeError:
|
||||
# Simulate the error conversion in FileApi.post()
|
||||
with pytest.raises(UnsupportedFileTypeError):
|
||||
raise UnsupportedFileTypeError()
|
||||
|
||||
# Test 5: File type security
|
||||
def test_should_identify_dangerous_file_extensions(self):
|
||||
"""Test detection of potentially dangerous file extensions"""
|
||||
dangerous_extensions = [
|
||||
".php",
|
||||
".PHP",
|
||||
".pHp", # PHP files (case variations)
|
||||
".exe",
|
||||
".EXE", # Executables
|
||||
".sh",
|
||||
".SH", # Shell scripts
|
||||
".bat",
|
||||
".BAT", # Batch files
|
||||
".cmd",
|
||||
".CMD", # Command files
|
||||
".ps1",
|
||||
".PS1", # PowerShell
|
||||
".jar",
|
||||
".JAR", # Java archives
|
||||
".vbs",
|
||||
".VBS", # VBScript
|
||||
]
|
||||
|
||||
safe_extensions = [".txt", ".pdf", ".jpg", ".png", ".doc", ".docx"]
|
||||
|
||||
# Just verify our test data is correct
|
||||
for ext in dangerous_extensions:
|
||||
assert ext.lower() in [".php", ".exe", ".sh", ".bat", ".cmd", ".ps1", ".jar", ".vbs"]
|
||||
|
||||
for ext in safe_extensions:
|
||||
assert ext.lower() not in [".php", ".exe", ".sh", ".bat", ".cmd", ".ps1", ".jar", ".vbs"]
|
||||
|
||||
def test_should_detect_double_extensions(self):
|
||||
"""Test detection of double extension attacks"""
|
||||
suspicious_filenames = [
|
||||
"image.jpg.php",
|
||||
"document.pdf.exe",
|
||||
"photo.png.sh",
|
||||
"file.txt.bat",
|
||||
]
|
||||
|
||||
for filename in suspicious_filenames:
|
||||
# Check that these have multiple extensions
|
||||
parts = filename.split(".")
|
||||
assert len(parts) > 2, f"Filename {filename} should have multiple extensions"
|
||||
|
||||
# Test 6: Configuration validation
|
||||
def test_upload_configuration_structure(self):
|
||||
"""Test that upload configuration has correct structure"""
|
||||
# Simulate the configuration returned by FileApi.get()
|
||||
config = {
|
||||
"file_size_limit": 15,
|
||||
"batch_count_limit": 5,
|
||||
"image_file_size_limit": 10,
|
||||
"video_file_size_limit": 500,
|
||||
"audio_file_size_limit": 50,
|
||||
"workflow_file_upload_limit": 10,
|
||||
}
|
||||
|
||||
# Verify all required fields are present
|
||||
required_fields = [
|
||||
"file_size_limit",
|
||||
"batch_count_limit",
|
||||
"image_file_size_limit",
|
||||
"video_file_size_limit",
|
||||
"audio_file_size_limit",
|
||||
"workflow_file_upload_limit",
|
||||
]
|
||||
|
||||
for field in required_fields:
|
||||
assert field in config, f"Missing required field: {field}"
|
||||
assert isinstance(config[field], int), f"Field {field} should be an integer"
|
||||
assert config[field] > 0, f"Field {field} should be positive"
|
||||
|
||||
# Test 7: Source parameter handling
|
||||
def test_source_parameter_normalization(self):
|
||||
"""Test that source parameter is properly normalized"""
|
||||
test_cases = [
|
||||
("datasets", "datasets"),
|
||||
("other", None),
|
||||
("", None),
|
||||
(None, None),
|
||||
]
|
||||
|
||||
for input_source, expected in test_cases:
|
||||
# Simulate the source normalization in FileApi.post()
|
||||
source = "datasets" if input_source == "datasets" else None
|
||||
if source not in ("datasets", None):
|
||||
source = None
|
||||
assert source == expected
|
||||
|
||||
# Test 8: Boundary conditions
|
||||
def test_should_handle_edge_case_file_sizes(self):
|
||||
"""Test handling of boundary file sizes"""
|
||||
test_cases = [
|
||||
(0, "Empty file"), # 0 bytes
|
||||
(1, "Single byte"), # 1 byte
|
||||
(15 * 1024 * 1024 - 1, "Just under limit"), # Just under 15MB
|
||||
(15 * 1024 * 1024, "At limit"), # Exactly 15MB
|
||||
(15 * 1024 * 1024 + 1, "Just over limit"), # Just over 15MB
|
||||
]
|
||||
|
||||
for size, description in test_cases:
|
||||
# Just verify our test data
|
||||
assert isinstance(size, int), f"{description}: Size should be integer"
|
||||
assert size >= 0, f"{description}: Size should be non-negative"
|
||||
|
||||
def test_should_handle_special_mime_types(self):
|
||||
"""Test handling of various MIME types"""
|
||||
mime_type_tests = [
|
||||
("application/octet-stream", "Generic binary"),
|
||||
("text/plain", "Plain text"),
|
||||
("image/jpeg", "JPEG image"),
|
||||
("application/pdf", "PDF document"),
|
||||
("", "Empty MIME type"),
|
||||
(None, "None MIME type"),
|
||||
]
|
||||
|
||||
for mime_type, description in mime_type_tests:
|
||||
# Verify test data structure
|
||||
if mime_type is not None:
|
||||
assert isinstance(mime_type, str), f"{description}: MIME type should be string or None"
|
||||
|
|
@ -102,9 +102,14 @@ class TestPhoenixConfig:
|
|||
assert config.project == "default"
|
||||
|
||||
def test_endpoint_validation_with_path(self):
|
||||
"""Test endpoint validation normalizes URL by removing path"""
|
||||
config = PhoenixConfig(endpoint="https://custom.phoenix.com/api/v1")
|
||||
assert config.endpoint == "https://custom.phoenix.com"
|
||||
"""Test endpoint validation with path"""
|
||||
config = PhoenixConfig(endpoint="https://app.phoenix.arize.com/s/dify-integration")
|
||||
assert config.endpoint == "https://app.phoenix.arize.com/s/dify-integration"
|
||||
|
||||
def test_endpoint_validation_without_path(self):
|
||||
"""Test endpoint validation without path"""
|
||||
config = PhoenixConfig(endpoint="https://app.phoenix.arize.com")
|
||||
assert config.endpoint == "https://app.phoenix.arize.com"
|
||||
|
||||
|
||||
class TestLangfuseConfig:
|
||||
|
|
@ -368,13 +373,15 @@ class TestConfigIntegration:
|
|||
"""Test that URL normalization works consistently across configs"""
|
||||
# Test that paths are removed from endpoints
|
||||
arize_config = ArizeConfig(endpoint="https://arize.com/api/v1/test")
|
||||
phoenix_config = PhoenixConfig(endpoint="https://phoenix.com/api/v2/")
|
||||
phoenix_with_path_config = PhoenixConfig(endpoint="https://app.phoenix.arize.com/s/dify-integration")
|
||||
phoenix_without_path_config = PhoenixConfig(endpoint="https://app.phoenix.arize.com")
|
||||
aliyun_config = AliyunConfig(
|
||||
license_key="test_license", endpoint="https://tracing-analysis-dc-hz.aliyuncs.com/api/v1/traces"
|
||||
)
|
||||
|
||||
assert arize_config.endpoint == "https://arize.com"
|
||||
assert phoenix_config.endpoint == "https://phoenix.com"
|
||||
assert phoenix_with_path_config.endpoint == "https://app.phoenix.arize.com/s/dify-integration"
|
||||
assert phoenix_without_path_config.endpoint == "https://app.phoenix.arize.com"
|
||||
assert aliyun_config.endpoint == "https://tracing-analysis-dc-hz.aliyuncs.com"
|
||||
|
||||
def test_project_default_values(self):
|
||||
|
|
|
|||
|
|
@ -653,6 +653,7 @@ TABLESTORE_ENDPOINT=https://instance-name.cn-hangzhou.ots.aliyuncs.com
|
|||
TABLESTORE_INSTANCE_NAME=instance-name
|
||||
TABLESTORE_ACCESS_KEY_ID=xxx
|
||||
TABLESTORE_ACCESS_KEY_SECRET=xxx
|
||||
TABLESTORE_NORMALIZE_FULLTEXT_BM25_SCORE=false
|
||||
|
||||
# ------------------------------
|
||||
# Knowledge Configuration
|
||||
|
|
|
|||
|
|
@ -312,6 +312,7 @@ x-shared-env: &shared-api-worker-env
|
|||
TABLESTORE_INSTANCE_NAME: ${TABLESTORE_INSTANCE_NAME:-instance-name}
|
||||
TABLESTORE_ACCESS_KEY_ID: ${TABLESTORE_ACCESS_KEY_ID:-xxx}
|
||||
TABLESTORE_ACCESS_KEY_SECRET: ${TABLESTORE_ACCESS_KEY_SECRET:-xxx}
|
||||
TABLESTORE_NORMALIZE_FULLTEXT_BM25_SCORE: ${TABLESTORE_NORMALIZE_FULLTEXT_BM25_SCORE:-false}
|
||||
UPLOAD_FILE_SIZE_LIMIT: ${UPLOAD_FILE_SIZE_LIMIT:-15}
|
||||
UPLOAD_FILE_BATCH_LIMIT: ${UPLOAD_FILE_BATCH_LIMIT:-5}
|
||||
ETL_TYPE: ${ETL_TYPE:-dify}
|
||||
|
|
|
|||
|
|
@ -49,9 +49,9 @@ describe('check-i18n script functionality', () => {
|
|||
}
|
||||
|
||||
vm.runInNewContext(transpile(content), context)
|
||||
const translationObj = moduleExports.default || moduleExports
|
||||
const translationObj = (context.module.exports as any).default || context.module.exports
|
||||
|
||||
if(!translationObj || typeof translationObj !== 'object')
|
||||
if (!translationObj || typeof translationObj !== 'object')
|
||||
throw new Error(`Error parsing file: ${filePath}`)
|
||||
|
||||
const nestedKeys: string[] = []
|
||||
|
|
@ -62,7 +62,7 @@ describe('check-i18n script functionality', () => {
|
|||
// This is an object (but not array), recurse into it but don't add it as a key
|
||||
iterateKeys(obj[key], nestedKey)
|
||||
}
|
||||
else {
|
||||
else {
|
||||
// This is a leaf node (string, number, boolean, array, etc.), add it as a key
|
||||
nestedKeys.push(nestedKey)
|
||||
}
|
||||
|
|
@ -73,7 +73,7 @@ describe('check-i18n script functionality', () => {
|
|||
const fileKeys = nestedKeys.map(key => `${camelCaseFileName}.${key}`)
|
||||
allKeys.push(...fileKeys)
|
||||
}
|
||||
catch (error) {
|
||||
catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
|
|
@ -272,9 +272,6 @@ export default translation
|
|||
const filteredEnKeys = allEnKeys.filter(key =>
|
||||
key.startsWith(targetFile.replace(/[-_](.)/g, (_, c) => c.toUpperCase())),
|
||||
)
|
||||
const filteredZhKeys = allZhKeys.filter(key =>
|
||||
key.startsWith(targetFile.replace(/[-_](.)/g, (_, c) => c.toUpperCase())),
|
||||
)
|
||||
|
||||
expect(allEnKeys).toHaveLength(4) // 2 keys from each file
|
||||
expect(filteredEnKeys).toHaveLength(2) // only components keys
|
||||
|
|
|
|||
|
|
@ -0,0 +1,207 @@
|
|||
/**
|
||||
* Test cases to reproduce the plugin tool workflow error
|
||||
* Issue: #23154 - Application error when loading plugin tools in workflow
|
||||
* Root cause: split() operation called on null/undefined values
|
||||
*/
|
||||
|
||||
describe('Plugin Tool Workflow Error Reproduction', () => {
|
||||
/**
|
||||
* Mock function to simulate the problematic code in switch-plugin-version.tsx:29
|
||||
* const [pluginId] = uniqueIdentifier.split(':')
|
||||
*/
|
||||
const mockSwitchPluginVersionLogic = (uniqueIdentifier: string | null | undefined) => {
|
||||
// This directly reproduces the problematic line from switch-plugin-version.tsx:29
|
||||
const [pluginId] = uniqueIdentifier!.split(':')
|
||||
return pluginId
|
||||
}
|
||||
|
||||
/**
|
||||
* Test case 1: Simulate null uniqueIdentifier
|
||||
* This should reproduce the error mentioned in the issue
|
||||
*/
|
||||
it('should reproduce error when uniqueIdentifier is null', () => {
|
||||
expect(() => {
|
||||
mockSwitchPluginVersionLogic(null)
|
||||
}).toThrow('Cannot read properties of null (reading \'split\')')
|
||||
})
|
||||
|
||||
/**
|
||||
* Test case 2: Simulate undefined uniqueIdentifier
|
||||
*/
|
||||
it('should reproduce error when uniqueIdentifier is undefined', () => {
|
||||
expect(() => {
|
||||
mockSwitchPluginVersionLogic(undefined)
|
||||
}).toThrow('Cannot read properties of undefined (reading \'split\')')
|
||||
})
|
||||
|
||||
/**
|
||||
* Test case 3: Simulate empty string uniqueIdentifier
|
||||
*/
|
||||
it('should handle empty string uniqueIdentifier', () => {
|
||||
expect(() => {
|
||||
const result = mockSwitchPluginVersionLogic('')
|
||||
expect(result).toBe('') // Empty string split by ':' returns ['']
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
/**
|
||||
* Test case 4: Simulate malformed uniqueIdentifier without colon separator
|
||||
*/
|
||||
it('should handle malformed uniqueIdentifier without colon separator', () => {
|
||||
expect(() => {
|
||||
const result = mockSwitchPluginVersionLogic('malformed-identifier-without-colon')
|
||||
expect(result).toBe('malformed-identifier-without-colon') // No colon means full string returned
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
/**
|
||||
* Test case 5: Simulate valid uniqueIdentifier
|
||||
*/
|
||||
it('should work correctly with valid uniqueIdentifier', () => {
|
||||
expect(() => {
|
||||
const result = mockSwitchPluginVersionLogic('valid-plugin-id:1.0.0')
|
||||
expect(result).toBe('valid-plugin-id')
|
||||
}).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Test for the variable processing split error in use-single-run-form-params
|
||||
*/
|
||||
describe('Variable Processing Split Error', () => {
|
||||
/**
|
||||
* Mock function to simulate the problematic code in use-single-run-form-params.ts:91
|
||||
* const getDependentVars = () => {
|
||||
* return varInputs.map(item => item.variable.slice(1, -1).split('.'))
|
||||
* }
|
||||
*/
|
||||
const mockGetDependentVars = (varInputs: Array<{ variable: string | null | undefined }>) => {
|
||||
return varInputs.map((item) => {
|
||||
// Guard against null/undefined variable to prevent app crash
|
||||
if (!item.variable || typeof item.variable !== 'string')
|
||||
return []
|
||||
|
||||
return item.variable.slice(1, -1).split('.')
|
||||
}).filter(arr => arr.length > 0) // Filter out empty arrays
|
||||
}
|
||||
|
||||
/**
|
||||
* Test case 1: Variable processing with null variable
|
||||
*/
|
||||
it('should handle null variable safely', () => {
|
||||
const varInputs = [{ variable: null }]
|
||||
|
||||
expect(() => {
|
||||
mockGetDependentVars(varInputs)
|
||||
}).not.toThrow()
|
||||
|
||||
const result = mockGetDependentVars(varInputs)
|
||||
expect(result).toEqual([]) // null variables are filtered out
|
||||
})
|
||||
|
||||
/**
|
||||
* Test case 2: Variable processing with undefined variable
|
||||
*/
|
||||
it('should handle undefined variable safely', () => {
|
||||
const varInputs = [{ variable: undefined }]
|
||||
|
||||
expect(() => {
|
||||
mockGetDependentVars(varInputs)
|
||||
}).not.toThrow()
|
||||
|
||||
const result = mockGetDependentVars(varInputs)
|
||||
expect(result).toEqual([]) // undefined variables are filtered out
|
||||
})
|
||||
|
||||
/**
|
||||
* Test case 3: Variable processing with empty string
|
||||
*/
|
||||
it('should handle empty string variable', () => {
|
||||
const varInputs = [{ variable: '' }]
|
||||
|
||||
expect(() => {
|
||||
mockGetDependentVars(varInputs)
|
||||
}).not.toThrow()
|
||||
|
||||
const result = mockGetDependentVars(varInputs)
|
||||
expect(result).toEqual([]) // Empty string is filtered out, so result is empty array
|
||||
})
|
||||
|
||||
/**
|
||||
* Test case 4: Variable processing with valid variable format
|
||||
*/
|
||||
it('should work correctly with valid variable format', () => {
|
||||
const varInputs = [{ variable: '{{workflow.node.output}}' }]
|
||||
|
||||
expect(() => {
|
||||
mockGetDependentVars(varInputs)
|
||||
}).not.toThrow()
|
||||
|
||||
const result = mockGetDependentVars(varInputs)
|
||||
expect(result[0]).toEqual(['{workflow', 'node', 'output}'])
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Integration test to simulate the complete workflow scenario
|
||||
*/
|
||||
describe('Plugin Tool Workflow Integration', () => {
|
||||
/**
|
||||
* Simulate the scenario where plugin metadata is incomplete or corrupted
|
||||
* This can happen when:
|
||||
* 1. Plugin is being loaded from marketplace but metadata request fails
|
||||
* 2. Plugin configuration is corrupted in database
|
||||
* 3. Network issues during plugin loading
|
||||
*/
|
||||
it('should reproduce the client-side exception scenario', () => {
|
||||
// Mock incomplete plugin data that could cause the error
|
||||
const incompletePluginData = {
|
||||
// Missing or null uniqueIdentifier
|
||||
uniqueIdentifier: null,
|
||||
meta: null,
|
||||
minimum_dify_version: undefined,
|
||||
}
|
||||
|
||||
// This simulates the error path that leads to the white screen
|
||||
expect(() => {
|
||||
// Simulate the code path in switch-plugin-version.tsx:29
|
||||
// The actual problematic code doesn't use optional chaining
|
||||
const _pluginId = (incompletePluginData.uniqueIdentifier as any).split(':')[0]
|
||||
}).toThrow('Cannot read properties of null (reading \'split\')')
|
||||
})
|
||||
|
||||
/**
|
||||
* Test the scenario mentioned in the issue where plugin tools are loaded in workflow
|
||||
*/
|
||||
it('should simulate plugin tool loading in workflow context', () => {
|
||||
// Mock the workflow context where plugin tools are being loaded
|
||||
const workflowPluginTools = [
|
||||
{
|
||||
provider_name: 'test-plugin',
|
||||
uniqueIdentifier: null, // This is the problematic case
|
||||
tool_name: 'test-tool',
|
||||
},
|
||||
{
|
||||
provider_name: 'valid-plugin',
|
||||
uniqueIdentifier: 'valid-plugin:1.0.0',
|
||||
tool_name: 'valid-tool',
|
||||
},
|
||||
]
|
||||
|
||||
// Process each plugin tool
|
||||
workflowPluginTools.forEach((tool, _index) => {
|
||||
if (tool.uniqueIdentifier === null) {
|
||||
// This reproduces the exact error scenario
|
||||
expect(() => {
|
||||
const _pluginId = (tool.uniqueIdentifier as any).split(':')[0]
|
||||
}).toThrow()
|
||||
}
|
||||
else {
|
||||
// Valid tools should work fine
|
||||
expect(() => {
|
||||
const _pluginId = tool.uniqueIdentifier.split(':')[0]
|
||||
}).not.toThrow()
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -271,16 +271,17 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
expand && (
|
||||
<div className='flex flex-col items-start gap-1'>
|
||||
<div className='flex w-full'>
|
||||
<div className='system-md-semibold truncate text-text-secondary'>{appDetail.name}</div>
|
||||
</div>
|
||||
<div className='system-2xs-medium-uppercase text-text-tertiary'>{appDetail.mode === 'advanced-chat' ? t('app.types.advanced') : appDetail.mode === 'agent-chat' ? t('app.types.agent') : appDetail.mode === 'chat' ? t('app.types.chatbot') : appDetail.mode === 'completion' ? t('app.types.completion') : t('app.types.workflow')}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<div className={cn(
|
||||
'flex flex-col items-start gap-1 transition-all duration-200 ease-in-out',
|
||||
expand
|
||||
? 'w-auto opacity-100'
|
||||
: 'pointer-events-none w-0 overflow-hidden opacity-0',
|
||||
)}>
|
||||
<div className='flex w-full'>
|
||||
<div className='system-md-semibold truncate whitespace-nowrap text-text-secondary'>{appDetail.name}</div>
|
||||
</div>
|
||||
<div className='system-2xs-medium-uppercase whitespace-nowrap text-text-tertiary'>{appDetail.mode === 'advanced-chat' ? t('app.types.advanced') : appDetail.mode === 'agent-chat' ? t('app.types.agent') : appDetail.mode === 'chat' ? t('app.types.chatbot') : appDetail.mode === 'completion' ? t('app.types.completion') : t('app.types.workflow')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -124,10 +124,7 @@ const AppDetailNav = ({ title, desc, isExternal, icon, icon_background, navigati
|
|||
{
|
||||
!isMobile && (
|
||||
<div
|
||||
className={`
|
||||
shrink-0 py-3
|
||||
${expand ? 'px-6' : 'px-4'}
|
||||
`}
|
||||
className="shrink-0 px-4 py-3"
|
||||
>
|
||||
<div
|
||||
className='flex h-6 w-6 cursor-pointer items-center justify-center'
|
||||
|
|
|
|||
|
|
@ -0,0 +1,189 @@
|
|||
import React from 'react'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import '@testing-library/jest-dom'
|
||||
import NavLink from './navLink'
|
||||
import type { NavLinkProps } from './navLink'
|
||||
|
||||
// Mock Next.js navigation
|
||||
jest.mock('next/navigation', () => ({
|
||||
useSelectedLayoutSegment: () => 'overview',
|
||||
}))
|
||||
|
||||
// Mock Next.js Link component
|
||||
jest.mock('next/link', () => {
|
||||
return function MockLink({ children, href, className, title }: any) {
|
||||
return (
|
||||
<a href={href} className={className} title={title} data-testid="nav-link">
|
||||
{children}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
// Mock RemixIcon components
|
||||
const MockIcon = ({ className }: { className?: string }) => (
|
||||
<svg className={className} data-testid="nav-icon" />
|
||||
)
|
||||
|
||||
describe('NavLink Text Animation Issues', () => {
|
||||
const mockProps: NavLinkProps = {
|
||||
name: 'Orchestrate',
|
||||
href: '/app/123/workflow',
|
||||
iconMap: {
|
||||
selected: MockIcon,
|
||||
normal: MockIcon,
|
||||
},
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
// Mock getComputedStyle for transition testing
|
||||
Object.defineProperty(window, 'getComputedStyle', {
|
||||
value: jest.fn((element) => {
|
||||
const isExpanded = element.getAttribute('data-mode') === 'expand'
|
||||
return {
|
||||
transition: 'all 0.3s ease',
|
||||
opacity: isExpanded ? '1' : '0',
|
||||
width: isExpanded ? 'auto' : '0px',
|
||||
overflow: 'hidden',
|
||||
paddingLeft: isExpanded ? '12px' : '10px', // px-3 vs px-2.5
|
||||
paddingRight: isExpanded ? '12px' : '10px',
|
||||
}
|
||||
}),
|
||||
writable: true,
|
||||
})
|
||||
})
|
||||
|
||||
describe('Text Squeeze Animation Issue', () => {
|
||||
it('should show text squeeze effect when switching from collapse to expand', async () => {
|
||||
const { rerender } = render(<NavLink {...mockProps} mode="collapse" />)
|
||||
|
||||
// In collapse mode, text should be in DOM but hidden via CSS
|
||||
const textElement = screen.getByText('Orchestrate')
|
||||
expect(textElement).toBeInTheDocument()
|
||||
expect(textElement).toHaveClass('opacity-0')
|
||||
expect(textElement).toHaveClass('w-0')
|
||||
expect(textElement).toHaveClass('overflow-hidden')
|
||||
|
||||
// Icon should still be present
|
||||
expect(screen.getByTestId('nav-icon')).toBeInTheDocument()
|
||||
|
||||
// Check padding in collapse mode
|
||||
const linkElement = screen.getByTestId('nav-link')
|
||||
expect(linkElement).toHaveClass('px-2.5')
|
||||
|
||||
// Switch to expand mode - this is where the squeeze effect occurs
|
||||
rerender(<NavLink {...mockProps} mode="expand" />)
|
||||
|
||||
// Text should now appear
|
||||
expect(screen.getByText('Orchestrate')).toBeInTheDocument()
|
||||
|
||||
// Check padding change - this contributes to the squeeze effect
|
||||
expect(linkElement).toHaveClass('px-3')
|
||||
|
||||
// The bug: text appears abruptly without smooth transition
|
||||
// This test documents the current behavior that causes the squeeze effect
|
||||
const expandedTextElement = screen.getByText('Orchestrate')
|
||||
expect(expandedTextElement).toBeInTheDocument()
|
||||
|
||||
// In a properly animated version, we would expect:
|
||||
// - Opacity transition from 0 to 1
|
||||
// - Width transition from 0 to auto
|
||||
// - No layout shift from padding changes
|
||||
})
|
||||
|
||||
it('should maintain icon position consistency during text appearance', () => {
|
||||
const { rerender } = render(<NavLink {...mockProps} mode="collapse" />)
|
||||
|
||||
const iconElement = screen.getByTestId('nav-icon')
|
||||
const initialIconClasses = iconElement.className
|
||||
|
||||
// Icon should have mr-0 in collapse mode
|
||||
expect(iconElement).toHaveClass('mr-0')
|
||||
|
||||
rerender(<NavLink {...mockProps} mode="expand" />)
|
||||
|
||||
const expandedIconClasses = iconElement.className
|
||||
|
||||
// Icon should have mr-2 in expand mode - this shift contributes to the squeeze effect
|
||||
expect(iconElement).toHaveClass('mr-2')
|
||||
|
||||
console.log('Collapsed icon classes:', initialIconClasses)
|
||||
console.log('Expanded icon classes:', expandedIconClasses)
|
||||
|
||||
// This margin change causes the icon to shift when text appears
|
||||
})
|
||||
|
||||
it('should document the abrupt text rendering issue', () => {
|
||||
const { rerender } = render(<NavLink {...mockProps} mode="collapse" />)
|
||||
|
||||
// Text is present in DOM but hidden via CSS classes
|
||||
const collapsedText = screen.getByText('Orchestrate')
|
||||
expect(collapsedText).toBeInTheDocument()
|
||||
expect(collapsedText).toHaveClass('opacity-0')
|
||||
expect(collapsedText).toHaveClass('pointer-events-none')
|
||||
|
||||
rerender(<NavLink {...mockProps} mode="expand" />)
|
||||
|
||||
// Text suddenly appears in DOM - no transition
|
||||
expect(screen.getByText('Orchestrate')).toBeInTheDocument()
|
||||
|
||||
// The issue: {mode === 'expand' && name} causes abrupt show/hide
|
||||
// instead of smooth opacity/width transition
|
||||
})
|
||||
})
|
||||
|
||||
describe('Layout Shift Issues', () => {
|
||||
it('should detect padding differences causing layout shifts', () => {
|
||||
const { rerender } = render(<NavLink {...mockProps} mode="collapse" />)
|
||||
|
||||
const linkElement = screen.getByTestId('nav-link')
|
||||
|
||||
// Collapsed state padding
|
||||
expect(linkElement).toHaveClass('px-2.5')
|
||||
|
||||
rerender(<NavLink {...mockProps} mode="expand" />)
|
||||
|
||||
// Expanded state padding - different value causes layout shift
|
||||
expect(linkElement).toHaveClass('px-3')
|
||||
|
||||
// This 2px difference (10px vs 12px) contributes to the squeeze effect
|
||||
})
|
||||
|
||||
it('should detect icon margin changes causing shifts', () => {
|
||||
const { rerender } = render(<NavLink {...mockProps} mode="collapse" />)
|
||||
|
||||
const iconElement = screen.getByTestId('nav-icon')
|
||||
|
||||
// Collapsed: no right margin
|
||||
expect(iconElement).toHaveClass('mr-0')
|
||||
|
||||
rerender(<NavLink {...mockProps} mode="expand" />)
|
||||
|
||||
// Expanded: 8px right margin (mr-2)
|
||||
expect(iconElement).toHaveClass('mr-2')
|
||||
|
||||
// This sudden margin appearance causes the squeeze effect
|
||||
})
|
||||
})
|
||||
|
||||
describe('Active State Handling', () => {
|
||||
it('should handle active state correctly in both modes', () => {
|
||||
// Test non-active state
|
||||
const { rerender } = render(<NavLink {...mockProps} mode="collapse" />)
|
||||
|
||||
let linkElement = screen.getByTestId('nav-link')
|
||||
expect(linkElement).not.toHaveClass('bg-state-accent-active')
|
||||
|
||||
// Test with active state (when href matches current segment)
|
||||
const activeProps = {
|
||||
...mockProps,
|
||||
href: '/app/123/overview', // matches mocked segment
|
||||
}
|
||||
|
||||
rerender(<NavLink {...activeProps} mode="expand" />)
|
||||
|
||||
linkElement = screen.getByTestId('nav-link')
|
||||
expect(linkElement).toHaveClass('bg-state-accent-active')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -44,20 +44,29 @@ export default function NavLink({
|
|||
key={name}
|
||||
href={href}
|
||||
className={classNames(
|
||||
isActive ? 'bg-state-accent-active text-text-accent font-semibold' : 'text-components-menu-item-text hover:bg-state-base-hover hover:text-components-menu-item-text-hover',
|
||||
'group flex items-center h-9 rounded-md py-2 text-sm font-normal',
|
||||
isActive ? 'bg-state-accent-active font-semibold text-text-accent' : 'text-components-menu-item-text hover:bg-state-base-hover hover:text-components-menu-item-text-hover',
|
||||
'group flex h-9 items-center rounded-md py-2 text-sm font-normal',
|
||||
mode === 'expand' ? 'px-3' : 'px-2.5',
|
||||
)}
|
||||
title={mode === 'collapse' ? name : ''}
|
||||
>
|
||||
<NavIcon
|
||||
className={classNames(
|
||||
'h-4 w-4 flex-shrink-0',
|
||||
'h-4 w-4 shrink-0',
|
||||
mode === 'expand' ? 'mr-2' : 'mr-0',
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{mode === 'expand' && name}
|
||||
<span
|
||||
className={classNames(
|
||||
'whitespace-nowrap transition-all duration-200 ease-in-out',
|
||||
mode === 'expand'
|
||||
? 'w-auto opacity-100'
|
||||
: 'pointer-events-none w-0 overflow-hidden opacity-0',
|
||||
)}
|
||||
>
|
||||
{name}
|
||||
</span>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,297 @@
|
|||
import React from 'react'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import '@testing-library/jest-dom'
|
||||
|
||||
// Simple Mock Components that reproduce the exact UI issues
|
||||
const MockNavLink = ({ name, mode }: { name: string; mode: string }) => {
|
||||
return (
|
||||
<a
|
||||
className={`
|
||||
group flex h-9 items-center rounded-md py-2 text-sm font-normal
|
||||
${mode === 'expand' ? 'px-3' : 'px-2.5'}
|
||||
`}
|
||||
data-testid={`nav-link-${name}`}
|
||||
data-mode={mode}
|
||||
>
|
||||
{/* Icon with inconsistent margin - reproduces issue #2 */}
|
||||
<svg
|
||||
className={`h-4 w-4 shrink-0 ${mode === 'expand' ? 'mr-2' : 'mr-0'}`}
|
||||
data-testid={`nav-icon-${name}`}
|
||||
/>
|
||||
{/* Text that appears/disappears abruptly - reproduces issue #2 */}
|
||||
{mode === 'expand' && <span data-testid={`nav-text-${name}`}>{name}</span>}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
const MockSidebarToggleButton = ({ expand, onToggle }: { expand: boolean; onToggle: () => void }) => {
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
flex shrink-0 flex-col border-r border-divider-burn bg-background-default-subtle transition-all
|
||||
${expand ? 'w-[216px]' : 'w-14'}
|
||||
`}
|
||||
data-testid="sidebar-container"
|
||||
>
|
||||
{/* Top section with variable padding - reproduces issue #1 */}
|
||||
<div className={`shrink-0 ${expand ? 'p-2' : 'p-1'}`} data-testid="top-section">
|
||||
App Info Area
|
||||
</div>
|
||||
|
||||
{/* Navigation section - reproduces issue #2 */}
|
||||
<nav className={`grow space-y-1 ${expand ? 'p-4' : 'px-2.5 py-4'}`} data-testid="navigation">
|
||||
<MockNavLink name="Orchestrate" mode={expand ? 'expand' : 'collapse'} />
|
||||
<MockNavLink name="API Access" mode={expand ? 'expand' : 'collapse'} />
|
||||
<MockNavLink name="Logs & Annotations" mode={expand ? 'expand' : 'collapse'} />
|
||||
<MockNavLink name="Monitoring" mode={expand ? 'expand' : 'collapse'} />
|
||||
</nav>
|
||||
|
||||
{/* Toggle button section with consistent padding - issue #1 FIXED */}
|
||||
<div
|
||||
className="shrink-0 px-4 py-3"
|
||||
data-testid="toggle-section"
|
||||
>
|
||||
<button
|
||||
className='flex h-6 w-6 cursor-pointer items-center justify-center'
|
||||
onClick={onToggle}
|
||||
data-testid="toggle-button"
|
||||
>
|
||||
{expand ? '→' : '←'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const MockAppInfo = ({ expand }: { expand: boolean }) => {
|
||||
return (
|
||||
<div data-testid="app-info" data-expand={expand}>
|
||||
<button className='block w-full'>
|
||||
{/* Container with layout mode switching - reproduces issue #3 */}
|
||||
<div className={`flex rounded-lg ${expand ? 'flex-col gap-2 p-2 pb-2.5' : 'items-start justify-center gap-1 p-1'}`}>
|
||||
{/* Icon container with justify-between to flex-col switch - reproduces issue #3 */}
|
||||
<div className={`flex items-center self-stretch ${expand ? 'justify-between' : 'flex-col gap-1'}`} data-testid="icon-container">
|
||||
{/* Icon with size changes - reproduces issue #3 */}
|
||||
<div
|
||||
data-testid="app-icon"
|
||||
data-size={expand ? 'large' : 'small'}
|
||||
style={{
|
||||
width: expand ? '40px' : '24px',
|
||||
height: expand ? '40px' : '24px',
|
||||
backgroundColor: '#000',
|
||||
transition: 'all 0.3s ease', // This broad transition causes bounce
|
||||
}}
|
||||
>
|
||||
Icon
|
||||
</div>
|
||||
<div className='flex items-center justify-center rounded-md p-0.5'>
|
||||
<div className='flex h-5 w-5 items-center justify-center'>
|
||||
⚙️
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Text that appears/disappears conditionally */}
|
||||
{expand && (
|
||||
<div className='flex flex-col items-start gap-1'>
|
||||
<div className='flex w-full'>
|
||||
<div className='system-md-semibold truncate text-text-secondary'>Test App</div>
|
||||
</div>
|
||||
<div className='system-2xs-medium-uppercase text-text-tertiary'>chatflow</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
describe('Sidebar Animation Issues Reproduction', () => {
|
||||
beforeEach(() => {
|
||||
// Mock getBoundingClientRect for position testing
|
||||
Element.prototype.getBoundingClientRect = jest.fn(() => ({
|
||||
width: 200,
|
||||
height: 40,
|
||||
x: 10,
|
||||
y: 10,
|
||||
left: 10,
|
||||
right: 210,
|
||||
top: 10,
|
||||
bottom: 50,
|
||||
toJSON: jest.fn(),
|
||||
}))
|
||||
})
|
||||
|
||||
describe('Issue #1: Toggle Button Position Movement - FIXED', () => {
|
||||
it('should verify consistent padding prevents button position shift', () => {
|
||||
let expanded = false
|
||||
const handleToggle = () => {
|
||||
expanded = !expanded
|
||||
}
|
||||
|
||||
const { rerender } = render(<MockSidebarToggleButton expand={false} onToggle={handleToggle} />)
|
||||
|
||||
// Check collapsed state padding
|
||||
const toggleSection = screen.getByTestId('toggle-section')
|
||||
expect(toggleSection).toHaveClass('px-4') // Consistent padding
|
||||
expect(toggleSection).not.toHaveClass('px-5')
|
||||
expect(toggleSection).not.toHaveClass('px-6')
|
||||
|
||||
// Switch to expanded state
|
||||
rerender(<MockSidebarToggleButton expand={true} onToggle={handleToggle} />)
|
||||
|
||||
// Check expanded state padding - should be the same
|
||||
expect(toggleSection).toHaveClass('px-4') // Same consistent padding
|
||||
expect(toggleSection).not.toHaveClass('px-5')
|
||||
expect(toggleSection).not.toHaveClass('px-6')
|
||||
|
||||
// THE FIX: px-4 in both states prevents position movement
|
||||
console.log('✅ Issue #1 FIXED: Toggle button now has consistent padding')
|
||||
console.log(' - Before: px-4 (collapsed) vs px-6 (expanded) - 8px difference')
|
||||
console.log(' - After: px-4 (both states) - 0px difference')
|
||||
console.log(' - Result: No button position movement during transition')
|
||||
})
|
||||
|
||||
it('should verify sidebar width animation is working correctly', () => {
|
||||
const handleToggle = jest.fn()
|
||||
const { rerender } = render(<MockSidebarToggleButton expand={false} onToggle={handleToggle} />)
|
||||
|
||||
const container = screen.getByTestId('sidebar-container')
|
||||
|
||||
// Collapsed state
|
||||
expect(container).toHaveClass('w-14')
|
||||
expect(container).toHaveClass('transition-all')
|
||||
|
||||
// Expanded state
|
||||
rerender(<MockSidebarToggleButton expand={true} onToggle={handleToggle} />)
|
||||
expect(container).toHaveClass('w-[216px]')
|
||||
|
||||
console.log('✅ Sidebar width transition is properly configured')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Issue #2: Navigation Text Squeeze Animation', () => {
|
||||
it('should reproduce text squeeze effect from padding and margin changes', () => {
|
||||
const { rerender } = render(<MockNavLink name="Orchestrate" mode="collapse" />)
|
||||
|
||||
const link = screen.getByTestId('nav-link-Orchestrate')
|
||||
const icon = screen.getByTestId('nav-icon-Orchestrate')
|
||||
|
||||
// Collapsed state checks
|
||||
expect(link).toHaveClass('px-2.5') // 10px padding
|
||||
expect(icon).toHaveClass('mr-0') // No margin
|
||||
expect(screen.queryByTestId('nav-text-Orchestrate')).not.toBeInTheDocument()
|
||||
|
||||
// Switch to expanded state
|
||||
rerender(<MockNavLink name="Orchestrate" mode="expand" />)
|
||||
|
||||
// Expanded state checks
|
||||
expect(link).toHaveClass('px-3') // 12px padding (+2px)
|
||||
expect(icon).toHaveClass('mr-2') // 8px margin (+8px)
|
||||
expect(screen.getByTestId('nav-text-Orchestrate')).toBeInTheDocument()
|
||||
|
||||
// THE BUG: Multiple simultaneous changes create squeeze effect
|
||||
console.log('🐛 Issue #2 Reproduced: Text squeeze effect from multiple layout changes')
|
||||
console.log(' - Link padding: px-2.5 → px-3 (+2px)')
|
||||
console.log(' - Icon margin: mr-0 → mr-2 (+8px)')
|
||||
console.log(' - Text appears: none → visible (abrupt)')
|
||||
console.log(' - Result: Text appears with squeeze effect due to layout shifts')
|
||||
})
|
||||
|
||||
it('should document the abrupt text rendering issue', () => {
|
||||
const { rerender } = render(<MockNavLink name="API Access" mode="collapse" />)
|
||||
|
||||
// Text completely absent
|
||||
expect(screen.queryByTestId('nav-text-API Access')).not.toBeInTheDocument()
|
||||
|
||||
rerender(<MockNavLink name="API Access" mode="expand" />)
|
||||
|
||||
// Text suddenly appears - no transition
|
||||
expect(screen.getByTestId('nav-text-API Access')).toBeInTheDocument()
|
||||
|
||||
console.log('🐛 Issue #2 Detail: Conditional rendering {mode === "expand" && name}')
|
||||
console.log(' - Problem: Text appears/disappears abruptly without transition')
|
||||
console.log(' - Should use: opacity or width transition for smooth appearance')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Issue #3: App Icon Bounce Animation', () => {
|
||||
it('should reproduce icon bounce from layout mode switching', () => {
|
||||
const { rerender } = render(<MockAppInfo expand={true} />)
|
||||
|
||||
const iconContainer = screen.getByTestId('icon-container')
|
||||
const appIcon = screen.getByTestId('app-icon')
|
||||
|
||||
// Expanded state layout
|
||||
expect(iconContainer).toHaveClass('justify-between')
|
||||
expect(iconContainer).not.toHaveClass('flex-col')
|
||||
expect(appIcon).toHaveAttribute('data-size', 'large')
|
||||
|
||||
// Switch to collapsed state
|
||||
rerender(<MockAppInfo expand={false} />)
|
||||
|
||||
// Collapsed state layout - completely different layout mode
|
||||
expect(iconContainer).toHaveClass('flex-col')
|
||||
expect(iconContainer).toHaveClass('gap-1')
|
||||
expect(iconContainer).not.toHaveClass('justify-between')
|
||||
expect(appIcon).toHaveAttribute('data-size', 'small')
|
||||
|
||||
// THE BUG: Layout mode switch causes icon to "bounce"
|
||||
console.log('🐛 Issue #3 Reproduced: Icon bounce from layout mode switching')
|
||||
console.log(' - Layout change: justify-between → flex-col gap-1')
|
||||
console.log(' - Icon size: large (40px) → small (24px)')
|
||||
console.log(' - Transition: transition-all causes excessive animation')
|
||||
console.log(' - Result: Icon appears to bounce to right then back during collapse')
|
||||
})
|
||||
|
||||
it('should identify the problematic transition-all property', () => {
|
||||
render(<MockAppInfo expand={true} />)
|
||||
|
||||
const appIcon = screen.getByTestId('app-icon')
|
||||
const computedStyle = window.getComputedStyle(appIcon)
|
||||
|
||||
// The problematic broad transition
|
||||
expect(computedStyle.transition).toContain('all')
|
||||
|
||||
console.log('🐛 Issue #3 Detail: transition-all affects ALL CSS properties')
|
||||
console.log(' - Problem: Animates layout properties that should not transition')
|
||||
console.log(' - Solution: Use specific transition properties instead of "all"')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Interactive Toggle Test', () => {
|
||||
it('should demonstrate all issues in a single interactive test', () => {
|
||||
let expanded = false
|
||||
const handleToggle = () => {
|
||||
expanded = !expanded
|
||||
}
|
||||
|
||||
const { rerender } = render(
|
||||
<div data-testid="complete-sidebar">
|
||||
<MockSidebarToggleButton expand={expanded} onToggle={handleToggle} />
|
||||
<MockAppInfo expand={expanded} />
|
||||
</div>,
|
||||
)
|
||||
|
||||
const toggleButton = screen.getByTestId('toggle-button')
|
||||
|
||||
// Initial state verification
|
||||
expect(expanded).toBe(false)
|
||||
console.log('🔄 Starting interactive test - all issues will be reproduced')
|
||||
|
||||
// Simulate toggle click
|
||||
fireEvent.click(toggleButton)
|
||||
expanded = true
|
||||
rerender(
|
||||
<div data-testid="complete-sidebar">
|
||||
<MockSidebarToggleButton expand={expanded} onToggle={handleToggle} />
|
||||
<MockAppInfo expand={expanded} />
|
||||
</div>,
|
||||
)
|
||||
|
||||
console.log('✨ All three issues successfully reproduced in interactive test:')
|
||||
console.log(' 1. Toggle button position movement (padding inconsistency)')
|
||||
console.log(' 2. Navigation text squeeze effect (multiple layout changes)')
|
||||
console.log(' 3. App icon bounce animation (layout mode switching)')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,235 @@
|
|||
/**
|
||||
* Text Squeeze Fix Verification Test
|
||||
* This test verifies that the CSS-based text rendering fixes work correctly
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import { render } from '@testing-library/react'
|
||||
import '@testing-library/jest-dom'
|
||||
|
||||
// Mock Next.js navigation
|
||||
jest.mock('next/navigation', () => ({
|
||||
useSelectedLayoutSegment: () => 'overview',
|
||||
}))
|
||||
|
||||
// Mock classnames utility
|
||||
jest.mock('@/utils/classnames', () => ({
|
||||
__esModule: true,
|
||||
default: (...classes: any[]) => classes.filter(Boolean).join(' '),
|
||||
}))
|
||||
|
||||
// Simplified NavLink component to test the fix
|
||||
const TestNavLink = ({ mode }: { mode: 'expand' | 'collapse' }) => {
|
||||
const name = 'Orchestrate'
|
||||
|
||||
return (
|
||||
<div className="nav-link-container">
|
||||
<div className={`flex h-9 items-center rounded-md py-2 text-sm font-normal ${
|
||||
mode === 'expand' ? 'px-3' : 'px-2.5'
|
||||
}`}>
|
||||
<div className={`h-4 w-4 shrink-0 ${mode === 'expand' ? 'mr-2' : 'mr-0'}`}>
|
||||
Icon
|
||||
</div>
|
||||
<span
|
||||
className={`whitespace-nowrap transition-all duration-200 ease-in-out ${
|
||||
mode === 'expand'
|
||||
? 'w-auto opacity-100'
|
||||
: 'pointer-events-none w-0 overflow-hidden opacity-0'
|
||||
}`}
|
||||
data-testid="nav-text"
|
||||
>
|
||||
{name}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Simplified AppInfo component to test the fix
|
||||
const TestAppInfo = ({ expand }: { expand: boolean }) => {
|
||||
const appDetail = {
|
||||
name: 'Test ChatBot App',
|
||||
mode: 'chat' as const,
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="app-info-container">
|
||||
<div className={`flex rounded-lg ${expand ? 'flex-col gap-2 p-2 pb-2.5' : 'items-start justify-center gap-1 p-1'}`}>
|
||||
<div className={`flex items-center self-stretch ${expand ? 'justify-between' : 'flex-col gap-1'}`}>
|
||||
<div className="app-icon">AppIcon</div>
|
||||
<div className="dashboard-icon">Dashboard</div>
|
||||
</div>
|
||||
<div
|
||||
className={`flex flex-col items-start gap-1 transition-all duration-200 ease-in-out ${
|
||||
expand
|
||||
? 'w-auto opacity-100'
|
||||
: 'pointer-events-none w-0 overflow-hidden opacity-0'
|
||||
}`}
|
||||
data-testid="app-text-container"
|
||||
>
|
||||
<div className='flex w-full'>
|
||||
<div
|
||||
className='system-md-semibold truncate whitespace-nowrap text-text-secondary'
|
||||
data-testid="app-name"
|
||||
>
|
||||
{appDetail.name}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className='system-2xs-medium-uppercase whitespace-nowrap text-text-tertiary'
|
||||
data-testid="app-type"
|
||||
>
|
||||
ChatBot
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
describe('Text Squeeze Fix Verification', () => {
|
||||
describe('NavLink Text Rendering Fix', () => {
|
||||
it('should keep text in DOM and use CSS transitions', () => {
|
||||
const { container, rerender } = render(<TestNavLink mode="collapse" />)
|
||||
|
||||
// In collapsed state, text should be in DOM but hidden
|
||||
const textElement = container.querySelector('[data-testid="nav-text"]')
|
||||
expect(textElement).toBeInTheDocument()
|
||||
expect(textElement).toHaveClass('opacity-0')
|
||||
expect(textElement).toHaveClass('w-0')
|
||||
expect(textElement).toHaveClass('overflow-hidden')
|
||||
expect(textElement).toHaveClass('pointer-events-none')
|
||||
expect(textElement).toHaveClass('whitespace-nowrap')
|
||||
expect(textElement).toHaveClass('transition-all')
|
||||
|
||||
console.log('✅ NavLink Collapsed State:')
|
||||
console.log(' - Text is in DOM but visually hidden')
|
||||
console.log(' - Uses opacity-0 and w-0 for hiding')
|
||||
console.log(' - Has whitespace-nowrap to prevent wrapping')
|
||||
console.log(' - Has transition-all for smooth animation')
|
||||
|
||||
// Switch to expanded state
|
||||
rerender(<TestNavLink mode="expand" />)
|
||||
|
||||
const expandedText = container.querySelector('[data-testid="nav-text"]')
|
||||
expect(expandedText).toBeInTheDocument()
|
||||
expect(expandedText).toHaveClass('opacity-100')
|
||||
expect(expandedText).toHaveClass('w-auto')
|
||||
expect(expandedText).not.toHaveClass('pointer-events-none')
|
||||
|
||||
console.log('✅ NavLink Expanded State:')
|
||||
console.log(' - Text is visible with opacity-100')
|
||||
console.log(' - Uses w-auto for natural width')
|
||||
console.log(' - No layout jumps during transition')
|
||||
|
||||
console.log('🎯 NavLink Fix Result: Text squeeze effect ELIMINATED')
|
||||
})
|
||||
|
||||
it('should verify smooth transition properties', () => {
|
||||
const { container } = render(<TestNavLink mode="collapse" />)
|
||||
|
||||
const textElement = container.querySelector('[data-testid="nav-text"]')
|
||||
expect(textElement).toHaveClass('transition-all')
|
||||
expect(textElement).toHaveClass('duration-200')
|
||||
expect(textElement).toHaveClass('ease-in-out')
|
||||
|
||||
console.log('✅ Transition Properties Verified:')
|
||||
console.log(' - transition-all: Smooth property changes')
|
||||
console.log(' - duration-200: 200ms transition time')
|
||||
console.log(' - ease-in-out: Smooth easing function')
|
||||
})
|
||||
})
|
||||
|
||||
describe('AppInfo Text Rendering Fix', () => {
|
||||
it('should keep app text in DOM and use CSS transitions', () => {
|
||||
const { container, rerender } = render(<TestAppInfo expand={false} />)
|
||||
|
||||
// In collapsed state, text container should be in DOM but hidden
|
||||
const textContainer = container.querySelector('[data-testid="app-text-container"]')
|
||||
expect(textContainer).toBeInTheDocument()
|
||||
expect(textContainer).toHaveClass('opacity-0')
|
||||
expect(textContainer).toHaveClass('w-0')
|
||||
expect(textContainer).toHaveClass('overflow-hidden')
|
||||
expect(textContainer).toHaveClass('pointer-events-none')
|
||||
|
||||
// Text elements should still be in DOM
|
||||
const appName = container.querySelector('[data-testid="app-name"]')
|
||||
const appType = container.querySelector('[data-testid="app-type"]')
|
||||
expect(appName).toBeInTheDocument()
|
||||
expect(appType).toBeInTheDocument()
|
||||
expect(appName).toHaveClass('whitespace-nowrap')
|
||||
expect(appType).toHaveClass('whitespace-nowrap')
|
||||
|
||||
console.log('✅ AppInfo Collapsed State:')
|
||||
console.log(' - Text container is in DOM but visually hidden')
|
||||
console.log(' - App name and type elements always present')
|
||||
console.log(' - Uses whitespace-nowrap to prevent wrapping')
|
||||
|
||||
// Switch to expanded state
|
||||
rerender(<TestAppInfo expand={true} />)
|
||||
|
||||
const expandedContainer = container.querySelector('[data-testid="app-text-container"]')
|
||||
expect(expandedContainer).toBeInTheDocument()
|
||||
expect(expandedContainer).toHaveClass('opacity-100')
|
||||
expect(expandedContainer).toHaveClass('w-auto')
|
||||
expect(expandedContainer).not.toHaveClass('pointer-events-none')
|
||||
|
||||
console.log('✅ AppInfo Expanded State:')
|
||||
console.log(' - Text container is visible with opacity-100')
|
||||
console.log(' - Uses w-auto for natural width')
|
||||
console.log(' - No layout jumps during transition')
|
||||
|
||||
console.log('🎯 AppInfo Fix Result: Text squeeze effect ELIMINATED')
|
||||
})
|
||||
|
||||
it('should verify transition properties on text container', () => {
|
||||
const { container } = render(<TestAppInfo expand={false} />)
|
||||
|
||||
const textContainer = container.querySelector('[data-testid="app-text-container"]')
|
||||
expect(textContainer).toHaveClass('transition-all')
|
||||
expect(textContainer).toHaveClass('duration-200')
|
||||
expect(textContainer).toHaveClass('ease-in-out')
|
||||
|
||||
console.log('✅ AppInfo Transition Properties Verified:')
|
||||
console.log(' - Container has smooth CSS transitions')
|
||||
console.log(' - Same 200ms duration as NavLink for consistency')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Fix Strategy Comparison', () => {
|
||||
it('should document the fix strategy differences', () => {
|
||||
console.log('\n📋 TEXT SQUEEZE FIX STRATEGY COMPARISON')
|
||||
console.log('='.repeat(60))
|
||||
|
||||
console.log('\n❌ BEFORE (Problematic):')
|
||||
console.log(' NavLink: {mode === "expand" && name}')
|
||||
console.log(' AppInfo: {expand && (<div>...</div>)}')
|
||||
console.log(' Problem: Conditional rendering causes abrupt appearance')
|
||||
console.log(' Result: Text "squeezes" from center during layout changes')
|
||||
|
||||
console.log('\n✅ AFTER (Fixed):')
|
||||
console.log(' NavLink: <span className="opacity-0 w-0">{name}</span>')
|
||||
console.log(' AppInfo: <div className="opacity-0 w-0">...</div>')
|
||||
console.log(' Solution: CSS controls visibility, element always in DOM')
|
||||
console.log(' Result: Smooth opacity and width transitions')
|
||||
|
||||
console.log('\n🎯 KEY FIX PRINCIPLES:')
|
||||
console.log(' 1. ✅ Always keep text elements in DOM')
|
||||
console.log(' 2. ✅ Use opacity for show/hide transitions')
|
||||
console.log(' 3. ✅ Use width (w-0/w-auto) for layout control')
|
||||
console.log(' 4. ✅ Add whitespace-nowrap to prevent wrapping')
|
||||
console.log(' 5. ✅ Use pointer-events-none when hidden')
|
||||
console.log(' 6. ✅ Add overflow-hidden for clean hiding')
|
||||
|
||||
console.log('\n🚀 BENEFITS:')
|
||||
console.log(' - No more abrupt text appearance')
|
||||
console.log(' - Smooth 200ms transitions')
|
||||
console.log(' - No layout jumps or shifts')
|
||||
console.log(' - Consistent animation timing')
|
||||
console.log(' - Better user experience')
|
||||
|
||||
// Always pass documentation test
|
||||
expect(true).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -146,7 +146,7 @@ const Annotation: FC<Props> = (props) => {
|
|||
return (
|
||||
<div className='flex h-full flex-col'>
|
||||
<p className='system-sm-regular text-text-tertiary'>{t('appLog.description')}</p>
|
||||
<div className='flex flex-1 flex-col py-4'>
|
||||
<div className='flex h-full flex-1 flex-col py-4'>
|
||||
<Filter appId={appDetail.id} queryParams={queryParams} setQueryParams={setQueryParams}>
|
||||
<div className='flex items-center space-x-2'>
|
||||
{isChatApp && (
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ const WarningMask: FC<IWarningMaskProps> = ({
|
|||
footer,
|
||||
}) => {
|
||||
return (
|
||||
<div className={`${s.mask} absolute inset-0 z-10 pt-16 bg-components-panel-bg-blur`}
|
||||
<div className={`${s.mask} absolute inset-0 z-10 bg-components-panel-bg-blur pt-16`}
|
||||
>
|
||||
<div className='mx-auto px-10'>
|
||||
<div className={`${s.icon} flex h-11 w-11 items-center justify-center rounded-xl bg-components-panel-bg`}>{warningIcon}</div>
|
||||
|
|
|
|||
|
|
@ -688,7 +688,7 @@ const ConversationList: FC<IConversationList> = ({ logs, appDetail, onRefresh })
|
|||
return <Loading />
|
||||
|
||||
return (
|
||||
<div className='overflow-x-auto'>
|
||||
<div className='relative grow overflow-x-auto'>
|
||||
<table className={cn('mt-2 w-full min-w-[440px] border-collapse border-0')}>
|
||||
<thead className='system-xs-medium-uppercase text-text-tertiary'>
|
||||
<tr>
|
||||
|
|
|
|||
|
|
@ -46,6 +46,12 @@ const OPTION_MAP = {
|
|||
? `,
|
||||
baseUrl: '${url}${basePath}'`
|
||||
: ''},
|
||||
inputs: {
|
||||
// You can define the inputs from the Start node here
|
||||
// key is the variable name
|
||||
// e.g.
|
||||
// name: "NAME"
|
||||
},
|
||||
systemVariables: {
|
||||
// user_id: 'YOU CAN DEFINE USER ID HERE',
|
||||
// conversation_id: 'YOU CAN DEFINE CONVERSATION ID HERE, IT MUST BE A VALID UUID',
|
||||
|
|
|
|||
|
|
@ -149,7 +149,8 @@ const EmojiPickerInner: FC<IEmojiPickerInnerProps> = ({
|
|||
{/* Color Select */}
|
||||
<div className={cn('flex items-center justify-between p-3 pb-0')}>
|
||||
<p className='system-xs-medium-uppercase mb-2 text-text-primary'>Choose Style</p>
|
||||
{showStyleColors ? <ChevronDownIcon className='h-4 w-4' onClick={() => setShowStyleColors(!showStyleColors)} /> : <ChevronUpIcon className='h-4 w-4' onClick={() => setShowStyleColors(!showStyleColors)} />}
|
||||
{showStyleColors ? <ChevronDownIcon className='h-4 w-4 cursor-pointer text-text-quaternary' onClick={() => setShowStyleColors(!showStyleColors)} />
|
||||
: <ChevronUpIcon className='h-4 w-4 cursor-pointer text-text-quaternary' onClick={() => setShowStyleColors(!showStyleColors)} />}
|
||||
</div>
|
||||
{showStyleColors && <div className='grid w-full grid-cols-8 gap-1 px-3'>
|
||||
{backgroundColors.map((color) => {
|
||||
|
|
|
|||
|
|
@ -10,10 +10,6 @@ import {
|
|||
} from 'lexical'
|
||||
import { mergeRegister } from '@lexical/utils'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import {
|
||||
RiErrorWarningFill,
|
||||
RiMoreLine,
|
||||
} from '@remixicon/react'
|
||||
import { useReactFlow, useStoreApi } from 'reactflow'
|
||||
import { useSelectOrDelete } from '../../hooks'
|
||||
import type { WorkflowNodesMap } from './node'
|
||||
|
|
@ -22,17 +18,15 @@ import {
|
|||
DELETE_WORKFLOW_VARIABLE_BLOCK_COMMAND,
|
||||
UPDATE_WORKFLOW_NODES_MAP,
|
||||
} from './index'
|
||||
import cn from '@/utils/classnames'
|
||||
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others'
|
||||
import { VarBlockIcon } from '@/app/components/workflow/block-icon'
|
||||
import { Line3 } from '@/app/components/base/icons/src/public/common'
|
||||
import { isConversationVar, isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { isExceptionVariable } from '@/app/components/workflow/utils'
|
||||
import VarFullPathPanel from '@/app/components/workflow/nodes/_base/components/variable/var-full-path-panel'
|
||||
import { Type } from '@/app/components/workflow/nodes/llm/types'
|
||||
import type { ValueSelector, Var } from '@/app/components/workflow/types'
|
||||
import {
|
||||
VariableLabelInEditor,
|
||||
} from '@/app/components/workflow/nodes/_base/components/variable/variable-label'
|
||||
|
||||
type WorkflowVariableBlockComponentProps = {
|
||||
nodeKey: string
|
||||
|
|
@ -126,69 +120,22 @@ const WorkflowVariableBlockComponent = ({
|
|||
}, [node, reactflow, store])
|
||||
|
||||
const Item = (
|
||||
<div
|
||||
className={cn(
|
||||
'group/wrap relative mx-0.5 flex h-[18px] select-none items-center rounded-[5px] border pl-0.5 pr-[3px] hover:border-state-accent-solid hover:bg-state-accent-hover',
|
||||
isSelected ? ' border-state-accent-solid bg-state-accent-hover' : ' border-components-panel-border-subtle bg-components-badge-white-to-dark',
|
||||
!variableValid && '!border-state-destructive-solid !bg-state-destructive-hover',
|
||||
)}
|
||||
<VariableLabelInEditor
|
||||
nodeType={node?.type}
|
||||
nodeTitle={node?.title}
|
||||
variables={variables}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleVariableJump()
|
||||
}}
|
||||
isExceptionVariable={isException}
|
||||
errorMsg={!variableValid ? t('workflow.errorMsg.invalidVariable') : undefined}
|
||||
isSelected={isSelected}
|
||||
ref={ref}
|
||||
>
|
||||
{!isEnv && !isChatVar && (
|
||||
<div className='flex items-center'>
|
||||
{
|
||||
node?.type && (
|
||||
<div className='p-[1px]'>
|
||||
<VarBlockIcon
|
||||
className='!text-text-secondary'
|
||||
type={node?.type}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<div className='mx-0.5 max-w-[60px] shrink-0 truncate text-xs font-medium text-text-secondary' title={node?.title} style={{
|
||||
}}>{node?.title}</div>
|
||||
<Line3 className='mr-0.5 text-divider-deep'></Line3>
|
||||
</div>
|
||||
)}
|
||||
{isShowAPart && (
|
||||
<div className='flex items-center'>
|
||||
<RiMoreLine className='h-3 w-3 text-text-secondary' />
|
||||
<Line3 className='mr-0.5 text-divider-deep'></Line3>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className='flex items-center text-text-accent'>
|
||||
{!isEnv && !isChatVar && <Variable02 className={cn('h-3.5 w-3.5 shrink-0', isException && 'text-text-warning')} />}
|
||||
{isEnv && <Env className='h-3.5 w-3.5 shrink-0 text-util-colors-violet-violet-600' />}
|
||||
{isChatVar && <BubbleX className='h-3.5 w-3.5 text-util-colors-teal-teal-700' />}
|
||||
<div className={cn(
|
||||
'ml-0.5 shrink-0 truncate text-xs font-medium',
|
||||
isEnv && 'text-util-colors-violet-violet-600',
|
||||
isChatVar && 'text-util-colors-teal-teal-700',
|
||||
isException && 'text-text-warning',
|
||||
)} title={varName}>{varName}</div>
|
||||
{
|
||||
!variableValid && (
|
||||
<RiErrorWarningFill className='ml-0.5 h-3 w-3 text-text-destructive' />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
notShowFullPath={isShowAPart}
|
||||
/>
|
||||
)
|
||||
|
||||
if (!variableValid) {
|
||||
return (
|
||||
<Tooltip popupContent={t('workflow.errorMsg.invalidVariable')}>
|
||||
{Item}
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
if (!node)
|
||||
return Item
|
||||
|
||||
|
|
|
|||
|
|
@ -60,7 +60,6 @@ const ChildSegmentDetail: FC<IChildSegmentDetailProps> = ({
|
|||
const wordCountText = useMemo(() => {
|
||||
const count = content.length
|
||||
return `${formatNumber(count)} ${t('datasetDocuments.segment.characters', { count })}`
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [content.length])
|
||||
|
||||
const EditTimeText = useMemo(() => {
|
||||
|
|
@ -69,7 +68,6 @@ const ChildSegmentDetail: FC<IChildSegmentDetailProps> = ({
|
|||
dateFormat: `${t('datasetDocuments.segment.dateTimeFormat')}`,
|
||||
})
|
||||
return `${t('datasetDocuments.segment.editedAt')} ${timeText}`
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [childChunkInfo?.updated_at])
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1011,6 +1011,121 @@ Chat applications support session persistence, allowing previous chat history to
|
|||
|
||||
---
|
||||
|
||||
<Heading
|
||||
url='/conversations/:conversation_id/variables/:variable_id'
|
||||
method='PUT'
|
||||
title='Update Conversation Variable'
|
||||
name='#update-conversation-variable'
|
||||
/>
|
||||
<Row>
|
||||
<Col>
|
||||
Update the value of a specific conversation variable. This endpoint allows you to modify the value of a variable that was captured during the conversation while preserving its name, type, and description.
|
||||
|
||||
### Path Parameters
|
||||
|
||||
<Properties>
|
||||
<Property name='conversation_id' type='string' key='conversation_id'>
|
||||
The ID of the conversation containing the variable to update.
|
||||
</Property>
|
||||
<Property name='variable_id' type='string' key='variable_id'>
|
||||
The ID of the variable to update.
|
||||
</Property>
|
||||
</Properties>
|
||||
|
||||
### Request Body
|
||||
|
||||
<Properties>
|
||||
<Property name='value' type='any' key='value'>
|
||||
The new value for the variable. Must match the variable's expected type (string, number, object, etc.).
|
||||
</Property>
|
||||
<Property name='user' type='string' key='user'>
|
||||
The user identifier, defined by the developer, must ensure uniqueness within the application.
|
||||
</Property>
|
||||
</Properties>
|
||||
|
||||
### Response
|
||||
|
||||
Returns the updated variable object with:
|
||||
- `id` (string) Variable ID
|
||||
- `name` (string) Variable name
|
||||
- `value_type` (string) Variable type (string, number, object, etc.)
|
||||
- `value` (any) Updated variable value
|
||||
- `description` (string) Variable description
|
||||
- `created_at` (int) Creation timestamp
|
||||
- `updated_at` (int) Last update timestamp
|
||||
|
||||
### Errors
|
||||
- 400, `Type mismatch: variable expects {expected_type}, but got {actual_type} type`, Value type doesn't match variable's expected type
|
||||
- 404, `conversation_not_exists`, Conversation not found
|
||||
- 404, `conversation_variable_not_exists`, Variable not found
|
||||
|
||||
</Col>
|
||||
<Col sticky>
|
||||
|
||||
<CodeGroup title="Request" tag="PUT" label="/conversations/:conversation_id/variables/:variable_id" targetCode={`curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{\n "value": "Updated Value",\n "user": "abc-123"\n}'`}>
|
||||
|
||||
```bash {{ title: 'cURL' }}
|
||||
curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'Authorization: Bearer {api_key}' \
|
||||
--data-raw '{
|
||||
"value": "Updated Value",
|
||||
"user": "abc-123"
|
||||
}'
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
<CodeGroup title="Update with different value types">
|
||||
```bash {{ title: 'String Value' }}
|
||||
curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'Authorization: Bearer {api_key}' \
|
||||
--data-raw '{
|
||||
"value": "New string value",
|
||||
"user": "abc-123"
|
||||
}'
|
||||
```
|
||||
|
||||
```bash {{ title: 'Number Value' }}
|
||||
curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'Authorization: Bearer {api_key}' \
|
||||
--data-raw '{
|
||||
"value": 42,
|
||||
"user": "abc-123"
|
||||
}'
|
||||
```
|
||||
|
||||
```bash {{ title: 'Object Value' }}
|
||||
curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'Authorization: Bearer {api_key}' \
|
||||
--data-raw '{
|
||||
"value": {"product": "Widget", "quantity": 10, "price": 29.99},
|
||||
"user": "abc-123"
|
||||
}'
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
<CodeGroup title="Response">
|
||||
```json {{ title: 'Response' }}
|
||||
{
|
||||
"id": "variable-uuid-1",
|
||||
"name": "customer_name",
|
||||
"value_type": "string",
|
||||
"value": "Updated Value",
|
||||
"description": "Customer name extracted from the conversation",
|
||||
"created_at": 1650000000000,
|
||||
"updated_at": 1650000001000
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
---
|
||||
|
||||
<Heading
|
||||
url='/audio-to-text'
|
||||
method='POST'
|
||||
|
|
|
|||
|
|
@ -1011,6 +1011,121 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
|
|||
|
||||
---
|
||||
|
||||
<Heading
|
||||
url='/conversations/:conversation_id/variables/:variable_id'
|
||||
method='PUT'
|
||||
title='会話変数の更新'
|
||||
name='#update-conversation-variable'
|
||||
/>
|
||||
<Row>
|
||||
<Col>
|
||||
特定の会話変数の値を更新します。このエンドポイントは、名前、型、説明を保持しながら、会話中にキャプチャされた変数の値を変更することを可能にします。
|
||||
|
||||
### パスパラメータ
|
||||
|
||||
<Properties>
|
||||
<Property name='conversation_id' type='string' key='conversation_id'>
|
||||
更新する変数を含む会話のID。
|
||||
</Property>
|
||||
<Property name='variable_id' type='string' key='variable_id'>
|
||||
更新する変数のID。
|
||||
</Property>
|
||||
</Properties>
|
||||
|
||||
### リクエストボディ
|
||||
|
||||
<Properties>
|
||||
<Property name='value' type='any' key='value'>
|
||||
変数の新しい値。変数の期待される型(文字列、数値、オブジェクトなど)と一致する必要があります。
|
||||
</Property>
|
||||
<Property name='user' type='string' key='user'>
|
||||
ユーザー識別子。開発者によって定義されたルールに従い、アプリケーション内で一意である必要があります。
|
||||
</Property>
|
||||
</Properties>
|
||||
|
||||
### レスポンス
|
||||
|
||||
以下を含む更新された変数オブジェクトを返します:
|
||||
- `id` (string) 変数ID
|
||||
- `name` (string) 変数名
|
||||
- `value_type` (string) 変数型(文字列、数値、オブジェクトなど)
|
||||
- `value` (any) 更新された変数値
|
||||
- `description` (string) 変数の説明
|
||||
- `created_at` (int) 作成タイムスタンプ
|
||||
- `updated_at` (int) 最終更新タイムスタンプ
|
||||
|
||||
### エラー
|
||||
- 400, `Type mismatch: variable expects {expected_type}, but got {actual_type} type`, 値の型が変数の期待される型と一致しません
|
||||
- 404, `conversation_not_exists`, 会話が見つかりません
|
||||
- 404, `conversation_variable_not_exists`, 変数が見つかりません
|
||||
|
||||
</Col>
|
||||
<Col sticky>
|
||||
|
||||
<CodeGroup title="Request" tag="PUT" label="/conversations/:conversation_id/variables/:variable_id" targetCode={`curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{\n "value": "Updated Value",\n "user": "abc-123"\n}'`}>
|
||||
|
||||
```bash {{ title: 'cURL' }}
|
||||
curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'Authorization: Bearer {api_key}' \
|
||||
--data-raw '{
|
||||
"value": "Updated Value",
|
||||
"user": "abc-123"
|
||||
}'
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
<CodeGroup title="異なる値型での更新">
|
||||
```bash {{ title: '文字列値' }}
|
||||
curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'Authorization: Bearer {api_key}' \
|
||||
--data-raw '{
|
||||
"value": "新しい文字列値",
|
||||
"user": "abc-123"
|
||||
}'
|
||||
```
|
||||
|
||||
```bash {{ title: '数値' }}
|
||||
curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'Authorization: Bearer {api_key}' \
|
||||
--data-raw '{
|
||||
"value": 42,
|
||||
"user": "abc-123"
|
||||
}'
|
||||
```
|
||||
|
||||
```bash {{ title: 'オブジェクト値' }}
|
||||
curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'Authorization: Bearer {api_key}' \
|
||||
--data-raw '{
|
||||
"value": {"product": "Widget", "quantity": 10, "price": 29.99},
|
||||
"user": "abc-123"
|
||||
}'
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
<CodeGroup title="Response">
|
||||
```json {{ title: 'Response' }}
|
||||
{
|
||||
"id": "variable-uuid-1",
|
||||
"name": "customer_name",
|
||||
"value_type": "string",
|
||||
"value": "Updated Value",
|
||||
"description": "会話から抽出された顧客名",
|
||||
"created_at": 1650000000000,
|
||||
"updated_at": 1650000001000
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
---
|
||||
|
||||
<Heading
|
||||
url='/audio-to-text'
|
||||
method='POST'
|
||||
|
|
|
|||
|
|
@ -1049,6 +1049,121 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
|
|||
|
||||
---
|
||||
|
||||
<Heading
|
||||
url='/conversations/:conversation_id/variables/:variable_id'
|
||||
method='PUT'
|
||||
title='更新对话变量'
|
||||
name='#update-conversation-variable'
|
||||
/>
|
||||
<Row>
|
||||
<Col>
|
||||
更新特定对话变量的值。此端点允许您修改在对话过程中捕获的变量值,同时保留其名称、类型和描述。
|
||||
|
||||
### 路径参数
|
||||
|
||||
<Properties>
|
||||
<Property name='conversation_id' type='string' key='conversation_id'>
|
||||
包含要更新变量的对话ID。
|
||||
</Property>
|
||||
<Property name='variable_id' type='string' key='variable_id'>
|
||||
要更新的变量ID。
|
||||
</Property>
|
||||
</Properties>
|
||||
|
||||
### 请求体
|
||||
|
||||
<Properties>
|
||||
<Property name='value' type='any' key='value'>
|
||||
变量的新值。必须匹配变量的预期类型(字符串、数字、对象等)。
|
||||
</Property>
|
||||
<Property name='user' type='string' key='user'>
|
||||
用户标识符,由开发人员定义的规则,在应用程序内必须唯一。
|
||||
</Property>
|
||||
</Properties>
|
||||
|
||||
### 响应
|
||||
|
||||
返回包含以下内容的更新变量对象:
|
||||
- `id` (string) 变量ID
|
||||
- `name` (string) 变量名称
|
||||
- `value_type` (string) 变量类型(字符串、数字、对象等)
|
||||
- `value` (any) 更新后的变量值
|
||||
- `description` (string) 变量描述
|
||||
- `created_at` (int) 创建时间戳
|
||||
- `updated_at` (int) 最后更新时间戳
|
||||
|
||||
### 错误
|
||||
- 400, `Type mismatch: variable expects {expected_type}, but got {actual_type} type`, 值类型与变量的预期类型不匹配
|
||||
- 404, `conversation_not_exists`, 对话不存在
|
||||
- 404, `conversation_variable_not_exists`, 变量不存在
|
||||
|
||||
</Col>
|
||||
<Col sticky>
|
||||
|
||||
<CodeGroup title="Request" tag="PUT" label="/conversations/:conversation_id/variables/:variable_id" targetCode={`curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{\n "value": "Updated Value",\n "user": "abc-123"\n}'`}>
|
||||
|
||||
```bash {{ title: 'cURL' }}
|
||||
curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'Authorization: Bearer {api_key}' \
|
||||
--data-raw '{
|
||||
"value": "Updated Value",
|
||||
"user": "abc-123"
|
||||
}'
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
<CodeGroup title="使用不同值类型更新">
|
||||
```bash {{ title: '字符串值' }}
|
||||
curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'Authorization: Bearer {api_key}' \
|
||||
--data-raw '{
|
||||
"value": "新的字符串值",
|
||||
"user": "abc-123"
|
||||
}'
|
||||
```
|
||||
|
||||
```bash {{ title: '数字值' }}
|
||||
curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'Authorization: Bearer {api_key}' \
|
||||
--data-raw '{
|
||||
"value": 42,
|
||||
"user": "abc-123"
|
||||
}'
|
||||
```
|
||||
|
||||
```bash {{ title: '对象值' }}
|
||||
curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'Authorization: Bearer {api_key}' \
|
||||
--data-raw '{
|
||||
"value": {"product": "Widget", "quantity": 10, "price": 29.99},
|
||||
"user": "abc-123"
|
||||
}'
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
<CodeGroup title="Response">
|
||||
```json {{ title: 'Response' }}
|
||||
{
|
||||
"id": "variable-uuid-1",
|
||||
"name": "customer_name",
|
||||
"value_type": "string",
|
||||
"value": "Updated Value",
|
||||
"description": "客户名称(从对话中提取)",
|
||||
"created_at": 1650000000000,
|
||||
"updated_at": 1650000001000
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
---
|
||||
|
||||
<Heading
|
||||
url='/audio-to-text'
|
||||
method='POST'
|
||||
|
|
|
|||
|
|
@ -1045,6 +1045,121 @@ Chat applications support session persistence, allowing previous chat history to
|
|||
|
||||
---
|
||||
|
||||
<Heading
|
||||
url='/conversations/:conversation_id/variables/:variable_id'
|
||||
method='PUT'
|
||||
title='Update Conversation Variable'
|
||||
name='#update-conversation-variable'
|
||||
/>
|
||||
<Row>
|
||||
<Col>
|
||||
Update the value of a specific conversation variable. This endpoint allows you to modify the value of a variable that was captured during the conversation while preserving its name, type, and description.
|
||||
|
||||
### Path Parameters
|
||||
|
||||
<Properties>
|
||||
<Property name='conversation_id' type='string' key='conversation_id'>
|
||||
The ID of the conversation containing the variable to update.
|
||||
</Property>
|
||||
<Property name='variable_id' type='string' key='variable_id'>
|
||||
The ID of the variable to update.
|
||||
</Property>
|
||||
</Properties>
|
||||
|
||||
### Request Body
|
||||
|
||||
<Properties>
|
||||
<Property name='value' type='any' key='value'>
|
||||
The new value for the variable. Must match the variable's expected type (string, number, object, etc.).
|
||||
</Property>
|
||||
<Property name='user' type='string' key='user'>
|
||||
The user identifier, defined by the developer, must ensure uniqueness within the application.
|
||||
</Property>
|
||||
</Properties>
|
||||
|
||||
### Response
|
||||
|
||||
Returns the updated variable object with:
|
||||
- `id` (string) Variable ID
|
||||
- `name` (string) Variable name
|
||||
- `value_type` (string) Variable type (string, number, object, etc.)
|
||||
- `value` (any) Updated variable value
|
||||
- `description` (string) Variable description
|
||||
- `created_at` (int) Creation timestamp
|
||||
- `updated_at` (int) Last update timestamp
|
||||
|
||||
### Errors
|
||||
- 400, `Type mismatch: variable expects {expected_type}, but got {actual_type} type`, Value type doesn't match variable's expected type
|
||||
- 404, `conversation_not_exists`, Conversation not found
|
||||
- 404, `conversation_variable_not_exists`, Variable not found
|
||||
|
||||
</Col>
|
||||
<Col sticky>
|
||||
|
||||
<CodeGroup title="Request" tag="PUT" label="/conversations/:conversation_id/variables/:variable_id" targetCode={`curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{\n "value": "Updated Value",\n "user": "abc-123"\n}'`}>
|
||||
|
||||
```bash {{ title: 'cURL' }}
|
||||
curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'Authorization: Bearer {api_key}' \
|
||||
--data-raw '{
|
||||
"value": "Updated Value",
|
||||
"user": "abc-123"
|
||||
}'
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
<CodeGroup title="Update with different value types">
|
||||
```bash {{ title: 'String Value' }}
|
||||
curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'Authorization: Bearer {api_key}' \
|
||||
--data-raw '{
|
||||
"value": "New string value",
|
||||
"user": "abc-123"
|
||||
}'
|
||||
```
|
||||
|
||||
```bash {{ title: 'Number Value' }}
|
||||
curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'Authorization: Bearer {api_key}' \
|
||||
--data-raw '{
|
||||
"value": 42,
|
||||
"user": "abc-123"
|
||||
}'
|
||||
```
|
||||
|
||||
```bash {{ title: 'Object Value' }}
|
||||
curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'Authorization: Bearer {api_key}' \
|
||||
--data-raw '{
|
||||
"value": {"product": "Widget", "quantity": 10, "price": 29.99},
|
||||
"user": "abc-123"
|
||||
}'
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
<CodeGroup title="Response">
|
||||
```json {{ title: 'Response' }}
|
||||
{
|
||||
"id": "variable-uuid-1",
|
||||
"name": "customer_name",
|
||||
"value_type": "string",
|
||||
"value": "Updated Value",
|
||||
"description": "Customer name extracted from the conversation",
|
||||
"created_at": 1650000000000,
|
||||
"updated_at": 1650000001000
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
---
|
||||
|
||||
<Heading
|
||||
url='/audio-to-text'
|
||||
method='POST'
|
||||
|
|
|
|||
|
|
@ -1044,6 +1044,121 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
|
|||
|
||||
---
|
||||
|
||||
<Heading
|
||||
url='/conversations/:conversation_id/variables/:variable_id'
|
||||
method='PUT'
|
||||
title='会話変数の更新'
|
||||
name='#update-conversation-variable'
|
||||
/>
|
||||
<Row>
|
||||
<Col>
|
||||
特定の会話変数の値を更新します。このエンドポイントは、名前、型、説明を保持しながら、会話中にキャプチャされた変数の値を変更することを可能にします。
|
||||
|
||||
### パスパラメータ
|
||||
|
||||
<Properties>
|
||||
<Property name='conversation_id' type='string' key='conversation_id'>
|
||||
更新する変数を含む会話のID。
|
||||
</Property>
|
||||
<Property name='variable_id' type='string' key='variable_id'>
|
||||
更新する変数のID。
|
||||
</Property>
|
||||
</Properties>
|
||||
|
||||
### リクエストボディ
|
||||
|
||||
<Properties>
|
||||
<Property name='value' type='any' key='value'>
|
||||
変数の新しい値。変数の期待される型(文字列、数値、オブジェクトなど)と一致する必要があります。
|
||||
</Property>
|
||||
<Property name='user' type='string' key='user'>
|
||||
ユーザー識別子。開発者によって定義されたルールに従い、アプリケーション内で一意である必要があります。
|
||||
</Property>
|
||||
</Properties>
|
||||
|
||||
### レスポンス
|
||||
|
||||
以下を含む更新された変数オブジェクトを返します:
|
||||
- `id` (string) 変数ID
|
||||
- `name` (string) 変数名
|
||||
- `value_type` (string) 変数型(文字列、数値、オブジェクトなど)
|
||||
- `value` (any) 更新された変数値
|
||||
- `description` (string) 変数の説明
|
||||
- `created_at` (int) 作成タイムスタンプ
|
||||
- `updated_at` (int) 最終更新タイムスタンプ
|
||||
|
||||
### エラー
|
||||
- 400, `Type mismatch: variable expects {expected_type}, but got {actual_type} type`, 値の型が変数の期待される型と一致しません
|
||||
- 404, `conversation_not_exists`, 会話が見つかりません
|
||||
- 404, `conversation_variable_not_exists`, 変数が見つかりません
|
||||
|
||||
</Col>
|
||||
<Col sticky>
|
||||
|
||||
<CodeGroup title="Request" tag="PUT" label="/conversations/:conversation_id/variables/:variable_id" targetCode={`curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{\n "value": "Updated Value",\n "user": "abc-123"\n}'`}>
|
||||
|
||||
```bash {{ title: 'cURL' }}
|
||||
curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'Authorization: Bearer {api_key}' \
|
||||
--data-raw '{
|
||||
"value": "Updated Value",
|
||||
"user": "abc-123"
|
||||
}'
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
<CodeGroup title="異なる値型での更新">
|
||||
```bash {{ title: '文字列値' }}
|
||||
curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'Authorization: Bearer {api_key}' \
|
||||
--data-raw '{
|
||||
"value": "新しい文字列値",
|
||||
"user": "abc-123"
|
||||
}'
|
||||
```
|
||||
|
||||
```bash {{ title: '数値' }}
|
||||
curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'Authorization: Bearer {api_key}' \
|
||||
--data-raw '{
|
||||
"value": 42,
|
||||
"user": "abc-123"
|
||||
}'
|
||||
```
|
||||
|
||||
```bash {{ title: 'オブジェクト値' }}
|
||||
curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'Authorization: Bearer {api_key}' \
|
||||
--data-raw '{
|
||||
"value": {"product": "Widget", "quantity": 10, "price": 29.99},
|
||||
"user": "abc-123"
|
||||
}'
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
<CodeGroup title="Response">
|
||||
```json {{ title: 'Response' }}
|
||||
{
|
||||
"id": "variable-uuid-1",
|
||||
"name": "customer_name",
|
||||
"value_type": "string",
|
||||
"value": "Updated Value",
|
||||
"description": "会話から抽出された顧客名",
|
||||
"created_at": 1650000000000,
|
||||
"updated_at": 1650000001000
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
---
|
||||
|
||||
<Heading
|
||||
url='/audio-to-text'
|
||||
method='POST'
|
||||
|
|
|
|||
|
|
@ -1060,6 +1060,121 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
|
|||
|
||||
---
|
||||
|
||||
<Heading
|
||||
url='/conversations/:conversation_id/variables/:variable_id'
|
||||
method='PUT'
|
||||
title='更新对话变量'
|
||||
name='#update-conversation-variable'
|
||||
/>
|
||||
<Row>
|
||||
<Col>
|
||||
更新特定对话变量的值。此端点允许您修改在对话过程中捕获的变量值,同时保留其名称、类型和描述。
|
||||
|
||||
### 路径参数
|
||||
|
||||
<Properties>
|
||||
<Property name='conversation_id' type='string' key='conversation_id'>
|
||||
包含要更新变量的对话ID。
|
||||
</Property>
|
||||
<Property name='variable_id' type='string' key='variable_id'>
|
||||
要更新的变量ID。
|
||||
</Property>
|
||||
</Properties>
|
||||
|
||||
### 请求体
|
||||
|
||||
<Properties>
|
||||
<Property name='value' type='any' key='value'>
|
||||
变量的新值。必须匹配变量的预期类型(字符串、数字、对象等)。
|
||||
</Property>
|
||||
<Property name='user' type='string' key='user'>
|
||||
用户标识符,由开发人员定义的规则,在应用程序内必须唯一。
|
||||
</Property>
|
||||
</Properties>
|
||||
|
||||
### 响应
|
||||
|
||||
返回包含以下内容的更新变量对象:
|
||||
- `id` (string) 变量ID
|
||||
- `name` (string) 变量名称
|
||||
- `value_type` (string) 变量类型(字符串、数字、对象等)
|
||||
- `value` (any) 更新后的变量值
|
||||
- `description` (string) 变量描述
|
||||
- `created_at` (int) 创建时间戳
|
||||
- `updated_at` (int) 最后更新时间戳
|
||||
|
||||
### 错误
|
||||
- 400, `Type mismatch: variable expects {expected_type}, but got {actual_type} type`, 值类型与变量的预期类型不匹配
|
||||
- 404, `conversation_not_exists`, 对话不存在
|
||||
- 404, `conversation_variable_not_exists`, 变量不存在
|
||||
|
||||
</Col>
|
||||
<Col sticky>
|
||||
|
||||
<CodeGroup title="Request" tag="PUT" label="/conversations/:conversation_id/variables/:variable_id" targetCode={`curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{\n "value": "Updated Value",\n "user": "abc-123"\n}'`}>
|
||||
|
||||
```bash {{ title: 'cURL' }}
|
||||
curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'Authorization: Bearer {api_key}' \
|
||||
--data-raw '{
|
||||
"value": "Updated Value",
|
||||
"user": "abc-123"
|
||||
}'
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
<CodeGroup title="使用不同值类型更新">
|
||||
```bash {{ title: '字符串值' }}
|
||||
curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'Authorization: Bearer {api_key}' \
|
||||
--data-raw '{
|
||||
"value": "新的字符串值",
|
||||
"user": "abc-123"
|
||||
}'
|
||||
```
|
||||
|
||||
```bash {{ title: '数字值' }}
|
||||
curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'Authorization: Bearer {api_key}' \
|
||||
--data-raw '{
|
||||
"value": 42,
|
||||
"user": "abc-123"
|
||||
}'
|
||||
```
|
||||
|
||||
```bash {{ title: '对象值' }}
|
||||
curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'Authorization: Bearer {api_key}' \
|
||||
--data-raw '{
|
||||
"value": {"product": "Widget", "quantity": 10, "price": 29.99},
|
||||
"user": "abc-123"
|
||||
}'
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
<CodeGroup title="Response">
|
||||
```json {{ title: 'Response' }}
|
||||
{
|
||||
"id": "variable-uuid-1",
|
||||
"name": "customer_name",
|
||||
"value_type": "string",
|
||||
"value": "Updated Value",
|
||||
"description": "客户名称(从对话中提取)",
|
||||
"created_at": 1650000000000,
|
||||
"updated_at": 1650000001000
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
---
|
||||
|
||||
<Heading
|
||||
url='/audio-to-text'
|
||||
method='POST'
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ const Empty = ({
|
|||
<div
|
||||
key={index}
|
||||
className={cn(
|
||||
'mb-3 mr-3 h-[144px] w-[calc((100%-36px)/4)] rounded-xl bg-background-section-burn',
|
||||
'mb-3 mr-3 h-[144px] w-[calc((100%-36px)/4)] rounded-xl bg-background-section-burn',
|
||||
index % 4 === 3 && 'mr-0',
|
||||
index > 11 && 'mb-0',
|
||||
lightCard && 'bg-background-default-lighter opacity-75',
|
||||
|
|
@ -46,7 +46,7 @@ const Empty = ({
|
|||
}
|
||||
<div className='absolute left-1/2 top-1/2 z-[2] flex -translate-x-1/2 -translate-y-1/2 flex-col items-center'>
|
||||
<div className='relative mb-3 flex h-14 w-14 items-center justify-center rounded-xl border border-dashed border-divider-deep bg-components-card-bg shadow-lg'>
|
||||
<Group className='h-5 w-5' />
|
||||
<Group className='h-5 w-5 text-text-primary' />
|
||||
<Line className='absolute right-[-1px] top-1/2 -translate-y-1/2' />
|
||||
<Line className='absolute left-[-1px] top-1/2 -translate-y-1/2' />
|
||||
<Line className='absolute left-1/2 top-0 -translate-x-1/2 -translate-y-1/2 rotate-90' />
|
||||
|
|
|
|||
|
|
@ -47,7 +47,22 @@ const EndpointModal: FC<Props> = ({
|
|||
return
|
||||
}
|
||||
}
|
||||
onSaved(tempCredential)
|
||||
|
||||
// Fix: Process boolean fields to ensure they are sent as proper boolean values
|
||||
const processedCredential = { ...tempCredential }
|
||||
formSchemas.forEach((field) => {
|
||||
if (field.type === 'boolean' && processedCredential[field.name] !== undefined) {
|
||||
const value = processedCredential[field.name]
|
||||
if (typeof value === 'string')
|
||||
processedCredential[field.name] = value === 'true' || value === '1' || value === 'True'
|
||||
else if (typeof value === 'number')
|
||||
processedCredential[field.name] = value === 1
|
||||
else if (typeof value === 'boolean')
|
||||
processedCredential[field.name] = value
|
||||
}
|
||||
})
|
||||
|
||||
onSaved(processedCredential)
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { RiArrowRightUpLine } from '@remixicon/react'
|
|||
import Link from 'next/link'
|
||||
import cn from '@/utils/classnames'
|
||||
import { NoToolPlaceholder } from '../../base/icons/src/vender/other'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
type Props = {
|
||||
type?: ToolTypeEnum
|
||||
isAgent?: boolean
|
||||
|
|
@ -25,6 +26,7 @@ const Empty = ({
|
|||
isAgent,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const { theme } = useTheme()
|
||||
|
||||
const hasLink = type && [ToolTypeEnum.Custom, ToolTypeEnum.MCP].includes(type)
|
||||
const Comp = (hasLink ? Link : 'div') as any
|
||||
|
|
@ -34,7 +36,7 @@ const Empty = ({
|
|||
|
||||
return (
|
||||
<div className='flex h-[336px] flex-col items-center justify-center'>
|
||||
<NoToolPlaceholder />
|
||||
<NoToolPlaceholder className={theme === 'dark' ? 'invert' : ''} />
|
||||
<div className='mb-1 mt-2 text-[13px] font-medium leading-[18px] text-text-primary'>
|
||||
{hasTitle ? t(`tools.addToolModal.${renderType}.title`) : 'No tools available'}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -63,6 +63,16 @@ export const addDefaultValue = (value: Record<string, any>, formSchemas: { varia
|
|||
const itemValue = value[formSchema.variable]
|
||||
if ((formSchema.default !== undefined) && (value === undefined || itemValue === null || itemValue === '' || itemValue === undefined))
|
||||
newValues[formSchema.variable] = formSchema.default
|
||||
|
||||
// Fix: Convert boolean field values to proper boolean type
|
||||
if (formSchema.type === 'boolean' && itemValue !== undefined && itemValue !== null && itemValue !== '') {
|
||||
if (typeof itemValue === 'string')
|
||||
newValues[formSchema.variable] = itemValue === 'true' || itemValue === '1' || itemValue === 'True'
|
||||
else if (typeof itemValue === 'number')
|
||||
newValues[formSchema.variable] = itemValue === 1
|
||||
else if (typeof itemValue === 'boolean')
|
||||
newValues[formSchema.variable] = itemValue
|
||||
}
|
||||
})
|
||||
return newValues
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,9 +27,12 @@ import { useInvalidateAppWorkflow, usePublishWorkflow, useResetWorkflowVersionHi
|
|||
import type { PublishWorkflowParams } from '@/types/workflow'
|
||||
import { fetchAppDetail } from '@/service/apps'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
const FeaturesTrigger = () => {
|
||||
const { t } = useTranslation()
|
||||
const { theme } = useTheme()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const appDetail = useAppStore(s => s.appDetail)
|
||||
const appID = appDetail?.id
|
||||
|
|
@ -121,7 +124,13 @@ const FeaturesTrigger = () => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Button className='text-components-button-secondary-text' onClick={handleShowFeatures}>
|
||||
<Button
|
||||
className={cn(
|
||||
'text-components-button-secondary-text',
|
||||
theme === 'dark' && 'rounded-lg border border-black/5 bg-white/10 backdrop-blur-sm',
|
||||
)}
|
||||
onClick={handleShowFeatures}
|
||||
>
|
||||
<RiApps2AddLine className='mr-1 h-4 w-4 text-components-button-secondary-text' />
|
||||
{t('workflow.common.features')}
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ const ToolItem: FC<Props> = ({
|
|||
<Tooltip
|
||||
key={payload.name}
|
||||
position='right'
|
||||
needsDelay={false}
|
||||
popupClassName='!p-0 !px-3 !py-2.5 !w-[200px] !leading-[18px] !text-xs !text-gray-700 !border-[0.5px] !border-black/5 !rounded-xl !shadow-lg'
|
||||
popupContent={(
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -2,8 +2,11 @@ import { memo } from 'react'
|
|||
import Button from '@/app/components/base/button'
|
||||
import { BubbleX } from '@/app/components/base/icons/src/vender/line/others'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
const ChatVariableButton = ({ disabled }: { disabled: boolean }) => {
|
||||
const { theme } = useTheme()
|
||||
const setShowChatVariablePanel = useStore(s => s.setShowChatVariablePanel)
|
||||
const setShowEnvPanel = useStore(s => s.setShowEnvPanel)
|
||||
const setShowDebugAndPreviewPanel = useStore(s => s.setShowDebugAndPreviewPanel)
|
||||
|
|
@ -15,7 +18,14 @@ const ChatVariableButton = ({ disabled }: { disabled: boolean }) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<Button className='p-2' disabled={disabled} onClick={handleClick}>
|
||||
<Button
|
||||
className={cn(
|
||||
'p-2',
|
||||
theme === 'dark' && 'rounded-lg border border-black/5 bg-white/10 backdrop-blur-sm',
|
||||
)}
|
||||
disabled={disabled}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<BubbleX className='h-4 w-4 text-components-button-secondary-text' />
|
||||
</Button>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,8 +2,11 @@ import { memo } from 'react'
|
|||
import Button from '@/app/components/base/button'
|
||||
import { Env } from '@/app/components/base/icons/src/vender/line/others'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
const EnvButton = ({ disabled }: { disabled: boolean }) => {
|
||||
const { theme } = useTheme()
|
||||
const setShowChatVariablePanel = useStore(s => s.setShowChatVariablePanel)
|
||||
const setShowEnvPanel = useStore(s => s.setShowEnvPanel)
|
||||
const setShowDebugAndPreviewPanel = useStore(s => s.setShowDebugAndPreviewPanel)
|
||||
|
|
@ -15,7 +18,14 @@ const EnvButton = ({ disabled }: { disabled: boolean }) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<Button className='p-2' disabled={disabled} onClick={handleClick}>
|
||||
<Button
|
||||
className={cn(
|
||||
'p-2',
|
||||
theme === 'dark' && 'rounded-lg border border-black/5 bg-white/10 backdrop-blur-sm',
|
||||
)}
|
||||
disabled={disabled}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<Env className='h-4 w-4 text-components-button-secondary-text' />
|
||||
</Button>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ import RestoringTitle from './restoring-title'
|
|||
import Button from '@/app/components/base/button'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { useInvalidAllLastRun } from '@/service/use-workflow'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
export type HeaderInRestoringProps = {
|
||||
onRestoreSettled?: () => void
|
||||
|
|
@ -27,6 +29,7 @@ const HeaderInRestoring = ({
|
|||
onRestoreSettled,
|
||||
}: HeaderInRestoringProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { theme } = useTheme()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const appDetail = useAppStore.getState().appDetail
|
||||
|
||||
|
|
@ -78,21 +81,27 @@ const HeaderInRestoring = ({
|
|||
<div>
|
||||
<RestoringTitle />
|
||||
</div>
|
||||
<div className='flex items-center justify-end gap-x-2'>
|
||||
<Button
|
||||
onClick={handleRestore}
|
||||
disabled={!currentVersion || currentVersion.version === WorkflowVersion.Draft}
|
||||
variant='primary'
|
||||
>
|
||||
{t('workflow.common.restore')}
|
||||
</Button>
|
||||
<Button
|
||||
className='text-components-button-secondary-accent-text'
|
||||
onClick={handleCancelRestore}
|
||||
>
|
||||
<div className='flex items-center gap-x-0.5'>
|
||||
<RiHistoryLine className='h-4 w-4' />
|
||||
<span className='px-0.5'>{t('workflow.common.exitVersions')}</span>
|
||||
<div className=' flex items-center justify-end gap-x-2'>
|
||||
<Button
|
||||
onClick={handleRestore}
|
||||
disabled={!currentVersion || currentVersion.version === WorkflowVersion.Draft}
|
||||
variant='primary'
|
||||
className={cn(
|
||||
theme === 'dark' && 'rounded-lg border border-black/5 bg-white/10 backdrop-blur-sm',
|
||||
)}
|
||||
>
|
||||
{t('workflow.common.restore')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleCancelRestore}
|
||||
className={cn(
|
||||
'text-components-button-secondary-accent-text',
|
||||
theme === 'dark' && 'rounded-lg border border-black/5 bg-white/10 backdrop-blur-sm',
|
||||
)}
|
||||
>
|
||||
<div className='flex items-center gap-x-0.5'>
|
||||
<RiHistoryLine className='h-4 w-4' />
|
||||
<span className='px-0.5'>{t('workflow.common.exitVersions')}</span>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import {
|
|||
} from '@/app/components/base/icons/src/vender/line/mediaAndDevices'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import { EVENT_WORKFLOW_STOP } from '@/app/components/workflow/variable-inspect/types'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
|
||||
const RunMode = memo(() => {
|
||||
const { t } = useTranslation()
|
||||
|
|
@ -102,21 +103,27 @@ const PreviewMode = memo(() => {
|
|||
PreviewMode.displayName = 'PreviewMode'
|
||||
|
||||
const RunAndHistory: FC = () => {
|
||||
const { theme } = useTheme()
|
||||
const isChatMode = useIsChatMode()
|
||||
const { nodesReadOnly } = useNodesReadOnly()
|
||||
|
||||
return (
|
||||
<div className='flex h-8 items-center rounded-lg border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg px-0.5 shadow-xs'>
|
||||
{
|
||||
!isChatMode && <RunMode />
|
||||
}
|
||||
{
|
||||
isChatMode && <PreviewMode />
|
||||
}
|
||||
<div className='mx-0.5 h-3.5 w-[1px] bg-divider-regular'></div>
|
||||
<ViewHistory />
|
||||
<Checklist disabled={nodesReadOnly} />
|
||||
</div>
|
||||
<>
|
||||
<div className={cn(
|
||||
'flex h-8 items-center rounded-lg border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg px-0.5 shadow-xs',
|
||||
theme === 'dark' && 'rounded-lg border border-black/5 bg-white/10 backdrop-blur-sm',
|
||||
)}>
|
||||
{
|
||||
!isChatMode && <RunMode />
|
||||
}
|
||||
{
|
||||
isChatMode && <PreviewMode />
|
||||
}
|
||||
<div className='mx-0.5 h-3.5 w-[1px] bg-divider-regular'></div>
|
||||
<ViewHistory />
|
||||
<Checklist disabled={nodesReadOnly} />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import { useKeyPress } from 'ahooks'
|
|||
import Button from '../../base/button'
|
||||
import Tooltip from '../../base/tooltip'
|
||||
import { getKeyboardKeyCodeBySystem } from '../utils'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type VersionHistoryButtonProps = {
|
||||
onClick: () => Promise<unknown> | unknown
|
||||
|
|
@ -38,6 +40,7 @@ PopupContent.displayName = 'PopupContent'
|
|||
const VersionHistoryButton: FC<VersionHistoryButtonProps> = ({
|
||||
onClick,
|
||||
}) => {
|
||||
const { theme } = useTheme()
|
||||
const handleViewVersionHistory = useCallback(async () => {
|
||||
await onClick?.()
|
||||
}, [onClick])
|
||||
|
|
@ -46,7 +49,7 @@ const VersionHistoryButton: FC<VersionHistoryButtonProps> = ({
|
|||
e.preventDefault()
|
||||
handleViewVersionHistory()
|
||||
},
|
||||
{ exactMatch: true, useCapture: true })
|
||||
{ exactMatch: true, useCapture: true })
|
||||
|
||||
return <Tooltip
|
||||
popupContent={<PopupContent />}
|
||||
|
|
@ -54,12 +57,15 @@ const VersionHistoryButton: FC<VersionHistoryButtonProps> = ({
|
|||
popupClassName='rounded-lg border-[0.5px] border-components-panel-border bg-components-tooltip-bg
|
||||
shadow-lg shadow-shadow-shadow-5 backdrop-blur-[5px] p-1.5'
|
||||
>
|
||||
<Button
|
||||
className={'p-2'}
|
||||
onClick={handleViewVersionHistory}
|
||||
>
|
||||
<RiHistoryLine className='h-4 w-4 text-components-button-secondary-text' />
|
||||
</Button>
|
||||
<Button
|
||||
className={cn(
|
||||
'p-2',
|
||||
theme === 'dark' && 'rounded-lg border border-black/5 bg-white/10 backdrop-blur-sm',
|
||||
)}
|
||||
onClick={handleViewVersionHistory}
|
||||
>
|
||||
<RiHistoryLine className='h-4 w-4 text-components-button-secondary-text' />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -190,7 +190,6 @@ export const Workflow: FC<WorkflowProps> = memo(({
|
|||
return () => {
|
||||
handleSyncWorkflowDraft(true, true)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
const { handleRefreshWorkflowDraft } = useWorkflowRefreshDraft()
|
||||
|
|
@ -282,7 +281,6 @@ export const Workflow: FC<WorkflowProps> = memo(({
|
|||
const { fetchInspectVars } = useSetWorkflowVarsWithValue()
|
||||
useEffect(() => {
|
||||
fetchInspectVars()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
const store = useStoreApi()
|
||||
|
|
|
|||
|
|
@ -143,7 +143,6 @@ export const AgentStrategySelector = memo((props: AgentStrategySelectorProps) =>
|
|||
category: PluginType.agent,
|
||||
})
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [query])
|
||||
|
||||
const pluginRef = useRef<ListRef>(null)
|
||||
|
|
|
|||
|
|
@ -4,12 +4,10 @@ import React from 'react'
|
|||
import cn from 'classnames'
|
||||
import { useWorkflow } from '../../../hooks'
|
||||
import { BlockEnum } from '../../../types'
|
||||
import { VarBlockIcon } from '../../../block-icon'
|
||||
import { getNodeInfoById, isConversationVar, isENV, isSystemVar } from './variable/utils'
|
||||
import { Line3 } from '@/app/components/base/icons/src/public/common'
|
||||
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others'
|
||||
import { RiMoreLine } from '@remixicon/react'
|
||||
import { getNodeInfoById, isSystemVar } from './variable/utils'
|
||||
import {
|
||||
VariableLabelInText,
|
||||
} from '@/app/components/workflow/nodes/_base/components/variable/variable-label'
|
||||
type Props = {
|
||||
nodeId: string
|
||||
value: string
|
||||
|
|
@ -42,40 +40,17 @@ const ReadonlyInputWithSelectVar: FC<Props> = ({
|
|||
|
||||
const value = vars[index].split('.')
|
||||
const isSystem = isSystemVar(value)
|
||||
const isEnv = isENV(value)
|
||||
const isChatVar = isConversationVar(value)
|
||||
const node = (isSystem ? startNode : getNodeInfoById(availableNodes, value[0]))?.data
|
||||
const varName = `${isSystem ? 'sys.' : ''}${value[value.length - 1]}`
|
||||
const isShowAPart = value.length > 2
|
||||
|
||||
return (<span key={index}>
|
||||
<span className='relative top-[-3px] leading-[16px]'>{str}</span>
|
||||
<div className=' inline-flex h-[16px] items-center rounded-[5px] bg-components-badge-white-to-dark px-1.5'>
|
||||
{!isEnv && !isChatVar && (
|
||||
<div className='flex items-center'>
|
||||
<div className='p-[1px]'>
|
||||
<VarBlockIcon
|
||||
className='!text-text-primary'
|
||||
type={node?.type || BlockEnum.Start}
|
||||
/>
|
||||
</div>
|
||||
<div className='mx-0.5 max-w-[60px] truncate text-xs font-medium text-text-secondary' title={node?.title}>{node?.title}</div>
|
||||
<Line3 className='mr-0.5'></Line3>
|
||||
</div>
|
||||
)}
|
||||
{isShowAPart && (
|
||||
<div className='flex items-center'>
|
||||
<RiMoreLine className='h-3 w-3 text-text-secondary' />
|
||||
<Line3 className='mr-0.5 text-divider-deep'></Line3>
|
||||
</div>
|
||||
)}
|
||||
<div className='flex items-center text-text-accent'>
|
||||
{!isEnv && !isChatVar && <Variable02 className='h-3.5 w-3.5 shrink-0' />}
|
||||
{isEnv && <Env className='h-3.5 w-3.5 shrink-0 text-util-colors-violet-violet-600' />}
|
||||
{isChatVar && <BubbleX className='h-3.5 w-3.5 text-util-colors-teal-teal-700' />}
|
||||
<div className={cn('ml-0.5 max-w-[50px] truncate text-xs font-medium', (isEnv || isChatVar) && 'text-text-primary')} title={varName}>{varName}</div>
|
||||
</div>
|
||||
</div>
|
||||
<VariableLabelInText
|
||||
nodeTitle={node?.title}
|
||||
nodeType={node?.type}
|
||||
notShowFullPath={isShowAPart}
|
||||
variables={value}
|
||||
/>
|
||||
</span>)
|
||||
})
|
||||
return html
|
||||
|
|
|
|||
|
|
@ -26,7 +26,8 @@ export type SwitchPluginVersionProps = {
|
|||
|
||||
export const SwitchPluginVersion: FC<SwitchPluginVersionProps> = (props) => {
|
||||
const { uniqueIdentifier, tooltip, onChange, className } = props
|
||||
const [pluginId] = uniqueIdentifier.split(':')
|
||||
|
||||
const [pluginId] = uniqueIdentifier?.split(':') || ['']
|
||||
const [isShow, setIsShow] = useState(false)
|
||||
const [isShowUpdateModal, { setTrue: showUpdateModal, setFalse: hideUpdateModal }] = useBoolean(false)
|
||||
const [target, setTarget] = useState<{
|
||||
|
|
@ -60,6 +61,11 @@ export const SwitchPluginVersion: FC<SwitchPluginVersionProps> = (props) => {
|
|||
})
|
||||
}
|
||||
const { t } = useTranslation()
|
||||
|
||||
// Guard against null/undefined uniqueIdentifier to prevent app crash
|
||||
if (!uniqueIdentifier || !pluginId)
|
||||
return null
|
||||
|
||||
return <Tooltip popupContent={!isShow && !isShowUpdateModal && tooltip} triggerMethod='hover'>
|
||||
<div className={cn('flex w-fit items-center justify-center', className)} onClick={e => e.stopPropagation()}>
|
||||
{isShowUpdateModal && pluginDetail && <PluginMutationModel
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
import { useCallback, useMemo } from 'react'
|
||||
import { useNodes, useReactFlow, useStoreApi } from 'reactflow'
|
||||
import { capitalize } from 'lodash-es'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiErrorWarningFill } from '@remixicon/react'
|
||||
import { VarBlockIcon } from '@/app/components/workflow/block-icon'
|
||||
import type {
|
||||
CommonNodeType,
|
||||
Node,
|
||||
|
|
@ -11,13 +8,11 @@ import type {
|
|||
VarType,
|
||||
} from '@/app/components/workflow/types'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { Line3 } from '@/app/components/base/icons/src/public/common'
|
||||
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others'
|
||||
import { getNodeInfoById, isConversationVar, isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import cn from '@/utils/classnames'
|
||||
import { isExceptionVariable } from '@/app/components/workflow/utils'
|
||||
import {
|
||||
VariableLabelInSelect,
|
||||
} from '@/app/components/workflow/nodes/_base/components/variable/variable-label'
|
||||
|
||||
type VariableTagProps = {
|
||||
valueSelector: ValueSelector
|
||||
|
|
@ -73,51 +68,20 @@ const VariableTag = ({
|
|||
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<Tooltip popupContent={!isValid && t('workflow.errorMsg.invalidVariable')}>
|
||||
<div className={cn('border-[rgba(16, 2440,0.08)] inline-flex h-6 max-w-full items-center rounded-md border-[0.5px] border-divider-subtle bg-components-badge-white-to-dark px-1.5 text-xs shadow-xs',
|
||||
!isValid && 'border-red-400 !bg-[#FEF3F2]',
|
||||
)}
|
||||
onClick={(e) => {
|
||||
if (e.metaKey || e.ctrlKey) {
|
||||
e.stopPropagation()
|
||||
handleVariableJump()
|
||||
}
|
||||
}}
|
||||
>
|
||||
{(!isEnv && !isChatVar && <>
|
||||
{node && (
|
||||
<>
|
||||
<VarBlockIcon
|
||||
type={node.data.type || BlockEnum.Start}
|
||||
className='mr-0.5 !text-text-primary'
|
||||
/>
|
||||
<div
|
||||
className='max-w-[60px] truncate font-medium text-text-secondary'
|
||||
title={node?.data.title}
|
||||
>
|
||||
{node?.data.title}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<Line3 className='mx-0.5 shrink-0' />
|
||||
<Variable02 className={cn('mr-0.5 h-3.5 w-3.5 shrink-0 text-text-accent', isException && 'text-text-warning')} />
|
||||
</>)}
|
||||
{isEnv && <Env className='mr-0.5 h-3.5 w-3.5 shrink-0 text-util-colors-violet-violet-600' />}
|
||||
{isChatVar && <BubbleX className='h-3.5 w-3.5 text-util-colors-teal-teal-700' />}
|
||||
<div
|
||||
className={cn('ml-0.5 truncate font-medium text-text-accent', (isEnv || isChatVar) && 'text-text-secondary', isException && 'text-text-warning')}
|
||||
title={variableName}
|
||||
>
|
||||
{variableName}
|
||||
</div>
|
||||
{
|
||||
!isShort && varType && (
|
||||
<div className='ml-0.5 shrink-0 text-text-tertiary'>{capitalize(varType)}</div>
|
||||
)
|
||||
<VariableLabelInSelect
|
||||
variables={valueSelector}
|
||||
nodeType={node?.data.type}
|
||||
nodeTitle={node?.data.title}
|
||||
variableType={!isShort ? varType : undefined}
|
||||
onClick={(e) => {
|
||||
if (e.metaKey || e.ctrlKey) {
|
||||
e.stopPropagation()
|
||||
handleVariableJump()
|
||||
}
|
||||
{!isValid && <RiErrorWarningFill className='ml-0.5 h-3 w-3 text-[#D92D20]' />}
|
||||
</div>
|
||||
</Tooltip>
|
||||
}}
|
||||
errorMsg={!isValid ? t('workflow.errorMsg.invalidVariable') : undefined}
|
||||
isExceptionVariable={isException}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ import { type CredentialFormSchema, type FormOption, FormTypeEnum } from '@/app/
|
|||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { VarBlockIcon } from '@/app/components/workflow/block-icon'
|
||||
import { Line3 } from '@/app/components/base/icons/src/public/common'
|
||||
import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others'
|
||||
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
|
|
@ -44,6 +43,7 @@ import VarFullPathPanel from './var-full-path-panel'
|
|||
import { noop } from 'lodash-es'
|
||||
import { useFetchDynamicOptions } from '@/service/use-plugins'
|
||||
import type { Tool } from '@/app/components/tools/types'
|
||||
import { VariableIconWithColor } from '@/app/components/workflow/nodes/_base/components/variable/variable-label'
|
||||
|
||||
const TRIGGER_DEFAULT_WIDTH = 227
|
||||
|
||||
|
|
@ -138,7 +138,6 @@ const VarReferencePicker: FC<Props> = ({
|
|||
useEffect(() => {
|
||||
if (triggerRef.current)
|
||||
setTriggerWidth(triggerRef.current.clientWidth)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [triggerRef.current])
|
||||
|
||||
const [varKindType, setVarKindType] = useState<VarKindType>(defaultVarKindType)
|
||||
|
|
@ -149,7 +148,6 @@ const VarReferencePicker: FC<Props> = ({
|
|||
const [open, setOpen] = useState(false)
|
||||
useEffect(() => {
|
||||
onOpen()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [open])
|
||||
const hasValue = !isConstant && value.length > 0
|
||||
|
||||
|
|
@ -362,6 +360,13 @@ const VarReferencePicker: FC<Props> = ({
|
|||
return schema
|
||||
}, [dynamicOptions])
|
||||
|
||||
const variableCategory = useMemo(() => {
|
||||
if (isEnv) return 'environment'
|
||||
if (isChatVar) return 'conversation'
|
||||
if (isLoopVar) return 'loop'
|
||||
return 'system'
|
||||
}, [isEnv, isChatVar, isLoopVar])
|
||||
|
||||
return (
|
||||
<div className={cn(className, !readonly && 'cursor-pointer')}>
|
||||
<PortalToFollowElem
|
||||
|
|
@ -458,10 +463,11 @@ const VarReferencePicker: FC<Props> = ({
|
|||
</div>
|
||||
)}
|
||||
<div className='flex items-center text-text-accent'>
|
||||
{!hasValue && <Variable02 className='h-3.5 w-3.5' />}
|
||||
{isLoading && <RiLoader4Line className='h-3.5 w-3.5 animate-spin text-text-secondary' />}
|
||||
{isEnv && <Env className='h-3.5 w-3.5 text-util-colors-violet-violet-600' />}
|
||||
{isChatVar && <BubbleX className='h-3.5 w-3.5 text-util-colors-teal-teal-700' />}
|
||||
<VariableIconWithColor
|
||||
variableCategory={variableCategory}
|
||||
isExceptionVariable={isException}
|
||||
/>
|
||||
<div className={cn('ml-0.5 truncate text-xs font-medium', isEnv && '!text-text-secondary', isChatVar && 'text-util-colors-teal-teal-700', isException && 'text-text-warning')} title={varName} style={{
|
||||
maxWidth: maxVarNameWidth,
|
||||
}}>{varName}</div>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import { useHover } from 'ahooks'
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import cn from '@/utils/classnames'
|
||||
import { type NodeOutPutVar, type ValueSelector, type Var, VarType } from '@/app/components/workflow/types'
|
||||
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
import { ChevronRight } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
|
|
@ -13,7 +12,6 @@ import {
|
|||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import Input from '@/app/components/base/input'
|
||||
import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others'
|
||||
import { checkKeys } from '@/utils/var'
|
||||
import type { StructuredOutput } from '../../../llm/types'
|
||||
import { Type } from '../../../llm/types'
|
||||
|
|
@ -21,8 +19,8 @@ import PickerStructurePanel from '@/app/components/workflow/nodes/_base/componen
|
|||
import { varTypeToStructType } from './utils'
|
||||
import type { Field } from '@/app/components/workflow/nodes/llm/types'
|
||||
import { FILE_STRUCT } from '@/app/components/workflow/constants'
|
||||
import { Loop } from '@/app/components/base/icons/src/vender/workflow'
|
||||
import { noop } from 'lodash-es'
|
||||
import { VariableIconWithColor } from '@/app/components/workflow/nodes/_base/components/variable/variable-label'
|
||||
|
||||
type ObjectChildrenProps = {
|
||||
nodeId: string
|
||||
|
|
@ -118,7 +116,6 @@ const Item: FC<ItemProps> = ({
|
|||
const open = (isObj || isStructureOutput) && isHovering
|
||||
useEffect(() => {
|
||||
onHovering && onHovering(isHovering)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isHovering])
|
||||
const handleChosen = (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
|
|
@ -132,6 +129,12 @@ const Item: FC<ItemProps> = ({
|
|||
onChange([nodeId, ...objPath, itemData.variable], itemData)
|
||||
}
|
||||
}
|
||||
const variableCategory = useMemo(() => {
|
||||
if (isEnv) return 'environment'
|
||||
if (isChatVar) return 'conversation'
|
||||
if (isLoopVar) return 'loop'
|
||||
return 'system'
|
||||
}, [isEnv, isChatVar, isSys, isLoopVar])
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
|
|
@ -150,10 +153,10 @@ const Item: FC<ItemProps> = ({
|
|||
onMouseDown={e => e.preventDefault()}
|
||||
>
|
||||
<div className='flex w-0 grow items-center'>
|
||||
{!isEnv && !isChatVar && !isLoopVar && <Variable02 className={cn('h-3.5 w-3.5 shrink-0 text-text-accent', isException && 'text-text-warning')} />}
|
||||
{isEnv && <Env className='h-3.5 w-3.5 shrink-0 text-util-colors-violet-violet-600' />}
|
||||
{isChatVar && <BubbleX className='h-3.5 w-3.5 shrink-0 text-util-colors-teal-teal-700' />}
|
||||
{isLoopVar && <Loop className='h-3.5 w-3.5 shrink-0 text-util-colors-cyan-cyan-500' />}
|
||||
<VariableIconWithColor
|
||||
variableCategory={variableCategory}
|
||||
isExceptionVariable={isException}
|
||||
/>
|
||||
{!isEnv && !isChatVar && (
|
||||
<div title={itemData.variable} className='system-sm-medium ml-1 w-0 grow truncate text-text-secondary'>{itemData.variable}</div>
|
||||
)}
|
||||
|
|
@ -219,11 +222,9 @@ const ObjectChildren: FC<ObjectChildrenProps> = ({
|
|||
const isHovering = isItemHovering || isChildrenHovering
|
||||
useEffect(() => {
|
||||
onHovering && onHovering(isHovering)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isHovering])
|
||||
useEffect(() => {
|
||||
onHovering && onHovering(isItemHovering)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isItemHovering])
|
||||
// absolute top-[-2px]
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
import { memo } from 'react'
|
||||
import cn from '@/utils/classnames'
|
||||
import { useVarIcon } from '../hooks'
|
||||
import type { VarInInspectType } from '@/types/workflow'
|
||||
|
||||
export type VariableIconProps = {
|
||||
className?: string
|
||||
variables?: string[]
|
||||
variableCategory?: VarInInspectType | string
|
||||
}
|
||||
const VariableIcon = ({
|
||||
className,
|
||||
variables = [],
|
||||
variableCategory,
|
||||
}: VariableIconProps) => {
|
||||
const VarIcon = useVarIcon(variables, variableCategory)
|
||||
|
||||
return VarIcon && (
|
||||
<VarIcon
|
||||
className={cn(
|
||||
'size-3.5 shrink-0',
|
||||
className,
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(VariableIcon)
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
import { memo } from 'react'
|
||||
import { capitalize } from 'lodash-es'
|
||||
import {
|
||||
RiErrorWarningFill,
|
||||
RiMoreLine,
|
||||
} from '@remixicon/react'
|
||||
import type { VariablePayload } from '../types'
|
||||
import { useVarColor } from '../hooks'
|
||||
import VariableNodeLabel from './variable-node-label'
|
||||
import VariableIcon from './variable-icon'
|
||||
import VariableName from './variable-name'
|
||||
import cn from '@/utils/classnames'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
|
||||
const VariableLabel = ({
|
||||
nodeType,
|
||||
nodeTitle,
|
||||
variables,
|
||||
variableType,
|
||||
className,
|
||||
errorMsg,
|
||||
onClick,
|
||||
isExceptionVariable,
|
||||
ref,
|
||||
notShowFullPath,
|
||||
rightSlot,
|
||||
}: VariablePayload) => {
|
||||
const varColorClassName = useVarColor(variables, isExceptionVariable)
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'inline-flex h-6 max-w-full items-center space-x-0.5 rounded-md border-[0.5px] border-components-panel-border-subtle bg-components-badge-white-to-dark px-1.5 shadow-xs',
|
||||
className,
|
||||
)}
|
||||
onClick={onClick}
|
||||
ref={ref}
|
||||
>
|
||||
<VariableNodeLabel
|
||||
nodeType={nodeType}
|
||||
nodeTitle={nodeTitle}
|
||||
/>
|
||||
{
|
||||
notShowFullPath && (
|
||||
<>
|
||||
<RiMoreLine className='h-3 w-3 shrink-0 text-text-secondary' />
|
||||
<div className='system-xs-regular shrink-0 text-divider-deep'>/</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
<VariableIcon
|
||||
variables={variables}
|
||||
className={varColorClassName}
|
||||
/>
|
||||
<VariableName
|
||||
variables={variables}
|
||||
className={cn(varColorClassName)}
|
||||
notShowFullPath={notShowFullPath}
|
||||
/>
|
||||
{
|
||||
variableType && (
|
||||
<div className='system-xs-regular shrink-0 text-text-tertiary'>
|
||||
{capitalize(variableType)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
!!errorMsg && (
|
||||
<Tooltip
|
||||
popupContent={errorMsg}
|
||||
asChild
|
||||
>
|
||||
<RiErrorWarningFill className='h-3 w-3 shrink-0 text-text-destructive' />
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
{
|
||||
rightSlot
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(VariableLabel)
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import { memo } from 'react'
|
||||
import { useVarName } from '../hooks'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type VariableNameProps = {
|
||||
variables: string[]
|
||||
className?: string
|
||||
notShowFullPath?: boolean
|
||||
}
|
||||
const VariableName = ({
|
||||
variables,
|
||||
className,
|
||||
notShowFullPath,
|
||||
}: VariableNameProps) => {
|
||||
const varName = useVarName(variables, notShowFullPath)
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'system-xs-medium truncate',
|
||||
className,
|
||||
)}
|
||||
title={varName}
|
||||
>
|
||||
{varName}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(VariableName)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue