mirror of
https://github.com/langgenius/dify.git
synced 2026-04-25 09:36:40 +08:00
Merge remote-tracking branch 'origin/main' into feat/trigger
This commit is contained in:
commit
1bfa8e6662
@ -1,9 +1,11 @@
|
|||||||
|
import logging
|
||||||
import queue
|
import queue
|
||||||
import time
|
import time
|
||||||
from abc import abstractmethod
|
from abc import abstractmethod
|
||||||
from enum import IntEnum, auto
|
from enum import IntEnum, auto
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from redis.exceptions import RedisError
|
||||||
from sqlalchemy.orm import DeclarativeMeta
|
from sqlalchemy.orm import DeclarativeMeta
|
||||||
|
|
||||||
from configs import dify_config
|
from configs import dify_config
|
||||||
@ -18,6 +20,8 @@ from core.app.entities.queue_entities import (
|
|||||||
)
|
)
|
||||||
from extensions.ext_redis import redis_client
|
from extensions.ext_redis import redis_client
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class PublishFrom(IntEnum):
|
class PublishFrom(IntEnum):
|
||||||
APPLICATION_MANAGER = auto()
|
APPLICATION_MANAGER = auto()
|
||||||
@ -35,9 +39,8 @@ class AppQueueManager:
|
|||||||
self.invoke_from = invoke_from # Public accessor for invoke_from
|
self.invoke_from = invoke_from # Public accessor for invoke_from
|
||||||
|
|
||||||
user_prefix = "account" if self._invoke_from in {InvokeFrom.EXPLORE, InvokeFrom.DEBUGGER} else "end-user"
|
user_prefix = "account" if self._invoke_from in {InvokeFrom.EXPLORE, InvokeFrom.DEBUGGER} else "end-user"
|
||||||
redis_client.setex(
|
self._task_belong_cache_key = AppQueueManager._generate_task_belong_cache_key(self._task_id)
|
||||||
AppQueueManager._generate_task_belong_cache_key(self._task_id), 1800, f"{user_prefix}-{self._user_id}"
|
redis_client.setex(self._task_belong_cache_key, 1800, f"{user_prefix}-{self._user_id}")
|
||||||
)
|
|
||||||
|
|
||||||
q: queue.Queue[WorkflowQueueMessage | MessageQueueMessage | None] = queue.Queue()
|
q: queue.Queue[WorkflowQueueMessage | MessageQueueMessage | None] = queue.Queue()
|
||||||
|
|
||||||
@ -79,9 +82,21 @@ class AppQueueManager:
|
|||||||
Stop listen to queue
|
Stop listen to queue
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
|
self._clear_task_belong_cache()
|
||||||
self._q.put(None)
|
self._q.put(None)
|
||||||
|
|
||||||
def publish_error(self, e, pub_from: PublishFrom):
|
def _clear_task_belong_cache(self) -> None:
|
||||||
|
"""
|
||||||
|
Remove the task belong cache key once listening is finished.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
redis_client.delete(self._task_belong_cache_key)
|
||||||
|
except RedisError:
|
||||||
|
logger.exception(
|
||||||
|
"Failed to clear task belong cache for task %s (key: %s)", self._task_id, self._task_belong_cache_key
|
||||||
|
)
|
||||||
|
|
||||||
|
def publish_error(self, e, pub_from: PublishFrom) -> None:
|
||||||
"""
|
"""
|
||||||
Publish error
|
Publish error
|
||||||
:param e: error
|
:param e: error
|
||||||
|
|||||||
@ -74,7 +74,7 @@ class TextPromptMessageContent(PromptMessageContent):
|
|||||||
Model class for text prompt message content.
|
Model class for text prompt message content.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
type: Literal[PromptMessageContentType.TEXT] = PromptMessageContentType.TEXT
|
type: Literal[PromptMessageContentType.TEXT] = PromptMessageContentType.TEXT # type: ignore
|
||||||
data: str
|
data: str
|
||||||
|
|
||||||
|
|
||||||
@ -95,11 +95,11 @@ class MultiModalPromptMessageContent(PromptMessageContent):
|
|||||||
|
|
||||||
|
|
||||||
class VideoPromptMessageContent(MultiModalPromptMessageContent):
|
class VideoPromptMessageContent(MultiModalPromptMessageContent):
|
||||||
type: Literal[PromptMessageContentType.VIDEO] = PromptMessageContentType.VIDEO
|
type: Literal[PromptMessageContentType.VIDEO] = PromptMessageContentType.VIDEO # type: ignore
|
||||||
|
|
||||||
|
|
||||||
class AudioPromptMessageContent(MultiModalPromptMessageContent):
|
class AudioPromptMessageContent(MultiModalPromptMessageContent):
|
||||||
type: Literal[PromptMessageContentType.AUDIO] = PromptMessageContentType.AUDIO
|
type: Literal[PromptMessageContentType.AUDIO] = PromptMessageContentType.AUDIO # type: ignore
|
||||||
|
|
||||||
|
|
||||||
class ImagePromptMessageContent(MultiModalPromptMessageContent):
|
class ImagePromptMessageContent(MultiModalPromptMessageContent):
|
||||||
@ -111,12 +111,12 @@ class ImagePromptMessageContent(MultiModalPromptMessageContent):
|
|||||||
LOW = auto()
|
LOW = auto()
|
||||||
HIGH = auto()
|
HIGH = auto()
|
||||||
|
|
||||||
type: Literal[PromptMessageContentType.IMAGE] = PromptMessageContentType.IMAGE
|
type: Literal[PromptMessageContentType.IMAGE] = PromptMessageContentType.IMAGE # type: ignore
|
||||||
detail: DETAIL = DETAIL.LOW
|
detail: DETAIL = DETAIL.LOW
|
||||||
|
|
||||||
|
|
||||||
class DocumentPromptMessageContent(MultiModalPromptMessageContent):
|
class DocumentPromptMessageContent(MultiModalPromptMessageContent):
|
||||||
type: Literal[PromptMessageContentType.DOCUMENT] = PromptMessageContentType.DOCUMENT
|
type: Literal[PromptMessageContentType.DOCUMENT] = PromptMessageContentType.DOCUMENT # type: ignore
|
||||||
|
|
||||||
|
|
||||||
PromptMessageContentUnionTypes = Annotated[
|
PromptMessageContentUnionTypes = Annotated[
|
||||||
|
|||||||
@ -15,7 +15,7 @@ class GPT2Tokenizer:
|
|||||||
use gpt2 tokenizer to get num tokens
|
use gpt2 tokenizer to get num tokens
|
||||||
"""
|
"""
|
||||||
_tokenizer = GPT2Tokenizer.get_encoder()
|
_tokenizer = GPT2Tokenizer.get_encoder()
|
||||||
tokens = _tokenizer.encode(text)
|
tokens = _tokenizer.encode(text) # type: ignore
|
||||||
return len(tokens)
|
return len(tokens)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
@ -196,15 +196,15 @@ def jsonable_encoder(
|
|||||||
return encoder(obj)
|
return encoder(obj)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
data = dict(obj)
|
data = dict(obj) # type: ignore
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
errors: list[Exception] = []
|
errors: list[Exception] = []
|
||||||
errors.append(e)
|
errors.append(e)
|
||||||
try:
|
try:
|
||||||
data = vars(obj)
|
data = vars(obj) # type: ignore
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
errors.append(e)
|
errors.append(e)
|
||||||
raise ValueError(errors) from e
|
raise ValueError(str(errors)) from e
|
||||||
return jsonable_encoder(
|
return jsonable_encoder(
|
||||||
data,
|
data,
|
||||||
by_alias=by_alias,
|
by_alias=by_alias,
|
||||||
|
|||||||
@ -3,7 +3,8 @@ from dataclasses import dataclass
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from opentelemetry import trace as trace_api
|
from opentelemetry import trace as trace_api
|
||||||
from opentelemetry.sdk.trace import Event, Status, StatusCode
|
from opentelemetry.sdk.trace import Event
|
||||||
|
from opentelemetry.trace import Status, StatusCode
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -155,7 +155,10 @@ class OpsTraceManager:
|
|||||||
if key in tracing_config:
|
if key in tracing_config:
|
||||||
if "*" in tracing_config[key]:
|
if "*" in tracing_config[key]:
|
||||||
# If the key contains '*', retain the original value from the current config
|
# If the key contains '*', retain the original value from the current config
|
||||||
new_config[key] = current_trace_config.get(key, tracing_config[key])
|
if current_trace_config:
|
||||||
|
new_config[key] = current_trace_config.get(key, tracing_config[key])
|
||||||
|
else:
|
||||||
|
new_config[key] = tracing_config[key]
|
||||||
else:
|
else:
|
||||||
# Otherwise, encrypt the key
|
# Otherwise, encrypt the key
|
||||||
new_config[key] = encrypt_token(tenant_id, tracing_config[key])
|
new_config[key] = encrypt_token(tenant_id, tracing_config[key])
|
||||||
|
|||||||
@ -62,7 +62,8 @@ class WeaveDataTrace(BaseTraceInstance):
|
|||||||
self,
|
self,
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
project_url = f"https://wandb.ai/{self.weave_client._project_id()}"
|
project_identifier = f"{self.entity}/{self.project_name}" if self.entity else self.project_name
|
||||||
|
project_url = f"https://wandb.ai/{project_identifier}"
|
||||||
return project_url
|
return project_url
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug("Weave get run url failed: %s", str(e))
|
logger.debug("Weave get run url failed: %s", str(e))
|
||||||
@ -424,7 +425,23 @@ class WeaveDataTrace(BaseTraceInstance):
|
|||||||
raise ValueError(f"Weave API check failed: {str(e)}")
|
raise ValueError(f"Weave API check failed: {str(e)}")
|
||||||
|
|
||||||
def start_call(self, run_data: WeaveTraceModel, parent_run_id: str | None = None):
|
def start_call(self, run_data: WeaveTraceModel, parent_run_id: str | None = None):
|
||||||
call = self.weave_client.create_call(op=run_data.op, inputs=run_data.inputs, attributes=run_data.attributes)
|
inputs = run_data.inputs
|
||||||
|
if inputs is None:
|
||||||
|
inputs = {}
|
||||||
|
elif not isinstance(inputs, dict):
|
||||||
|
inputs = {"inputs": str(inputs)}
|
||||||
|
|
||||||
|
attributes = run_data.attributes
|
||||||
|
if attributes is None:
|
||||||
|
attributes = {}
|
||||||
|
elif not isinstance(attributes, dict):
|
||||||
|
attributes = {"attributes": str(attributes)}
|
||||||
|
|
||||||
|
call = self.weave_client.create_call(
|
||||||
|
op=run_data.op,
|
||||||
|
inputs=inputs,
|
||||||
|
attributes=attributes,
|
||||||
|
)
|
||||||
self.calls[run_data.id] = call
|
self.calls[run_data.id] = call
|
||||||
if parent_run_id:
|
if parent_run_id:
|
||||||
self.calls[run_data.id].parent_id = parent_run_id
|
self.calls[run_data.id].parent_id = parent_run_id
|
||||||
@ -432,6 +449,7 @@ class WeaveDataTrace(BaseTraceInstance):
|
|||||||
def finish_call(self, run_data: WeaveTraceModel):
|
def finish_call(self, run_data: WeaveTraceModel):
|
||||||
call = self.calls.get(run_data.id)
|
call = self.calls.get(run_data.id)
|
||||||
if call:
|
if call:
|
||||||
self.weave_client.finish_call(call=call, output=run_data.outputs, exception=run_data.exception)
|
exception = Exception(run_data.exception) if run_data.exception else None
|
||||||
|
self.weave_client.finish_call(call=call, output=run_data.outputs, exception=exception)
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Call with id {run_data.id} not found")
|
raise ValueError(f"Call with id {run_data.id} not found")
|
||||||
|
|||||||
@ -106,7 +106,9 @@ class RetrievalService:
|
|||||||
if exceptions:
|
if exceptions:
|
||||||
raise ValueError(";\n".join(exceptions))
|
raise ValueError(";\n".join(exceptions))
|
||||||
|
|
||||||
|
# Deduplicate documents for hybrid search to avoid duplicate chunks
|
||||||
if retrieval_method == RetrievalMethod.HYBRID_SEARCH.value:
|
if retrieval_method == RetrievalMethod.HYBRID_SEARCH.value:
|
||||||
|
all_documents = cls._deduplicate_documents(all_documents)
|
||||||
data_post_processor = DataPostProcessor(
|
data_post_processor = DataPostProcessor(
|
||||||
str(dataset.tenant_id), reranking_mode, reranking_model, weights, False
|
str(dataset.tenant_id), reranking_mode, reranking_model, weights, False
|
||||||
)
|
)
|
||||||
@ -143,6 +145,40 @@ class RetrievalService:
|
|||||||
)
|
)
|
||||||
return all_documents
|
return all_documents
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _deduplicate_documents(cls, documents: list[Document]) -> list[Document]:
|
||||||
|
"""Deduplicate documents based on doc_id to avoid duplicate chunks in hybrid search."""
|
||||||
|
if not documents:
|
||||||
|
return documents
|
||||||
|
|
||||||
|
unique_documents = []
|
||||||
|
seen_doc_ids = set()
|
||||||
|
|
||||||
|
for document in documents:
|
||||||
|
# For dify provider documents, use doc_id for deduplication
|
||||||
|
if document.provider == "dify" and document.metadata is not None and "doc_id" in document.metadata:
|
||||||
|
doc_id = document.metadata["doc_id"]
|
||||||
|
if doc_id not in seen_doc_ids:
|
||||||
|
seen_doc_ids.add(doc_id)
|
||||||
|
unique_documents.append(document)
|
||||||
|
# If duplicate, keep the one with higher score
|
||||||
|
elif "score" in document.metadata:
|
||||||
|
# Find existing document with same doc_id and compare scores
|
||||||
|
for i, existing_doc in enumerate(unique_documents):
|
||||||
|
if (
|
||||||
|
existing_doc.metadata
|
||||||
|
and existing_doc.metadata.get("doc_id") == doc_id
|
||||||
|
and existing_doc.metadata.get("score", 0) < document.metadata.get("score", 0)
|
||||||
|
):
|
||||||
|
unique_documents[i] = document
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
# For non-dify documents, use content-based deduplication
|
||||||
|
if document not in unique_documents:
|
||||||
|
unique_documents.append(document)
|
||||||
|
|
||||||
|
return unique_documents
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _get_dataset(cls, dataset_id: str) -> Dataset | None:
|
def _get_dataset(cls, dataset_id: str) -> Dataset | None:
|
||||||
with Session(db.engine) as session:
|
with Session(db.engine) as session:
|
||||||
|
|||||||
@ -20,6 +20,7 @@ class ModelInvokeCompletedEvent(NodeEventBase):
|
|||||||
usage: LLMUsage
|
usage: LLMUsage
|
||||||
finish_reason: str | None = None
|
finish_reason: str | None = None
|
||||||
reasoning_content: str | None = None
|
reasoning_content: str | None = None
|
||||||
|
structured_output: dict | None = None
|
||||||
|
|
||||||
|
|
||||||
class RunRetryEvent(NodeEventBase):
|
class RunRetryEvent(NodeEventBase):
|
||||||
|
|||||||
@ -87,7 +87,7 @@ class Executor:
|
|||||||
node_data.authorization.config.api_key
|
node_data.authorization.config.api_key
|
||||||
).text
|
).text
|
||||||
|
|
||||||
self.url: str = node_data.url
|
self.url = node_data.url
|
||||||
self.method = node_data.method
|
self.method = node_data.method
|
||||||
self.auth = node_data.authorization
|
self.auth = node_data.authorization
|
||||||
self.timeout = timeout
|
self.timeout = timeout
|
||||||
@ -349,11 +349,10 @@ class Executor:
|
|||||||
"timeout": (self.timeout.connect, self.timeout.read, self.timeout.write),
|
"timeout": (self.timeout.connect, self.timeout.read, self.timeout.write),
|
||||||
"ssl_verify": self.ssl_verify,
|
"ssl_verify": self.ssl_verify,
|
||||||
"follow_redirects": True,
|
"follow_redirects": True,
|
||||||
"max_retries": self.max_retries,
|
|
||||||
}
|
}
|
||||||
# request_args = {k: v for k, v in request_args.items() if v is not None}
|
# request_args = {k: v for k, v in request_args.items() if v is not None}
|
||||||
try:
|
try:
|
||||||
response: httpx.Response = _METHOD_MAP[method_lc](**request_args)
|
response: httpx.Response = _METHOD_MAP[method_lc](**request_args, max_retries=self.max_retries)
|
||||||
except (ssrf_proxy.MaxRetriesExceededError, httpx.RequestError) as e:
|
except (ssrf_proxy.MaxRetriesExceededError, httpx.RequestError) as e:
|
||||||
raise HttpRequestNodeError(str(e)) from e
|
raise HttpRequestNodeError(str(e)) from e
|
||||||
# FIXME: fix type ignore, this maybe httpx type issue
|
# FIXME: fix type ignore, this maybe httpx type issue
|
||||||
|
|||||||
@ -165,6 +165,8 @@ class HttpRequestNode(Node):
|
|||||||
body_type = typed_node_data.body.type
|
body_type = typed_node_data.body.type
|
||||||
data = typed_node_data.body.data
|
data = typed_node_data.body.data
|
||||||
match body_type:
|
match body_type:
|
||||||
|
case "none":
|
||||||
|
pass
|
||||||
case "binary":
|
case "binary":
|
||||||
if len(data) != 1:
|
if len(data) != 1:
|
||||||
raise RequestBodyError("invalid body data, should have only one item")
|
raise RequestBodyError("invalid body data, should have only one item")
|
||||||
|
|||||||
@ -83,7 +83,7 @@ class IfElseNode(Node):
|
|||||||
else:
|
else:
|
||||||
# TODO: Update database then remove this
|
# TODO: Update database then remove this
|
||||||
# Fallback to old structure if cases are not defined
|
# Fallback to old structure if cases are not defined
|
||||||
input_conditions, group_result, final_result = _should_not_use_old_function( # ty: ignore [deprecated]
|
input_conditions, group_result, final_result = _should_not_use_old_function( # pyright: ignore [reportDeprecated]
|
||||||
condition_processor=condition_processor,
|
condition_processor=condition_processor,
|
||||||
variable_pool=self.graph_runtime_state.variable_pool,
|
variable_pool=self.graph_runtime_state.variable_pool,
|
||||||
conditions=self._node_data.conditions or [],
|
conditions=self._node_data.conditions or [],
|
||||||
|
|||||||
@ -136,6 +136,11 @@ class KnowledgeIndexNode(Node):
|
|||||||
document = db.session.query(Document).filter_by(id=document_id.value).first()
|
document = db.session.query(Document).filter_by(id=document_id.value).first()
|
||||||
if not document:
|
if not document:
|
||||||
raise KnowledgeIndexNodeError(f"Document {document_id.value} not found.")
|
raise KnowledgeIndexNodeError(f"Document {document_id.value} not found.")
|
||||||
|
doc_id_value = document.id
|
||||||
|
ds_id_value = dataset.id
|
||||||
|
dataset_name_value = dataset.name
|
||||||
|
document_name_value = document.name
|
||||||
|
created_at_value = document.created_at
|
||||||
# chunk nodes by chunk size
|
# chunk nodes by chunk size
|
||||||
indexing_start_at = time.perf_counter()
|
indexing_start_at = time.perf_counter()
|
||||||
index_processor = IndexProcessorFactory(dataset.chunk_structure).init_index_processor()
|
index_processor = IndexProcessorFactory(dataset.chunk_structure).init_index_processor()
|
||||||
@ -161,16 +166,16 @@ class KnowledgeIndexNode(Node):
|
|||||||
document.word_count = (
|
document.word_count = (
|
||||||
db.session.query(func.sum(DocumentSegment.word_count))
|
db.session.query(func.sum(DocumentSegment.word_count))
|
||||||
.where(
|
.where(
|
||||||
DocumentSegment.document_id == document.id,
|
DocumentSegment.document_id == doc_id_value,
|
||||||
DocumentSegment.dataset_id == dataset.id,
|
DocumentSegment.dataset_id == ds_id_value,
|
||||||
)
|
)
|
||||||
.scalar()
|
.scalar()
|
||||||
)
|
)
|
||||||
db.session.add(document)
|
db.session.add(document)
|
||||||
# update document segment status
|
# update document segment status
|
||||||
db.session.query(DocumentSegment).where(
|
db.session.query(DocumentSegment).where(
|
||||||
DocumentSegment.document_id == document.id,
|
DocumentSegment.document_id == doc_id_value,
|
||||||
DocumentSegment.dataset_id == dataset.id,
|
DocumentSegment.dataset_id == ds_id_value,
|
||||||
).update(
|
).update(
|
||||||
{
|
{
|
||||||
DocumentSegment.status: "completed",
|
DocumentSegment.status: "completed",
|
||||||
@ -182,13 +187,13 @@ class KnowledgeIndexNode(Node):
|
|||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"dataset_id": dataset.id,
|
"dataset_id": ds_id_value,
|
||||||
"dataset_name": dataset.name,
|
"dataset_name": dataset_name_value,
|
||||||
"batch": batch.value,
|
"batch": batch.value,
|
||||||
"document_id": document.id,
|
"document_id": doc_id_value,
|
||||||
"document_name": document.name,
|
"document_name": document_name_value,
|
||||||
"created_at": document.created_at.timestamp(),
|
"created_at": created_at_value.timestamp(),
|
||||||
"display_status": document.indexing_status,
|
"display_status": "completed",
|
||||||
}
|
}
|
||||||
|
|
||||||
def _get_preview_output(self, chunk_structure: str, chunks: Any) -> Mapping[str, Any]:
|
def _get_preview_output(self, chunk_structure: str, chunks: Any) -> Mapping[str, Any]:
|
||||||
|
|||||||
@ -107,7 +107,7 @@ class KnowledgeRetrievalNode(Node):
|
|||||||
graph_runtime_state=graph_runtime_state,
|
graph_runtime_state=graph_runtime_state,
|
||||||
)
|
)
|
||||||
# LLM file outputs, used for MultiModal outputs.
|
# LLM file outputs, used for MultiModal outputs.
|
||||||
self._file_outputs: list[File] = []
|
self._file_outputs = []
|
||||||
|
|
||||||
if llm_file_saver is None:
|
if llm_file_saver is None:
|
||||||
llm_file_saver = FileSaverImpl(
|
llm_file_saver = FileSaverImpl(
|
||||||
|
|||||||
@ -161,6 +161,8 @@ class ListOperatorNode(Node):
|
|||||||
elif isinstance(variable, ArrayFileSegment):
|
elif isinstance(variable, ArrayFileSegment):
|
||||||
if isinstance(condition.value, str):
|
if isinstance(condition.value, str):
|
||||||
value = self.graph_runtime_state.variable_pool.convert_template(condition.value).text
|
value = self.graph_runtime_state.variable_pool.convert_template(condition.value).text
|
||||||
|
elif isinstance(condition.value, bool):
|
||||||
|
raise ValueError(f"File filter expects a string value, got {type(condition.value)}")
|
||||||
else:
|
else:
|
||||||
value = condition.value
|
value = condition.value
|
||||||
filter_func = _get_file_filter_func(
|
filter_func = _get_file_filter_func(
|
||||||
|
|||||||
@ -46,7 +46,7 @@ class LLMFileSaver(tp.Protocol):
|
|||||||
dot (`.`). For example, `.py` and `.tar.gz` are both valid values, while `py`
|
dot (`.`). For example, `.py` and `.tar.gz` are both valid values, while `py`
|
||||||
and `tar.gz` are not.
|
and `tar.gz` are not.
|
||||||
"""
|
"""
|
||||||
pass
|
raise NotImplementedError()
|
||||||
|
|
||||||
def save_remote_url(self, url: str, file_type: FileType) -> File:
|
def save_remote_url(self, url: str, file_type: FileType) -> File:
|
||||||
"""save_remote_url saves the file from a remote url returned by LLM.
|
"""save_remote_url saves the file from a remote url returned by LLM.
|
||||||
@ -56,7 +56,7 @@ class LLMFileSaver(tp.Protocol):
|
|||||||
:param url: the url of the file.
|
:param url: the url of the file.
|
||||||
:param file_type: the file type of the file, check `FileType` enum for reference.
|
:param file_type: the file type of the file, check `FileType` enum for reference.
|
||||||
"""
|
"""
|
||||||
pass
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
EngineFactory: tp.TypeAlias = tp.Callable[[], Engine]
|
EngineFactory: tp.TypeAlias = tp.Callable[[], Engine]
|
||||||
|
|||||||
@ -23,6 +23,7 @@ from core.model_runtime.entities.llm_entities import (
|
|||||||
LLMResult,
|
LLMResult,
|
||||||
LLMResultChunk,
|
LLMResultChunk,
|
||||||
LLMResultChunkWithStructuredOutput,
|
LLMResultChunkWithStructuredOutput,
|
||||||
|
LLMResultWithStructuredOutput,
|
||||||
LLMStructuredOutput,
|
LLMStructuredOutput,
|
||||||
LLMUsage,
|
LLMUsage,
|
||||||
)
|
)
|
||||||
@ -127,7 +128,7 @@ class LLMNode(Node):
|
|||||||
graph_runtime_state=graph_runtime_state,
|
graph_runtime_state=graph_runtime_state,
|
||||||
)
|
)
|
||||||
# LLM file outputs, used for MultiModal outputs.
|
# LLM file outputs, used for MultiModal outputs.
|
||||||
self._file_outputs: list[File] = []
|
self._file_outputs = []
|
||||||
|
|
||||||
if llm_file_saver is None:
|
if llm_file_saver is None:
|
||||||
llm_file_saver = FileSaverImpl(
|
llm_file_saver = FileSaverImpl(
|
||||||
@ -165,6 +166,7 @@ class LLMNode(Node):
|
|||||||
node_inputs: dict[str, Any] = {}
|
node_inputs: dict[str, Any] = {}
|
||||||
process_data: dict[str, Any] = {}
|
process_data: dict[str, Any] = {}
|
||||||
result_text = ""
|
result_text = ""
|
||||||
|
clean_text = ""
|
||||||
usage = LLMUsage.empty_usage()
|
usage = LLMUsage.empty_usage()
|
||||||
finish_reason = None
|
finish_reason = None
|
||||||
reasoning_content = None
|
reasoning_content = None
|
||||||
@ -278,6 +280,13 @@ class LLMNode(Node):
|
|||||||
# Extract clean text from <think> tags
|
# Extract clean text from <think> tags
|
||||||
clean_text, _ = LLMNode._split_reasoning(result_text, self._node_data.reasoning_format)
|
clean_text, _ = LLMNode._split_reasoning(result_text, self._node_data.reasoning_format)
|
||||||
|
|
||||||
|
# Process structured output if available from the event.
|
||||||
|
structured_output = (
|
||||||
|
LLMStructuredOutput(structured_output=event.structured_output)
|
||||||
|
if event.structured_output
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
# deduct quota
|
# deduct quota
|
||||||
llm_utils.deduct_llm_quota(tenant_id=self.tenant_id, model_instance=model_instance, usage=usage)
|
llm_utils.deduct_llm_quota(tenant_id=self.tenant_id, model_instance=model_instance, usage=usage)
|
||||||
break
|
break
|
||||||
@ -1048,7 +1057,7 @@ class LLMNode(Node):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def handle_blocking_result(
|
def handle_blocking_result(
|
||||||
*,
|
*,
|
||||||
invoke_result: LLMResult,
|
invoke_result: LLMResult | LLMResultWithStructuredOutput,
|
||||||
saver: LLMFileSaver,
|
saver: LLMFileSaver,
|
||||||
file_outputs: list["File"],
|
file_outputs: list["File"],
|
||||||
reasoning_format: Literal["separated", "tagged"] = "tagged",
|
reasoning_format: Literal["separated", "tagged"] = "tagged",
|
||||||
@ -1079,6 +1088,8 @@ class LLMNode(Node):
|
|||||||
finish_reason=None,
|
finish_reason=None,
|
||||||
# Reasoning content for workflow variables and downstream nodes
|
# Reasoning content for workflow variables and downstream nodes
|
||||||
reasoning_content=reasoning_content,
|
reasoning_content=reasoning_content,
|
||||||
|
# Pass structured output if enabled
|
||||||
|
structured_output=getattr(invoke_result, "structured_output", None),
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
@ -179,6 +179,6 @@ CHAT_EXAMPLE = [
|
|||||||
"required": ["food"],
|
"required": ["food"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"assistant": {"text": "I need to output a valid JSON object.", "json": {"result": "apple pie"}},
|
"assistant": {"text": "I need to output a valid JSON object.", "json": {"food": "apple pie"}},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@ -68,7 +68,7 @@ class QuestionClassifierNode(Node):
|
|||||||
graph_runtime_state=graph_runtime_state,
|
graph_runtime_state=graph_runtime_state,
|
||||||
)
|
)
|
||||||
# LLM file outputs, used for MultiModal outputs.
|
# LLM file outputs, used for MultiModal outputs.
|
||||||
self._file_outputs: list[File] = []
|
self._file_outputs = []
|
||||||
|
|
||||||
if llm_file_saver is None:
|
if llm_file_saver is None:
|
||||||
llm_file_saver = FileSaverImpl(
|
llm_file_saver = FileSaverImpl(
|
||||||
@ -111,9 +111,9 @@ class QuestionClassifierNode(Node):
|
|||||||
query = variable.value if variable else None
|
query = variable.value if variable else None
|
||||||
variables = {"query": query}
|
variables = {"query": query}
|
||||||
# fetch model config
|
# fetch model config
|
||||||
model_instance, model_config = LLMNode._fetch_model_config(
|
model_instance, model_config = llm_utils.fetch_model_config(
|
||||||
node_data_model=node_data.model,
|
|
||||||
tenant_id=self.tenant_id,
|
tenant_id=self.tenant_id,
|
||||||
|
node_data_model=node_data.model,
|
||||||
)
|
)
|
||||||
# fetch memory
|
# fetch memory
|
||||||
memory = llm_utils.fetch_memory(
|
memory = llm_utils.fetch_memory(
|
||||||
|
|||||||
@ -416,4 +416,8 @@ class WorkflowEntry:
|
|||||||
|
|
||||||
# append variable and value to variable pool
|
# append variable and value to variable pool
|
||||||
if variable_node_id != ENVIRONMENT_VARIABLE_NODE_ID:
|
if variable_node_id != ENVIRONMENT_VARIABLE_NODE_ID:
|
||||||
|
# In single run, the input_value is set as the LLM's structured output value within the variable_pool.
|
||||||
|
if len(variable_key_list) == 2 and variable_key_list[0] == "structured_output":
|
||||||
|
input_value = {variable_key_list[1]: input_value}
|
||||||
|
variable_key_list = variable_key_list[0:1]
|
||||||
variable_pool.add([variable_node_id] + variable_key_list, input_value)
|
variable_pool.add([variable_node_id] + variable_key_list, input_value)
|
||||||
|
|||||||
@ -6,9 +6,6 @@
|
|||||||
"migrations/",
|
"migrations/",
|
||||||
"core/rag",
|
"core/rag",
|
||||||
"extensions",
|
"extensions",
|
||||||
"core/ops",
|
|
||||||
"core/model_runtime",
|
|
||||||
"core/workflow/nodes",
|
|
||||||
"core/app/app_config/easy_ui_based_app/dataset"
|
"core/app/app_config/easy_ui_based_app/dataset"
|
||||||
],
|
],
|
||||||
"typeCheckingMode": "strict",
|
"typeCheckingMode": "strict",
|
||||||
@ -37,4 +34,4 @@
|
|||||||
"reportAttributeAccessIssue": "hint",
|
"reportAttributeAccessIssue": "hint",
|
||||||
"pythonVersion": "3.11",
|
"pythonVersion": "3.11",
|
||||||
"pythonPlatform": "All"
|
"pythonPlatform": "All"
|
||||||
}
|
}
|
||||||
@ -52,7 +52,8 @@ def check_upgradable_plugin_task():
|
|||||||
strategy.include_plugins,
|
strategy.include_plugins,
|
||||||
)
|
)
|
||||||
|
|
||||||
if batch_interval_time > 0.0001: # if lower than 1ms, skip
|
# Only sleep if batch_interval_time > 0.0001 AND current batch is not the last one
|
||||||
|
if batch_interval_time > 0.0001 and i + MAX_CONCURRENT_CHECK_TASKS < total_strategies:
|
||||||
time.sleep(batch_interval_time)
|
time.sleep(batch_interval_time)
|
||||||
|
|
||||||
end_at = time.perf_counter()
|
end_at = time.perf_counter()
|
||||||
|
|||||||
@ -93,7 +93,7 @@ logger = logging.getLogger(__name__)
|
|||||||
class DatasetService:
|
class DatasetService:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_datasets(page, per_page, tenant_id=None, user=None, search=None, tag_ids=None, include_all=False):
|
def get_datasets(page, per_page, tenant_id=None, user=None, search=None, tag_ids=None, include_all=False):
|
||||||
query = select(Dataset).where(Dataset.tenant_id == tenant_id).order_by(Dataset.created_at.desc())
|
query = select(Dataset).where(Dataset.tenant_id == tenant_id).order_by(Dataset.created_at.desc(), Dataset.id)
|
||||||
|
|
||||||
if user:
|
if user:
|
||||||
# get permitted dataset ids
|
# get permitted dataset ids
|
||||||
|
|||||||
@ -152,23 +152,20 @@ const Apps = ({
|
|||||||
<div className={cn(
|
<div className={cn(
|
||||||
'mt-6 flex items-center justify-between px-12',
|
'mt-6 flex items-center justify-between px-12',
|
||||||
)}>
|
)}>
|
||||||
<>
|
<Category
|
||||||
<Category
|
list={categories}
|
||||||
list={categories}
|
value={currCategory}
|
||||||
value={currCategory}
|
onChange={setCurrCategory}
|
||||||
onChange={setCurrCategory}
|
allCategoriesEn={allCategoriesEn}
|
||||||
allCategoriesEn={allCategoriesEn}
|
/>
|
||||||
/>
|
|
||||||
</>
|
|
||||||
<Input
|
<Input
|
||||||
showLeftIcon
|
showLeftIcon
|
||||||
showClearIcon
|
showClearIcon
|
||||||
wrapperClassName='w-[200px]'
|
wrapperClassName='w-[200px] self-start'
|
||||||
value={keywords}
|
value={keywords}
|
||||||
onChange={e => handleKeywordsChange(e.target.value)}
|
onChange={e => handleKeywordsChange(e.target.value)}
|
||||||
onClear={() => handleKeywordsChange('')}
|
onClear={() => handleKeywordsChange('')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
|
|||||||
@ -941,9 +941,9 @@ const translation = {
|
|||||||
chunkIsRequired: 'ساختار تکه ای مورد نیاز است',
|
chunkIsRequired: 'ساختار تکه ای مورد نیاز است',
|
||||||
chooseChunkStructure: 'یک ساختار تکه ای را انتخاب کنید',
|
chooseChunkStructure: 'یک ساختار تکه ای را انتخاب کنید',
|
||||||
chunksInput: 'تکهها',
|
chunksInput: 'تکهها',
|
||||||
chunksInputTip: 'متغیر ورودی گره پایگاه دانش چانکها است. نوع متغیر یک شیء با یک طرح JSON خاص است که باید با ساختار چانک انتخاب شده سازگار باشد.',
|
chunksInputTip: 'متغیر ورودی گره پایگاه دانش تکهها است. نوع متغیر یک شیء با یک طرح JSON خاص است که باید با ساختار تکه انتخاب شده سازگار باشد.',
|
||||||
embeddingModelIsRequired: 'مدل جاسازی مورد نیاز است',
|
embeddingModelIsRequired: 'مدل جاسازی مورد نیاز است',
|
||||||
chunksVariableIsRequired: 'متغیر Chunks الزامی است',
|
chunksVariableIsRequired: 'متغیر تکهها الزامی است',
|
||||||
rerankingModelIsRequired: 'مدل رتبهبندی مجدد مورد نیاز است',
|
rerankingModelIsRequired: 'مدل رتبهبندی مجدد مورد نیاز است',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -961,8 +961,8 @@ const translation = {
|
|||||||
aboutRetrieval: 'पुनर्प्राप्ति विधि के बारे में।',
|
aboutRetrieval: 'पुनर्प्राप्ति विधि के बारे में।',
|
||||||
chooseChunkStructure: 'एक चंक संरचना चुनें',
|
chooseChunkStructure: 'एक चंक संरचना चुनें',
|
||||||
chunksInput: 'टुकड़े',
|
chunksInput: 'टुकड़े',
|
||||||
chunksInputTip: 'ज्ञान आधार नोड का इनपुट वेरिएबल चंक्स है। वेरिएबल प्रकार एक ऑब्जेक्ट है जिसमें एक विशेष JSON स्कीमा है जो चयनित चंक संरचना के साथ सुसंगत होना चाहिए।',
|
chunksInputTip: 'ज्ञान आधार नोड का इनपुट वेरिएबल टुकड़े है। वेरिएबल प्रकार एक ऑब्जेक्ट है जिसमें एक विशेष JSON स्कीमा है जो चयनित चंक संरचना के साथ सुसंगत होना चाहिए।',
|
||||||
chunksVariableIsRequired: 'Chunks चर आवश्यक है',
|
chunksVariableIsRequired: 'टुकड़े चर आवश्यक है',
|
||||||
embeddingModelIsRequired: 'एम्बेडिंग मॉडल आवश्यक है',
|
embeddingModelIsRequired: 'एम्बेडिंग मॉडल आवश्यक है',
|
||||||
rerankingModelIsRequired: 'पुनः क्रमांकन मॉडल की आवश्यकता है',
|
rerankingModelIsRequired: 'पुनः क्रमांकन मॉडल की आवश्यकता है',
|
||||||
},
|
},
|
||||||
|
|||||||
@ -950,7 +950,7 @@ const translation = {
|
|||||||
chunksInput: 'Kosi',
|
chunksInput: 'Kosi',
|
||||||
chunksInputTip: 'Vhodna spremenljivka vozlišča podatkovne baze je Chunks. Tip spremenljivke je objekt s specifično JSON shemo, ki mora biti skladna z izbrano strukturo kosov.',
|
chunksInputTip: 'Vhodna spremenljivka vozlišča podatkovne baze je Chunks. Tip spremenljivke je objekt s specifično JSON shemo, ki mora biti skladna z izbrano strukturo kosov.',
|
||||||
chunksVariableIsRequired: 'Spremenljivka Chunks je obvezna',
|
chunksVariableIsRequired: 'Spremenljivka Chunks je obvezna',
|
||||||
embeddingModelIsRequired: 'Zahtuje se vgrajevalni model',
|
embeddingModelIsRequired: 'Zahteva se vgrajevalni model',
|
||||||
rerankingModelIsRequired: 'Potreben je model za ponovno razvrščanje',
|
rerankingModelIsRequired: 'Potreben je model za ponovno razvrščanje',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -944,7 +944,7 @@ const translation = {
|
|||||||
chunksInputTip: 'Вхідна змінна вузла бази знань - це Частини. Тип змінної - об\'єкт з певною JSON-схемою, яка повинна відповідати вибраній структурі частин.',
|
chunksInputTip: 'Вхідна змінна вузла бази знань - це Частини. Тип змінної - об\'єкт з певною JSON-схемою, яка повинна відповідати вибраній структурі частин.',
|
||||||
chunksVariableIsRequired: 'Змінна chunks є обов\'язковою',
|
chunksVariableIsRequired: 'Змінна chunks є обов\'язковою',
|
||||||
embeddingModelIsRequired: 'Потрібна модель вбудовування',
|
embeddingModelIsRequired: 'Потрібна модель вбудовування',
|
||||||
rerankingModelIsRequired: 'Потрібна модель перенавчання',
|
rerankingModelIsRequired: 'Потрібна модель повторного ранжування',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
tracing: {
|
tracing: {
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
"name": "dify-web",
|
"name": "dify-web",
|
||||||
"version": "1.9.1",
|
"version": "1.9.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"packageManager": "pnpm@10.16.0",
|
"packageManager": "pnpm@10.17.1",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=v22.11.0"
|
"node": ">=v22.11.0"
|
||||||
},
|
},
|
||||||
@ -39,13 +39,12 @@
|
|||||||
"storybook": "storybook dev -p 6006",
|
"storybook": "storybook dev -p 6006",
|
||||||
"build-storybook": "storybook build",
|
"build-storybook": "storybook build",
|
||||||
"preinstall": "npx only-allow pnpm",
|
"preinstall": "npx only-allow pnpm",
|
||||||
"analyze": "ANALYZE=true pnpm build"
|
"analyze": "ANALYZE=true pnpm build",
|
||||||
|
"knip": "knip"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.22.3",
|
|
||||||
"@dagrejs/dagre": "^1.1.4",
|
"@dagrejs/dagre": "^1.1.4",
|
||||||
"@emoji-mart/data": "^1.2.1",
|
"@emoji-mart/data": "^1.2.1",
|
||||||
"@eslint/compat": "^1.2.4",
|
|
||||||
"@floating-ui/react": "^0.26.25",
|
"@floating-ui/react": "^0.26.25",
|
||||||
"@formatjs/intl-localematcher": "^0.5.6",
|
"@formatjs/intl-localematcher": "^0.5.6",
|
||||||
"@headlessui/react": "2.2.1",
|
"@headlessui/react": "2.2.1",
|
||||||
@ -63,7 +62,6 @@
|
|||||||
"@octokit/request-error": "^6.1.5",
|
"@octokit/request-error": "^6.1.5",
|
||||||
"@remixicon/react": "^4.5.0",
|
"@remixicon/react": "^4.5.0",
|
||||||
"@sentry/react": "^8.54.0",
|
"@sentry/react": "^8.54.0",
|
||||||
"@sentry/utils": "^8.54.0",
|
|
||||||
"@svgdotjs/svg.js": "^3.2.4",
|
"@svgdotjs/svg.js": "^3.2.4",
|
||||||
"@tailwindcss/typography": "^0.5.15",
|
"@tailwindcss/typography": "^0.5.15",
|
||||||
"@tanstack/react-form": "^1.3.3",
|
"@tanstack/react-form": "^1.3.3",
|
||||||
@ -76,7 +74,6 @@
|
|||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"copy-to-clipboard": "^3.3.3",
|
"copy-to-clipboard": "^3.3.3",
|
||||||
"cron-parser": "^5.4.0",
|
"cron-parser": "^5.4.0",
|
||||||
"crypto-js": "^4.2.0",
|
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"decimal.js": "^10.4.3",
|
"decimal.js": "^10.4.3",
|
||||||
"dompurify": "^3.2.4",
|
"dompurify": "^3.2.4",
|
||||||
@ -92,7 +89,6 @@
|
|||||||
"js-audio-recorder": "^1.0.7",
|
"js-audio-recorder": "^1.0.7",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"jsonschema": "^1.5.0",
|
"jsonschema": "^1.5.0",
|
||||||
"jwt-decode": "^4.0.0",
|
|
||||||
"katex": "^0.16.21",
|
"katex": "^0.16.21",
|
||||||
"ky": "^1.7.2",
|
"ky": "^1.7.2",
|
||||||
"lamejs": "^1.2.1",
|
"lamejs": "^1.2.1",
|
||||||
@ -113,12 +109,9 @@
|
|||||||
"react-18-input-autosize": "^3.0.0",
|
"react-18-input-autosize": "^3.0.0",
|
||||||
"react-dom": "19.1.1",
|
"react-dom": "19.1.1",
|
||||||
"react-easy-crop": "^5.1.0",
|
"react-easy-crop": "^5.1.0",
|
||||||
"react-error-boundary": "^4.1.2",
|
|
||||||
"react-headless-pagination": "^1.1.6",
|
|
||||||
"react-hook-form": "^7.53.1",
|
"react-hook-form": "^7.53.1",
|
||||||
"react-hotkeys-hook": "^4.6.1",
|
"react-hotkeys-hook": "^4.6.1",
|
||||||
"react-i18next": "^15.1.0",
|
"react-i18next": "^15.1.0",
|
||||||
"react-infinite-scroll-component": "^6.1.0",
|
|
||||||
"react-markdown": "^9.0.1",
|
"react-markdown": "^9.0.1",
|
||||||
"react-multi-email": "^1.0.25",
|
"react-multi-email": "^1.0.25",
|
||||||
"react-papaparse": "^4.4.0",
|
"react-papaparse": "^4.4.0",
|
||||||
@ -127,11 +120,8 @@
|
|||||||
"react-sortablejs": "^6.1.4",
|
"react-sortablejs": "^6.1.4",
|
||||||
"react-syntax-highlighter": "^15.6.1",
|
"react-syntax-highlighter": "^15.6.1",
|
||||||
"react-textarea-autosize": "^8.5.8",
|
"react-textarea-autosize": "^8.5.8",
|
||||||
"react-tooltip": "5.8.3",
|
|
||||||
"react-window": "^1.8.10",
|
"react-window": "^1.8.10",
|
||||||
"react-window-infinite-loader": "^1.0.9",
|
|
||||||
"reactflow": "^11.11.3",
|
"reactflow": "^11.11.3",
|
||||||
"recordrtc": "^5.6.2",
|
|
||||||
"rehype-katex": "^7.0.1",
|
"rehype-katex": "^7.0.1",
|
||||||
"rehype-raw": "^7.0.0",
|
"rehype-raw": "^7.0.0",
|
||||||
"remark-breaks": "^4.0.0",
|
"remark-breaks": "^4.0.0",
|
||||||
@ -139,9 +129,7 @@
|
|||||||
"remark-math": "^6.0.0",
|
"remark-math": "^6.0.0",
|
||||||
"scheduler": "^0.26.0",
|
"scheduler": "^0.26.0",
|
||||||
"semver": "^7.6.3",
|
"semver": "^7.6.3",
|
||||||
"server-only": "^0.0.1",
|
|
||||||
"sharp": "^0.33.2",
|
"sharp": "^0.33.2",
|
||||||
"shave": "^5.0.4",
|
|
||||||
"sortablejs": "^1.15.0",
|
"sortablejs": "^1.15.0",
|
||||||
"swr": "^2.3.0",
|
"swr": "^2.3.0",
|
||||||
"tailwind-merge": "^2.5.4",
|
"tailwind-merge": "^2.5.4",
|
||||||
@ -154,13 +142,8 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@antfu/eslint-config": "^5.0.0",
|
"@antfu/eslint-config": "^5.0.0",
|
||||||
"@babel/core": "^7.28.3",
|
|
||||||
"@babel/preset-env": "^7.28.3",
|
|
||||||
"@chromatic-com/storybook": "^3.1.0",
|
"@chromatic-com/storybook": "^3.1.0",
|
||||||
"@eslint-react/eslint-plugin": "^1.15.0",
|
"@eslint-react/eslint-plugin": "^1.15.0",
|
||||||
"@eslint/eslintrc": "^3.1.0",
|
|
||||||
"@eslint/js": "^9.36.0",
|
|
||||||
"@faker-js/faker": "^9.0.3",
|
|
||||||
"@happy-dom/jest-environment": "^17.4.4",
|
"@happy-dom/jest-environment": "^17.4.4",
|
||||||
"@mdx-js/loader": "^3.1.0",
|
"@mdx-js/loader": "^3.1.0",
|
||||||
"@mdx-js/react": "^3.1.0",
|
"@mdx-js/react": "^3.1.0",
|
||||||
@ -173,14 +156,13 @@
|
|||||||
"@storybook/addon-links": "8.5.0",
|
"@storybook/addon-links": "8.5.0",
|
||||||
"@storybook/addon-onboarding": "8.5.0",
|
"@storybook/addon-onboarding": "8.5.0",
|
||||||
"@storybook/addon-themes": "8.5.0",
|
"@storybook/addon-themes": "8.5.0",
|
||||||
"@storybook/blocks": "8.5.0",
|
|
||||||
"@storybook/nextjs": "8.5.0",
|
"@storybook/nextjs": "8.5.0",
|
||||||
"@storybook/react": "8.5.0",
|
"@storybook/react": "8.5.0",
|
||||||
"@storybook/test": "8.5.0",
|
"@storybook/test": "8.5.0",
|
||||||
"@testing-library/dom": "^10.4.0",
|
"@testing-library/dom": "^10.4.0",
|
||||||
"@testing-library/jest-dom": "^6.8.0",
|
"@testing-library/jest-dom": "^6.8.0",
|
||||||
"@testing-library/react": "^16.0.1",
|
"@testing-library/react": "^16.0.1",
|
||||||
"@types/crypto-js": "^4.2.2",
|
"@babel/core": "^7.28.3",
|
||||||
"@types/dagre": "^0.7.52",
|
"@types/dagre": "^0.7.52",
|
||||||
"@types/jest": "^29.5.13",
|
"@types/jest": "^29.5.13",
|
||||||
"@types/js-cookie": "^3.0.6",
|
"@types/js-cookie": "^3.0.6",
|
||||||
@ -193,18 +175,14 @@
|
|||||||
"@types/react-slider": "^1.3.6",
|
"@types/react-slider": "^1.3.6",
|
||||||
"@types/react-syntax-highlighter": "^15.5.13",
|
"@types/react-syntax-highlighter": "^15.5.13",
|
||||||
"@types/react-window": "^1.8.8",
|
"@types/react-window": "^1.8.8",
|
||||||
"@types/react-window-infinite-loader": "^1.0.9",
|
|
||||||
"@types/recordrtc": "^5.6.14",
|
|
||||||
"@types/semver": "^7.5.8",
|
"@types/semver": "^7.5.8",
|
||||||
"@types/sortablejs": "^1.15.1",
|
"@types/sortablejs": "^1.15.1",
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"babel-loader": "^10.0.0",
|
|
||||||
"bing-translate-api": "^4.0.2",
|
"bing-translate-api": "^4.0.2",
|
||||||
"code-inspector-plugin": "1.2.9",
|
"code-inspector-plugin": "1.2.9",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"eslint": "^9.35.0",
|
"eslint": "^9.35.0",
|
||||||
"eslint-config-next": "15.5.0",
|
|
||||||
"eslint-plugin-oxlint": "^1.6.0",
|
"eslint-plugin-oxlint": "^1.6.0",
|
||||||
"eslint-plugin-react-hooks": "^5.1.0",
|
"eslint-plugin-react-hooks": "^5.1.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.19",
|
"eslint-plugin-react-refresh": "^0.4.19",
|
||||||
@ -214,6 +192,7 @@
|
|||||||
"globals": "^15.11.0",
|
"globals": "^15.11.0",
|
||||||
"husky": "^9.1.6",
|
"husky": "^9.1.6",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
|
"knip": "^5.64.1",
|
||||||
"lint-staged": "^15.2.10",
|
"lint-staged": "^15.2.10",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"magicast": "^0.3.4",
|
"magicast": "^0.3.4",
|
||||||
@ -221,10 +200,9 @@
|
|||||||
"sass": "^1.92.1",
|
"sass": "^1.92.1",
|
||||||
"storybook": "8.5.0",
|
"storybook": "8.5.0",
|
||||||
"tailwindcss": "^3.4.14",
|
"tailwindcss": "^3.4.14",
|
||||||
"ts-node": "^10.9.2",
|
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"typescript-eslint": "^8.38.0",
|
"uglify-js": "^3.19.3",
|
||||||
"uglify-js": "^3.19.3"
|
"babel-loader": "^9.2.1"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"@types/react": "19.1.11",
|
"@types/react": "19.1.11",
|
||||||
|
|||||||
1660
web/pnpm-lock.yaml
generated
1660
web/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user