Compare commits
26
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
626e71cb3b | ||
|
|
07047487c3 | ||
|
|
c7064d44af | ||
|
|
e93bfe3d41 | ||
|
|
ab910c736c | ||
|
|
4047a6bb12 | ||
|
|
df2478dc26 | ||
|
|
4cc3f6045b | ||
|
|
1550316b8d | ||
|
|
87394d2512 | ||
|
|
bad59c95bc | ||
|
|
9f138ef246 | ||
|
|
6453fc4973 | ||
|
|
f62f926537 | ||
|
|
b3dafd913b | ||
|
|
b2d8a7eaf1 | ||
|
|
3e54414191 | ||
|
|
a173546c8d | ||
|
|
aa69d90489 | ||
|
|
4ba1292455 | ||
|
|
bb01c31f30 | ||
|
|
cd90b2ca9e | ||
|
|
9a65350cf7 | ||
|
|
680eb7a9f6 | ||
|
|
878420463c | ||
|
|
4692e20daf |
@@ -1,15 +1,16 @@
|
||||
#!/bin/bash
|
||||
WORKSPACE_ROOT=$(pwd)
|
||||
|
||||
corepack enable
|
||||
cd web && pnpm install
|
||||
pipx install uv
|
||||
|
||||
echo 'alias start-api="cd /workspaces/dify/api && uv run python -m flask run --host 0.0.0.0 --port=5001 --debug"' >> ~/.bashrc
|
||||
echo 'alias start-worker="cd /workspaces/dify/api && uv run python -m celery -A app.celery worker -P gevent -c 1 --loglevel INFO -Q dataset,generation,mail,ops_trace,app_deletion,plugin,workflow_storage"' >> ~/.bashrc
|
||||
echo 'alias start-web="cd /workspaces/dify/web && pnpm dev"' >> ~/.bashrc
|
||||
echo 'alias start-web-prod="cd /workspaces/dify/web && pnpm build && pnpm start"' >> ~/.bashrc
|
||||
echo 'alias start-containers="cd /workspaces/dify/docker && docker-compose -f docker-compose.middleware.yaml -p dify --env-file middleware.env up -d"' >> ~/.bashrc
|
||||
echo 'alias stop-containers="cd /workspaces/dify/docker && docker-compose -f docker-compose.middleware.yaml -p dify --env-file middleware.env down"' >> ~/.bashrc
|
||||
echo "alias start-api=\"cd $WORKSPACE_ROOT/api && uv run python -m flask run --host 0.0.0.0 --port=5001 --debug\"" >> ~/.bashrc
|
||||
echo "alias start-worker=\"cd $WORKSPACE_ROOT/api && uv run python -m celery -A app.celery worker -P gevent -c 1 --loglevel INFO -Q dataset,generation,mail,ops_trace,app_deletion,plugin,workflow_storage\"" >> ~/.bashrc
|
||||
echo "alias start-web=\"cd $WORKSPACE_ROOT/web && pnpm dev\"" >> ~/.bashrc
|
||||
echo "alias start-web-prod=\"cd $WORKSPACE_ROOT/web && pnpm build && pnpm start\"" >> ~/.bashrc
|
||||
echo "alias start-containers=\"cd $WORKSPACE_ROOT/docker && docker-compose -f docker-compose.middleware.yaml -p dify --env-file middleware.env up -d\"" >> ~/.bashrc
|
||||
echo "alias stop-containers=\"cd $WORKSPACE_ROOT/docker && docker-compose -f docker-compose.middleware.yaml -p dify --env-file middleware.env down\"" >> ~/.bashrc
|
||||
|
||||
source /home/vscode/.bashrc
|
||||
|
||||
|
||||
+6
-1
@@ -147,6 +147,7 @@ api/.idea
|
||||
|
||||
api/.env
|
||||
api/storage/*
|
||||
api/Dockerfile.local
|
||||
|
||||
docker-legacy/volumes/app/storage/*
|
||||
docker-legacy/volumes/db/data/*
|
||||
@@ -230,4 +231,8 @@ api/.env.backup
|
||||
|
||||
# Benchmark
|
||||
scripts/stress-test/setup/config/
|
||||
scripts/stress-test/reports/
|
||||
scripts/stress-test/reports/
|
||||
|
||||
# mcp
|
||||
.playwright-mcp/
|
||||
.serena/
|
||||
@@ -61,8 +61,9 @@ check:
|
||||
@echo "✅ Code check complete"
|
||||
|
||||
lint:
|
||||
@echo "🔧 Running ruff format and check with fixes..."
|
||||
@uv run --directory api --dev sh -c 'ruff format ./api && ruff check --fix ./api'
|
||||
@echo "🔧 Running ruff format, check with fixes, and import linter..."
|
||||
@uv run --project api --dev sh -c 'ruff format ./api && ruff check --fix ./api'
|
||||
@uv run --directory api --dev lint-imports
|
||||
@echo "✅ Linting complete"
|
||||
|
||||
type-check:
|
||||
|
||||
@@ -30,6 +30,7 @@ select = [
|
||||
"RUF022", # unsorted-dunder-all
|
||||
"S506", # unsafe-yaml-load
|
||||
"SIM", # flake8-simplify rules
|
||||
"T201", # print-found
|
||||
"TRY400", # error-instead-of-exception
|
||||
"TRY401", # verbose-log-message
|
||||
"UP", # pyupgrade rules
|
||||
@@ -91,11 +92,18 @@ ignore = [
|
||||
"configs/*" = [
|
||||
"N802", # invalid-function-name
|
||||
]
|
||||
"core/model_runtime/callbacks/base_callback.py" = [
|
||||
"T201",
|
||||
]
|
||||
"core/workflow/callbacks/workflow_logging_callback.py" = [
|
||||
"T201",
|
||||
]
|
||||
"libs/gmpy2_pkcs10aep_cipher.py" = [
|
||||
"N803", # invalid-argument-name
|
||||
]
|
||||
"tests/*" = [
|
||||
"F811", # redefined-while-unused
|
||||
"T201", # allow print in tests
|
||||
]
|
||||
|
||||
[lint.pyflakes]
|
||||
|
||||
@@ -7,7 +7,7 @@ _logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _log(message: str):
|
||||
print(message, flush=True)
|
||||
_logger.debug(message)
|
||||
|
||||
|
||||
# grpc gevent
|
||||
|
||||
+4
-4
@@ -739,18 +739,18 @@ where sites.id is null limit 1000"""
|
||||
try:
|
||||
app = db.session.query(App).where(App.id == app_id).first()
|
||||
if not app:
|
||||
print(f"App {app_id} not found")
|
||||
logger.info("App %s not found", app_id)
|
||||
continue
|
||||
|
||||
tenant = app.tenant
|
||||
if tenant:
|
||||
accounts = tenant.get_accounts()
|
||||
if not accounts:
|
||||
print(f"Fix failed for app {app.id}")
|
||||
logger.info("Fix failed for app %s", app.id)
|
||||
continue
|
||||
|
||||
account = accounts[0]
|
||||
print(f"Fixing missing site for app {app.id}")
|
||||
logger.info("Fixing missing site for app %s", app.id)
|
||||
app_was_created.send(app, account=account)
|
||||
except Exception:
|
||||
failed_app_ids.append(app_id)
|
||||
@@ -1544,7 +1544,7 @@ def transform_datasource_credentials():
|
||||
if jina_plugin_id not in installed_plugins_ids:
|
||||
if jina_plugin_unique_identifier:
|
||||
# install jina plugin
|
||||
print(jina_plugin_unique_identifier)
|
||||
logger.debug("Installing Jina plugin %s", jina_plugin_unique_identifier)
|
||||
PluginService.install_from_marketplace_pkg(tenant_id, [jina_plugin_unique_identifier])
|
||||
|
||||
auth_count = 0
|
||||
|
||||
@@ -62,6 +62,9 @@ class ChatMessageListApi(Resource):
|
||||
@account_initialization_required
|
||||
@marshal_with(message_infinite_scroll_pagination_fields)
|
||||
def get(self, app_model):
|
||||
if not isinstance(current_user, Account) or not current_user.has_edit_permission:
|
||||
raise Forbidden()
|
||||
|
||||
parser = reqparse.RequestParser()
|
||||
parser.add_argument("conversation_id", required=True, type=uuid_value, location="args")
|
||||
parser.add_argument("first_id", type=uuid_value, location="args")
|
||||
|
||||
@@ -118,12 +118,14 @@ class RagPipelineExportApi(Resource):
|
||||
|
||||
# Add include_secret params
|
||||
parser = reqparse.RequestParser()
|
||||
parser.add_argument("include_secret", type=bool, default=False, location="args")
|
||||
parser.add_argument("include_secret", type=str, default="false", location="args")
|
||||
args = parser.parse_args()
|
||||
|
||||
with Session(db.engine) as session:
|
||||
export_service = RagPipelineDslService(session)
|
||||
result = export_service.export_rag_pipeline_dsl(pipeline=pipeline, include_secret=args["include_secret"])
|
||||
result = export_service.export_rag_pipeline_dsl(
|
||||
pipeline=pipeline, include_secret=args["include_secret"] == "true"
|
||||
)
|
||||
|
||||
return {"data": result}, 200
|
||||
|
||||
|
||||
@@ -261,6 +261,8 @@ class MessageSuggestedQuestionApi(WebApiResource):
|
||||
questions = MessageService.get_suggested_questions_after_answer(
|
||||
app_model=app_model, user=end_user, message_id=message_id, invoke_from=InvokeFrom.WEB_APP
|
||||
)
|
||||
# questions is a list of strings, not a list of Message objects
|
||||
# so we can directly return it
|
||||
except MessageNotExistsError:
|
||||
raise NotFound("Message not found")
|
||||
except ConversationNotExistsError:
|
||||
|
||||
@@ -417,7 +417,7 @@ class WeaveDataTrace(BaseTraceInstance):
|
||||
if not login_status:
|
||||
raise ValueError("Weave login failed")
|
||||
else:
|
||||
print("Weave login successful")
|
||||
logger.info("Weave login successful")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.debug("Weave API check failed: %s", str(e))
|
||||
|
||||
@@ -229,7 +229,7 @@ class OceanBaseVector(BaseVector):
|
||||
try:
|
||||
metadata = json.loads(metadata_str)
|
||||
except json.JSONDecodeError:
|
||||
print(f"Invalid JSON metadata: {metadata_str}")
|
||||
logger.warning("Invalid JSON metadata: %s", metadata_str)
|
||||
metadata = {}
|
||||
metadata["score"] = score
|
||||
docs.append(Document(page_content=_text, metadata=metadata))
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import array
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import uuid
|
||||
from typing import Any
|
||||
@@ -19,6 +20,8 @@ from core.rag.models.document import Document
|
||||
from extensions.ext_redis import redis_client
|
||||
from models.dataset import Dataset
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
oracledb.defaults.fetch_lobs = False
|
||||
|
||||
|
||||
@@ -180,8 +183,8 @@ class OracleVector(BaseVector):
|
||||
value,
|
||||
)
|
||||
conn.commit()
|
||||
except Exception as e:
|
||||
print(e)
|
||||
except Exception:
|
||||
logger.exception("Failed to insert record %s into %s", value[0], self.table_name)
|
||||
conn.close()
|
||||
return pks
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import json
|
||||
import logging
|
||||
import uuid
|
||||
from typing import Any
|
||||
|
||||
@@ -23,6 +24,8 @@ from core.rag.datasource.vdb.vector_base import BaseVector
|
||||
from core.rag.models.document import Document
|
||||
from extensions.ext_redis import redis_client
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
Base = declarative_base() # type: Any
|
||||
|
||||
|
||||
@@ -187,8 +190,8 @@ class RelytVector(BaseVector):
|
||||
delete_condition = chunks_table.c.id.in_(ids)
|
||||
conn.execute(chunks_table.delete().where(delete_condition))
|
||||
return True
|
||||
except Exception as e:
|
||||
print("Delete operation failed:", str(e))
|
||||
except Exception:
|
||||
logger.exception("Delete operation failed for collection %s", self._collection_name)
|
||||
return False
|
||||
|
||||
def delete_by_metadata_field(self, key: str, value: str):
|
||||
|
||||
@@ -164,8 +164,8 @@ class TiDBVector(BaseVector):
|
||||
delete_condition = table.c.id.in_(ids)
|
||||
conn.execute(table.delete().where(delete_condition))
|
||||
return True
|
||||
except Exception as e:
|
||||
print("Delete operation failed:", str(e))
|
||||
except Exception:
|
||||
logger.exception("Delete operation failed for collection %s", self._collection_name)
|
||||
return False
|
||||
|
||||
def get_ids_by_metadata_field(self, key: str, value: str):
|
||||
|
||||
@@ -93,6 +93,17 @@ class DatasetDocumentStore:
|
||||
|
||||
segment_document = self.get_document_segment(doc_id=doc.metadata["doc_id"])
|
||||
|
||||
# Check if a segment with the same content hash already exists in the dataset
|
||||
existing_segment_by_hash = db.session.query(DocumentSegment).filter_by(
|
||||
dataset_id=self._dataset.id,
|
||||
index_node_hash=doc.metadata["doc_hash"],
|
||||
enabled=True
|
||||
).first()
|
||||
|
||||
if existing_segment_by_hash:
|
||||
# Skip creating duplicate segment with same content hash
|
||||
continue
|
||||
|
||||
# NOTE: doc could already exist in the store, but we overwrite it
|
||||
if not allow_update and segment_document:
|
||||
raise ValueError(
|
||||
|
||||
@@ -417,12 +417,10 @@ class SQLAlchemyWorkflowNodeExecutionRepository(WorkflowNodeExecutionRepository)
|
||||
|
||||
if db_model is not None:
|
||||
offload_data = db_model.offload_data
|
||||
|
||||
else:
|
||||
db_model = self._to_db_model(domain_model)
|
||||
offload_data = []
|
||||
offload_data = db_model.offload_data
|
||||
|
||||
offload_data = db_model.offload_data
|
||||
if domain_model.inputs is not None:
|
||||
result = self._truncate_and_upload(
|
||||
domain_model.inputs,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import json
|
||||
import logging
|
||||
import threading
|
||||
from collections.abc import Mapping, MutableMapping
|
||||
from pathlib import Path
|
||||
@@ -8,6 +9,8 @@ from typing import Any, ClassVar, Optional
|
||||
class SchemaRegistry:
|
||||
"""Schema registry manages JSON schemas with version support"""
|
||||
|
||||
logger: ClassVar[logging.Logger] = logging.getLogger(__name__)
|
||||
|
||||
_default_instance: ClassVar[Optional["SchemaRegistry"]] = None
|
||||
_lock: ClassVar[threading.Lock] = threading.Lock()
|
||||
|
||||
@@ -83,7 +86,7 @@ class SchemaRegistry:
|
||||
self.metadata[uri] = metadata
|
||||
|
||||
except (OSError, json.JSONDecodeError) as e:
|
||||
print(f"Warning: failed to load schema {version}/{schema_name}: {e}")
|
||||
self.logger.warning("Failed to load schema %s/%s: %s", version, schema_name, e)
|
||||
|
||||
def get_schema(self, uri: str) -> Any | None:
|
||||
"""Retrieves a schema by URI with version support"""
|
||||
|
||||
@@ -396,6 +396,10 @@ class ApiTool(Tool):
|
||||
# assemble invoke message based on response type
|
||||
if parsed_response.is_json and isinstance(parsed_response.content, dict):
|
||||
yield self.create_json_message(parsed_response.content)
|
||||
|
||||
# FIXES: https://github.com/langgenius/dify/pull/23456#issuecomment-3182413088
|
||||
# We need never break the original flows
|
||||
yield self.create_text_message(response.text)
|
||||
else:
|
||||
# Convert to string if needed and create text message
|
||||
text_response = (
|
||||
|
||||
@@ -147,4 +147,4 @@ class ExecutionLimitsLayer(GraphEngineLayer):
|
||||
self.logger.debug("Abort command sent to engine")
|
||||
|
||||
except Exception:
|
||||
self.logger.exception("Failed to send abort command: %s")
|
||||
self.logger.exception("Failed to send abort command")
|
||||
|
||||
@@ -689,6 +689,7 @@ class DocumentSegment(Base):
|
||||
sa.Index("document_segment_tenant_document_idx", "document_id", "tenant_id"),
|
||||
sa.Index("document_segment_node_dataset_idx", "index_node_id", "dataset_id"),
|
||||
sa.Index("document_segment_tenant_idx", "tenant_id"),
|
||||
sa.Index("document_segment_dataset_hash_idx", "dataset_id", "index_node_hash"),
|
||||
)
|
||||
|
||||
# initial fields
|
||||
|
||||
@@ -532,7 +532,8 @@ class DatasetService:
|
||||
filtered_data["updated_by"] = user.id
|
||||
filtered_data["updated_at"] = naive_utc_now()
|
||||
# update Retrieval model
|
||||
filtered_data["retrieval_model"] = data["retrieval_model"]
|
||||
if data.get("retrieval_model"):
|
||||
filtered_data["retrieval_model"] = data["retrieval_model"]
|
||||
# update icon info
|
||||
if data.get("icon_info"):
|
||||
filtered_data["icon_info"] = data.get("icon_info")
|
||||
@@ -2622,6 +2623,17 @@ class SegmentService:
|
||||
tokens = embedding_model.get_text_embedding_num_tokens(texts=[content])[0]
|
||||
lock_name = f"add_segment_lock_document_id_{document.id}"
|
||||
with redis_client.lock(lock_name, timeout=600):
|
||||
# Check if a segment with the same content hash already exists
|
||||
existing_segment = db.session.query(DocumentSegment).filter_by(
|
||||
dataset_id=document.dataset_id,
|
||||
index_node_hash=segment_hash,
|
||||
enabled=True
|
||||
).first()
|
||||
|
||||
if existing_segment:
|
||||
logger.info(f"Segment with same content hash already exists: {segment_hash}")
|
||||
return existing_segment
|
||||
|
||||
max_position = (
|
||||
db.session.query(func.max(DocumentSegment.position))
|
||||
.where(DocumentSegment.document_id == document.id)
|
||||
@@ -2688,6 +2700,15 @@ class SegmentService:
|
||||
.where(DocumentSegment.document_id == document.id)
|
||||
.scalar()
|
||||
)
|
||||
# Batch query existing hashes before the loop
|
||||
segment_hashes = [helper.generate_text_hash(seg["content"]) for seg in segments]
|
||||
existing_segments = db.session.query(DocumentSegment.index_node_hash).filter(
|
||||
DocumentSegment.dataset_id == document.dataset_id,
|
||||
DocumentSegment.index_node_hash.in_(segment_hashes),
|
||||
DocumentSegment.enabled == True
|
||||
).all()
|
||||
existing_hashes = {seg.index_node_hash for seg in existing_segments}
|
||||
|
||||
pre_segment_data_list = []
|
||||
segment_data_list = []
|
||||
keywords_list = []
|
||||
@@ -2696,6 +2717,12 @@ class SegmentService:
|
||||
content = segment_item["content"]
|
||||
doc_id = str(uuid.uuid4())
|
||||
segment_hash = helper.generate_text_hash(content)
|
||||
|
||||
# Skip existing segments
|
||||
if segment_hash in existing_hashes:
|
||||
logger.info(f"Skipping duplicate segment with hash: {segment_hash}")
|
||||
continue
|
||||
|
||||
tokens = 0
|
||||
if dataset.indexing_technique == "high_quality" and embedding_model:
|
||||
# calc embedding use tokens
|
||||
|
||||
@@ -217,7 +217,7 @@ class MessageService:
|
||||
@classmethod
|
||||
def get_suggested_questions_after_answer(
|
||||
cls, app_model: App, user: Union[Account, EndUser] | None, message_id: str, invoke_from: InvokeFrom
|
||||
) -> list[Message]:
|
||||
) -> list[str]:
|
||||
if not user:
|
||||
raise ValueError("user cannot be None")
|
||||
|
||||
@@ -288,7 +288,7 @@ class MessageService:
|
||||
)
|
||||
|
||||
with measure_time() as timer:
|
||||
questions: list[Message] = LLMGenerator.generate_suggested_questions_after_answer(
|
||||
questions: list[str] = LLMGenerator.generate_suggested_questions_after_answer(
|
||||
tenant_id=app_model.tenant_id, histories=histories
|
||||
)
|
||||
|
||||
|
||||
@@ -46,7 +46,11 @@ limit 1000"""
|
||||
record_id = str(i.id)
|
||||
provider_name = str(i.provider_name)
|
||||
retrieval_model = i.retrieval_model
|
||||
print(type(retrieval_model))
|
||||
logger.debug(
|
||||
"Processing dataset %s with retrieval model of type %s",
|
||||
record_id,
|
||||
type(retrieval_model),
|
||||
)
|
||||
|
||||
if record_id in failed_ids:
|
||||
continue
|
||||
|
||||
@@ -1327,14 +1327,14 @@ class RagPipelineService:
|
||||
"""
|
||||
Retry error document
|
||||
"""
|
||||
document_pipeline_excution_log = (
|
||||
document_pipeline_execution_log = (
|
||||
db.session.query(DocumentPipelineExecutionLog)
|
||||
.where(DocumentPipelineExecutionLog.document_id == document.id)
|
||||
.first()
|
||||
)
|
||||
if not document_pipeline_excution_log:
|
||||
if not document_pipeline_execution_log:
|
||||
raise ValueError("Document pipeline execution log not found")
|
||||
pipeline = db.session.query(Pipeline).where(Pipeline.id == document_pipeline_excution_log.pipeline_id).first()
|
||||
pipeline = db.session.query(Pipeline).where(Pipeline.id == document_pipeline_execution_log.pipeline_id).first()
|
||||
if not pipeline:
|
||||
raise ValueError("Pipeline not found")
|
||||
# convert to app config
|
||||
@@ -1346,10 +1346,10 @@ class RagPipelineService:
|
||||
workflow=workflow,
|
||||
user=user,
|
||||
args={
|
||||
"inputs": document_pipeline_excution_log.input_data,
|
||||
"start_node_id": document_pipeline_excution_log.datasource_node_id,
|
||||
"datasource_type": document_pipeline_excution_log.datasource_type,
|
||||
"datasource_info_list": [json.loads(document_pipeline_excution_log.datasource_info)],
|
||||
"inputs": document_pipeline_execution_log.input_data,
|
||||
"start_node_id": document_pipeline_execution_log.datasource_node_id,
|
||||
"datasource_type": document_pipeline_execution_log.datasource_type,
|
||||
"datasource_info_list": [json.loads(document_pipeline_execution_log.datasource_info)],
|
||||
"original_document_id": document.id,
|
||||
},
|
||||
invoke_from=InvokeFrom.PUBLISHED,
|
||||
|
||||
@@ -685,12 +685,24 @@ class RagPipelineDslService:
|
||||
|
||||
workflow_dict = workflow.to_dict(include_secret=include_secret)
|
||||
for node in workflow_dict.get("graph", {}).get("nodes", []):
|
||||
if node.get("data", {}).get("type", "") == NodeType.KNOWLEDGE_RETRIEVAL.value:
|
||||
dataset_ids = node["data"].get("dataset_ids", [])
|
||||
node_data = node.get("data", {})
|
||||
if not node_data:
|
||||
continue
|
||||
data_type = node_data.get("type", "")
|
||||
if data_type == NodeType.KNOWLEDGE_RETRIEVAL.value:
|
||||
dataset_ids = node_data.get("dataset_ids", [])
|
||||
node["data"]["dataset_ids"] = [
|
||||
self.encrypt_dataset_id(dataset_id=dataset_id, tenant_id=pipeline.tenant_id)
|
||||
for dataset_id in dataset_ids
|
||||
]
|
||||
# filter credential id from tool node
|
||||
if not include_secret and data_type == NodeType.TOOL.value:
|
||||
node_data.pop("credential_id", None)
|
||||
# filter credential id from agent node
|
||||
if not include_secret and data_type == NodeType.AGENT.value:
|
||||
for tool in node_data.get("agent_parameters", {}).get("tools", {}).get("value", []):
|
||||
tool.pop("credential_id", None)
|
||||
|
||||
export_data["workflow"] = workflow_dict
|
||||
dependencies = self._extract_dependencies_from_workflow(workflow)
|
||||
export_data["dependencies"] = [
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import json
|
||||
import logging
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from uuid import uuid4
|
||||
@@ -17,6 +18,8 @@ from services.entities.knowledge_entities.rag_pipeline_entities import Knowledge
|
||||
from services.plugin.plugin_migration import PluginMigration
|
||||
from services.plugin.plugin_service import PluginService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RagPipelineTransformService:
|
||||
def transform_dataset(self, dataset_id: str):
|
||||
@@ -35,11 +38,11 @@ class RagPipelineTransformService:
|
||||
indexing_technique = dataset.indexing_technique
|
||||
|
||||
if not datasource_type and not indexing_technique:
|
||||
return self._transfrom_to_empty_pipeline(dataset)
|
||||
return self._transform_to_empty_pipeline(dataset)
|
||||
|
||||
doc_form = dataset.doc_form
|
||||
if not doc_form:
|
||||
return self._transfrom_to_empty_pipeline(dataset)
|
||||
return self._transform_to_empty_pipeline(dataset)
|
||||
retrieval_model = dataset.retrieval_model
|
||||
pipeline_yaml = self._get_transform_yaml(doc_form, datasource_type, indexing_technique)
|
||||
# deal dependencies
|
||||
@@ -257,10 +260,10 @@ class RagPipelineTransformService:
|
||||
if plugin_unique_identifier:
|
||||
need_install_plugin_unique_identifiers.append(plugin_unique_identifier)
|
||||
if need_install_plugin_unique_identifiers:
|
||||
print(need_install_plugin_unique_identifiers)
|
||||
logger.debug("Installing missing pipeline plugins %s", need_install_plugin_unique_identifiers)
|
||||
PluginService.install_from_marketplace_pkg(tenant_id, need_install_plugin_unique_identifiers)
|
||||
|
||||
def _transfrom_to_empty_pipeline(self, dataset: Dataset):
|
||||
def _transform_to_empty_pipeline(self, dataset: Dataset):
|
||||
pipeline = Pipeline(
|
||||
tenant_id=dataset.tenant_id,
|
||||
name=dataset.name,
|
||||
|
||||
@@ -450,7 +450,8 @@ class WorkflowService:
|
||||
)
|
||||
|
||||
if not default_provider:
|
||||
raise ValueError("No default credential found")
|
||||
# plugin does not require credentials, skip
|
||||
return
|
||||
|
||||
# Check credential policy compliance using the default credential ID
|
||||
from core.helper.credential_utils import check_credential_policy_compliance
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
"""Integration tests for ChatMessageApi permission verification."""
|
||||
|
||||
import uuid
|
||||
from types import SimpleNamespace
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
from flask.testing import FlaskClient
|
||||
|
||||
from controllers.console.app import completion as completion_api
|
||||
from controllers.console.app import message as message_api
|
||||
from controllers.console.app import wraps
|
||||
from libs.datetime_utils import naive_utc_now
|
||||
from models import Account, App, Tenant
|
||||
@@ -99,3 +101,106 @@ class TestChatMessageApiPermissions:
|
||||
)
|
||||
|
||||
assert response.status_code == status
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("role", "status"),
|
||||
[
|
||||
(TenantAccountRole.OWNER, 200),
|
||||
(TenantAccountRole.ADMIN, 200),
|
||||
(TenantAccountRole.EDITOR, 200),
|
||||
(TenantAccountRole.NORMAL, 403),
|
||||
(TenantAccountRole.DATASET_OPERATOR, 403),
|
||||
],
|
||||
)
|
||||
def test_get_requires_edit_permission(
|
||||
self,
|
||||
test_client: FlaskClient,
|
||||
auth_header,
|
||||
monkeypatch,
|
||||
mock_app_model,
|
||||
mock_account,
|
||||
role: TenantAccountRole,
|
||||
status: int,
|
||||
):
|
||||
"""Ensure GET chat-messages endpoint enforces edit permissions."""
|
||||
|
||||
mock_load_app_model = mock.Mock(return_value=mock_app_model)
|
||||
monkeypatch.setattr(wraps, "_load_app_model", mock_load_app_model)
|
||||
|
||||
conversation_id = uuid.uuid4()
|
||||
created_at = naive_utc_now()
|
||||
|
||||
mock_conversation = SimpleNamespace(id=str(conversation_id), app_id=str(mock_app_model.id))
|
||||
mock_message = SimpleNamespace(
|
||||
id=str(uuid.uuid4()),
|
||||
conversation_id=str(conversation_id),
|
||||
inputs=[],
|
||||
query="hello",
|
||||
message=[{"text": "hello"}],
|
||||
message_tokens=0,
|
||||
re_sign_file_url_answer="",
|
||||
answer_tokens=0,
|
||||
provider_response_latency=0.0,
|
||||
from_source="console",
|
||||
from_end_user_id=None,
|
||||
from_account_id=mock_account.id,
|
||||
feedbacks=[],
|
||||
workflow_run_id=None,
|
||||
annotation=None,
|
||||
annotation_hit_history=None,
|
||||
created_at=created_at,
|
||||
agent_thoughts=[],
|
||||
message_files=[],
|
||||
message_metadata_dict={},
|
||||
status="success",
|
||||
error="",
|
||||
parent_message_id=None,
|
||||
)
|
||||
|
||||
class MockQuery:
|
||||
def __init__(self, model):
|
||||
self.model = model
|
||||
|
||||
def where(self, *args, **kwargs):
|
||||
return self
|
||||
|
||||
def first(self):
|
||||
if getattr(self.model, "__name__", "") == "Conversation":
|
||||
return mock_conversation
|
||||
return None
|
||||
|
||||
def order_by(self, *args, **kwargs):
|
||||
return self
|
||||
|
||||
def limit(self, *_):
|
||||
return self
|
||||
|
||||
def all(self):
|
||||
if getattr(self.model, "__name__", "") == "Message":
|
||||
return [mock_message]
|
||||
return []
|
||||
|
||||
mock_session = mock.Mock()
|
||||
mock_session.query.side_effect = MockQuery
|
||||
mock_session.scalar.return_value = False
|
||||
|
||||
monkeypatch.setattr(message_api, "db", SimpleNamespace(session=mock_session))
|
||||
monkeypatch.setattr(message_api, "current_user", mock_account)
|
||||
|
||||
class DummyPagination:
|
||||
def __init__(self, data, limit, has_more):
|
||||
self.data = data
|
||||
self.limit = limit
|
||||
self.has_more = has_more
|
||||
|
||||
monkeypatch.setattr(message_api, "InfiniteScrollPagination", DummyPagination)
|
||||
|
||||
mock_account.role = role
|
||||
|
||||
response = test_client.get(
|
||||
f"/console/api/apps/{mock_app_model.id}/chat-messages",
|
||||
headers=auth_header,
|
||||
query_string={"conversation_id": str(conversation_id)},
|
||||
)
|
||||
|
||||
assert response.status_code == status
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
# 本地测试环境设置指南
|
||||
|
||||
本文档说明如何创建和使用本地的Docker Compose测试环境,该环境不会被提交到版本控制。
|
||||
|
||||
## 📁 文件结构
|
||||
|
||||
```
|
||||
docker/
|
||||
├── .env # 本地环境配置
|
||||
├── docker-compose.override.yaml # 本地覆盖配置
|
||||
├── start-local-test.bat # Windows启动脚本
|
||||
└── README-local-test.md # 本文档
|
||||
```
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 1. 准备环境配置文件
|
||||
|
||||
**使用 `.env`**
|
||||
```bash
|
||||
cd docker
|
||||
copy .env.example .env
|
||||
```
|
||||
|
||||
**注意**: 请确保 Docker Desktop 正在运行,然后执行启动脚本。
|
||||
|
||||
### 2. 修改配置(可选)
|
||||
|
||||
编辑你选择的环境文件,调整适合本地测试的配置:
|
||||
|
||||
```bash
|
||||
# 开发环境
|
||||
DEPLOY_ENV=DEVELOPMENT
|
||||
|
||||
# 启用调试
|
||||
DEBUG=true
|
||||
FLASK_DEBUG=true
|
||||
LOG_LEVEL=DEBUG
|
||||
|
||||
# 数据库配置(保持默认即可)
|
||||
DB_USERNAME=postgres
|
||||
DB_PASSWORD=difyai123456
|
||||
|
||||
# 向量存储(本地测试推荐Weaviate)
|
||||
VECTOR_STORE=weaviate
|
||||
```
|
||||
|
||||
### 3. 启动测试环境
|
||||
|
||||
**Windows用户**:
|
||||
```cmd
|
||||
cd docker
|
||||
start-local-test.bat
|
||||
```
|
||||
|
||||
**脚本会自动**:
|
||||
- 检查 Docker Desktop 是否运行
|
||||
- 验证 `.env` 配置文件存在
|
||||
- 构建 worker 镜像(使用本地 Dockerfile)
|
||||
- 启动所有服务
|
||||
|
||||
或者手动启动:
|
||||
|
||||
```bash
|
||||
# 启动中间件(数据库、Redis、向量存储)
|
||||
docker compose -f docker-compose.middleware.yaml --profile weaviate up -d
|
||||
|
||||
# 启动应用服务
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
## 🎯 服务说明
|
||||
|
||||
### 中间件服务(docker-compose.middleware.yaml)
|
||||
- **PostgreSQL**: 主数据库
|
||||
- **Redis**: 缓存和消息队列
|
||||
- **Weaviate**: 向量数据库(默认)
|
||||
- **其他**: 可根据需要启用不同的向量存储
|
||||
|
||||
### 应用服务(docker-compose.yaml + override)
|
||||
- **API**: 后端服务(开发模式,支持热重载)
|
||||
- **Web**: 前端服务(开发模式)
|
||||
- **Nginx**: 反向代理
|
||||
- **Worker**: 后台任务处理
|
||||
|
||||
## 📝 本地开发特性
|
||||
|
||||
### 热重载
|
||||
- API服务会自动检测代码变化并重启
|
||||
- Web服务支持前端热重载
|
||||
|
||||
### 数据持久化
|
||||
数据存储在 `docker/volumes/` 目录下,会在容器重启后保留。
|
||||
|
||||
### 调试支持
|
||||
- 启用Flask调试模式
|
||||
- 详细的日志输出
|
||||
- API文档自动生成
|
||||
|
||||
## 🛠️ 常用命令
|
||||
|
||||
```bash
|
||||
# 查看服务状态
|
||||
docker compose ps
|
||||
|
||||
# 查看日志
|
||||
docker compose logs -f [service_name]
|
||||
|
||||
# 重启特定服务
|
||||
docker compose restart api
|
||||
|
||||
# 进入容器调试
|
||||
docker compose exec api bash
|
||||
|
||||
# 停止所有服务
|
||||
docker compose down
|
||||
|
||||
# 停止并清理数据卷
|
||||
docker compose -f docker-compose.middleware.yaml down -v
|
||||
```
|
||||
|
||||
## 🔧 自定义配置
|
||||
|
||||
### 修改端口
|
||||
在环境文件中修改:
|
||||
```bash
|
||||
DIFY_PORT=5002 # API端口
|
||||
EXPOSE_NGINX_PORT=8080 # Web端口
|
||||
```
|
||||
|
||||
### 切换向量存储
|
||||
在环境文件中修改:
|
||||
```bash
|
||||
VECTOR_STORE=qdrant # 或 milvus, chroma 等
|
||||
```
|
||||
|
||||
然后重新启动中间件:
|
||||
```bash
|
||||
docker compose -f docker-compose.middleware.yaml --profile qdrant up -d
|
||||
```
|
||||
|
||||
### 使用本地 Dockerfile
|
||||
|
||||
如果需要使用自定义的 Dockerfile(比如使用国内镜像加速):
|
||||
|
||||
1. **创建本地 Dockerfile**:
|
||||
```bash
|
||||
# 复制原文件
|
||||
cp api/Dockerfile api/Dockerfile.local
|
||||
|
||||
# 编辑本地文件(比如取消阿里云镜像注释)
|
||||
# 第15行取消注释:RUN sed -i 's@deb.debian.org@mirrors.aliyun.com@g' /etc/apt/sources.list.d/debian.sources
|
||||
```
|
||||
|
||||
2. **配置 override 使用本地 Dockerfile**:
|
||||
`docker-compose.override.yaml` 已经配置好了使用 `Dockerfile.local`
|
||||
|
||||
3. **构建时会自动使用**:
|
||||
```bash
|
||||
docker compose --env-file .env build worker
|
||||
```
|
||||
|
||||
### 添加自定义服务
|
||||
编辑 `docker-compose.override.yaml` 添加新服务。
|
||||
|
||||
## 📚 最佳实践
|
||||
|
||||
1. **不要修改官方文件**: 不要直接修改 `docker-compose.yaml`,所有本地改动都放在 `docker-compose.override.yaml` 中。
|
||||
|
||||
2. **使用有意义的环境文件**: 使用 `.env` 文件进行本地配置。
|
||||
|
||||
3. **定期清理**: 测试完成后清理不需要的数据卷。
|
||||
|
||||
4. **版本控制**: 这些本地文件(`.env`, `docker-compose.override.yaml`, `Dockerfile.local`)会被 `.gitignore` 忽略,不会提交到仓库。
|
||||
|
||||
## 🐛 故障排除
|
||||
|
||||
### 服务启动失败
|
||||
```bash
|
||||
# 检查端口占用
|
||||
netstat -tulpn | grep :5001
|
||||
|
||||
# 检查Docker资源
|
||||
docker system df
|
||||
|
||||
# 查看详细日志
|
||||
docker compose logs
|
||||
```
|
||||
|
||||
### 数据库连接问题
|
||||
```bash
|
||||
# 检查数据库状态
|
||||
docker compose exec db pg_isready
|
||||
|
||||
# 重置数据库
|
||||
docker compose down
|
||||
docker volume rm dify_db_data
|
||||
docker compose up -d db
|
||||
```
|
||||
|
||||
### 内存不足
|
||||
减少服务资源使用:
|
||||
```yaml
|
||||
# 在 docker-compose.override.yaml 中添加
|
||||
services:
|
||||
db:
|
||||
environment:
|
||||
POSTGRES_SHARED_BUFFERS: 64MB
|
||||
redis:
|
||||
command: redis-server --maxmemory 64mb
|
||||
```
|
||||
|
||||
## 📞 获取帮助
|
||||
|
||||
如果遇到问题,请:
|
||||
1. 检查本文档
|
||||
2. 查看 [官方文档](https://docs.dify.ai)
|
||||
3. 在GitHub Issues中搜索类似问题
|
||||
@@ -0,0 +1,63 @@
|
||||
@echo off
|
||||
chcp 65001 >nul
|
||||
REM Dify Local Test Environment Startup Script (Windows)
|
||||
REM Used to quickly start local development and testing environment
|
||||
|
||||
echo [INFO] Starting Dify local test environment...
|
||||
|
||||
REM Ensure in docker directory
|
||||
cd /d "%~dp0"
|
||||
|
||||
REM Check if Docker is running
|
||||
docker info >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
echo [ERROR] Docker is not running. Please start Docker Desktop first.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Check if .env file exists
|
||||
if not exist ".env" (
|
||||
echo [ERROR] .env configuration file not found
|
||||
echo Please create first: copy .env.example .env
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo [INFO] Using config file: .env
|
||||
|
||||
REM Build worker image
|
||||
echo [INFO] Building worker image...
|
||||
docker compose --env-file .env build worker
|
||||
if errorlevel 1 (
|
||||
echo [ERROR] Failed to build worker image
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Start all services
|
||||
echo [INFO] Starting all services...
|
||||
docker compose --env-file .env up -d
|
||||
if errorlevel 1 (
|
||||
echo [ERROR] Failed to start services
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo [SUCCESS] Local test environment started successfully!
|
||||
echo.
|
||||
echo [SERVICES] Service URLs:
|
||||
echo - Web UI: http://localhost
|
||||
echo - API Docs: http://localhost/swagger-ui.html
|
||||
echo - API Service: http://localhost:5001
|
||||
echo.
|
||||
echo [COMMANDS] Available commands:
|
||||
echo - View logs: docker compose logs -f
|
||||
echo - Stop services: docker compose down
|
||||
echo - Clean data: docker compose -f docker-compose.middleware.yaml down -v
|
||||
echo - Restart services: docker compose restart
|
||||
echo.
|
||||
echo [TIP] If first run, wait a few minutes for services to fully start
|
||||
echo Use 'docker compose ps' to check service status
|
||||
|
||||
pause
|
||||
@@ -464,6 +464,7 @@ const Configuration: FC = () => {
|
||||
provider,
|
||||
modelId,
|
||||
completionParams,
|
||||
isAdvancedMode,
|
||||
)
|
||||
if (Object.keys(removedDetails).length)
|
||||
Toast.notify({ type: 'warning', message: `${t('common.modelProvider.parametersInvalidRemoved')}: ${Object.entries(removedDetails).map(([k, reason]) => `${k} (${reason})`).join(', ')}` })
|
||||
|
||||
@@ -122,19 +122,31 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
||||
setLocaleFromProps()
|
||||
}, [appData])
|
||||
|
||||
const [sidebarCollapseState, setSidebarCollapseState] = useState<boolean>(false)
|
||||
const [sidebarCollapseState, setSidebarCollapseState] = useState<boolean>(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
try {
|
||||
const localState = localStorage.getItem('webappSidebarCollapse')
|
||||
return localState === 'collapsed'
|
||||
}
|
||||
catch (e) {
|
||||
// localStorage may be disabled in private browsing mode or by security settings
|
||||
// fallback to default value
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
const handleSidebarCollapse = useCallback((state: boolean) => {
|
||||
if (appId) {
|
||||
setSidebarCollapseState(state)
|
||||
localStorage.setItem('webappSidebarCollapse', state ? 'collapsed' : 'expanded')
|
||||
try {
|
||||
localStorage.setItem('webappSidebarCollapse', state ? 'collapsed' : 'expanded')
|
||||
}
|
||||
catch (e) {
|
||||
// localStorage may be disabled, continue without persisting state
|
||||
}
|
||||
}
|
||||
}, [appId, setSidebarCollapseState])
|
||||
useEffect(() => {
|
||||
if (appId) {
|
||||
const localState = localStorage.getItem('webappSidebarCollapse')
|
||||
setSidebarCollapseState(localState === 'collapsed')
|
||||
}
|
||||
}, [appId])
|
||||
const [conversationIdInfo, setConversationIdInfo] = useLocalStorageState<Record<string, Record<string, string>>>(CONVERSATION_ID_INFO, {
|
||||
defaultValue: {},
|
||||
})
|
||||
|
||||
@@ -47,6 +47,11 @@ const ChatWithHistory: FC<ChatWithHistoryProps> = ({
|
||||
themeBuilder?.buildTheme(site?.chat_color_theme, site?.chat_color_theme_inverted)
|
||||
}, [site, customConfig, themeBuilder])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isSidebarCollapsed)
|
||||
setShowSidePanel(false)
|
||||
}, [isSidebarCollapsed])
|
||||
|
||||
useDocumentTitle(site?.title || 'Chat')
|
||||
|
||||
return (
|
||||
@@ -76,7 +81,7 @@ const ChatWithHistory: FC<ChatWithHistoryProps> = ({
|
||||
onMouseEnter={() => setShowSidePanel(true)}
|
||||
onMouseLeave={() => setShowSidePanel(false)}
|
||||
>
|
||||
<Sidebar isPanel />
|
||||
<Sidebar isPanel panelVisible={showSidePanel} />
|
||||
</div>
|
||||
)}
|
||||
<div className={cn('flex h-full flex-col overflow-hidden border-[0,5px] border-components-panel-border-subtle bg-chatbot-bg', isMobile ? 'rounded-t-2xl' : 'rounded-2xl')}>
|
||||
|
||||
@@ -23,9 +23,10 @@ import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
|
||||
type Props = {
|
||||
isPanel?: boolean
|
||||
panelVisible?: boolean
|
||||
}
|
||||
|
||||
const Sidebar = ({ isPanel }: Props) => {
|
||||
const Sidebar = ({ isPanel, panelVisible }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
isInstalledApp,
|
||||
@@ -138,7 +139,12 @@ const Sidebar = ({ isPanel }: Props) => {
|
||||
)}
|
||||
</div>
|
||||
<div className='flex shrink-0 items-center justify-between p-3'>
|
||||
<MenuDropdown hideLogout={isInstalledApp} placement='top-start' data={appData?.site} />
|
||||
<MenuDropdown
|
||||
hideLogout={isInstalledApp}
|
||||
placement='top-start'
|
||||
data={appData?.site}
|
||||
forceClose={isPanel && !panelVisible}
|
||||
/>
|
||||
{/* powered by */}
|
||||
<div className='shrink-0'>
|
||||
{!appData?.custom_config?.remove_webapp_brand && (
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+16
-8
@@ -63,7 +63,7 @@ const Details = ({
|
||||
>
|
||||
<RiCloseLine className='size-4 text-text-tertiary' />
|
||||
</button>
|
||||
<div className='flex items-center gap-x-3 pb-2 pl-4 pr-12 pt-6'>
|
||||
<div className='flex items-start gap-x-3 pb-2 pl-4 pr-12 pt-6'>
|
||||
<AppIcon
|
||||
size='large'
|
||||
iconType={appIcon.type as AppIconType}
|
||||
@@ -72,15 +72,23 @@ const Details = ({
|
||||
imageUrl={appIcon.type === 'image' ? appIcon.url : undefined}
|
||||
showEditIcon
|
||||
/>
|
||||
<div className='flex grow flex-col gap-y-1 py-px'>
|
||||
<div className='system-md-semibold text-text-secondary'>
|
||||
<div className='flex grow flex-col gap-y-1 overflow-hidden py-px'>
|
||||
<div
|
||||
className='system-md-semibold truncate text-text-secondary'
|
||||
title={pipelineTemplateInfo.name}
|
||||
>
|
||||
{pipelineTemplateInfo.name}
|
||||
</div>
|
||||
<div className='system-2xs-medium-uppercase text-text-tertiary'>
|
||||
{t('datasetPipeline.details.createdBy', {
|
||||
author: pipelineTemplateInfo.created_by,
|
||||
})}
|
||||
</div>
|
||||
{pipelineTemplateInfo.created_by && (
|
||||
<div
|
||||
className='system-2xs-medium-uppercase truncate text-text-tertiary'
|
||||
title={pipelineTemplateInfo.created_by}
|
||||
>
|
||||
{t('datasetPipeline.details.createdBy', {
|
||||
author: pipelineTemplateInfo.created_by,
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<p className='system-sm-regular px-4 pb-2 pt-1 text-text-secondary'>
|
||||
|
||||
+2
-3
@@ -9,8 +9,7 @@ import Button from '@/app/components/base/button'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import type { PipelineTemplate } from '@/models/pipeline'
|
||||
import { PipelineTemplateListQueryKeyPrefix, useUpdateTemplateInfo } from '@/service/use-pipeline'
|
||||
import { useInvalid } from '@/service/use-base'
|
||||
import { useInvalidCustomizedTemplateList, useUpdateTemplateInfo } from '@/service/use-pipeline'
|
||||
|
||||
type EditPipelineInfoProps = {
|
||||
onClose: () => void
|
||||
@@ -63,7 +62,7 @@ const EditPipelineInfo = ({
|
||||
}, [])
|
||||
|
||||
const { mutateAsync: updatePipeline } = useUpdateTemplateInfo()
|
||||
const invalidCustomizedTemplateList = useInvalid([...PipelineTemplateListQueryKeyPrefix, 'customized'])
|
||||
const invalidCustomizedTemplateList = useInvalidCustomizedTemplateList()
|
||||
|
||||
const handleSave = useCallback(async () => {
|
||||
if (!name) {
|
||||
|
||||
@@ -5,9 +5,9 @@ import EditPipelineInfo from './edit-pipeline-info'
|
||||
import type { PipelineTemplate } from '@/models/pipeline'
|
||||
import Confirm from '@/app/components/base/confirm'
|
||||
import {
|
||||
PipelineTemplateListQueryKeyPrefix,
|
||||
useDeleteTemplate,
|
||||
useExportTemplateDSL,
|
||||
useInvalidCustomizedTemplateList,
|
||||
usePipelineTemplateById,
|
||||
} from '@/service/use-pipeline'
|
||||
import { downloadFile } from '@/utils/format'
|
||||
@@ -18,7 +18,6 @@ import Details from './details'
|
||||
import Content from './content'
|
||||
import Actions from './actions'
|
||||
import { useCreatePipelineDatasetFromCustomized } from '@/service/knowledge/use-create-dataset'
|
||||
import { useInvalid } from '@/service/use-base'
|
||||
import { useInvalidDatasetList } from '@/service/knowledge/use-dataset'
|
||||
|
||||
type TemplateCardProps = {
|
||||
@@ -128,7 +127,7 @@ const TemplateCard = ({
|
||||
}, [])
|
||||
|
||||
const { mutateAsync: deletePipeline } = useDeleteTemplate()
|
||||
const invalidCustomizedTemplateList = useInvalid([...PipelineTemplateListQueryKeyPrefix, 'customized'])
|
||||
const invalidCustomizedTemplateList = useInvalidCustomizedTemplateList()
|
||||
|
||||
const onConfirmDelete = useCallback(async () => {
|
||||
await deletePipeline(pipeline.id, {
|
||||
|
||||
@@ -167,10 +167,39 @@ import { appAction } from './app'
|
||||
import { knowledgeAction } from './knowledge'
|
||||
import { pluginAction } from './plugin'
|
||||
import { workflowNodesAction } from './workflow-nodes'
|
||||
import { ragPipelineNodesAction } from './rag-pipeline-nodes'
|
||||
import type { ActionItem, SearchResult } from './types'
|
||||
import { slashAction } from './commands'
|
||||
import { slashCommandRegistry } from './commands/registry'
|
||||
|
||||
// Create dynamic Actions based on context
|
||||
export const createActions = (isWorkflowPage: boolean, isRagPipelinePage: boolean) => {
|
||||
const baseActions = {
|
||||
slash: slashAction,
|
||||
app: appAction,
|
||||
knowledge: knowledgeAction,
|
||||
plugin: pluginAction,
|
||||
}
|
||||
|
||||
// Add appropriate node search based on context
|
||||
if (isRagPipelinePage) {
|
||||
return {
|
||||
...baseActions,
|
||||
node: ragPipelineNodesAction,
|
||||
}
|
||||
}
|
||||
else if (isWorkflowPage) {
|
||||
return {
|
||||
...baseActions,
|
||||
node: workflowNodesAction,
|
||||
}
|
||||
}
|
||||
|
||||
// Default actions without node search
|
||||
return baseActions
|
||||
}
|
||||
|
||||
// Legacy export for backward compatibility
|
||||
export const Actions = {
|
||||
slash: slashAction,
|
||||
app: appAction,
|
||||
@@ -183,6 +212,7 @@ export const searchAnything = async (
|
||||
locale: string,
|
||||
query: string,
|
||||
actionItem?: ActionItem,
|
||||
dynamicActions?: Record<string, ActionItem>,
|
||||
): Promise<SearchResult[]> => {
|
||||
if (actionItem) {
|
||||
const searchTerm = query.replace(actionItem.key, '').replace(actionItem.shortcut, '').trim()
|
||||
@@ -198,7 +228,7 @@ export const searchAnything = async (
|
||||
if (query.startsWith('@') || query.startsWith('/'))
|
||||
return []
|
||||
|
||||
const globalSearchActions = Object.values(Actions)
|
||||
const globalSearchActions = Object.values(dynamicActions || Actions)
|
||||
|
||||
// Use Promise.allSettled to handle partial failures gracefully
|
||||
const searchPromises = globalSearchActions.map(async (action) => {
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { ActionItem } from './types'
|
||||
|
||||
// Create the RAG pipeline nodes action
|
||||
export const ragPipelineNodesAction: ActionItem = {
|
||||
key: '@node',
|
||||
shortcut: '@node',
|
||||
title: 'Search RAG Pipeline Nodes',
|
||||
description: 'Find and jump to nodes in the current RAG pipeline by name or type',
|
||||
searchFn: undefined, // Will be set by useRagPipelineSearch hook
|
||||
search: async (_, searchTerm = '', _locale) => {
|
||||
try {
|
||||
// Use the searchFn if available (set by useRagPipelineSearch hook)
|
||||
if (ragPipelineNodesAction.searchFn)
|
||||
return ragPipelineNodesAction.searchFn(searchTerm)
|
||||
|
||||
// If not in RAG pipeline context, return empty array
|
||||
return []
|
||||
}
|
||||
catch (error) {
|
||||
console.warn('RAG pipeline nodes search failed:', error)
|
||||
return []
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -7,7 +7,7 @@ export const workflowNodesAction: ActionItem = {
|
||||
title: 'Search Workflow Nodes',
|
||||
description: 'Find and jump to nodes in the current workflow by name or type',
|
||||
searchFn: undefined, // Will be set by useWorkflowSearch hook
|
||||
search: async (_, searchTerm = '', locale) => {
|
||||
search: async (_, searchTerm = '', _locale) => {
|
||||
try {
|
||||
// Use the searchFn if available (set by useWorkflowSearch hook)
|
||||
if (workflowNodesAction.searchFn)
|
||||
|
||||
@@ -12,11 +12,16 @@ type GotoAnythingContextType = {
|
||||
* Whether the current page is a workflow page
|
||||
*/
|
||||
isWorkflowPage: boolean
|
||||
/**
|
||||
* Whether the current page is a RAG pipeline page
|
||||
*/
|
||||
isRagPipelinePage: boolean
|
||||
}
|
||||
|
||||
// Create context with default values
|
||||
const GotoAnythingContext = createContext<GotoAnythingContextType>({
|
||||
isWorkflowPage: false,
|
||||
isRagPipelinePage: false,
|
||||
})
|
||||
|
||||
/**
|
||||
@@ -33,17 +38,28 @@ type GotoAnythingProviderProps = {
|
||||
*/
|
||||
export const GotoAnythingProvider: React.FC<GotoAnythingProviderProps> = ({ children }) => {
|
||||
const [isWorkflowPage, setIsWorkflowPage] = useState(false)
|
||||
const [isRagPipelinePage, setIsRagPipelinePage] = useState(false)
|
||||
const pathname = usePathname()
|
||||
|
||||
// Update context based on current pathname
|
||||
// Update context based on current pathname using more robust route matching
|
||||
useEffect(() => {
|
||||
// Check if current path contains workflow
|
||||
const isWorkflow = pathname?.includes('/workflow') || false
|
||||
if (!pathname) {
|
||||
setIsWorkflowPage(false)
|
||||
setIsRagPipelinePage(false)
|
||||
return
|
||||
}
|
||||
|
||||
// Workflow pages: /app/[appId]/workflow or /workflow/[token] (shared)
|
||||
const isWorkflow = /^\/app\/[^/]+\/workflow$/.test(pathname) || /^\/workflow\/[^/]+$/.test(pathname)
|
||||
// RAG Pipeline pages: /datasets/[datasetId]/pipeline
|
||||
const isRagPipeline = /^\/datasets\/[^/]+\/pipeline$/.test(pathname)
|
||||
|
||||
setIsWorkflowPage(isWorkflow)
|
||||
setIsRagPipelinePage(isRagPipeline)
|
||||
}, [pathname])
|
||||
|
||||
return (
|
||||
<GotoAnythingContext.Provider value={{ isWorkflowPage }}>
|
||||
<GotoAnythingContext.Provider value={{ isWorkflowPage, isRagPipelinePage }}>
|
||||
{children}
|
||||
</GotoAnythingContext.Provider>
|
||||
)
|
||||
|
||||
@@ -9,7 +9,7 @@ import { useDebounce, useKeyPress } from 'ahooks'
|
||||
import { getKeyboardKeyCodeBySystem, isEventTargetInputArea, isMac } from '@/app/components/workflow/utils/common'
|
||||
import { selectWorkflowNode } from '@/app/components/workflow/utils/node-navigation'
|
||||
import { RiSearchLine } from '@remixicon/react'
|
||||
import { Actions as AllActions, type SearchResult, matchAction, searchAnything } from './actions'
|
||||
import { type SearchResult, createActions, matchAction, searchAnything } from './actions'
|
||||
import { GotoAnythingProvider, useGotoAnythingContext } from './context'
|
||||
import { slashCommandRegistry } from './actions/commands/registry'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
@@ -29,7 +29,7 @@ const GotoAnything: FC<Props> = ({
|
||||
}) => {
|
||||
const router = useRouter()
|
||||
const defaultLocale = useGetLanguage()
|
||||
const { isWorkflowPage } = useGotoAnythingContext()
|
||||
const { isWorkflowPage, isRagPipelinePage } = useGotoAnythingContext()
|
||||
const { t } = useTranslation()
|
||||
const [show, setShow] = useState<boolean>(false)
|
||||
const [searchQuery, setSearchQuery] = useState<string>('')
|
||||
@@ -38,16 +38,9 @@ const GotoAnything: FC<Props> = ({
|
||||
|
||||
// Filter actions based on context
|
||||
const Actions = useMemo(() => {
|
||||
// Create a filtered copy of actions based on current page context
|
||||
if (isWorkflowPage) {
|
||||
// Include all actions on workflow pages
|
||||
return AllActions
|
||||
}
|
||||
else {
|
||||
const { app, knowledge, plugin, slash } = AllActions
|
||||
return { app, knowledge, plugin, slash }
|
||||
}
|
||||
}, [isWorkflowPage])
|
||||
// Create actions based on current page context
|
||||
return createActions(isWorkflowPage, isRagPipelinePage)
|
||||
}, [isWorkflowPage, isRagPipelinePage])
|
||||
|
||||
const [activePlugin, setActivePlugin] = useState<Plugin>()
|
||||
|
||||
@@ -99,9 +92,11 @@ const GotoAnything: FC<Props> = ({
|
||||
|
||||
const query = searchQueryDebouncedValue.toLowerCase()
|
||||
const action = matchAction(query, Actions)
|
||||
return action
|
||||
? (action.key === '/' ? '@command' : action.key)
|
||||
: 'general'
|
||||
|
||||
if (!action)
|
||||
return 'general'
|
||||
|
||||
return action.key === '/' ? '@command' : action.key
|
||||
}, [searchQueryDebouncedValue, Actions, isCommandsMode, searchQuery])
|
||||
|
||||
const { data: searchResults = [], isLoading, isError, error } = useQuery(
|
||||
@@ -112,13 +107,14 @@ const GotoAnything: FC<Props> = ({
|
||||
searchQueryDebouncedValue,
|
||||
searchMode,
|
||||
isWorkflowPage,
|
||||
isRagPipelinePage,
|
||||
defaultLocale,
|
||||
Object.keys(Actions).sort().join(','),
|
||||
],
|
||||
queryFn: async () => {
|
||||
const query = searchQueryDebouncedValue.toLowerCase()
|
||||
const action = matchAction(query, Actions)
|
||||
return await searchAnything(defaultLocale, query, action)
|
||||
return await searchAnything(defaultLocale, query, action, Actions)
|
||||
},
|
||||
enabled: !!searchQueryDebouncedValue && !isCommandsMode,
|
||||
staleTime: 30000,
|
||||
@@ -321,7 +317,7 @@ const GotoAnything: FC<Props> = ({
|
||||
autoFocus
|
||||
/>
|
||||
{searchMode !== 'general' && (
|
||||
<div className='flex items-center gap-1 rounded bg-blue-50 px-2 py-[2px] text-xs font-medium text-blue-600 dark:bg-blue-900/40 dark:text-blue-300'>
|
||||
<div className='flex items-center gap-1 rounded bg-gray-100 px-2 py-[2px] text-xs font-medium text-gray-700 dark:bg-gray-800 dark:text-gray-300'>
|
||||
<span>{(() => {
|
||||
if (searchMode === 'scopes')
|
||||
return 'SCOPES'
|
||||
@@ -446,18 +442,20 @@ const GotoAnything: FC<Props> = ({
|
||||
) : (
|
||||
<>
|
||||
<span className='opacity-60'>
|
||||
{isCommandsMode
|
||||
? t('app.gotoAnything.selectToNavigate')
|
||||
: searchQuery.trim()
|
||||
? t('app.gotoAnything.searching')
|
||||
: t('app.gotoAnything.startTyping')
|
||||
}
|
||||
{(() => {
|
||||
if (isCommandsMode)
|
||||
return t('app.gotoAnything.selectToNavigate')
|
||||
|
||||
if (searchQuery.trim())
|
||||
return t('app.gotoAnything.searching')
|
||||
|
||||
return t('app.gotoAnything.startTyping')
|
||||
})()}
|
||||
</span>
|
||||
<span className='opacity-60'>
|
||||
{searchQuery.trim() || isCommandsMode
|
||||
? t('app.gotoAnything.tips')
|
||||
: t('app.gotoAnything.pressEscToClose')
|
||||
}
|
||||
: t('app.gotoAnything.pressEscToClose')}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
|
||||
+2
-21
@@ -29,7 +29,7 @@ import {
|
||||
import { fetchModelParameterRules } from '@/service/common'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { TONE_LIST } from '@/config'
|
||||
import { PROVIDER_WITH_PRESET_TONE, STOP_PARAMETER_RULE, TONE_LIST } from '@/config'
|
||||
import { ArrowNarrowLeft } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
|
||||
export type ModelParameterModalProps = {
|
||||
@@ -50,26 +50,7 @@ export type ModelParameterModalProps = {
|
||||
isInWorkflow?: boolean
|
||||
scope?: string
|
||||
}
|
||||
const stopParameterRule: ModelParameterRule = {
|
||||
default: [],
|
||||
help: {
|
||||
en_US: 'Up to four sequences where the API will stop generating further tokens. The returned text will not contain the stop sequence.',
|
||||
zh_Hans: '最多四个序列,API 将停止生成更多的 token。返回的文本将不包含停止序列。',
|
||||
},
|
||||
label: {
|
||||
en_US: 'Stop sequences',
|
||||
zh_Hans: '停止序列',
|
||||
},
|
||||
name: 'stop',
|
||||
required: false,
|
||||
type: 'tag',
|
||||
tagPlaceholder: {
|
||||
en_US: 'Enter sequence and press Tab',
|
||||
zh_Hans: '输入序列并按 Tab 键',
|
||||
},
|
||||
}
|
||||
|
||||
const PROVIDER_WITH_PRESET_TONE = ['langgenius/openai/openai', 'langgenius/azure_openai/azure_openai']
|
||||
const ModelParameterModal: FC<ModelParameterModalProps> = ({
|
||||
popupClassName,
|
||||
portalToFollowElemContentClassName,
|
||||
@@ -230,7 +211,7 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
|
||||
!isLoading && !!parameterRules.length && (
|
||||
[
|
||||
...parameterRules,
|
||||
...(isAdvancedMode ? [stopParameterRule] : []),
|
||||
...(isAdvancedMode ? [STOP_PARAMETER_RULE] : []),
|
||||
].map(parameter => (
|
||||
<ParameterItem
|
||||
key={`${modelId}-${parameter.name}`}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useInvalidateAllBuiltInTools, useInvalidateAllToolProviders } from '@/s
|
||||
import { useInvalidateStrategyProviders } from '@/service/use-strategy'
|
||||
import type { Plugin, PluginDeclaration, PluginManifestInMarket } from '../../types'
|
||||
import { PluginType } from '../../types'
|
||||
import { useInvalidDataSourceList } from '@/service/use-pipeline'
|
||||
|
||||
const useRefreshPluginList = () => {
|
||||
const invalidateInstalledPluginList = useInvalidateInstalledPluginList()
|
||||
@@ -16,6 +17,7 @@ const useRefreshPluginList = () => {
|
||||
|
||||
const invalidateAllToolProviders = useInvalidateAllToolProviders()
|
||||
const invalidateAllBuiltInTools = useInvalidateAllBuiltInTools()
|
||||
const invalidateAllDataSources = useInvalidDataSourceList()
|
||||
|
||||
const invalidateStrategyProviders = useInvalidateStrategyProviders()
|
||||
return {
|
||||
@@ -30,6 +32,9 @@ const useRefreshPluginList = () => {
|
||||
// TODO: update suggested tools. It's a function in hook useMarketplacePlugins,handleUpdatePlugins
|
||||
}
|
||||
|
||||
if ((manifest && PluginType.datasource.includes(manifest.category)) || refreshAllType)
|
||||
invalidateAllDataSources()
|
||||
|
||||
// model select
|
||||
if ((manifest && PluginType.model.includes(manifest.category)) || refreshAllType) {
|
||||
refreshModelProviders()
|
||||
|
||||
@@ -136,6 +136,7 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
|
||||
provider,
|
||||
model,
|
||||
value?.completion_params,
|
||||
isAdvancedMode,
|
||||
)
|
||||
nextCompletionParams = filtered
|
||||
|
||||
|
||||
@@ -10,29 +10,9 @@ import type {
|
||||
} from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import type { ParameterValue } from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal/parameter-item'
|
||||
import { fetchModelParameterRules } from '@/service/common'
|
||||
import { TONE_LIST } from '@/config'
|
||||
import { PROVIDER_WITH_PRESET_TONE, STOP_PARAMETER_RULE, TONE_LIST } from '@/config'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
const PROVIDER_WITH_PRESET_TONE = ['langgenius/openai/openai', 'langgenius/azure_openai/azure_openai']
|
||||
const stopParameterRule: ModelParameterRule = {
|
||||
default: [],
|
||||
help: {
|
||||
en_US: 'Up to four sequences where the API will stop generating further tokens. The returned text will not contain the stop sequence.',
|
||||
zh_Hans: '最多四个序列,API 将停止生成更多的 token。返回的文本将不包含停止序列。',
|
||||
},
|
||||
label: {
|
||||
en_US: 'Stop sequences',
|
||||
zh_Hans: '停止序列',
|
||||
},
|
||||
name: 'stop',
|
||||
required: false,
|
||||
type: 'tag',
|
||||
tagPlaceholder: {
|
||||
en_US: 'Enter sequence and press Tab',
|
||||
zh_Hans: '输入序列并按 Tab 键',
|
||||
},
|
||||
}
|
||||
|
||||
type Props = {
|
||||
isAdvancedMode: boolean
|
||||
provider: string
|
||||
@@ -108,7 +88,7 @@ const LLMParamsPanel = ({
|
||||
{!!parameterRules.length && (
|
||||
[
|
||||
...parameterRules,
|
||||
...(isAdvancedMode ? [stopParameterRule] : []),
|
||||
...(isAdvancedMode ? [STOP_PARAMETER_RULE] : []),
|
||||
].map(parameter => (
|
||||
<ParameterItem
|
||||
key={`${modelId}-${parameter.name}`}
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
} from '@/app/components/workflow/hooks'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import PublishToast from './publish-toast'
|
||||
import { useRagPipelineSearch } from '../hooks/use-rag-pipeline-search'
|
||||
|
||||
const RagPipelineChildren = () => {
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
@@ -30,6 +31,9 @@ const RagPipelineChildren = () => {
|
||||
handleExportDSL,
|
||||
} = useDSL()
|
||||
|
||||
// Initialize RAG pipeline search functionality
|
||||
useRagPipelineSearch()
|
||||
|
||||
eventEmitter?.useSubscription((v: any) => {
|
||||
if (v.type === DSL_EXPORT_CHECK)
|
||||
setSecretEnvList(v.payload.data as EnvironmentVariable[])
|
||||
|
||||
@@ -33,6 +33,7 @@ import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
|
||||
import { useInvalid } from '@/service/use-base'
|
||||
import {
|
||||
publishedPipelineInfoQueryKeyPrefix,
|
||||
useInvalidCustomizedTemplateList,
|
||||
usePublishAsCustomizedPipeline,
|
||||
} from '@/service/use-pipeline'
|
||||
import Confirm from '@/app/components/base/confirm'
|
||||
@@ -158,6 +159,8 @@ const Popup = () => {
|
||||
push(`/datasets/${datasetId}/documents/create-from-pipeline`)
|
||||
}, [datasetId, push])
|
||||
|
||||
const invalidCustomizedTemplateList = useInvalidCustomizedTemplateList()
|
||||
|
||||
const handlePublishAsKnowledgePipeline = useCallback(async (
|
||||
name: string,
|
||||
icon: IconInfo,
|
||||
@@ -189,6 +192,7 @@ const Popup = () => {
|
||||
</div>
|
||||
),
|
||||
})
|
||||
invalidCustomizedTemplateList()
|
||||
}
|
||||
catch {
|
||||
notify({ type: 'error', message: t('datasetPipeline.publishTemplate.error.message') })
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useEffect, useMemo } from 'react'
|
||||
import { useNodes } from 'reactflow'
|
||||
import { useNodesInteractions } from '@/app/components/workflow/hooks/use-nodes-interactions'
|
||||
import type { CommonNodeType } from '@/app/components/workflow/types'
|
||||
import { ragPipelineNodesAction } from '@/app/components/goto-anything/actions/rag-pipeline-nodes'
|
||||
import BlockIcon from '@/app/components/workflow/block-icon'
|
||||
import { setupNodeSelectionListener } from '@/app/components/workflow/utils/node-navigation'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import type { LLMNodeType } from '@/app/components/workflow/nodes/llm/types'
|
||||
import type { ToolNodeType } from '@/app/components/workflow/nodes/tool/types'
|
||||
import type { KnowledgeRetrievalNodeType } from '@/app/components/workflow/nodes/knowledge-retrieval/types'
|
||||
import { useGetToolIcon } from '@/app/components/workflow/hooks/use-tool-icon'
|
||||
|
||||
/**
|
||||
* Hook to register RAG pipeline nodes search functionality
|
||||
*/
|
||||
export const useRagPipelineSearch = () => {
|
||||
const nodes = useNodes()
|
||||
const { handleNodeSelect } = useNodesInteractions()
|
||||
const getToolIcon = useGetToolIcon()
|
||||
|
||||
// Process nodes to create searchable data structure
|
||||
const searchableNodes = useMemo(() => {
|
||||
return nodes.map((node) => {
|
||||
const nodeData = node.data as CommonNodeType
|
||||
const title = nodeData.title || nodeData.type || 'Untitled Node'
|
||||
let desc = nodeData.desc || ''
|
||||
|
||||
// Keep the original node title for consistency with workflow display
|
||||
// Only enhance description for better search context
|
||||
if (nodeData.type === BlockEnum.Tool) {
|
||||
const toolData = nodeData as ToolNodeType
|
||||
desc = toolData.tool_description || toolData.tool_label || desc
|
||||
}
|
||||
|
||||
if (nodeData.type === BlockEnum.LLM) {
|
||||
const llmData = nodeData as LLMNodeType
|
||||
if (llmData.model?.provider && llmData.model?.name)
|
||||
desc = `${llmData.model.name} (${llmData.model.provider}) - ${llmData.model.mode || desc}`
|
||||
}
|
||||
|
||||
if (nodeData.type === BlockEnum.KnowledgeRetrieval) {
|
||||
const knowledgeData = nodeData as KnowledgeRetrievalNodeType
|
||||
if (knowledgeData.dataset_ids?.length)
|
||||
desc = `Knowledge Retrieval with ${knowledgeData.dataset_ids.length} datasets - ${desc}`
|
||||
}
|
||||
|
||||
return {
|
||||
id: node.id,
|
||||
title,
|
||||
desc,
|
||||
type: nodeData.type,
|
||||
blockType: nodeData.type,
|
||||
nodeData,
|
||||
toolIcon: getToolIcon(nodeData),
|
||||
modelInfo: nodeData.type === BlockEnum.LLM ? {
|
||||
provider: (nodeData as LLMNodeType).model?.provider,
|
||||
name: (nodeData as LLMNodeType).model?.name,
|
||||
mode: (nodeData as LLMNodeType).model?.mode,
|
||||
} : {
|
||||
provider: undefined,
|
||||
name: undefined,
|
||||
mode: undefined,
|
||||
},
|
||||
}
|
||||
})
|
||||
}, [nodes, getToolIcon])
|
||||
|
||||
// Calculate relevance score for search results
|
||||
const calculateScore = useCallback((node: {
|
||||
title: string;
|
||||
type: string;
|
||||
desc: string;
|
||||
modelInfo: { provider?: string; name?: string; mode?: string }
|
||||
}, searchTerm: string): number => {
|
||||
if (!searchTerm) return 1
|
||||
|
||||
let score = 0
|
||||
const term = searchTerm.toLowerCase()
|
||||
|
||||
// Title match (highest priority)
|
||||
if (node.title.toLowerCase().includes(term))
|
||||
score += 10
|
||||
|
||||
// Type match
|
||||
if (node.type.toLowerCase().includes(term))
|
||||
score += 8
|
||||
|
||||
// Description match
|
||||
if (node.desc.toLowerCase().includes(term))
|
||||
score += 5
|
||||
|
||||
// Model info matches (for LLM nodes)
|
||||
if (node.modelInfo.provider?.toLowerCase().includes(term))
|
||||
score += 6
|
||||
if (node.modelInfo.name?.toLowerCase().includes(term))
|
||||
score += 6
|
||||
if (node.modelInfo.mode?.toLowerCase().includes(term))
|
||||
score += 4
|
||||
|
||||
return score
|
||||
}, [])
|
||||
|
||||
// Create search function for RAG pipeline nodes
|
||||
const searchRagPipelineNodes = useCallback((query: string) => {
|
||||
if (!searchableNodes.length) return []
|
||||
|
||||
const searchTerm = query.toLowerCase().trim()
|
||||
|
||||
const results = searchableNodes
|
||||
.map((node) => {
|
||||
const score = calculateScore(node, searchTerm)
|
||||
|
||||
return score > 0 ? {
|
||||
id: node.id,
|
||||
title: node.title,
|
||||
description: node.desc || node.type,
|
||||
type: 'workflow-node' as const,
|
||||
path: `#${node.id}`,
|
||||
icon: (
|
||||
<BlockIcon
|
||||
type={node.blockType}
|
||||
className="shrink-0"
|
||||
size="sm"
|
||||
toolIcon={node.toolIcon}
|
||||
/>
|
||||
),
|
||||
metadata: {
|
||||
nodeId: node.id,
|
||||
nodeData: node.nodeData,
|
||||
},
|
||||
data: node.nodeData,
|
||||
score,
|
||||
} : null
|
||||
})
|
||||
.filter((node): node is NonNullable<typeof node> => node !== null)
|
||||
.sort((a, b) => {
|
||||
// If no search term, sort alphabetically
|
||||
if (!searchTerm) return a.title.localeCompare(b.title)
|
||||
// Sort by relevance score (higher score first)
|
||||
return (b.score || 0) - (a.score || 0)
|
||||
})
|
||||
|
||||
return results
|
||||
}, [searchableNodes, calculateScore])
|
||||
|
||||
// Directly set the search function on the action object
|
||||
useEffect(() => {
|
||||
if (searchableNodes.length > 0) {
|
||||
// Set the search function directly on the action
|
||||
ragPipelineNodesAction.searchFn = searchRagPipelineNodes
|
||||
}
|
||||
|
||||
return () => {
|
||||
// Clean up when component unmounts
|
||||
ragPipelineNodesAction.searchFn = undefined
|
||||
}
|
||||
}, [searchableNodes, searchRagPipelineNodes])
|
||||
|
||||
// Set up node selection event listener using the utility function
|
||||
useEffect(() => {
|
||||
return setupNodeSelectionListener(handleNodeSelect)
|
||||
}, [handleNodeSelect])
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback, useRef, useState } from 'react'
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { Placement } from '@floating-ui/react'
|
||||
import {
|
||||
@@ -25,12 +25,14 @@ type Props = {
|
||||
data?: SiteInfo
|
||||
placement?: Placement
|
||||
hideLogout?: boolean
|
||||
forceClose?: boolean
|
||||
}
|
||||
|
||||
const MenuDropdown: FC<Props> = ({
|
||||
data,
|
||||
placement,
|
||||
hideLogout,
|
||||
forceClose,
|
||||
}) => {
|
||||
const webAppAccessMode = useWebAppStore(s => s.webAppAccessMode)
|
||||
const router = useRouter()
|
||||
@@ -55,6 +57,11 @@ const MenuDropdown: FC<Props> = ({
|
||||
|
||||
const [show, setShow] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (forceClose)
|
||||
setOpen(false)
|
||||
}, [forceClose, setOpen])
|
||||
|
||||
return (
|
||||
<>
|
||||
<PortalToFollowElem
|
||||
|
||||
@@ -38,7 +38,7 @@ const Header = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
className='absolute left-0 top-0 z-10 flex h-14 w-full items-center justify-between bg-mask-top2bottom-gray-50-to-transparent px-3'
|
||||
className='absolute left-0 top-7 z-10 flex h-0 w-full items-center justify-between bg-mask-top2bottom-gray-50-to-transparent px-3'
|
||||
>
|
||||
{(inWorkflowCanvas || isPipelineCanvas) && maximizeCanvas && <div className='h-14 w-[52px]' />}
|
||||
{
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ import type { AnyObj } from './match-schema-type'
|
||||
import matchTheSchemaType from './match-schema-type'
|
||||
|
||||
export const getMatchedSchemaType = (obj: AnyObj, schemaTypeDefinitions?: SchemaTypeDefinition[]): string => {
|
||||
if(!schemaTypeDefinitions) return ''
|
||||
if(!schemaTypeDefinitions || obj === undefined || obj === null) return ''
|
||||
const matched = schemaTypeDefinitions.find(def => matchTheSchemaType(obj, def.schema))
|
||||
return matched ? matched.name : ''
|
||||
}
|
||||
|
||||
@@ -78,6 +78,7 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
|
||||
model.provider,
|
||||
model.modelId,
|
||||
inputs.model.completion_params,
|
||||
true,
|
||||
)
|
||||
const keys = Object.keys(removedDetails)
|
||||
if (keys.length)
|
||||
|
||||
@@ -4,6 +4,7 @@ import { PromptRole } from '@/models/debug'
|
||||
import { PipelineInputVarType } from '@/models/pipeline'
|
||||
import { DatasetAttr } from '@/types/feature'
|
||||
import pkg from '../package.json'
|
||||
import type { ModelParameterRule } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
|
||||
const getBooleanConfig = (
|
||||
envVar: string | undefined,
|
||||
@@ -403,3 +404,24 @@ export const ZENDESK_FIELD_IDS = {
|
||||
export const APP_VERSION = pkg.version
|
||||
|
||||
export const RAG_PIPELINE_PREVIEW_CHUNK_NUM = 20
|
||||
|
||||
export const PROVIDER_WITH_PRESET_TONE = ['langgenius/openai/openai', 'langgenius/azure_openai/azure_openai']
|
||||
|
||||
export const STOP_PARAMETER_RULE: ModelParameterRule = {
|
||||
default: [],
|
||||
help: {
|
||||
en_US: 'Up to four sequences where the API will stop generating further tokens. The returned text will not contain the stop sequence.',
|
||||
zh_Hans: '最多四个序列,API 将停止生成更多的 token。返回的文本将不包含停止序列。',
|
||||
},
|
||||
label: {
|
||||
en_US: 'Stop sequences',
|
||||
zh_Hans: '停止序列',
|
||||
},
|
||||
name: 'stop',
|
||||
required: false,
|
||||
type: 'tag',
|
||||
tagPlaceholder: {
|
||||
en_US: 'Enter sequence and press Tab',
|
||||
zh_Hans: '输入序列并按 Tab 键',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -500,6 +500,7 @@ const translation = {
|
||||
editModelCredential: 'Bearbeiten von Modellanmeldeinformationen',
|
||||
customModelCredentialsDeleteTip: 'Anmeldeinformationen werden verwendet und können nicht gelöscht werden',
|
||||
},
|
||||
parametersInvalidRemoved: 'Einige Parameter sind ungültig und wurden entfernt.',
|
||||
},
|
||||
dataSource: {
|
||||
add: 'Eine Datenquelle hinzufügen',
|
||||
|
||||
@@ -527,6 +527,7 @@ const translation = {
|
||||
selectModelCredential: 'Select a model credential',
|
||||
customModelCredentialsDeleteTip: 'Credential is in use and cannot be deleted',
|
||||
},
|
||||
parametersInvalidRemoved: 'Some parameters are invalid and have been removed',
|
||||
},
|
||||
dataSource: {
|
||||
add: 'Add a data source',
|
||||
|
||||
@@ -504,6 +504,7 @@ const translation = {
|
||||
customModelCredentialsDeleteTip: 'La credencial está en uso y no se puede eliminar',
|
||||
editModelCredential: 'Editar credencial de modelo',
|
||||
},
|
||||
parametersInvalidRemoved: 'Algunos parámetros son inválidos y han sido eliminados',
|
||||
},
|
||||
dataSource: {
|
||||
add: 'Agregar una fuente de datos',
|
||||
|
||||
@@ -504,6 +504,7 @@ const translation = {
|
||||
customModelCredentials: 'اعتبار مدل سفارشی',
|
||||
customModelCredentialsDeleteTip: 'اعتبار در حال استفاده است و قابل حذف نیست',
|
||||
},
|
||||
parametersInvalidRemoved: 'برخی پارامترها نامعتبر هستند و حذف شدهاند',
|
||||
},
|
||||
dataSource: {
|
||||
add: 'افزودن منبع داده',
|
||||
|
||||
@@ -501,6 +501,7 @@ const translation = {
|
||||
removeModel: 'Supprimer le modèle',
|
||||
editModelCredential: 'Modifier les informations d’identification du modèle',
|
||||
},
|
||||
parametersInvalidRemoved: 'Certains paramètres sont invalides et ont été supprimés.',
|
||||
},
|
||||
dataSource: {
|
||||
add: 'Ajouter une source de données',
|
||||
|
||||
@@ -520,6 +520,7 @@ const translation = {
|
||||
customModelCredentials: 'कस्टम मॉडल क्रेडेंशियल्स',
|
||||
editModelCredential: 'मॉडल की क्रेडेंशियल संपादित करें',
|
||||
},
|
||||
parametersInvalidRemoved: 'कुछ पैरामीटर अमान्य हैं और हटा दिए गए हैं',
|
||||
},
|
||||
dataSource: {
|
||||
add: 'डेटा स्रोत जोड़ें',
|
||||
|
||||
@@ -500,6 +500,7 @@ const translation = {
|
||||
installProvider: 'Menginstal penyedia model',
|
||||
callTimes: 'Waktu panggilan',
|
||||
getFreeTokens: 'Dapatkan Token gratis',
|
||||
parametersInvalidRemoved: 'Beberapa parameter tidak valid dan telah dihapus',
|
||||
},
|
||||
dataSource: {
|
||||
notion: {
|
||||
|
||||
@@ -526,6 +526,7 @@ const translation = {
|
||||
removeModel: 'Rimuovi modello',
|
||||
editModelCredential: 'Modificare le credenziali del modello',
|
||||
},
|
||||
parametersInvalidRemoved: 'Alcuni parametri non sono validi e sono stati rimossi.',
|
||||
},
|
||||
dataSource: {
|
||||
add: 'Aggiungi una fonte di dati',
|
||||
|
||||
@@ -518,6 +518,7 @@ const translation = {
|
||||
customModelCredentials: 'カスタムモデルの認証情報',
|
||||
selectModelCredential: 'モデルの資格情報を選択する',
|
||||
},
|
||||
parametersInvalidRemoved: 'いくつかのパラメータが無効であり、削除されました。',
|
||||
},
|
||||
dataSource: {
|
||||
add: 'データソースの追加',
|
||||
|
||||
@@ -496,6 +496,7 @@ const translation = {
|
||||
customModelCredentials: '사용자 지정 모델 자격 증명',
|
||||
customModelCredentialsDeleteTip: '자격 증명이 사용 중이며 삭제할 수 없습니다.',
|
||||
},
|
||||
parametersInvalidRemoved: '일부 매개변수가 유효하지 않아 제거되었습니다.',
|
||||
},
|
||||
dataSource: {
|
||||
add: '데이터 소스 추가하기',
|
||||
|
||||
@@ -513,6 +513,7 @@ const translation = {
|
||||
selectModelCredential: 'Wybieranie poświadczeń modelu',
|
||||
editModelCredential: 'Edytowanie poświadczeń modelu',
|
||||
},
|
||||
parametersInvalidRemoved: 'Niektóre parametry są nieprawidłowe i zostały usunięte.',
|
||||
},
|
||||
dataSource: {
|
||||
add: 'Dodaj źródło danych',
|
||||
|
||||
@@ -500,6 +500,7 @@ const translation = {
|
||||
customModelCredentialsDeleteTip: 'A credencial está em uso e não pode ser excluída',
|
||||
addNewModelCredential: 'Adicionar nova credencial de modelo',
|
||||
},
|
||||
parametersInvalidRemoved: 'Alguns parâmetros são inválidos e foram removidos',
|
||||
},
|
||||
dataSource: {
|
||||
add: 'Adicionar uma fonte de dados',
|
||||
|
||||
@@ -500,6 +500,7 @@ const translation = {
|
||||
manageCredentials: 'Gestionați acreditările',
|
||||
customModelCredentialsDeleteTip: 'Acreditarea este în uz și nu poate fi ștearsă',
|
||||
},
|
||||
parametersInvalidRemoved: 'Unele parametrii sunt invalizi și au fost eliminați.',
|
||||
},
|
||||
dataSource: {
|
||||
add: 'Adăugați o sursă de date',
|
||||
|
||||
@@ -504,6 +504,7 @@ const translation = {
|
||||
manageCredentials: 'Управление учетными данными',
|
||||
customModelCredentialsDeleteTip: 'Учетные данные используются и не могут быть удалены',
|
||||
},
|
||||
parametersInvalidRemoved: 'Некоторые параметры недействительны и были удалены',
|
||||
},
|
||||
dataSource: {
|
||||
add: 'Добавить источник данных',
|
||||
|
||||
@@ -585,6 +585,7 @@ const translation = {
|
||||
customModelCredentialsDeleteTip: 'Poverilnice so v uporabi in jih ni mogoče izbrisati',
|
||||
customModelCredentials: 'Poverilnice modela po meri',
|
||||
},
|
||||
parametersInvalidRemoved: 'Nekateri parametri so neveljavni in so bili odstranjeni.',
|
||||
},
|
||||
dataSource: {
|
||||
notion: {
|
||||
|
||||
@@ -499,6 +499,7 @@ const translation = {
|
||||
customModelCredentials: 'ข้อมูลประจําตัวของโมเดลแบบกําหนดเอง',
|
||||
addNewModelCredential: 'เพิ่มข้อมูลประจําตัวของโมเดลใหม่',
|
||||
},
|
||||
parametersInvalidRemoved: 'บางพารามิเตอร์ไม่ถูกต้องและถูกนำออก',
|
||||
},
|
||||
dataSource: {
|
||||
add: 'เพิ่มแหล่งข้อมูล',
|
||||
|
||||
@@ -504,6 +504,7 @@ const translation = {
|
||||
addNewModelCredential: 'Yeni model kimlik bilgisi ekleme',
|
||||
customModelCredentialsDeleteTip: 'Kimlik bilgisi kullanımda ve silinemiyor',
|
||||
},
|
||||
parametersInvalidRemoved: 'Bazı parametreler geçersizdir ve kaldırılmıştır.',
|
||||
},
|
||||
dataSource: {
|
||||
add: 'Bir veri kaynağı ekle',
|
||||
|
||||
@@ -501,6 +501,7 @@ const translation = {
|
||||
editModelCredential: 'Редагувати облікові дані моделі',
|
||||
customModelCredentialsDeleteTip: 'Облікові дані використовуються і не можуть бути видалені',
|
||||
},
|
||||
parametersInvalidRemoved: 'Деякі параметри є недійсними і були видалені',
|
||||
},
|
||||
dataSource: {
|
||||
add: 'Додати джерело даних',
|
||||
|
||||
@@ -500,6 +500,7 @@ const translation = {
|
||||
addNewModelCredential: 'Thêm thông tin xác thực mô hình mới',
|
||||
selectModelCredential: 'Chọn thông tin xác thực mô hình',
|
||||
},
|
||||
parametersInvalidRemoved: 'Một số tham số không hợp lệ và đã được loại bỏ',
|
||||
},
|
||||
dataSource: {
|
||||
add: 'Thêm nguồn dữ liệu',
|
||||
|
||||
@@ -521,6 +521,7 @@ const translation = {
|
||||
selectModelCredential: '选择模型凭据',
|
||||
customModelCredentialsDeleteTip: '模型凭据正在使用中,无法删除',
|
||||
},
|
||||
parametersInvalidRemoved: '部分参数无效,已移除',
|
||||
},
|
||||
dataSource: {
|
||||
add: '添加数据源',
|
||||
|
||||
@@ -500,6 +500,7 @@ const translation = {
|
||||
addNewModelCredential: '新增模型認證',
|
||||
selectModelCredential: '選取模型認證',
|
||||
},
|
||||
parametersInvalidRemoved: '一些參數無效,已被移除',
|
||||
},
|
||||
dataSource: {
|
||||
add: '新增資料來源',
|
||||
|
||||
@@ -48,6 +48,10 @@ export const usePipelineTemplateList = (params: PipelineTemplateListParams) => {
|
||||
})
|
||||
}
|
||||
|
||||
export const useInvalidCustomizedTemplateList = () => {
|
||||
return useInvalid([...PipelineTemplateListQueryKeyPrefix, 'customized'])
|
||||
}
|
||||
|
||||
export const usePipelineTemplateById = (params: PipelineTemplateByIdRequest, enabled: boolean) => {
|
||||
const { template_id, type } = params
|
||||
return useQuery<PipelineTemplateByIdResponse>({
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { FormValue, ModelParameterRule } from '@/app/components/header/acco
|
||||
export const mergeValidCompletionParams = (
|
||||
oldParams: FormValue | undefined,
|
||||
rules: ModelParameterRule[],
|
||||
isAdvancedMode: boolean = false,
|
||||
): { params: FormValue; removedDetails: Record<string, string> } => {
|
||||
if (!oldParams || Object.keys(oldParams).length === 0)
|
||||
return { params: {}, removedDetails: {} }
|
||||
@@ -16,6 +17,11 @@ export const mergeValidCompletionParams = (
|
||||
const removedDetails: Record<string, string> = {}
|
||||
|
||||
Object.entries(oldParams).forEach(([key, value]) => {
|
||||
if (key === 'stop' && isAdvancedMode) {
|
||||
// keep stop in advanced mode
|
||||
nextParams[key] = value
|
||||
return
|
||||
}
|
||||
const rule = ruleMap[key]
|
||||
if (!rule) {
|
||||
removedDetails[key] = 'unsupported'
|
||||
@@ -74,9 +80,10 @@ export const fetchAndMergeValidCompletionParams = async (
|
||||
provider: string,
|
||||
modelId: string,
|
||||
oldParams: FormValue | undefined,
|
||||
isAdvancedMode: boolean = false,
|
||||
): Promise<{ params: FormValue; removedDetails: Record<string, string> }> => {
|
||||
const { fetchModelParameterRules } = await import('@/service/common')
|
||||
const url = `/workspaces/current/model-providers/${provider}/models/parameter-rules?model=${modelId}`
|
||||
const { data: parameterRules } = await fetchModelParameterRules(url)
|
||||
return mergeValidCompletionParams(oldParams, parameterRules ?? [])
|
||||
return mergeValidCompletionParams(oldParams, parameterRules ?? [], isAdvancedMode)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user