mirror of
https://github.com/langgenius/dify.git
synced 2026-01-18 04:49:57 +00:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a1c6cecf10 | ||
|
|
c5ccf382df | ||
|
|
8358d0abfa | ||
|
|
bad3b14438 | ||
|
|
f42ef494f8 | ||
|
|
bb7f454ecd | ||
|
|
7f48fadd41 | ||
|
|
af2138e8b8 | ||
|
|
091beffae7 | ||
|
|
408fb502a1 | ||
|
|
7660539689 | ||
|
|
5a6061ff61 | ||
|
|
970950e3a8 | ||
|
|
431b2fd4a8 | ||
|
|
88545184be | ||
|
|
2c23caacd4 | ||
|
|
9edea9bc49 | ||
|
|
d43279a1cc | ||
|
|
10848d74a0 | ||
|
|
f9df23a091 | ||
|
|
17a1c05728 | ||
|
|
66782ef19c | ||
|
|
fb7f509e5c | ||
|
|
1a5acf43aa |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -130,7 +130,6 @@ dmypy.json
|
||||
.idea/'
|
||||
|
||||
.DS_Store
|
||||
.vscode
|
||||
|
||||
# Intellij IDEA Files
|
||||
.idea/
|
||||
|
||||
@@ -78,7 +78,7 @@ class Config:
|
||||
self.CONSOLE_URL = get_env('CONSOLE_URL')
|
||||
self.API_URL = get_env('API_URL')
|
||||
self.APP_URL = get_env('APP_URL')
|
||||
self.CURRENT_VERSION = "0.3.0"
|
||||
self.CURRENT_VERSION = "0.3.1"
|
||||
self.COMMIT_SHA = get_env('COMMIT_SHA')
|
||||
self.EDITION = "SELF_HOSTED"
|
||||
self.DEPLOY_ENV = get_env('DEPLOY_ENV')
|
||||
|
||||
@@ -34,5 +34,9 @@ class DatasetIndexToolCallbackHandler(IndexToolCallbackHandler):
|
||||
db.session.query(DocumentSegment).filter(
|
||||
DocumentSegment.dataset_id == self.dataset_id,
|
||||
DocumentSegment.index_node_id == index_node_id
|
||||
).update({DocumentSegment.hit_count: DocumentSegment.hit_count + 1}, synchronize_session=False)
|
||||
).update(
|
||||
{DocumentSegment.hit_count: DocumentSegment.hit_count + 1},
|
||||
synchronize_session=False
|
||||
)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
135
api/core/chain/llm_router_chain.py
Normal file
135
api/core/chain/llm_router_chain.py
Normal file
@@ -0,0 +1,135 @@
|
||||
"""Base classes for LLM-powered router chains."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any, Dict, List, Optional, Type, cast, NamedTuple
|
||||
|
||||
from langchain.chains.base import Chain
|
||||
from pydantic import root_validator
|
||||
|
||||
from langchain.chains import LLMChain
|
||||
from langchain.prompts import BasePromptTemplate
|
||||
from langchain.schema import BaseOutputParser, OutputParserException, BaseLanguageModel
|
||||
|
||||
|
||||
class Route(NamedTuple):
|
||||
destination: Optional[str]
|
||||
next_inputs: Dict[str, Any]
|
||||
|
||||
|
||||
class LLMRouterChain(Chain):
|
||||
"""A router chain that uses an LLM chain to perform routing."""
|
||||
|
||||
llm_chain: LLMChain
|
||||
"""LLM chain used to perform routing"""
|
||||
|
||||
@root_validator()
|
||||
def validate_prompt(cls, values: dict) -> dict:
|
||||
prompt = values["llm_chain"].prompt
|
||||
if prompt.output_parser is None:
|
||||
raise ValueError(
|
||||
"LLMRouterChain requires base llm_chain prompt to have an output"
|
||||
" parser that converts LLM text output to a dictionary with keys"
|
||||
" 'destination' and 'next_inputs'. Received a prompt with no output"
|
||||
" parser."
|
||||
)
|
||||
return values
|
||||
|
||||
@property
|
||||
def input_keys(self) -> List[str]:
|
||||
"""Will be whatever keys the LLM chain prompt expects.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
return self.llm_chain.input_keys
|
||||
|
||||
def _validate_outputs(self, outputs: Dict[str, Any]) -> None:
|
||||
super()._validate_outputs(outputs)
|
||||
if not isinstance(outputs["next_inputs"], dict):
|
||||
raise ValueError
|
||||
|
||||
def _call(
|
||||
self,
|
||||
inputs: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
output = cast(
|
||||
Dict[str, Any],
|
||||
self.llm_chain.predict_and_parse(**inputs),
|
||||
)
|
||||
return output
|
||||
|
||||
@classmethod
|
||||
def from_llm(
|
||||
cls, llm: BaseLanguageModel, prompt: BasePromptTemplate, **kwargs: Any
|
||||
) -> LLMRouterChain:
|
||||
"""Convenience constructor."""
|
||||
llm_chain = LLMChain(llm=llm, prompt=prompt)
|
||||
return cls(llm_chain=llm_chain, **kwargs)
|
||||
|
||||
@property
|
||||
def output_keys(self) -> List[str]:
|
||||
return ["destination", "next_inputs"]
|
||||
|
||||
def route(self, inputs: Dict[str, Any]) -> Route:
|
||||
result = self(inputs)
|
||||
return Route(result["destination"], result["next_inputs"])
|
||||
|
||||
|
||||
class RouterOutputParser(BaseOutputParser[Dict[str, str]]):
|
||||
"""Parser for output of router chain int he multi-prompt chain."""
|
||||
|
||||
default_destination: str = "DEFAULT"
|
||||
next_inputs_type: Type = str
|
||||
next_inputs_inner_key: str = "input"
|
||||
|
||||
def parse_json_markdown(self, json_string: str) -> dict:
|
||||
# Remove the triple backticks if present
|
||||
start_index = json_string.find("```json")
|
||||
end_index = json_string.find("```", start_index + len("```json"))
|
||||
|
||||
if start_index != -1 and end_index != -1:
|
||||
extracted_content = json_string[start_index + len("```json"):end_index].strip()
|
||||
|
||||
# Parse the JSON string into a Python dictionary
|
||||
parsed = json.loads(extracted_content)
|
||||
else:
|
||||
raise Exception("Could not find JSON block in the output.")
|
||||
|
||||
return parsed
|
||||
|
||||
def parse_and_check_json_markdown(self, text: str, expected_keys: List[str]) -> dict:
|
||||
try:
|
||||
json_obj = self.parse_json_markdown(text)
|
||||
except json.JSONDecodeError as e:
|
||||
raise OutputParserException(f"Got invalid JSON object. Error: {e}")
|
||||
for key in expected_keys:
|
||||
if key not in json_obj:
|
||||
raise OutputParserException(
|
||||
f"Got invalid return object. Expected key `{key}` "
|
||||
f"to be present, but got {json_obj}"
|
||||
)
|
||||
return json_obj
|
||||
|
||||
def parse(self, text: str) -> Dict[str, Any]:
|
||||
try:
|
||||
expected_keys = ["destination", "next_inputs"]
|
||||
parsed = self.parse_and_check_json_markdown(text, expected_keys)
|
||||
if not isinstance(parsed["destination"], str):
|
||||
raise ValueError("Expected 'destination' to be a string.")
|
||||
if not isinstance(parsed["next_inputs"], self.next_inputs_type):
|
||||
raise ValueError(
|
||||
f"Expected 'next_inputs' to be {self.next_inputs_type}."
|
||||
)
|
||||
parsed["next_inputs"] = {self.next_inputs_inner_key: parsed["next_inputs"]}
|
||||
if (
|
||||
parsed["destination"].strip().lower()
|
||||
== self.default_destination.lower()
|
||||
):
|
||||
parsed["destination"] = None
|
||||
else:
|
||||
parsed["destination"] = parsed["destination"].strip()
|
||||
return parsed
|
||||
except Exception as e:
|
||||
raise OutputParserException(
|
||||
f"Parsing text\n{text}\n raised following error:\n{e}"
|
||||
)
|
||||
@@ -1,18 +1,18 @@
|
||||
from typing import Optional, List
|
||||
|
||||
from langchain.callbacks import SharedCallbackManager
|
||||
from langchain.callbacks import SharedCallbackManager, CallbackManager
|
||||
from langchain.chains import SequentialChain
|
||||
from langchain.chains.base import Chain
|
||||
from langchain.memory.chat_memory import BaseChatMemory
|
||||
|
||||
from core.agent.agent_builder import AgentBuilder
|
||||
from core.callback_handler.agent_loop_gather_callback_handler import AgentLoopGatherCallbackHandler
|
||||
from core.callback_handler.dataset_tool_callback_handler import DatasetToolCallbackHandler
|
||||
from core.callback_handler.main_chain_gather_callback_handler import MainChainGatherCallbackHandler
|
||||
from core.callback_handler.std_out_callback_handler import DifyStdOutCallbackHandler
|
||||
from core.chain.chain_builder import ChainBuilder
|
||||
from core.constant import llm_constant
|
||||
from core.chain.multi_dataset_router_chain import MultiDatasetRouterChain
|
||||
from core.conversation_message_task import ConversationMessageTask
|
||||
from core.tool.dataset_tool_builder import DatasetToolBuilder
|
||||
from extensions.ext_database import db
|
||||
from models.dataset import Dataset
|
||||
|
||||
|
||||
class MainChainBuilder:
|
||||
@@ -31,8 +31,7 @@ class MainChainBuilder:
|
||||
tenant_id=tenant_id,
|
||||
agent_mode=agent_mode,
|
||||
memory=memory,
|
||||
dataset_tool_callback_handler=DatasetToolCallbackHandler(conversation_message_task),
|
||||
agent_loop_gather_callback_handler=chain_callback_handler.agent_loop_gather_callback_handler
|
||||
conversation_message_task=conversation_message_task
|
||||
)
|
||||
chains += tool_chains
|
||||
|
||||
@@ -59,15 +58,15 @@ class MainChainBuilder:
|
||||
|
||||
@classmethod
|
||||
def get_agent_chains(cls, tenant_id: str, agent_mode: dict, memory: Optional[BaseChatMemory],
|
||||
dataset_tool_callback_handler: DatasetToolCallbackHandler,
|
||||
agent_loop_gather_callback_handler: AgentLoopGatherCallbackHandler):
|
||||
conversation_message_task: ConversationMessageTask):
|
||||
# agent mode
|
||||
chains = []
|
||||
if agent_mode and agent_mode.get('enabled'):
|
||||
tools = agent_mode.get('tools', [])
|
||||
|
||||
pre_fixed_chains = []
|
||||
agent_tools = []
|
||||
# agent_tools = []
|
||||
datasets = []
|
||||
for tool in tools:
|
||||
tool_type = list(tool.keys())[0]
|
||||
tool_config = list(tool.values())[0]
|
||||
@@ -76,34 +75,27 @@ class MainChainBuilder:
|
||||
if chain:
|
||||
pre_fixed_chains.append(chain)
|
||||
elif tool_type == "dataset":
|
||||
dataset_tool = DatasetToolBuilder.build_dataset_tool(
|
||||
tenant_id=tenant_id,
|
||||
dataset_id=tool_config.get("id"),
|
||||
response_mode='no_synthesizer', # "compact"
|
||||
callback_handler=dataset_tool_callback_handler
|
||||
)
|
||||
# get dataset from dataset id
|
||||
dataset = db.session.query(Dataset).filter(
|
||||
Dataset.tenant_id == tenant_id,
|
||||
Dataset.id == tool_config.get("id")
|
||||
).first()
|
||||
|
||||
if dataset_tool:
|
||||
agent_tools.append(dataset_tool)
|
||||
if dataset:
|
||||
datasets.append(dataset)
|
||||
|
||||
# add pre-fixed chains
|
||||
chains += pre_fixed_chains
|
||||
|
||||
if len(agent_tools) == 1:
|
||||
if len(datasets) > 0:
|
||||
# tool to chain
|
||||
tool_chain = ChainBuilder.to_tool_chain(tool=agent_tools[0], output_key='tool_output')
|
||||
chains.append(tool_chain)
|
||||
elif len(agent_tools) > 1:
|
||||
# build agent config
|
||||
agent_chain = AgentBuilder.to_agent_chain(
|
||||
multi_dataset_router_chain = MultiDatasetRouterChain.from_datasets(
|
||||
tenant_id=tenant_id,
|
||||
tools=agent_tools,
|
||||
memory=memory,
|
||||
dataset_tool_callback_handler=dataset_tool_callback_handler,
|
||||
agent_loop_gather_callback_handler=agent_loop_gather_callback_handler
|
||||
datasets=datasets,
|
||||
conversation_message_task=conversation_message_task,
|
||||
callback_manager=CallbackManager([DifyStdOutCallbackHandler()])
|
||||
)
|
||||
|
||||
chains.append(agent_chain)
|
||||
chains.append(multi_dataset_router_chain)
|
||||
|
||||
final_output_key = cls.get_chains_output_key(chains)
|
||||
|
||||
|
||||
140
api/core/chain/multi_dataset_router_chain.py
Normal file
140
api/core/chain/multi_dataset_router_chain.py
Normal file
@@ -0,0 +1,140 @@
|
||||
from typing import Mapping, List, Dict, Any, Optional
|
||||
|
||||
from langchain import LLMChain, PromptTemplate, ConversationChain
|
||||
from langchain.callbacks import CallbackManager
|
||||
from langchain.chains.base import Chain
|
||||
from langchain.schema import BaseLanguageModel
|
||||
from pydantic import Extra
|
||||
|
||||
from core.callback_handler.dataset_tool_callback_handler import DatasetToolCallbackHandler
|
||||
from core.callback_handler.std_out_callback_handler import DifyStdOutCallbackHandler
|
||||
from core.chain.llm_router_chain import LLMRouterChain, RouterOutputParser
|
||||
from core.conversation_message_task import ConversationMessageTask
|
||||
from core.llm.llm_builder import LLMBuilder
|
||||
from core.tool.dataset_tool_builder import DatasetToolBuilder
|
||||
from core.tool.llama_index_tool import EnhanceLlamaIndexTool
|
||||
from models.dataset import Dataset
|
||||
|
||||
MULTI_PROMPT_ROUTER_TEMPLATE = """
|
||||
Given a raw text input to a language model select the model prompt best suited for \
|
||||
the input. You will be given the names of the available prompts and a description of \
|
||||
what the prompt is best suited for. You may also revise the original input if you \
|
||||
think that revising it will ultimately lead to a better response from the language \
|
||||
model.
|
||||
|
||||
<< FORMATTING >>
|
||||
Return a markdown code snippet with a JSON object formatted to look like:
|
||||
```json
|
||||
{{{{
|
||||
"destination": string \\ name of the prompt to use or "DEFAULT"
|
||||
"next_inputs": string \\ a potentially modified version of the original input
|
||||
}}}}
|
||||
```
|
||||
|
||||
REMEMBER: "destination" MUST be one of the candidate prompt names specified below OR \
|
||||
it can be "DEFAULT" if the input is not well suited for any of the candidate prompts.
|
||||
REMEMBER: "next_inputs" can just be the original input if you don't think any \
|
||||
modifications are needed.
|
||||
|
||||
<< CANDIDATE PROMPTS >>
|
||||
{destinations}
|
||||
|
||||
<< INPUT >>
|
||||
{{input}}
|
||||
|
||||
<< OUTPUT >>
|
||||
"""
|
||||
|
||||
|
||||
class MultiDatasetRouterChain(Chain):
|
||||
"""Use a single chain to route an input to one of multiple candidate chains."""
|
||||
|
||||
router_chain: LLMRouterChain
|
||||
"""Chain for deciding a destination chain and the input to it."""
|
||||
dataset_tools: Mapping[str, EnhanceLlamaIndexTool]
|
||||
"""Map of name to candidate chains that inputs can be routed to."""
|
||||
|
||||
class Config:
|
||||
"""Configuration for this pydantic object."""
|
||||
|
||||
extra = Extra.forbid
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
@property
|
||||
def input_keys(self) -> List[str]:
|
||||
"""Will be whatever keys the router chain prompt expects.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
return self.router_chain.input_keys
|
||||
|
||||
@property
|
||||
def output_keys(self) -> List[str]:
|
||||
return ["text"]
|
||||
|
||||
@classmethod
|
||||
def from_datasets(
|
||||
cls,
|
||||
tenant_id: str,
|
||||
datasets: List[Dataset],
|
||||
conversation_message_task: ConversationMessageTask,
|
||||
**kwargs: Any,
|
||||
):
|
||||
"""Convenience constructor for instantiating from destination prompts."""
|
||||
llm_callback_manager = CallbackManager([DifyStdOutCallbackHandler()])
|
||||
llm = LLMBuilder.to_llm(
|
||||
tenant_id=tenant_id,
|
||||
model_name='gpt-3.5-turbo',
|
||||
temperature=0,
|
||||
max_tokens=1024,
|
||||
callback_manager=llm_callback_manager
|
||||
)
|
||||
|
||||
destinations = ["{}: {}".format(d.id, d.description.replace('\n', ' ') if d.description
|
||||
else ('useful for when you want to answer queries about the ' + d.name))
|
||||
for d in datasets]
|
||||
destinations_str = "\n".join(destinations)
|
||||
router_template = MULTI_PROMPT_ROUTER_TEMPLATE.format(
|
||||
destinations=destinations_str
|
||||
)
|
||||
router_prompt = PromptTemplate(
|
||||
template=router_template,
|
||||
input_variables=["input"],
|
||||
output_parser=RouterOutputParser(),
|
||||
)
|
||||
router_chain = LLMRouterChain.from_llm(llm, router_prompt)
|
||||
dataset_tools = {}
|
||||
for dataset in datasets:
|
||||
dataset_tool = DatasetToolBuilder.build_dataset_tool(
|
||||
dataset=dataset,
|
||||
response_mode='no_synthesizer', # "compact"
|
||||
callback_handler=DatasetToolCallbackHandler(conversation_message_task)
|
||||
)
|
||||
dataset_tools[dataset.id] = dataset_tool
|
||||
return cls(
|
||||
router_chain=router_chain,
|
||||
dataset_tools=dataset_tools,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def _call(
|
||||
self,
|
||||
inputs: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
if len(self.dataset_tools) == 0:
|
||||
return {"text": ''}
|
||||
elif len(self.dataset_tools) == 1:
|
||||
return {"text": next(iter(self.dataset_tools.values())).run(inputs['input'])}
|
||||
|
||||
route = self.router_chain.route(inputs)
|
||||
|
||||
if not route.destination:
|
||||
return {"text": ''}
|
||||
elif route.destination in self.dataset_tools:
|
||||
return {"text": self.dataset_tools[route.destination].run(
|
||||
route.next_inputs['input']
|
||||
)}
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Received invalid destination chain name '{route.destination}'"
|
||||
)
|
||||
@@ -1,14 +1,17 @@
|
||||
import logging
|
||||
from typing import Optional, List, Union, Tuple
|
||||
|
||||
from langchain.callbacks import CallbackManager
|
||||
from langchain.chat_models.base import BaseChatModel
|
||||
from langchain.llms import BaseLLM
|
||||
from langchain.schema import BaseMessage, BaseLanguageModel, HumanMessage
|
||||
from requests.exceptions import ChunkedEncodingError
|
||||
|
||||
from core.constant import llm_constant
|
||||
from core.callback_handler.llm_callback_handler import LLMCallbackHandler
|
||||
from core.callback_handler.std_out_callback_handler import DifyStreamingStdOutCallbackHandler, \
|
||||
DifyStdOutCallbackHandler
|
||||
from core.conversation_message_task import ConversationMessageTask, ConversationTaskStoppedException
|
||||
from core.conversation_message_task import ConversationMessageTask, ConversationTaskStoppedException, PubHandler
|
||||
from core.llm.error import LLMBadRequestError
|
||||
from core.llm.llm_builder import LLMBuilder
|
||||
from core.chain.main_chain_builder import MainChainBuilder
|
||||
@@ -84,6 +87,11 @@ class Completion:
|
||||
)
|
||||
except ConversationTaskStoppedException:
|
||||
return
|
||||
except ChunkedEncodingError as e:
|
||||
# Interrupt by LLM (like OpenAI), handle it.
|
||||
logging.warning(f'ChunkedEncodingError: {e}')
|
||||
conversation_message_task.end()
|
||||
return
|
||||
|
||||
@classmethod
|
||||
def run_final_llm(cls, tenant_id: str, mode: str, app_model_config: AppModelConfig, query: str, inputs: dict,
|
||||
|
||||
@@ -80,7 +80,10 @@ class ConversationMessageTask:
|
||||
if introduction:
|
||||
prompt_template = OutLinePromptTemplate.from_template(template=PromptBuilder.process_template(introduction))
|
||||
prompt_inputs = {k: self.inputs[k] for k in prompt_template.input_variables if k in self.inputs}
|
||||
introduction = prompt_template.format(**prompt_inputs)
|
||||
try:
|
||||
introduction = prompt_template.format(**prompt_inputs)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
if self.app_model_config.pre_prompt:
|
||||
pre_prompt = PromptBuilder.process_template(self.app_model_config.pre_prompt)
|
||||
@@ -171,7 +174,7 @@ class ConversationMessageTask:
|
||||
)
|
||||
|
||||
if not by_stopped:
|
||||
self._pub_handler.pub_end()
|
||||
self.end()
|
||||
|
||||
def update_provider_quota(self):
|
||||
llm_provider_service = LLMProviderService(
|
||||
@@ -268,6 +271,9 @@ class ConversationMessageTask:
|
||||
total_price = message_tokens_per_1k * message_unit_price + answer_tokens_per_1k * answer_unit_price
|
||||
return total_price.quantize(decimal.Decimal('0.0000001'), rounding=decimal.ROUND_HALF_UP)
|
||||
|
||||
def end(self):
|
||||
self._pub_handler.pub_end()
|
||||
|
||||
|
||||
class PubHandler:
|
||||
def __init__(self, user: Union[Account | EndUser], task_id: str,
|
||||
|
||||
@@ -32,6 +32,6 @@ class PromptBuilder:
|
||||
|
||||
@classmethod
|
||||
def process_template(cls, template: str):
|
||||
processed_template = re.sub(r'\{(.+?)\}', r'\1', template)
|
||||
processed_template = re.sub(r'\{\{(.+?)\}\}', r'{\1}', processed_template)
|
||||
processed_template = re.sub(r'\{([a-zA-Z_]\w+?)\}', r'\1', template)
|
||||
processed_template = re.sub(r'\{\{([a-zA-Z_]\w+?)\}\}', r'{\1}', processed_template)
|
||||
return processed_template
|
||||
|
||||
@@ -10,24 +10,14 @@ from core.index.keyword_table_index import KeywordTableIndex
|
||||
from core.index.vector_index import VectorIndex
|
||||
from core.prompt.prompts import QUERY_KEYWORD_EXTRACT_TEMPLATE
|
||||
from core.tool.llama_index_tool import EnhanceLlamaIndexTool
|
||||
from extensions.ext_database import db
|
||||
from models.dataset import Dataset
|
||||
|
||||
|
||||
class DatasetToolBuilder:
|
||||
@classmethod
|
||||
def build_dataset_tool(cls, tenant_id: str, dataset_id: str,
|
||||
def build_dataset_tool(cls, dataset: Dataset,
|
||||
response_mode: str = "no_synthesizer",
|
||||
callback_handler: Optional[DatasetToolCallbackHandler] = None):
|
||||
# get dataset from dataset id
|
||||
dataset = db.session.query(Dataset).filter(
|
||||
Dataset.tenant_id == tenant_id,
|
||||
Dataset.id == dataset_id
|
||||
).first()
|
||||
|
||||
if not dataset:
|
||||
return None
|
||||
|
||||
if dataset.indexing_technique == "economy":
|
||||
# use keyword table query
|
||||
index = KeywordTableIndex(dataset=dataset).query_index
|
||||
@@ -65,7 +55,7 @@ class DatasetToolBuilder:
|
||||
|
||||
index_tool_config = IndexToolConfig(
|
||||
index=index,
|
||||
name=f"dataset-{dataset_id}",
|
||||
name=f"dataset-{dataset.id}",
|
||||
description=description,
|
||||
index_query_kwargs=query_kwargs,
|
||||
tool_kwargs={
|
||||
@@ -75,7 +65,7 @@ class DatasetToolBuilder:
|
||||
# return_direct: Whether to return LLM results directly or process the output data with an Output Parser
|
||||
)
|
||||
|
||||
index_callback_handler = DatasetIndexToolCallbackHandler(dataset_id=dataset_id)
|
||||
index_callback_handler = DatasetIndexToolCallbackHandler(dataset_id=dataset.id)
|
||||
|
||||
return EnhanceLlamaIndexTool.from_tool_config(
|
||||
tool_config=index_tool_config,
|
||||
|
||||
@@ -2,7 +2,7 @@ version: '3.1'
|
||||
services:
|
||||
# API service
|
||||
api:
|
||||
image: langgenius/dify-api:0.3.0
|
||||
image: langgenius/dify-api:0.3.1
|
||||
restart: always
|
||||
environment:
|
||||
# Startup mode, 'api' starts the API server.
|
||||
@@ -110,7 +110,7 @@ services:
|
||||
# worker service
|
||||
# The Celery worker for processing the queue.
|
||||
worker:
|
||||
image: langgenius/dify-api:0.3.0
|
||||
image: langgenius/dify-api:0.3.1
|
||||
restart: always
|
||||
environment:
|
||||
# Startup mode, 'worker' starts the Celery worker for processing the queue.
|
||||
@@ -156,7 +156,7 @@ services:
|
||||
|
||||
# Frontend web application.
|
||||
web:
|
||||
image: langgenius/dify-web:0.3.0
|
||||
image: langgenius/dify-web:0.3.1
|
||||
restart: always
|
||||
environment:
|
||||
EDITION: SELF_HOSTED
|
||||
|
||||
7
web/.eslintignore
Normal file
7
web/.eslintignore
Normal file
@@ -0,0 +1,7 @@
|
||||
/**/node_modules/*
|
||||
node_modules/
|
||||
|
||||
dist/
|
||||
build/
|
||||
out/
|
||||
.next/
|
||||
@@ -23,6 +23,6 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"react-hooks/exhaustive-deps": "warning"
|
||||
"react-hooks/exhaustive-deps": "warn"
|
||||
}
|
||||
}
|
||||
}
|
||||
4
web/.husky/pre-commit
Executable file
4
web/.husky/pre-commit
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
cd ./web && npx lint-staged
|
||||
25
web/.vscode/settings.json
vendored
Normal file
25
web/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"prettier.enable": false,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true
|
||||
},
|
||||
"eslint.format.enable": true,
|
||||
"[python]": {
|
||||
"editor.formatOnType": true
|
||||
},
|
||||
"[html]": {
|
||||
"editor.defaultFormatter": "vscode.html-language-features"
|
||||
},
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "vscode.typescript-language-features"
|
||||
},
|
||||
"[javascriptreact]": {
|
||||
"editor.defaultFormatter": "vscode.typescript-language-features"
|
||||
},
|
||||
"[jsonc]": {
|
||||
"editor.defaultFormatter": "vscode.json-language-features"
|
||||
},
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"typescript.enablePromptUseWorkspaceTsdk": true
|
||||
}
|
||||
@@ -7,9 +7,9 @@ export type IAppDetail = {
|
||||
|
||||
const AppDetail: FC<IAppDetail> = ({ children }) => {
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
</>
|
||||
<>
|
||||
{children}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import { AppListResponse } from '@/models/app'
|
||||
import { fetchAppList } from '@/service/apps'
|
||||
import { useSelector } from '@/context/app-context'
|
||||
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const getKey = (pageIndex: number, previousPageData: AppListResponse) => {
|
||||
if (!pageIndex || previousPageData.has_more)
|
||||
@@ -17,12 +18,14 @@ const getKey = (pageIndex: number, previousPageData: AppListResponse) => {
|
||||
}
|
||||
|
||||
const Apps = () => {
|
||||
const { t } = useTranslation()
|
||||
const { data, isLoading, setSize, mutate } = useSWRInfinite(getKey, fetchAppList, { revalidateFirstPage: false })
|
||||
const loadingStateRef = useRef(false)
|
||||
const pageContainerRef = useSelector(state => state.pageContainerRef)
|
||||
const anchorRef = useRef<HTMLAnchorElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
document.title = `${t('app.title')} - Dify`;
|
||||
if(localStorage.getItem(NEED_REFRESH_APP_LIST_KEY) === '1') {
|
||||
localStorage.removeItem(NEED_REFRESH_APP_LIST_KEY)
|
||||
mutate()
|
||||
|
||||
@@ -37,7 +37,7 @@ const NewAppDialog = ({ show, onSuccess, onClose }: NewAppDialogProps) => {
|
||||
|
||||
// Emoji Picker
|
||||
const [showEmojiPicker, setShowEmojiPicker] = useState(false)
|
||||
const [emoji, setEmoji] = useState({ icon: '🍌', icon_background: '#FFEAD5' })
|
||||
const [emoji, setEmoji] = useState({ icon: '🤖', icon_background: '#FFEAD5' })
|
||||
|
||||
const mutateApps = useContextSelector(AppsContext, state => state.mutateApps)
|
||||
|
||||
@@ -102,7 +102,7 @@ const NewAppDialog = ({ show, onSuccess, onClose }: NewAppDialogProps) => {
|
||||
setShowEmojiPicker(false)
|
||||
}}
|
||||
onClose={() => {
|
||||
setEmoji({ icon: '🍌', icon_background: '#FFEAD5' })
|
||||
setEmoji({ icon: '🤖', icon_background: '#FFEAD5' })
|
||||
setShowEmojiPicker(false)
|
||||
}}
|
||||
/>}
|
||||
|
||||
@@ -14,23 +14,19 @@ const AppList = async () => {
|
||||
<footer className='px-12 py-6 grow-0 shrink-0'>
|
||||
<h3 className='text-xl font-semibold leading-tight text-gradient'>{t('join')}</h3>
|
||||
<p className='mt-1 text-sm font-normal leading-tight text-gray-700'>{t('communityIntro')}</p>
|
||||
{/*<p className='mt-3 text-sm'>*/}
|
||||
{/* <a className='inline-flex items-center gap-1 link' target='_blank' href={`https://docs.dify.ai${locale === 'en' ? '' : '/v/zh-hans'}/community/product-roadmap`}>*/}
|
||||
{/* {t('roadmap')}*/}
|
||||
{/* <span className={style.linkIcon} />*/}
|
||||
{/* </a>*/}
|
||||
{/*</p>*/}
|
||||
{/* <p className='mt-3 text-sm'> */}
|
||||
{/* <a className='inline-flex items-center gap-1 link' target='_blank' href={`https://docs.dify.ai${locale === 'en' ? '' : '/v/zh-hans'}/community/product-roadmap`}> */}
|
||||
{/* {t('roadmap')} */}
|
||||
{/* <span className={style.linkIcon} /> */}
|
||||
{/* </a> */}
|
||||
{/* </p> */}
|
||||
<div className='flex items-center gap-2 mt-3'>
|
||||
<a className={style.socialMediaLink} target='_blank' href='https://github.com/langgenius'><span className={classNames(style.socialMediaIcon, style.githubIcon)} /></a>
|
||||
<a className={style.socialMediaLink} target='_blank' href='https://discord.gg/AhzKf7dNgk'><span className={classNames(style.socialMediaIcon, style.discordIcon)} /></a>
|
||||
<a className={style.socialMediaLink} target='_blank' href='https://github.com/langgenius/dify'><span className={classNames(style.socialMediaIcon, style.githubIcon)} /></a>
|
||||
<a className={style.socialMediaLink} target='_blank' href='https://discord.gg/FngNHpbcY7'><span className={classNames(style.socialMediaIcon, style.discordIcon)} /></a>
|
||||
</div>
|
||||
</footer>
|
||||
</div >
|
||||
)
|
||||
}
|
||||
|
||||
export const metadata = {
|
||||
title: 'Apps - Dify',
|
||||
}
|
||||
|
||||
export default AppList
|
||||
|
||||
@@ -8,6 +8,8 @@ import { UserCircleIcon } from '@heroicons/react/24/solid'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { randomString } from '../../app-sidebar/basic'
|
||||
import s from './style.module.css'
|
||||
import LoadingAnim from './loading-anim'
|
||||
import CopyBtn from './copy-btn'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import AutoHeightTextarea from '@/app/components/base/auto-height-textarea'
|
||||
@@ -15,9 +17,8 @@ import Button from '@/app/components/base/button'
|
||||
import type { Annotation, MessageRating } from '@/models/log'
|
||||
import AppContext from '@/context/app-context'
|
||||
import { Markdown } from '@/app/components/base/markdown'
|
||||
import LoadingAnim from './loading-anim'
|
||||
import { formatNumber } from '@/utils/format'
|
||||
import CopyBtn from './copy-btn'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
|
||||
const stopIcon = (
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
@@ -285,8 +286,8 @@ const Answer: FC<IAnswerProps> = ({ item, feedbackDisabled = false, isHideFeedba
|
||||
<div key={id}>
|
||||
<div className='flex items-start'>
|
||||
<div className={`${s.answerIcon} w-10 h-10 shrink-0`}>
|
||||
{isResponsing &&
|
||||
<div className={s.typeingIcon}>
|
||||
{isResponsing
|
||||
&& <div className={s.typeingIcon}>
|
||||
<LoadingAnim type='avatar' />
|
||||
</div>
|
||||
}
|
||||
@@ -301,13 +302,15 @@ const Answer: FC<IAnswerProps> = ({ item, feedbackDisabled = false, isHideFeedba
|
||||
<div className='text-xs text-gray-500'>{t('appDebug.openingStatement.title')}</div>
|
||||
</div>
|
||||
)}
|
||||
{(isResponsing && !content) ? (
|
||||
<div className='flex items-center justify-center w-6 h-5'>
|
||||
<LoadingAnim type='text' />
|
||||
</div>
|
||||
) : (
|
||||
<Markdown content={content} />
|
||||
)}
|
||||
{(isResponsing && !content)
|
||||
? (
|
||||
<div className='flex items-center justify-center w-6 h-5'>
|
||||
<LoadingAnim type='text' />
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<Markdown content={content} />
|
||||
)}
|
||||
{!showEdit
|
||||
? (annotation?.content
|
||||
&& <>
|
||||
@@ -384,13 +387,15 @@ const Question: FC<IQuestionProps> = ({ id, content, more, useCurrentUserAvatar
|
||||
</div>
|
||||
{more && <MoreInfo more={more} isQuestion={true} />}
|
||||
</div>
|
||||
{useCurrentUserAvatar ? (
|
||||
<div className='w-10 h-10 shrink-0 leading-10 text-center mr-2 rounded-full bg-primary-600 text-white'>
|
||||
{userName?.[0].toLocaleUpperCase()}
|
||||
</div>
|
||||
) : (
|
||||
<div className={`${s.questionIcon} w-10 h-10 shrink-0 `}></div>
|
||||
)}
|
||||
{useCurrentUserAvatar
|
||||
? (
|
||||
<div className='w-10 h-10 shrink-0 leading-10 text-center mr-2 rounded-full bg-primary-600 text-white'>
|
||||
{userName?.[0].toLocaleUpperCase()}
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<div className={`${s.questionIcon} w-10 h-10 shrink-0 `}></div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -411,7 +416,7 @@ const Chat: FC<IChatProps> = ({
|
||||
controlClearQuery,
|
||||
controlFocus,
|
||||
isShowSuggestion,
|
||||
suggestionList
|
||||
suggestionList,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useContext(ToastContext)
|
||||
@@ -436,27 +441,24 @@ const Chat: FC<IChatProps> = ({
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (controlClearQuery) {
|
||||
if (controlClearQuery)
|
||||
setQuery('')
|
||||
}
|
||||
}, [controlClearQuery])
|
||||
|
||||
const handleSend = () => {
|
||||
if (!valid() || (checkCanSend && !checkCanSend()))
|
||||
return
|
||||
onSend(query)
|
||||
if (!isResponsing) {
|
||||
if (!isResponsing)
|
||||
setQuery('')
|
||||
}
|
||||
}
|
||||
|
||||
const handleKeyUp = (e: any) => {
|
||||
if (e.code === 'Enter') {
|
||||
e.preventDefault()
|
||||
// prevent send message when using input method enter
|
||||
if (!e.shiftKey && !isUseInputMethod.current) {
|
||||
if (!e.shiftKey && !isUseInputMethod.current)
|
||||
handleSend()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -468,6 +470,10 @@ const Chat: FC<IChatProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
const media = useBreakpoints()
|
||||
const isMobile = media === MediaType.mobile
|
||||
const sendBtn = <div className={cn(!(!query || query.trim() === '') && s.sendBtnActive, `${s.sendBtn} w-8 h-8 cursor-pointer rounded-md`)} onClick={handleSend}></div>
|
||||
|
||||
return (
|
||||
<div className={cn(!feedbackDisabled && 'px-3.5', 'h-full')}>
|
||||
{/* Chat List */}
|
||||
@@ -506,7 +512,7 @@ const Chat: FC<IChatProps> = ({
|
||||
<div className='flex items-center justify-center mb-2.5'>
|
||||
<div className='grow h-[1px]'
|
||||
style={{
|
||||
background: 'linear-gradient(270deg, #F3F4F6 0%, rgba(243, 244, 246, 0) 100%)'
|
||||
background: 'linear-gradient(270deg, #F3F4F6 0%, rgba(243, 244, 246, 0) 100%)',
|
||||
}}></div>
|
||||
<div className='shrink-0 flex items-center px-3 space-x-1'>
|
||||
{TryToAskIcon}
|
||||
@@ -514,7 +520,7 @@ const Chat: FC<IChatProps> = ({
|
||||
</div>
|
||||
<div className='grow h-[1px]'
|
||||
style={{
|
||||
background: 'linear-gradient(270deg, rgba(243, 244, 246, 0) 0%, #F3F4F6 100%)'
|
||||
background: 'linear-gradient(270deg, rgba(243, 244, 246, 0) 0%, #F3F4F6 100%)',
|
||||
}}></div>
|
||||
</div>
|
||||
<div className='flex justify-center overflow-x-scroll pb-2'>
|
||||
@@ -544,17 +550,21 @@ const Chat: FC<IChatProps> = ({
|
||||
/>
|
||||
<div className="absolute top-0 right-2 flex items-center h-[48px]">
|
||||
<div className={`${s.count} mr-4 h-5 leading-5 text-sm bg-gray-50 text-gray-500`}>{query.trim().length}</div>
|
||||
<Tooltip
|
||||
selector='send-tip'
|
||||
htmlContent={
|
||||
<div>
|
||||
<div>{t('common.operation.send')} Enter</div>
|
||||
<div>{t('common.operation.lineBreak')} Shift Enter</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className={`${s.sendBtn} w-8 h-8 cursor-pointer rounded-md`} onClick={handleSend}></div>
|
||||
</Tooltip>
|
||||
{isMobile
|
||||
? sendBtn
|
||||
: (
|
||||
<Tooltip
|
||||
selector='send-tip'
|
||||
htmlContent={
|
||||
<div>
|
||||
<div>{t('common.operation.send')} Enter</div>
|
||||
<div>{t('common.operation.lineBreak')} Shift Enter</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{sendBtn}
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -102,6 +102,10 @@
|
||||
background: url(./icons/send.svg) center center no-repeat;
|
||||
}
|
||||
|
||||
.sendBtnActive {
|
||||
background-image: url(./icons/send-active.svg);
|
||||
}
|
||||
|
||||
.sendBtn:hover {
|
||||
background-image: url(./icons/send-active.svg);
|
||||
background-color: #EBF5FF;
|
||||
|
||||
@@ -12,7 +12,6 @@ import { formatNumber } from '@/utils/format'
|
||||
import Link from 'next/link'
|
||||
|
||||
import s from './style.module.css'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
|
||||
export interface ISelectDataSetProps {
|
||||
isShow: boolean
|
||||
@@ -32,8 +31,7 @@ const SelectDataSet: FC<ISelectDataSetProps> = ({
|
||||
const [loaded, setLoaded] = React.useState(false)
|
||||
const [datasets, setDataSets] = React.useState<DataSet[] | null>(null)
|
||||
const hasNoData = !datasets || datasets?.length === 0
|
||||
// Only one dataset can be selected. Historical data retains data and supports multiple selections, but when saving, only one can be selected. This is based on considerations of performance and accuracy.
|
||||
const canSelectMulti = selectedIds.length > 1
|
||||
const canSelectMulti = true
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const { data } = await fetchDatasets({ url: '/datasets', params: { page: 1 } })
|
||||
@@ -57,13 +55,6 @@ const SelectDataSet: FC<ISelectDataSetProps> = ({
|
||||
}
|
||||
|
||||
const handleSelect = () => {
|
||||
if (selected.length > 1) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t('appDebug.feature.dataSet.notSupportSelectMulti')
|
||||
})
|
||||
return
|
||||
}
|
||||
onSelect(selected)
|
||||
}
|
||||
return (
|
||||
|
||||
@@ -56,10 +56,11 @@ const OpeningStatement: FC<IOpeningStatementProps> = ({
|
||||
}, [value])
|
||||
|
||||
const coloredContent = (tempValue || '')
|
||||
.replace(regex, varHighlightHTML({ name: '$1' })) // `<span class="${highLightClassName}">{{$1}}</span>`
|
||||
.replace(/\n/g, '<br />')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(regex, varHighlightHTML({ name: '$1' })) // `<span class="${highLightClassName}">{{$1}}</span>`
|
||||
.replace(/\n/g, '<br />')
|
||||
|
||||
|
||||
|
||||
const handleEdit = () => {
|
||||
|
||||
@@ -39,7 +39,7 @@ const AppIcon: FC<AppIconProps> = ({
|
||||
}}
|
||||
onClick={onClick}
|
||||
>
|
||||
{innerIcon ? innerIcon : icon && icon !== '' ? <em-emoji id={icon} /> : <em-emoji id={'banana'} />}
|
||||
{innerIcon ? innerIcon : icon && icon !== '' ? <em-emoji id={icon} /> : <em-emoji id='🤖' />}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -68,10 +68,11 @@ const BlockInput: FC<IBlockInputProps> = ({
|
||||
})
|
||||
|
||||
const coloredContent = (currentValue || '')
|
||||
.replace(regex, varHighlightHTML({ name: '$1' })) // `<span class="${highLightClassName}">{{$1}}</span>`
|
||||
.replace(/\n/g, '<br />')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(regex, varHighlightHTML({ name: '$1' })) // `<span class="${highLightClassName}">{{$1}}</span>`
|
||||
.replace(/\n/g, '<br />')
|
||||
|
||||
|
||||
// Not use useCallback. That will cause out callback get old data.
|
||||
const handleSubmit = () => {
|
||||
|
||||
@@ -28,7 +28,7 @@ const CreateAppModal = ({
|
||||
const [name, setName] = React.useState('')
|
||||
|
||||
const [showEmojiPicker, setShowEmojiPicker] = useState(false)
|
||||
const [emoji, setEmoji] = useState({ icon: '🍌', icon_background: '#FFEAD5' })
|
||||
const [emoji, setEmoji] = useState({ icon: '🤖', icon_background: '#FFEAD5' })
|
||||
|
||||
const submit = () => {
|
||||
if(!name.trim()) {
|
||||
@@ -74,7 +74,7 @@ const CreateAppModal = ({
|
||||
setShowEmojiPicker(false)
|
||||
}}
|
||||
onClose={() => {
|
||||
setEmoji({ icon: '🍌', icon_background: '#FFEAD5' })
|
||||
setEmoji({ icon: '🤖', icon_background: '#FFEAD5' })
|
||||
setShowEmojiPicker(false)
|
||||
}}
|
||||
/>}
|
||||
|
||||
@@ -5,6 +5,7 @@ import Sidebar from '@/app/components/explore/sidebar'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { fetchMembers } from '@/service/common'
|
||||
import { InstalledApp } from '@/models/explore'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export interface IExploreProps {
|
||||
children: React.ReactNode
|
||||
@@ -13,12 +14,14 @@ export interface IExploreProps {
|
||||
const Explore: FC<IExploreProps> = ({
|
||||
children
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [controlUpdateInstalledApps, setControlUpdateInstalledApps] = useState(0)
|
||||
const { userProfile } = useAppContext()
|
||||
const [hasEditPermission, setHasEditPermission] = useState(false)
|
||||
const [installedApps, setInstalledApps] = useState<InstalledApp[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
document.title = `${t('explore.title')} - Dify`;
|
||||
(async () => {
|
||||
const { accounts } = await fetchMembers({ url: '/workspaces/current/members', params: {}})
|
||||
if(!accounts) return
|
||||
|
||||
@@ -67,7 +67,7 @@ export default function AccountAbout({
|
||||
<div className='flex items-center'>
|
||||
<Link
|
||||
className={classNames(buttonClassName, 'mr-2')}
|
||||
href={'https://github.com/langgenius'}
|
||||
href={'https://github.com/langgenius/dify/releases'}
|
||||
target='_blank'
|
||||
>
|
||||
{t('common.about.changeLog')}
|
||||
|
||||
@@ -71,14 +71,14 @@ const Header: FC<IHeaderProps> = ({ appItems, curApp, userProfile, onLogout, lan
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex items-center'>
|
||||
{/* <Link href="/explore/apps" className={classNames(
|
||||
<Link href="/explore/apps" className={classNames(
|
||||
navClassName, 'group',
|
||||
isExplore && 'bg-white shadow-[0_2px_5px_-1px_rgba(0,0,0,0.05),0_2px_4px_-2px_rgba(0,0,0,0.05)]',
|
||||
isExplore ? 'text-primary-600' : 'text-gray-500 hover:bg-gray-200 hover:text-gray-700'
|
||||
)}>
|
||||
<Squares2X2Icon className='mr-1 w-[18px] h-[18px]' />
|
||||
{t('common.menus.explore')}
|
||||
</Link> */}
|
||||
</Link>
|
||||
<Nav
|
||||
icon={<BuildAppsIcon isSelected={['apps', 'app'].includes(selectedSegment || '')} />}
|
||||
text={t('common.menus.apps')}
|
||||
|
||||
@@ -60,8 +60,9 @@ const ConfigSence: FC<IConfigSenceProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className='mt-6 h-[1px] bg-gray-100'></div>
|
||||
{promptConfig.prompt_variables.length > 0 && (
|
||||
<div className='mt-6 h-[1px] bg-gray-100'></div>
|
||||
)}
|
||||
<div className='w-full mt-5'>
|
||||
<label className='text-gray-900 text-sm font-medium'>{t('share.generation.queryTitle')}</label>
|
||||
<div className="mt-2 overflow-hidden rounded-lg bg-gray-50 ">
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 26 KiB |
@@ -4,7 +4,6 @@ import { useTranslation } from 'react-i18next'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
import cn from 'classnames'
|
||||
import { useBoolean, useClickAway } from 'ahooks'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import ConfigScence from '@/app/components/share/text-generation/config-scence'
|
||||
import NoData from '@/app/components/share/text-generation/no-data'
|
||||
// import History from '@/app/components/share/text-generation/history'
|
||||
@@ -12,6 +11,7 @@ import { fetchAppInfo, fetchAppParams, sendCompletionMessage, updateFeedback, sa
|
||||
import type { SiteInfo } from '@/models/share'
|
||||
import type { PromptConfig, MoreLikeThisConfig, SavedMessage } from '@/models/debug'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import { Feedbacktype } from '@/app/components/app/chat'
|
||||
import { changeLanguage } from '@/i18n/i18next-config'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
@@ -24,6 +24,7 @@ import s from './style.module.css'
|
||||
import Button from '../../base/button'
|
||||
import { App } from '@/types/app'
|
||||
import { InstalledApp } from '@/models/explore'
|
||||
import { appDefaultIconBackground } from '@/config'
|
||||
|
||||
export type IMainProps = {
|
||||
isInstalledApp?: boolean,
|
||||
@@ -283,7 +284,7 @@ const TextGeneration: FC<IMainProps> = ({
|
||||
<div className='mb-6'>
|
||||
<div className='flex justify-between items-center'>
|
||||
<div className='flex items-center space-x-3'>
|
||||
<div className={cn(s.appIcon, 'shrink-0')}></div>
|
||||
<AppIcon size="small" icon={siteInfo.icon} background={siteInfo.icon_background || appDefaultIconBackground} />
|
||||
<div className='text-lg text-gray-800 font-semibold'>{siteInfo.title}</div>
|
||||
</div>
|
||||
{!isPC && (
|
||||
|
||||
@@ -4,13 +4,6 @@
|
||||
box-shadow: 0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03);
|
||||
}
|
||||
|
||||
.appIcon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: url(./icons/app-icon.svg) center center no-repeat;
|
||||
background-size: contain;
|
||||
}
|
||||
|
||||
.starIcon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
|
||||
@@ -779,7 +779,7 @@
|
||||
max-width: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
overflow-x: scroll;
|
||||
overflow-x: auto;
|
||||
line-height: inherit;
|
||||
word-wrap: normal;
|
||||
background-color: transparent;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const translation = {
|
||||
title: 'Apps',
|
||||
createApp: 'Create new App',
|
||||
modes: {
|
||||
completion: 'Text Generator',
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const translation = {
|
||||
title: '应用',
|
||||
createApp: '创建应用',
|
||||
modes: {
|
||||
completion: '文本生成型',
|
||||
|
||||
@@ -82,11 +82,11 @@ const translation = {
|
||||
settings: {
|
||||
accountGroup: 'ACCOUNT',
|
||||
workplaceGroup: 'WORKPLACE',
|
||||
account: "My account",
|
||||
members: "Members",
|
||||
integrations: "Integrations",
|
||||
language: "Language",
|
||||
provider: "Model Provider"
|
||||
account: 'My account',
|
||||
members: 'Members',
|
||||
integrations: 'Integrations',
|
||||
language: 'Language',
|
||||
provider: 'Model Provider',
|
||||
},
|
||||
account: {
|
||||
avatar: 'Avatar',
|
||||
@@ -99,7 +99,7 @@ const translation = {
|
||||
},
|
||||
members: {
|
||||
team: 'Team',
|
||||
invite: 'Invite',
|
||||
invite: 'Add',
|
||||
name: 'NAME',
|
||||
lastActive: 'LAST ACTIVE',
|
||||
role: 'ROLES',
|
||||
@@ -109,14 +109,14 @@ const translation = {
|
||||
adminTip: 'Can build apps & manage team settings',
|
||||
normal: 'Normal',
|
||||
normalTip: 'Only can use apps,can not build apps',
|
||||
inviteTeamMember: 'Invite team member',
|
||||
inviteTeamMemberTip: 'The other person will receive an email. If he\'s already a Dify user, he can access your team data directly after signing in.',
|
||||
inviteTeamMember: 'Add team member',
|
||||
inviteTeamMemberTip: 'He can access your team data directly after signing in.',
|
||||
email: 'Email',
|
||||
emailInvalid: 'Invalid Email Format',
|
||||
emailPlaceholder: 'Input Email',
|
||||
sendInvite: 'Send Invite',
|
||||
invitationSent: 'Invitation sent',
|
||||
invitationSentTip: 'The invitation is sent, and they can sign in to Dify to access your team data.',
|
||||
sendInvite: 'Add',
|
||||
invitationSent: 'Added',
|
||||
invitationSentTip: 'Added, and they can sign in to Dify to access your team data.',
|
||||
ok: 'OK',
|
||||
removeFromTeam: 'Remove from team',
|
||||
removeFromTeamTip: 'Will remove team access',
|
||||
@@ -132,20 +132,20 @@ const translation = {
|
||||
googleAccount: 'Login with Google account',
|
||||
github: 'GitHub',
|
||||
githubAccount: 'Login with GitHub account',
|
||||
connect: 'Connect'
|
||||
connect: 'Connect',
|
||||
},
|
||||
language: {
|
||||
displayLanguage: 'Display Language',
|
||||
timezone: 'Time Zone',
|
||||
},
|
||||
provider: {
|
||||
apiKey: "API Key",
|
||||
enterYourKey: "Enter your API key here",
|
||||
invalidKey: "Invalid OpenAI API key",
|
||||
validatedError: "Validation failed: ",
|
||||
validating: "Validating key...",
|
||||
saveFailed: "Save api key failed",
|
||||
apiKeyExceedBill: "This API KEY has no quota available, please read",
|
||||
apiKey: 'API Key',
|
||||
enterYourKey: 'Enter your API key here',
|
||||
invalidKey: 'Invalid OpenAI API key',
|
||||
validatedError: 'Validation failed: ',
|
||||
validating: 'Validating key...',
|
||||
saveFailed: 'Save api key failed',
|
||||
apiKeyExceedBill: 'This API KEY has no quota available, please read',
|
||||
addKey: 'Add Key',
|
||||
comingSoon: 'Coming Soon',
|
||||
editKey: 'Edit',
|
||||
@@ -170,7 +170,7 @@ const translation = {
|
||||
encrypted: {
|
||||
front: 'Your API KEY will be encrypted and stored using',
|
||||
back: ' technology.',
|
||||
}
|
||||
},
|
||||
},
|
||||
about: {
|
||||
changeLog: 'Changlog',
|
||||
|
||||
@@ -50,7 +50,7 @@ const translation = {
|
||||
'Frequency penalty 是根据重复词在目前文本中的出现频率来对其进行惩罚。正值将不太可能重复常用单词和短语。',
|
||||
maxToken: '最大 Token',
|
||||
maxTokenTip:
|
||||
'生成的最大令牌数为 2,048 或 4,000,取决于模型。提示和完成共享令牌数限制。一个令牌约等于 1 个英文或 4 个中文字符。',
|
||||
'生成的最大令牌数为 2,048 或 4,000,取决于模型。提示和完成共享令牌数限制。一个令牌约等于 1 个英文或 半个中文字符。',
|
||||
setToCurrentModelMaxTokenTip: '最大令牌数更新为当前模型最大的令牌数 4,000。',
|
||||
},
|
||||
tone: {
|
||||
@@ -82,11 +82,11 @@ const translation = {
|
||||
settings: {
|
||||
accountGroup: '账户',
|
||||
workplaceGroup: '工作空间',
|
||||
account: "我的账户",
|
||||
members: "成员",
|
||||
integrations: "集成",
|
||||
language: "语言",
|
||||
provider: "模型供应商"
|
||||
account: '我的账户',
|
||||
members: '成员',
|
||||
integrations: '集成',
|
||||
language: '语言',
|
||||
provider: '模型供应商',
|
||||
},
|
||||
account: {
|
||||
avatar: '头像',
|
||||
@@ -100,7 +100,7 @@ const translation = {
|
||||
},
|
||||
members: {
|
||||
team: '团队',
|
||||
invite: '邀请',
|
||||
invite: '添加',
|
||||
name: '姓名',
|
||||
lastActive: '上次活动时间',
|
||||
role: '角色',
|
||||
@@ -110,14 +110,14 @@ const translation = {
|
||||
adminTip: '能够建立应用程序和管理团队设置',
|
||||
normal: '正常人',
|
||||
normalTip: '只能使用应用程序,不能建立应用程序',
|
||||
inviteTeamMember: '邀请团队成员',
|
||||
inviteTeamMemberTip: '对方会收到一封邮件。如果他已经是 Dify 用户则可直接在登录后访问你的团队数据。',
|
||||
inviteTeamMember: '添加团队成员',
|
||||
inviteTeamMemberTip: '对方在登录后可以访问你的团队数据。',
|
||||
email: '邮箱',
|
||||
emailInvalid: '邮箱格式无效',
|
||||
emailPlaceholder: '输入邮箱',
|
||||
sendInvite: '发送邀请',
|
||||
invitationSent: '邀请已发送',
|
||||
invitationSentTip: '邀请已发送,对方登录 Dify 后即可访问你的团队数据。',
|
||||
sendInvite: '添加',
|
||||
invitationSent: '已添加',
|
||||
invitationSentTip: '已添加,对方登录 Dify 后即可访问你的团队数据。',
|
||||
ok: '好的',
|
||||
removeFromTeam: '移除团队',
|
||||
removeFromTeamTip: '将取消团队访问',
|
||||
@@ -133,20 +133,20 @@ const translation = {
|
||||
googleAccount: 'Google 账号登录',
|
||||
github: 'GitHub',
|
||||
githubAccount: 'GitHub 账号登录',
|
||||
connect: '绑定'
|
||||
connect: '绑定',
|
||||
},
|
||||
language: {
|
||||
displayLanguage: '界面语言',
|
||||
timezone: '时区',
|
||||
},
|
||||
provider: {
|
||||
apiKey: "API 密钥",
|
||||
enterYourKey: "输入你的 API 密钥",
|
||||
apiKey: 'API 密钥',
|
||||
enterYourKey: '输入你的 API 密钥',
|
||||
invalidKey: '无效的 OpenAI API 密钥',
|
||||
validatedError: "校验失败:",
|
||||
validating: "验证密钥中...",
|
||||
saveFailed: "API 密钥保存失败",
|
||||
apiKeyExceedBill: "此 API KEY 已没有可用配额,请阅读",
|
||||
validatedError: '校验失败:',
|
||||
validating: '验证密钥中...',
|
||||
saveFailed: 'API 密钥保存失败',
|
||||
apiKeyExceedBill: '此 API KEY 已没有可用配额,请阅读',
|
||||
addKey: '添加 密钥',
|
||||
comingSoon: '即将推出',
|
||||
editKey: '编辑',
|
||||
@@ -171,7 +171,7 @@ const translation = {
|
||||
encrypted: {
|
||||
front: '密钥将使用 ',
|
||||
back: ' 技术进行加密和存储。',
|
||||
}
|
||||
},
|
||||
},
|
||||
about: {
|
||||
changeLog: '更新日志',
|
||||
|
||||
@@ -9,7 +9,7 @@ const translation = {
|
||||
'删除数据集是不可逆的。用户将无法再访问您的数据集,所有的提示配置和日志将被永久删除。',
|
||||
datasetDeleted: '数据集已删除',
|
||||
datasetDeleteFailed: '删除数据集失败',
|
||||
didYouKnow: '你知道吗??',
|
||||
didYouKnow: '你知道吗?',
|
||||
intro1: '数据集可以被集成到 Dify 应用中',
|
||||
intro2: '作为上下文',
|
||||
intro3: ',',
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const translation = {
|
||||
title: 'My Apps',
|
||||
sidebar: {
|
||||
discovery: 'Discovery',
|
||||
workspace: 'Workspace',
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const translation = {
|
||||
title: '我的应用',
|
||||
sidebar: {
|
||||
discovery: '发现',
|
||||
workspace: '工作区',
|
||||
|
||||
@@ -23,11 +23,12 @@ export const getLocale = (request: NextRequest): Locale => {
|
||||
}
|
||||
|
||||
// match locale
|
||||
let matchedLocale:Locale = i18n.defaultLocale
|
||||
let matchedLocale: Locale = i18n.defaultLocale
|
||||
try {
|
||||
// If languages is ['*'], Error would happen in match function.
|
||||
matchedLocale = match(languages, locales, i18n.defaultLocale) as Locale
|
||||
} catch(e) {}
|
||||
}
|
||||
catch (e) {}
|
||||
return matchedLocale
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
const { withSentryConfig } = require('@sentry/nextjs')
|
||||
|
||||
const withMDX = require('@next/mdx')({
|
||||
extension: /\.mdx?$/,
|
||||
options: {
|
||||
@@ -29,6 +31,9 @@ const nextConfig = {
|
||||
// https://nextjs.org/docs/api-reference/next.config.js/ignoring-typescript-errors
|
||||
ignoreBuildErrors: true,
|
||||
},
|
||||
sentry: {
|
||||
hideSourceMaps: true,
|
||||
},
|
||||
async redirects() {
|
||||
return [
|
||||
{
|
||||
@@ -40,4 +45,17 @@ const nextConfig = {
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = withMDX(nextConfig)
|
||||
// https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup
|
||||
const sentryWebpackPluginOptions = {
|
||||
org: 'perfectworld',
|
||||
project: 'javascript-nextjs',
|
||||
silent: true, // Suppresses all logs
|
||||
sourcemaps: {
|
||||
assets: './**',
|
||||
ignore: ['./node_modules/**'],
|
||||
},
|
||||
|
||||
// https://github.com/getsentry/sentry-webpack-plugin#options.
|
||||
}
|
||||
|
||||
module.exports = withMDX(withSentryConfig(nextConfig, sentryWebpackPluginOptions))
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
{
|
||||
"name": "dify-web",
|
||||
"version": "0.3.0",
|
||||
"version": "0.3.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"fix": "next lint --fix"
|
||||
"fix": "next lint --fix",
|
||||
"eslint-fix": "eslint --fix",
|
||||
"prepare": "cd ../ && husky install ./web/.husky"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emoji-mart/data": "^1.1.2",
|
||||
@@ -17,6 +19,7 @@
|
||||
"@mdx-js/loader": "^2.3.0",
|
||||
"@mdx-js/react": "^2.3.0",
|
||||
"@next/mdx": "^13.2.4",
|
||||
"@sentry/nextjs": "^7.53.1",
|
||||
"@tailwindcss/line-clamp": "^0.4.2",
|
||||
"@types/crypto-js": "^4.1.1",
|
||||
"@types/lodash-es": "^4.17.7",
|
||||
@@ -77,8 +80,18 @@
|
||||
"@types/qs": "^6.9.7",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"husky": "^8.0.3",
|
||||
"lint-staged": "^13.2.2",
|
||||
"miragejs": "^0.1.47",
|
||||
"postcss": "^8.4.21",
|
||||
"tailwindcss": "^3.2.7"
|
||||
},
|
||||
"lint-staged": {
|
||||
"**/*.js?(x)": [
|
||||
"eslint --fix"
|
||||
],
|
||||
"**/*.ts?(x)": [
|
||||
"eslint --fix"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
23
web/sentry.client.config.js
Normal file
23
web/sentry.client.config.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import * as Sentry from '@sentry/nextjs'
|
||||
|
||||
Sentry.init({
|
||||
dsn: 'https://6bf48a450f054d749398c02a61bae343@o4505264807215104.ingest.sentry.io/4505264809115648',
|
||||
// Replay may only be enabled for the client-side
|
||||
integrations: [new Sentry.Replay()],
|
||||
|
||||
// Set tracesSampleRate to 1.0 to capture 100%
|
||||
// of transactions for performance monitoring.
|
||||
// We recommend adjusting this value in production
|
||||
tracesSampleRate: 1.0,
|
||||
|
||||
// Capture Replay for 10% of all sessions,
|
||||
// plus for 100% of sessions with an error
|
||||
replaysSessionSampleRate: 0.1,
|
||||
replaysOnErrorSampleRate: 1.0,
|
||||
|
||||
// ...
|
||||
|
||||
// Note: if you want to override the automatic release value, do not set a
|
||||
// `release` value here - use the environment variable `SENTRY_RELEASE`, so
|
||||
// that it will also get attached to your source maps
|
||||
})
|
||||
16
web/sentry.edge.config.js
Normal file
16
web/sentry.edge.config.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import * as Sentry from '@sentry/nextjs'
|
||||
|
||||
Sentry.init({
|
||||
dsn: 'https://6bf48a450f054d749398c02a61bae343@o4505264807215104.ingest.sentry.io/4505264809115648',
|
||||
|
||||
// Set tracesSampleRate to 1.0 to capture 100%
|
||||
// of transactions for performance monitoring.
|
||||
// We recommend adjusting this value in production
|
||||
tracesSampleRate: 1.0,
|
||||
|
||||
// ...
|
||||
|
||||
// Note: if you want to override the automatic release value, do not set a
|
||||
// `release` value here - use the environment variable `SENTRY_RELEASE`, so
|
||||
// that it will also get attached to your source maps
|
||||
})
|
||||
10
web/sentry.server.config.js
Normal file
10
web/sentry.server.config.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import * as Sentry from '@sentry/nextjs'
|
||||
|
||||
Sentry.init({
|
||||
dsn: 'https://6bf48a450f054d749398c02a61bae343@o4505264807215104.ingest.sentry.io/4505264809115648',
|
||||
|
||||
// Set tracesSampleRate to 1.0 to capture 100%
|
||||
// of transactions for performance monitoring.
|
||||
// We recommend adjusting this value in production
|
||||
tracesSampleRate: 1.0,
|
||||
})
|
||||
@@ -50,21 +50,20 @@ module.exports = {
|
||||
indigo: {
|
||||
25: '#F5F8FF',
|
||||
100: '#E0EAFF',
|
||||
600: '#444CE7'
|
||||
}
|
||||
600: '#444CE7',
|
||||
},
|
||||
},
|
||||
screens: {
|
||||
'mobile': '100px',
|
||||
mobile: '100px',
|
||||
// => @media (min-width: 100px) { ... }
|
||||
'tablet': '640px', // 391
|
||||
tablet: '640px', // 391
|
||||
// => @media (min-width: 600px) { ... }
|
||||
'pc': '769px',
|
||||
pc: '769px',
|
||||
// => @media (min-width: 769px) { ... }
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
require('@tailwindcss/typography'),
|
||||
require('@tailwindcss/line-clamp'),
|
||||
],
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user