diff --git a/api/app.py b/api/app.py
index bcf3856c13..ac8bf27df1 100644
--- a/api/app.py
+++ b/api/app.py
@@ -38,10 +38,11 @@ from extensions import (
from extensions.ext_database import db
from extensions.ext_login import login_manager
from libs.passport import PassportService
-
-# DO NOT REMOVE BELOW
from services.account_service import AccountService
+# DO NOT REMOVE BELOW
+from events import event_handlers
+from models import account, dataset, model, source, task, tool, tools, web
# DO NOT REMOVE ABOVE
diff --git a/api/config.py b/api/config.py
index 1728a18d0b..b6a8ce1438 100644
--- a/api/config.py
+++ b/api/config.py
@@ -86,7 +86,7 @@ class Config:
# ------------------------
# General Configurations.
# ------------------------
- self.CURRENT_VERSION = "0.5.5"
+ self.CURRENT_VERSION = "0.5.6"
self.COMMIT_SHA = get_env('COMMIT_SHA')
self.EDITION = "SELF_HOSTED"
self.DEPLOY_ENV = get_env('DEPLOY_ENV')
diff --git a/api/controllers/console/app/audio.py b/api/controllers/console/app/audio.py
index ac90dfcc8d..d2c1891b65 100644
--- a/api/controllers/console/app/audio.py
+++ b/api/controllers/console/app/audio.py
@@ -47,7 +47,6 @@ class ChatMessageAudioApi(Resource):
tenant_id=app_model.tenant_id,
file=file,
end_user=None,
- promot=app_model.app_model_config.pre_prompt
)
return response
diff --git a/api/core/data_loader/file_extractor.py b/api/core/data_loader/file_extractor.py
index 4a6eb3654d..4741014c96 100644
--- a/api/core/data_loader/file_extractor.py
+++ b/api/core/data_loader/file_extractor.py
@@ -69,7 +69,7 @@ class FileExtractor:
else MarkdownLoader(file_path, autodetect_encoding=True)
elif file_extension in ['.htm', '.html']:
loader = HTMLLoader(file_path)
- elif file_extension in ['.docx', '.doc']:
+ elif file_extension in ['.docx']:
loader = Docx2txtLoader(file_path)
elif file_extension == '.csv':
loader = CSVLoader(file_path, autodetect_encoding=True)
@@ -96,7 +96,7 @@ class FileExtractor:
loader = MarkdownLoader(file_path, autodetect_encoding=True)
elif file_extension in ['.htm', '.html']:
loader = HTMLLoader(file_path)
- elif file_extension in ['.docx', '.doc']:
+ elif file_extension in ['.docx']:
loader = Docx2txtLoader(file_path)
elif file_extension == '.csv':
loader = CSVLoader(file_path, autodetect_encoding=True)
diff --git a/api/core/features/assistant_base_runner.py b/api/core/features/assistant_base_runner.py
index c62028eaf0..c4a5767b04 100644
--- a/api/core/features/assistant_base_runner.py
+++ b/api/core/features/assistant_base_runner.py
@@ -1,5 +1,6 @@
import json
import logging
+import uuid
from datetime import datetime
from mimetypes import guess_extension
from typing import Optional, Union, cast
@@ -20,7 +21,14 @@ from core.file.message_file_parser import FileTransferMethod
from core.memory.token_buffer_memory import TokenBufferMemory
from core.model_manager import ModelInstance
from core.model_runtime.entities.llm_entities import LLMUsage
-from core.model_runtime.entities.message_entities import PromptMessage, PromptMessageTool
+from core.model_runtime.entities.message_entities import (
+ AssistantPromptMessage,
+ PromptMessage,
+ PromptMessageTool,
+ SystemPromptMessage,
+ ToolPromptMessage,
+ UserPromptMessage,
+)
from core.model_runtime.entities.model_entities import ModelFeature
from core.model_runtime.model_providers.__base.large_language_model import LargeLanguageModel
from core.model_runtime.utils.encoders import jsonable_encoder
@@ -77,7 +85,9 @@ class BaseAssistantApplicationRunner(AppRunner):
self.message = message
self.user_id = user_id
self.memory = memory
- self.history_prompt_messages = prompt_messages
+ self.history_prompt_messages = self.organize_agent_history(
+ prompt_messages=prompt_messages or []
+ )
self.variables_pool = variables_pool
self.db_variables_pool = db_variables
self.model_instance = model_instance
@@ -504,17 +514,6 @@ class BaseAssistantApplicationRunner(AppRunner):
agent_thought.tool_labels_str = json.dumps(labels)
db.session.commit()
-
- def get_history_prompt_messages(self) -> list[PromptMessage]:
- """
- Get history prompt messages
- """
- if self.history_prompt_messages is None:
- self.history_prompt_messages = db.session.query(PromptMessage).filter(
- PromptMessage.message_id == self.message.id,
- ).order_by(PromptMessage.position.asc()).all()
-
- return self.history_prompt_messages
def transform_tool_invoke_messages(self, messages: list[ToolInvokeMessage]) -> list[ToolInvokeMessage]:
"""
@@ -589,4 +588,54 @@ class BaseAssistantApplicationRunner(AppRunner):
"""
db_variables.updated_at = datetime.utcnow()
db_variables.variables_str = json.dumps(jsonable_encoder(tool_variables.pool))
- db.session.commit()
\ No newline at end of file
+ db.session.commit()
+
+ def organize_agent_history(self, prompt_messages: list[PromptMessage]) -> list[PromptMessage]:
+ """
+ Organize agent history
+ """
+ result = []
+ # check if there is a system message in the beginning of the conversation
+ if prompt_messages and isinstance(prompt_messages[0], SystemPromptMessage):
+ result.append(prompt_messages[0])
+
+ messages: list[Message] = db.session.query(Message).filter(
+ Message.conversation_id == self.message.conversation_id,
+ ).order_by(Message.created_at.asc()).all()
+
+ for message in messages:
+ result.append(UserPromptMessage(content=message.query))
+ agent_thoughts: list[MessageAgentThought] = message.agent_thoughts
+ for agent_thought in agent_thoughts:
+ tools = agent_thought.tool
+ if tools:
+ tools = tools.split(';')
+ tool_calls: list[AssistantPromptMessage.ToolCall] = []
+ tool_call_response: list[ToolPromptMessage] = []
+ tool_inputs = json.loads(agent_thought.tool_input)
+ for tool in tools:
+ # generate a uuid for tool call
+ tool_call_id = str(uuid.uuid4())
+ tool_calls.append(AssistantPromptMessage.ToolCall(
+ id=tool_call_id,
+ type='function',
+ function=AssistantPromptMessage.ToolCall.ToolCallFunction(
+ name=tool,
+ arguments=json.dumps(tool_inputs.get(tool, {})),
+ )
+ ))
+ tool_call_response.append(ToolPromptMessage(
+ content=agent_thought.observation,
+ name=tool,
+ tool_call_id=tool_call_id,
+ ))
+
+ result.extend([
+ AssistantPromptMessage(
+ content=agent_thought.thought,
+ tool_calls=tool_calls,
+ ),
+ *tool_call_response
+ ])
+
+ return result
\ No newline at end of file
diff --git a/api/core/features/assistant_cot_runner.py b/api/core/features/assistant_cot_runner.py
index b8d08bb5d3..aa4a6797cd 100644
--- a/api/core/features/assistant_cot_runner.py
+++ b/api/core/features/assistant_cot_runner.py
@@ -12,6 +12,7 @@ from core.model_runtime.entities.message_entities import (
PromptMessage,
PromptMessageTool,
SystemPromptMessage,
+ ToolPromptMessage,
UserPromptMessage,
)
from core.model_runtime.utils.encoders import jsonable_encoder
@@ -39,6 +40,7 @@ class AssistantCotApplicationRunner(BaseAssistantApplicationRunner):
self._repack_app_orchestration_config(app_orchestration_config)
agent_scratchpad: list[AgentScratchpadUnit] = []
+ self._init_agent_scratchpad(agent_scratchpad, self.history_prompt_messages)
# check model mode
if self.app_orchestration_config.model_config.mode == "completion":
@@ -131,61 +133,95 @@ class AssistantCotApplicationRunner(BaseAssistantApplicationRunner):
# recale llm max tokens
self.recale_llm_max_tokens(self.model_config, prompt_messages)
# invoke model
- llm_result: LLMResult = model_instance.invoke_llm(
+ chunks: Generator[LLMResultChunk, None, None] = model_instance.invoke_llm(
prompt_messages=prompt_messages,
model_parameters=app_orchestration_config.model_config.parameters,
tools=[],
stop=app_orchestration_config.model_config.stop,
- stream=False,
+ stream=True,
user=self.user_id,
callbacks=[],
)
# check llm result
- if not llm_result:
+ if not chunks:
raise ValueError("failed to invoke llm")
-
- # get scratchpad
- scratchpad = self._extract_response_scratchpad(llm_result.message.content)
- agent_scratchpad.append(scratchpad)
-
- # get llm usage
- if llm_result.usage:
- increase_usage(llm_usage, llm_result.usage)
+ usage_dict = {}
+ react_chunks = self._handle_stream_react(chunks, usage_dict)
+ scratchpad = AgentScratchpadUnit(
+ agent_response='',
+ thought='',
+ action_str='',
+ observation='',
+ action=None
+ )
+
# publish agent thought if it's first iteration
if iteration_step == 1:
self.queue_manager.publish_agent_thought(agent_thought, PublishFrom.APPLICATION_MANAGER)
+ for chunk in react_chunks:
+ if isinstance(chunk, dict):
+ scratchpad.agent_response += json.dumps(chunk)
+ try:
+ if scratchpad.action:
+ raise Exception("")
+ scratchpad.action_str = json.dumps(chunk)
+ scratchpad.action = AgentScratchpadUnit.Action(
+ action_name=chunk['action'],
+ action_input=chunk['action_input']
+ )
+ except:
+ scratchpad.thought += json.dumps(chunk)
+ yield LLMResultChunk(
+ model=self.model_config.model,
+ prompt_messages=prompt_messages,
+ system_fingerprint='',
+ delta=LLMResultChunkDelta(
+ index=0,
+ message=AssistantPromptMessage(
+ content=json.dumps(chunk)
+ ),
+ usage=None
+ )
+ )
+ else:
+ scratchpad.agent_response += chunk
+ scratchpad.thought += chunk
+ yield LLMResultChunk(
+ model=self.model_config.model,
+ prompt_messages=prompt_messages,
+ system_fingerprint='',
+ delta=LLMResultChunkDelta(
+ index=0,
+ message=AssistantPromptMessage(
+ content=chunk
+ ),
+ usage=None
+ )
+ )
+
+ agent_scratchpad.append(scratchpad)
+
+ # get llm usage
+ if 'usage' in usage_dict:
+ increase_usage(llm_usage, usage_dict['usage'])
+ else:
+ usage_dict['usage'] = LLMUsage.empty_usage()
+
self.save_agent_thought(agent_thought=agent_thought,
tool_name=scratchpad.action.action_name if scratchpad.action else '',
tool_input=scratchpad.action.action_input if scratchpad.action else '',
thought=scratchpad.thought,
observation='',
- answer=llm_result.message.content,
+ answer=scratchpad.agent_response,
messages_ids=[],
- llm_usage=llm_result.usage)
+ llm_usage=usage_dict['usage'])
if scratchpad.action and scratchpad.action.action_name.lower() != "final answer":
self.queue_manager.publish_agent_thought(agent_thought, PublishFrom.APPLICATION_MANAGER)
- # publish agent thought if it's not empty and there is a action
- if scratchpad.thought and scratchpad.action:
- # check if final answer
- if not scratchpad.action.action_name.lower() == "final answer":
- yield LLMResultChunk(
- model=model_instance.model,
- prompt_messages=prompt_messages,
- delta=LLMResultChunkDelta(
- index=0,
- message=AssistantPromptMessage(
- content=scratchpad.thought
- ),
- usage=llm_result.usage,
- ),
- system_fingerprint=''
- )
-
if not scratchpad.action:
# failed to extract action, return final answer directly
final_answer = scratchpad.agent_response or ''
@@ -260,7 +296,6 @@ class AssistantCotApplicationRunner(BaseAssistantApplicationRunner):
# save scratchpad
scratchpad.observation = observation
- scratchpad.agent_response = llm_result.message.content
# save agent thought
self.save_agent_thought(
@@ -269,7 +304,7 @@ class AssistantCotApplicationRunner(BaseAssistantApplicationRunner):
tool_input=tool_call_args,
thought=None,
observation=observation,
- answer=llm_result.message.content,
+ answer=scratchpad.agent_response,
messages_ids=message_file_ids,
)
self.queue_manager.publish_agent_thought(agent_thought, PublishFrom.APPLICATION_MANAGER)
@@ -316,6 +351,97 @@ class AssistantCotApplicationRunner(BaseAssistantApplicationRunner):
system_fingerprint=''
), PublishFrom.APPLICATION_MANAGER)
+ def _handle_stream_react(self, llm_response: Generator[LLMResultChunk, None, None], usage: dict) \
+ -> Generator[Union[str, dict], None, None]:
+ def parse_json(json_str):
+ try:
+ return json.loads(json_str.strip())
+ except:
+ return json_str
+
+ def extra_json_from_code_block(code_block) -> Generator[Union[dict, str], None, None]:
+ code_blocks = re.findall(r'```(.*?)```', code_block, re.DOTALL)
+ if not code_blocks:
+ return
+ for block in code_blocks:
+ json_text = re.sub(r'^[a-zA-Z]+\n', '', block.strip(), flags=re.MULTILINE)
+ yield parse_json(json_text)
+
+ code_block_cache = ''
+ code_block_delimiter_count = 0
+ in_code_block = False
+ json_cache = ''
+ json_quote_count = 0
+ in_json = False
+ got_json = False
+
+ for response in llm_response:
+ response = response.delta.message.content
+ if not isinstance(response, str):
+ continue
+
+ # stream
+ index = 0
+ while index < len(response):
+ steps = 1
+ delta = response[index:index+steps]
+ if delta == '`':
+ code_block_cache += delta
+ code_block_delimiter_count += 1
+ else:
+ if not in_code_block:
+ if code_block_delimiter_count > 0:
+ yield code_block_cache
+ code_block_cache = ''
+ else:
+ code_block_cache += delta
+ code_block_delimiter_count = 0
+
+ if code_block_delimiter_count == 3:
+ if in_code_block:
+ yield from extra_json_from_code_block(code_block_cache)
+ code_block_cache = ''
+
+ in_code_block = not in_code_block
+ code_block_delimiter_count = 0
+
+ if not in_code_block:
+ # handle single json
+ if delta == '{':
+ json_quote_count += 1
+ in_json = True
+ json_cache += delta
+ elif delta == '}':
+ json_cache += delta
+ if json_quote_count > 0:
+ json_quote_count -= 1
+ if json_quote_count == 0:
+ in_json = False
+ got_json = True
+ index += steps
+ continue
+ else:
+ if in_json:
+ json_cache += delta
+
+ if got_json:
+ got_json = False
+ yield parse_json(json_cache)
+ json_cache = ''
+ json_quote_count = 0
+ in_json = False
+
+ if not in_code_block and not in_json:
+ yield delta.replace('`', '')
+
+ index += steps
+
+ if code_block_cache:
+ yield code_block_cache
+
+ if json_cache:
+ yield parse_json(json_cache)
+
def _fill_in_inputs_from_external_data_tools(self, instruction: str, inputs: dict) -> str:
"""
fill in inputs from external data tools
@@ -327,122 +453,40 @@ class AssistantCotApplicationRunner(BaseAssistantApplicationRunner):
continue
return instruction
-
- def _extract_response_scratchpad(self, content: str) -> AgentScratchpadUnit:
+
+ def _init_agent_scratchpad(self,
+ agent_scratchpad: list[AgentScratchpadUnit],
+ messages: list[PromptMessage]
+ ) -> list[AgentScratchpadUnit]:
"""
- extract response from llm response
+ init agent scratchpad
"""
- def extra_quotes() -> AgentScratchpadUnit:
- agent_response = content
- # try to extract all quotes
- pattern = re.compile(r'```(.*?)```', re.DOTALL)
- quotes = pattern.findall(content)
-
- # try to extract action from end to start
- for i in range(len(quotes) - 1, 0, -1):
- """
- 1. use json load to parse action
- 2. use plain text `Action: xxx` to parse action
- """
- try:
- action = json.loads(quotes[i].replace('```', ''))
- action_name = action.get("action")
- action_input = action.get("action_input")
- agent_thought = agent_response.replace(quotes[i], '')
-
- if action_name and action_input:
- return AgentScratchpadUnit(
- agent_response=content,
- thought=agent_thought,
- action_str=quotes[i],
- action=AgentScratchpadUnit.Action(
- action_name=action_name,
- action_input=action_input,
- )
+ current_scratchpad: AgentScratchpadUnit = None
+ for message in messages:
+ if isinstance(message, AssistantPromptMessage):
+ current_scratchpad = AgentScratchpadUnit(
+ agent_response=message.content,
+ thought=message.content,
+ action_str='',
+ action=None,
+ observation=None
+ )
+ if message.tool_calls:
+ try:
+ current_scratchpad.action = AgentScratchpadUnit.Action(
+ action_name=message.tool_calls[0].function.name,
+ action_input=json.loads(message.tool_calls[0].function.arguments)
)
- except:
- # try to parse action from plain text
- action_name = re.findall(r'action: (.*)', quotes[i], re.IGNORECASE)
- action_input = re.findall(r'action input: (.*)', quotes[i], re.IGNORECASE)
- # delete action from agent response
- agent_thought = agent_response.replace(quotes[i], '')
- # remove extra quotes
- agent_thought = re.sub(r'```(json)*\n*```', '', agent_thought, flags=re.DOTALL)
- # remove Action: xxx from agent thought
- agent_thought = re.sub(r'Action:.*', '', agent_thought, flags=re.IGNORECASE)
+ except:
+ pass
+
+ agent_scratchpad.append(current_scratchpad)
+ elif isinstance(message, ToolPromptMessage):
+ if current_scratchpad:
+ current_scratchpad.observation = message.content
- if action_name and action_input:
- return AgentScratchpadUnit(
- agent_response=content,
- thought=agent_thought,
- action_str=quotes[i],
- action=AgentScratchpadUnit.Action(
- action_name=action_name[0],
- action_input=action_input[0],
- )
- )
+ return agent_scratchpad
- def extra_json():
- agent_response = content
- # try to extract all json
- structures, pair_match_stack = [], []
- started_at, end_at = 0, 0
- for i in range(len(content)):
- if content[i] == '{':
- pair_match_stack.append(i)
- if len(pair_match_stack) == 1:
- started_at = i
- elif content[i] == '}':
- begin = pair_match_stack.pop()
- if not pair_match_stack:
- end_at = i + 1
- structures.append((content[begin:i+1], (started_at, end_at)))
-
- # handle the last character
- if pair_match_stack:
- end_at = len(content)
- structures.append((content[pair_match_stack[0]:], (started_at, end_at)))
-
- for i in range(len(structures), 0, -1):
- try:
- json_content, (started_at, end_at) = structures[i - 1]
- action = json.loads(json_content)
- action_name = action.get("action")
- action_input = action.get("action_input")
- # delete json content from agent response
- agent_thought = agent_response[:started_at] + agent_response[end_at:]
- # remove extra quotes like ```(json)*\n\n```
- agent_thought = re.sub(r'```(json)*\n*```', '', agent_thought, flags=re.DOTALL)
- # remove Action: xxx from agent thought
- agent_thought = re.sub(r'Action:.*', '', agent_thought, flags=re.IGNORECASE)
-
- if action_name and action_input is not None:
- return AgentScratchpadUnit(
- agent_response=content,
- thought=agent_thought,
- action_str=json_content,
- action=AgentScratchpadUnit.Action(
- action_name=action_name,
- action_input=action_input,
- )
- )
- except:
- pass
-
- agent_scratchpad = extra_quotes()
- if agent_scratchpad:
- return agent_scratchpad
- agent_scratchpad = extra_json()
- if agent_scratchpad:
- return agent_scratchpad
-
- return AgentScratchpadUnit(
- agent_response=content,
- thought=content,
- action_str='',
- action=None
- )
-
def _check_cot_prompt_messages(self, mode: Literal["completion", "chat"],
agent_prompt_message: AgentPromptEntity,
):
@@ -556,15 +600,15 @@ class AssistantCotApplicationRunner(BaseAssistantApplicationRunner):
# organize prompt messages
if mode == "chat":
# override system message
- overrided = False
+ overridden = False
prompt_messages = prompt_messages.copy()
for prompt_message in prompt_messages:
if isinstance(prompt_message, SystemPromptMessage):
prompt_message.content = system_message
- overrided = True
+ overridden = True
break
- if not overrided:
+ if not overridden:
prompt_messages.insert(0, SystemPromptMessage(
content=system_message,
))
diff --git a/api/core/model_runtime/model_providers/openai/llm/gpt-4-0125-preview.yaml b/api/core/model_runtime/model_providers/openai/llm/gpt-4-0125-preview.yaml
index 943a6de321..007cfed0f3 100644
--- a/api/core/model_runtime/model_providers/openai/llm/gpt-4-0125-preview.yaml
+++ b/api/core/model_runtime/model_providers/openai/llm/gpt-4-0125-preview.yaml
@@ -37,9 +37,6 @@ parameter_rules:
the same result. Determinism is not guaranteed, and you should refer to the
system_fingerprint response parameter to monitor changes in the backend.
required: false
- precision: 2
- min: 0
- max: 1
- name: response_format
label:
zh_Hans: 回复格式
diff --git a/api/core/model_runtime/model_providers/openai/llm/gpt-4-1106-preview.yaml b/api/core/model_runtime/model_providers/openai/llm/gpt-4-1106-preview.yaml
index 7f3bdaeac1..f4fa6317af 100644
--- a/api/core/model_runtime/model_providers/openai/llm/gpt-4-1106-preview.yaml
+++ b/api/core/model_runtime/model_providers/openai/llm/gpt-4-1106-preview.yaml
@@ -37,9 +37,6 @@ parameter_rules:
the same result. Determinism is not guaranteed, and you should refer to the
system_fingerprint response parameter to monitor changes in the backend.
required: false
- precision: 2
- min: 0
- max: 1
- name: response_format
label:
zh_Hans: 回复格式
diff --git a/api/core/model_runtime/model_providers/openai/llm/gpt-4-32k.yaml b/api/core/model_runtime/model_providers/openai/llm/gpt-4-32k.yaml
index b1e61317e9..f92173ccfd 100644
--- a/api/core/model_runtime/model_providers/openai/llm/gpt-4-32k.yaml
+++ b/api/core/model_runtime/model_providers/openai/llm/gpt-4-32k.yaml
@@ -37,9 +37,6 @@ parameter_rules:
the same result. Determinism is not guaranteed, and you should refer to the
system_fingerprint response parameter to monitor changes in the backend.
required: false
- precision: 2
- min: 0
- max: 1
- name: response_format
label:
zh_Hans: 回复格式
diff --git a/api/core/model_runtime/model_providers/openai/llm/gpt-4-turbo-preview.yaml b/api/core/model_runtime/model_providers/openai/llm/gpt-4-turbo-preview.yaml
index b109cfc814..c0350ae2c6 100644
--- a/api/core/model_runtime/model_providers/openai/llm/gpt-4-turbo-preview.yaml
+++ b/api/core/model_runtime/model_providers/openai/llm/gpt-4-turbo-preview.yaml
@@ -37,9 +37,6 @@ parameter_rules:
the same result. Determinism is not guaranteed, and you should refer to the
system_fingerprint response parameter to monitor changes in the backend.
required: false
- precision: 2
- min: 0
- max: 1
- name: response_format
label:
zh_Hans: 回复格式
diff --git a/api/core/model_runtime/model_providers/openai/llm/gpt-4-vision-preview.yaml b/api/core/model_runtime/model_providers/openai/llm/gpt-4-vision-preview.yaml
index b7044d2066..a63b608423 100644
--- a/api/core/model_runtime/model_providers/openai/llm/gpt-4-vision-preview.yaml
+++ b/api/core/model_runtime/model_providers/openai/llm/gpt-4-vision-preview.yaml
@@ -35,9 +35,6 @@ parameter_rules:
the same result. Determinism is not guaranteed, and you should refer to the
system_fingerprint response parameter to monitor changes in the backend.
required: false
- precision: 2
- min: 0
- max: 1
- name: response_format
label:
zh_Hans: 回复格式
diff --git a/api/core/model_runtime/model_providers/openai/llm/gpt-4.yaml b/api/core/model_runtime/model_providers/openai/llm/gpt-4.yaml
index 48e8930608..a7a5bf3c86 100644
--- a/api/core/model_runtime/model_providers/openai/llm/gpt-4.yaml
+++ b/api/core/model_runtime/model_providers/openai/llm/gpt-4.yaml
@@ -37,9 +37,6 @@ parameter_rules:
the same result. Determinism is not guaranteed, and you should refer to the
system_fingerprint response parameter to monitor changes in the backend.
required: false
- precision: 2
- min: 0
- max: 1
- name: response_format
label:
zh_Hans: 回复格式
diff --git a/api/core/model_runtime/model_providers/openai_api_compatible/llm/llm.py b/api/core/model_runtime/model_providers/openai_api_compatible/llm/llm.py
index cf90633aa6..d294fcaa9c 100644
--- a/api/core/model_runtime/model_providers/openai_api_compatible/llm/llm.py
+++ b/api/core/model_runtime/model_providers/openai_api_compatible/llm/llm.py
@@ -376,7 +376,6 @@ class OAIAPICompatLargeLanguageModel(_CommonOAI_API_Compat, LargeLanguageModel):
chunk_json = json.loads(decoded_chunk)
# stream ended
except json.JSONDecodeError as e:
- logger.error(f"decoded_chunk error: {e}, delimiter={delimiter}, decoded_chunk={decoded_chunk}")
yield create_final_llm_result_chunk(
index=chunk_index + 1,
message=AssistantPromptMessage(content=""),
diff --git a/api/core/tools/provider/builtin/duckduckgo/_assets/icon.svg b/api/core/tools/provider/builtin/duckduckgo/_assets/icon.svg
new file mode 100644
index 0000000000..a816a6b49e
--- /dev/null
+++ b/api/core/tools/provider/builtin/duckduckgo/_assets/icon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/api/core/tools/provider/builtin/duckduckgo/duckduckgo.py b/api/core/tools/provider/builtin/duckduckgo/duckduckgo.py
new file mode 100644
index 0000000000..3e9b57ece7
--- /dev/null
+++ b/api/core/tools/provider/builtin/duckduckgo/duckduckgo.py
@@ -0,0 +1,20 @@
+from core.tools.errors import ToolProviderCredentialValidationError
+from core.tools.provider.builtin.duckduckgo.tools.duckduckgo_search import DuckDuckGoSearchTool
+from core.tools.provider.builtin_tool_provider import BuiltinToolProviderController
+
+
+class DuckDuckGoProvider(BuiltinToolProviderController):
+ def _validate_credentials(self, credentials: dict) -> None:
+ try:
+ DuckDuckGoSearchTool().fork_tool_runtime(
+ meta={
+ "credentials": credentials,
+ }
+ ).invoke(
+ user_id='',
+ tool_parameters={
+ "query": "John Doe",
+ },
+ )
+ except Exception as e:
+ raise ToolProviderCredentialValidationError(str(e))
\ No newline at end of file
diff --git a/api/core/tools/provider/builtin/duckduckgo/duckduckgo.yaml b/api/core/tools/provider/builtin/duckduckgo/duckduckgo.yaml
new file mode 100644
index 0000000000..8778dde625
--- /dev/null
+++ b/api/core/tools/provider/builtin/duckduckgo/duckduckgo.yaml
@@ -0,0 +1,10 @@
+identity:
+ author: Yash Parmar
+ name: duckduckgo
+ label:
+ en_US: DuckDuckGo
+ zh_Hans: DuckDuckGo
+ description:
+ en_US: A privacy-focused search engine.
+ zh_Hans: 一个注重隐私的搜索引擎。
+ icon: icon.svg
diff --git a/api/core/tools/provider/builtin/duckduckgo/tools/duckduckgo_search.py b/api/core/tools/provider/builtin/duckduckgo/tools/duckduckgo_search.py
new file mode 100644
index 0000000000..6046a18930
--- /dev/null
+++ b/api/core/tools/provider/builtin/duckduckgo/tools/duckduckgo_search.py
@@ -0,0 +1,40 @@
+from typing import Any
+
+from langchain.tools import DuckDuckGoSearchRun
+from pydantic import BaseModel, Field
+
+from core.tools.entities.tool_entities import ToolInvokeMessage
+from core.tools.tool.builtin_tool import BuiltinTool
+
+
+class DuckDuckGoInput(BaseModel):
+ query: str = Field(..., description="Search query.")
+
+
+class DuckDuckGoSearchTool(BuiltinTool):
+ """
+ Tool for performing a search using DuckDuckGo search engine.
+ """
+
+ def _invoke(self, user_id: str, tool_parameters: dict[str, Any]) -> ToolInvokeMessage | list[ToolInvokeMessage]:
+ """
+ Invoke the DuckDuckGo search tool.
+
+ Args:
+ user_id (str): The ID of the user invoking the tool.
+ tool_parameters (dict[str, Any]): The parameters for the tool invocation.
+
+ Returns:
+ ToolInvokeMessage | list[ToolInvokeMessage]: The result of the tool invocation.
+ """
+ query = tool_parameters.get('query', '')
+
+ if not query:
+ return self.create_text_message('Please input query')
+
+ tool = DuckDuckGoSearchRun(args_schema=DuckDuckGoInput)
+
+ result = tool.run(query)
+
+ return self.create_text_message(self.summary(user_id=user_id, content=result))
+
\ No newline at end of file
diff --git a/api/core/tools/provider/builtin/duckduckgo/tools/duckduckgo_search.yaml b/api/core/tools/provider/builtin/duckduckgo/tools/duckduckgo_search.yaml
new file mode 100644
index 0000000000..93c857010c
--- /dev/null
+++ b/api/core/tools/provider/builtin/duckduckgo/tools/duckduckgo_search.yaml
@@ -0,0 +1,23 @@
+identity:
+ name: duckduckgo_search
+ author: Yash Parmar
+ label:
+ en_US: DuckDuckGo Search
+ zh_Hans: DuckDuckGo 搜索
+description:
+ human:
+ en_US: Perform searches on DuckDuckGo and get results.
+ zh_Hans: 在 DuckDuckGo 上进行搜索并获取结果。
+ llm: Perform searches on DuckDuckGo and get results.
+parameters:
+ - name: query
+ type: string
+ required: true
+ label:
+ en_US: Query string
+ zh_Hans: 查询语句
+ human_description:
+ en_US: The search query.
+ zh_Hans: 搜索查询语句。
+ llm_description: Key words for searching
+ form: llm
diff --git a/api/core/tools/provider/builtin_tool_provider.py b/api/core/tools/provider/builtin_tool_provider.py
index 0dc29b8a04..93e7d5a39e 100644
--- a/api/core/tools/provider/builtin_tool_provider.py
+++ b/api/core/tools/provider/builtin_tool_provider.py
@@ -58,7 +58,7 @@ class BuiltinToolProviderController(ToolProviderController):
tool_files = list(filter(lambda x: x.endswith(".yaml") and not x.startswith("__"), listdir(tool_path)))
tools = []
for tool_file in tool_files:
- with open(path.join(tool_path, tool_file)) as f:
+ with open(path.join(tool_path, tool_file), encoding='utf-8') as f:
# get tool name
tool_name = tool_file.split(".")[0]
tool = load(f.read(), FullLoader)
@@ -287,4 +287,4 @@ class BuiltinToolProviderController(ToolProviderController):
:param tool_name: the name of the tool, defined in `get_tools`
:param credentials: the credentials of the tool
"""
- pass
\ No newline at end of file
+ pass
diff --git a/api/core/tools/tool/api_tool.py b/api/core/tools/tool/api_tool.py
index 30e3f96afd..f6914d3473 100644
--- a/api/core/tools/tool/api_tool.py
+++ b/api/core/tools/tool/api_tool.py
@@ -127,6 +127,8 @@ class ApiTool(Tool):
value = parameters[parameter['name']]
elif parameter['required']:
raise ToolProviderCredentialValidationError(f"Missing required parameter {parameter['name']}")
+ else:
+ value = (parameter.get('schema', {}) or {}).get('default', '')
path_params[parameter['name']] = value
elif parameter['in'] == 'query':
@@ -135,6 +137,8 @@ class ApiTool(Tool):
value = parameters[parameter['name']]
elif parameter['required']:
raise ToolProviderCredentialValidationError(f"Missing required parameter {parameter['name']}")
+ else:
+ value = (parameter.get('schema', {}) or {}).get('default', '')
params[parameter['name']] = value
elif parameter['in'] == 'cookie':
@@ -143,6 +147,8 @@ class ApiTool(Tool):
value = parameters[parameter['name']]
elif parameter['required']:
raise ToolProviderCredentialValidationError(f"Missing required parameter {parameter['name']}")
+ else:
+ value = (parameter.get('schema', {}) or {}).get('default', '')
cookies[parameter['name']] = value
elif parameter['in'] == 'header':
@@ -151,6 +157,8 @@ class ApiTool(Tool):
value = parameters[parameter['name']]
elif parameter['required']:
raise ToolProviderCredentialValidationError(f"Missing required parameter {parameter['name']}")
+ else:
+ value = (parameter.get('schema', {}) or {}).get('default', '')
headers[parameter['name']] = value
# check if there is a request body and handle it
diff --git a/api/core/tools/utils/parser.py b/api/core/tools/utils/parser.py
index 68a8adc11d..91c18be3f5 100644
--- a/api/core/tools/utils/parser.py
+++ b/api/core/tools/utils/parser.py
@@ -60,7 +60,7 @@ class ApiBasedToolSchemaParser:
required=parameter.get('required', False),
form=ToolParameter.ToolParameterForm.LLM,
llm_description=parameter.get('description'),
- default=parameter['default'] if 'default' in parameter else None,
+ default=parameter['schema']['default'] if 'schema' in parameter and 'default' in parameter['schema'] else None,
))
# create tool bundle
# check if there is a request body
@@ -115,7 +115,12 @@ class ApiBasedToolSchemaParser:
# check if there is a operation id, use $path_$method as operation id if not
if 'operationId' not in interface['operation']:
- interface['operation']['operationId'] = f'{interface["path"]}_{interface["method"]}'
+ # remove special characters like / to ensure the operation id is valid ^[a-zA-Z0-9_-]{1,64}$
+ path = interface['path']
+ if interface['path'].startswith('/'):
+ path = interface['path'][1:]
+ path = path.replace('/', '_')
+ interface['operation']['operationId'] = f'{path}_{interface["method"]}'
bundles.append(ApiBasedToolBundle(
server_url=server_url + interface['path'],
diff --git a/api/pyproject.toml b/api/pyproject.toml
index a04246bb52..3ec759386b 100644
--- a/api/pyproject.toml
+++ b/api/pyproject.toml
@@ -3,6 +3,7 @@ requires-python = ">=3.10"
[tool.ruff]
exclude = [
+ "app.py",
"__init__.py",
"tests/",
]
diff --git a/api/requirements.txt b/api/requirements.txt
index af29654248..5881c99903 100644
--- a/api/requirements.txt
+++ b/api/requirements.txt
@@ -65,4 +65,5 @@ matplotlib~=3.8.2
yfinance~=0.2.35
pydub~=0.25.1
gmpy2~=2.1.5
-numexpr~=2.9.0
\ No newline at end of file
+numexpr~=2.9.0
+duckduckgo-search==4.4.3
\ No newline at end of file
diff --git a/api/services/audio_service.py b/api/services/audio_service.py
index 0161fde7bb..a9fe65df6f 100644
--- a/api/services/audio_service.py
+++ b/api/services/audio_service.py
@@ -20,7 +20,7 @@ ALLOWED_EXTENSIONS = ['mp3', 'mp4', 'mpeg', 'mpga', 'm4a', 'wav', 'webm', 'amr']
class AudioService:
@classmethod
- def transcript_asr(cls, tenant_id: str, file: FileStorage, promot: str, end_user: Optional[str] = None):
+ def transcript_asr(cls, tenant_id: str, file: FileStorage, end_user: Optional[str] = None):
if file is None:
raise NoAudioUploadedServiceError()
diff --git a/api/services/file_service.py b/api/services/file_service.py
index 3c56e6808e..215ccf688a 100644
--- a/api/services/file_service.py
+++ b/api/services/file_service.py
@@ -20,9 +20,9 @@ from services.errors.file import FileTooLargeError, UnsupportedFileTypeError
IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'png', 'webp', 'gif', 'svg']
IMAGE_EXTENSIONS.extend([ext.upper() for ext in IMAGE_EXTENSIONS])
-ALLOWED_EXTENSIONS = ['txt', 'markdown', 'md', 'pdf', 'html', 'htm', 'xlsx', 'docx', 'doc', 'csv'] + IMAGE_EXTENSIONS
+ALLOWED_EXTENSIONS = ['txt', 'markdown', 'md', 'pdf', 'html', 'htm', 'xlsx', 'docx', 'csv'] + IMAGE_EXTENSIONS
UNSTRUSTURED_ALLOWED_EXTENSIONS = ['txt', 'markdown', 'md', 'pdf', 'html', 'htm', 'xlsx',
- 'docx', 'doc', 'csv', 'eml', 'msg', 'pptx', 'ppt', 'xml'] + IMAGE_EXTENSIONS
+ 'docx', 'csv', 'eml', 'msg', 'pptx', 'ppt', 'xml'] + IMAGE_EXTENSIONS
PREVIEW_WORDS_LIMIT = 3000
@@ -162,7 +162,7 @@ class FileService:
generator = storage.load(upload_file.key, stream=True)
return generator, upload_file.mime_type
-
+
@staticmethod
def get_public_image_preview(file_id: str) -> str:
upload_file = db.session.query(UploadFile) \
diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml
index 1d6f5d7f86..e3a7bdbbe2 100644
--- a/docker/docker-compose.yaml
+++ b/docker/docker-compose.yaml
@@ -2,7 +2,7 @@ version: '3.1'
services:
# API service
api:
- image: langgenius/dify-api:0.5.5
+ image: langgenius/dify-api:0.5.6
restart: always
environment:
# Startup mode, 'api' starts the API server.
@@ -135,7 +135,7 @@ services:
# worker service
# The Celery worker for processing the queue.
worker:
- image: langgenius/dify-api:0.5.5
+ image: langgenius/dify-api:0.5.6
restart: always
environment:
# Startup mode, 'worker' starts the Celery worker for processing the queue.
@@ -206,7 +206,7 @@ services:
# Frontend web application.
web:
- image: langgenius/dify-web:0.5.5
+ image: langgenius/dify-web:0.5.6
restart: always
environment:
EDITION: SELF_HOSTED
diff --git a/web/app/components/app/configuration/config-var/index.tsx b/web/app/components/app/configuration/config-var/index.tsx
index 671ff9746e..9aeb00de8f 100644
--- a/web/app/components/app/configuration/config-var/index.tsx
+++ b/web/app/components/app/configuration/config-var/index.tsx
@@ -147,6 +147,7 @@ const ConfigVar: FC = ({ promptVariables, readonly, onPromptVar
) => {
setShowExternalDataToolModal({
payload: {
+ type,
variable: key,
label: name,
config,
@@ -245,7 +246,7 @@ const ConfigVar: FC = ({ promptVariables, readonly, onPromptVar
const handleConfig = ({ key, type, index, name, config, icon, icon_background }: ExternalDataToolParams) => {
setCurrKey(key)
- if (type === 'api') {
+ if (type !== 'string' && type !== 'paragraph' && type !== 'select') {
handleOpenExternalDataToolModal({ key, type, index, name, config, icon, icon_background }, promptVariables)
return
}
diff --git a/web/app/components/app/configuration/debug/index.tsx b/web/app/components/app/configuration/debug/index.tsx
index 194bc1f8b5..68bbdacfad 100644
--- a/web/app/components/app/configuration/debug/index.tsx
+++ b/web/app/components/app/configuration/debug/index.tsx
@@ -149,7 +149,7 @@ const Debug: FC = ({
}
let hasEmptyInput = ''
const requiredVars = modelConfig.configs.prompt_variables.filter(({ key, name, required, type }) => {
- if (type === 'api')
+ if (type !== 'string' && type !== 'paragraph' && type !== 'select')
return false
const res = (!key || !key.trim()) || (!name || !name.trim()) || (required || required === undefined || required === null)
return res
diff --git a/web/app/components/app/configuration/toolbox/moderation/form-generation.tsx b/web/app/components/app/configuration/toolbox/moderation/form-generation.tsx
index 15a237efee..daf964447b 100644
--- a/web/app/components/app/configuration/toolbox/moderation/form-generation.tsx
+++ b/web/app/components/app/configuration/toolbox/moderation/form-generation.tsx
@@ -65,7 +65,7 @@ const FormGeneration: FC = ({
}
})}
onSelect={item => handleFormChange(form.variable, item.value as string)}
- popupClassName='w-[576px]'
+ popupClassName='w-[576px] !z-[102]'
/>
)
}
diff --git a/web/app/components/base/chat/chat/hooks.ts b/web/app/components/base/chat/chat/hooks.ts
index 896ef74369..d870cfc79f 100644
--- a/web/app/components/base/chat/chat/hooks.ts
+++ b/web/app/components/base/chat/chat/hooks.ts
@@ -42,7 +42,7 @@ export const useCheckPromptVariables = () => {
} = promptVariablesConfig
let hasEmptyInput = ''
const requiredVars = promptVariables.filter(({ key, name, required, type }) => {
- if (type === 'api')
+ if (type !== 'string' && type !== 'paragraph' && type !== 'select')
return false
const res = (!key || !key.trim()) || (!name || !name.trim()) || (required || required === undefined || required === null)
return res
@@ -146,8 +146,9 @@ export const useChat = (
}, [stopChat, handleResponsing])
const handleRestart = useCallback(() => {
- handleStop()
connversationId.current = ''
+ taskIdRef.current = ''
+ handleStop()
const newChatList = config?.opening_statement
? [{
id: `${Date.now()}`,
diff --git a/web/app/components/develop/template/template_chat.en.mdx b/web/app/components/develop/template/template_chat.en.mdx
index f3891621f0..7963b38b84 100644
--- a/web/app/components/develop/template/template_chat.en.mdx
+++ b/web/app/components/develop/template/template_chat.en.mdx
@@ -167,7 +167,7 @@ Chat applications support session persistence, allowing previous chat history to
-
+
```bash {{ title: 'cURL' }}
curl -X POST '${props.appDetail.api_base_url}/chat-messages' \
diff --git a/web/app/components/develop/template/template_chat.zh.mdx b/web/app/components/develop/template/template_chat.zh.mdx
index 0f17006d3b..71e101e208 100644
--- a/web/app/components/develop/template/template_chat.zh.mdx
+++ b/web/app/components/develop/template/template_chat.zh.mdx
@@ -170,7 +170,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
-
+
```bash {{ title: 'cURL' }}
curl -X POST '${props.appDetail.api_base_url}/chat-messages' \
diff --git a/web/config/index.ts b/web/config/index.ts
index 8baf6d9d25..b6b6eb3ee7 100644
--- a/web/config/index.ts
+++ b/web/config/index.ts
@@ -130,7 +130,7 @@ export const ANNOTATION_DEFAULT = {
score_threshold: 0.9,
}
-export const MAX_TOOLS_NUM = 5
+export const MAX_TOOLS_NUM = 10
export const DEFAULT_AGENT_SETTING = {
enabled: false,
diff --git a/web/package.json b/web/package.json
index 660873c6a7..ebe32df75e 100644
--- a/web/package.json
+++ b/web/package.json
@@ -1,6 +1,6 @@
{
"name": "dify-web",
- "version": "0.5.5",
+ "version": "0.5.6",
"private": true,
"scripts": {
"dev": "next dev",
diff --git a/web/utils/model-config.ts b/web/utils/model-config.ts
index c465b57dfc..66b8babb21 100644
--- a/web/utils/model-config.ts
+++ b/web/utils/model-config.ts
@@ -16,7 +16,7 @@ export const userInputsFormToPromptVariables = (useInputs: UserInputFormItem[] |
return ['string', item['text-input']]
if (item.external_data_tool)
- return ['api', item.external_data_tool]
+ return [item.external_data_tool.type, item.external_data_tool]
return ['select', item.select]
})()
@@ -33,7 +33,17 @@ export const userInputsFormToPromptVariables = (useInputs: UserInputFormItem[] |
is_context_var,
})
}
- else if (type === 'api') {
+ else if (type === 'select') {
+ promptVariables.push({
+ key: content.variable,
+ name: content.label,
+ required: content.required,
+ type: 'select',
+ options: content.options,
+ is_context_var,
+ })
+ }
+ else {
promptVariables.push({
key: content.variable,
name: content.label,
@@ -46,16 +56,6 @@ export const userInputsFormToPromptVariables = (useInputs: UserInputFormItem[] |
is_context_var,
})
}
- else {
- promptVariables.push({
- key: content.variable,
- name: content.label,
- required: content.required,
- type: 'select',
- options: content.options,
- is_context_var,
- })
- }
})
return promptVariables
}
@@ -79,7 +79,18 @@ export const promptVariablesToUserInputsForm = (promptVariables: PromptVariable[
},
} as any)
}
- else if (item.type === 'api') {
+ else if (item.type === 'select') {
+ userInputs.push({
+ select: {
+ label: item.name,
+ variable: item.key,
+ required: item.required !== false, // default true
+ options: item.options,
+ default: '',
+ },
+ } as any)
+ }
+ else {
userInputs.push({
external_data_tool: {
label: item.name,
@@ -93,17 +104,6 @@ export const promptVariablesToUserInputsForm = (promptVariables: PromptVariable[
},
} as any)
}
- else {
- userInputs.push({
- select: {
- label: item.name,
- variable: item.key,
- required: item.required !== false, // default true
- options: item.options,
- default: '',
- },
- } as any)
- }
})
return userInputs
}