Commit ec710d7f authored by StyleZhang's avatar StyleZhang

Merge branch 'main' into feat/workflow

parents 36718c39 31070ffb
...@@ -145,6 +145,9 @@ docker/volumes/db/data/* ...@@ -145,6 +145,9 @@ docker/volumes/db/data/*
docker/volumes/redis/data/* docker/volumes/redis/data/*
docker/volumes/weaviate/* docker/volumes/weaviate/*
docker/volumes/qdrant/* docker/volumes/qdrant/*
docker/volumes/etcd/*
docker/volumes/minio/*
docker/volumes/milvus/*
sdks/python-client/build sdks/python-client/build
sdks/python-client/dist sdks/python-client/dist
......
...@@ -26,6 +26,7 @@ from config import CloudEditionConfig, Config ...@@ -26,6 +26,7 @@ from config import CloudEditionConfig, Config
from extensions import ( from extensions import (
ext_celery, ext_celery,
ext_code_based_extension, ext_code_based_extension,
ext_compress,
ext_database, ext_database,
ext_hosting_provider, ext_hosting_provider,
ext_login, ext_login,
...@@ -96,6 +97,7 @@ def create_app(test_config=None) -> Flask: ...@@ -96,6 +97,7 @@ def create_app(test_config=None) -> Flask:
def initialize_extensions(app): def initialize_extensions(app):
# Since the application instance is now created, pass it to each Flask # Since the application instance is now created, pass it to each Flask
# extension instance to bind it to the Flask application instance (app) # extension instance to bind it to the Flask application instance (app)
ext_compress.init_app(app)
ext_code_based_extension.init() ext_code_based_extension.init()
ext_database.init_app(app) ext_database.init_app(app)
ext_migrate.init(app, db) ext_migrate.init(app, db)
......
...@@ -90,7 +90,7 @@ class Config: ...@@ -90,7 +90,7 @@ class Config:
# ------------------------ # ------------------------
# General Configurations. # General Configurations.
# ------------------------ # ------------------------
self.CURRENT_VERSION = "0.5.7" self.CURRENT_VERSION = "0.5.8"
self.COMMIT_SHA = get_env('COMMIT_SHA') self.COMMIT_SHA = get_env('COMMIT_SHA')
self.EDITION = "SELF_HOSTED" self.EDITION = "SELF_HOSTED"
self.DEPLOY_ENV = get_env('DEPLOY_ENV') self.DEPLOY_ENV = get_env('DEPLOY_ENV')
...@@ -293,6 +293,8 @@ class Config: ...@@ -293,6 +293,8 @@ class Config:
self.BATCH_UPLOAD_LIMIT = get_env('BATCH_UPLOAD_LIMIT') self.BATCH_UPLOAD_LIMIT = get_env('BATCH_UPLOAD_LIMIT')
self.API_COMPRESSION_ENABLED = get_bool_env('API_COMPRESSION_ENABLED')
class CloudEditionConfig(Config): class CloudEditionConfig(Config):
......
...@@ -88,7 +88,7 @@ class ChatMessageTextApi(Resource): ...@@ -88,7 +88,7 @@ class ChatMessageTextApi(Resource):
response = AudioService.transcript_tts( response = AudioService.transcript_tts(
tenant_id=app_model.tenant_id, tenant_id=app_model.tenant_id,
text=request.form['text'], text=request.form['text'],
voice=app_model.app_model_config.text_to_speech_dict.get('voice'), voice=request.form['voice'] if request.form['voice'] else app_model.app_model_config.text_to_speech_dict.get('voice'),
streaming=False streaming=False
) )
......
...@@ -85,7 +85,7 @@ class ChatTextApi(InstalledAppResource): ...@@ -85,7 +85,7 @@ class ChatTextApi(InstalledAppResource):
response = AudioService.transcript_tts( response = AudioService.transcript_tts(
tenant_id=app_model.tenant_id, tenant_id=app_model.tenant_id,
text=request.form['text'], text=request.form['text'],
voice=app_model.app_model_config.text_to_speech_dict.get('voice'), voice=request.form['voice'] if request.form['voice'] else app_model.app_model_config.text_to_speech_dict.get('voice'),
streaming=False streaming=False
) )
return {'data': response.data.decode('latin1')} return {'data': response.data.decode('latin1')}
......
...@@ -87,7 +87,7 @@ class TextApi(Resource): ...@@ -87,7 +87,7 @@ class TextApi(Resource):
tenant_id=app_model.tenant_id, tenant_id=app_model.tenant_id,
text=args['text'], text=args['text'],
end_user=end_user, end_user=end_user,
voice=app_model.app_model_config.text_to_speech_dict.get('voice'), voice=args['voice'] if args['voice'] else app_model.app_model_config.text_to_speech_dict.get('voice'),
streaming=args['streaming'] streaming=args['streaming']
) )
......
...@@ -84,7 +84,7 @@ class TextApi(WebApiResource): ...@@ -84,7 +84,7 @@ class TextApi(WebApiResource):
tenant_id=app_model.tenant_id, tenant_id=app_model.tenant_id,
text=request.form['text'], text=request.form['text'],
end_user=end_user.external_user_id, end_user=end_user.external_user_id,
voice=app_model.app_model_config.text_to_speech_dict.get('voice'), voice=request.form['voice'] if request.form['voice'] else app_model.app_model_config.text_to_speech_dict.get('voice'),
streaming=False streaming=False
) )
......
...@@ -28,6 +28,9 @@ from models.model import Conversation, Message ...@@ -28,6 +28,9 @@ from models.model import Conversation, Message
class AssistantCotApplicationRunner(BaseAssistantApplicationRunner): class AssistantCotApplicationRunner(BaseAssistantApplicationRunner):
_is_first_iteration = True
_ignore_observation_providers = ['wenxin']
def run(self, conversation: Conversation, def run(self, conversation: Conversation,
message: Message, message: Message,
query: str, query: str,
...@@ -42,10 +45,8 @@ class AssistantCotApplicationRunner(BaseAssistantApplicationRunner): ...@@ -42,10 +45,8 @@ class AssistantCotApplicationRunner(BaseAssistantApplicationRunner):
agent_scratchpad: list[AgentScratchpadUnit] = [] agent_scratchpad: list[AgentScratchpadUnit] = []
self._init_agent_scratchpad(agent_scratchpad, self.history_prompt_messages) self._init_agent_scratchpad(agent_scratchpad, self.history_prompt_messages)
# check model mode if 'Observation' not in app_orchestration_config.model_config.stop:
if self.app_orchestration_config.model_config.mode == "completion": if app_orchestration_config.model_config.provider not in self._ignore_observation_providers:
# TODO: stop words
if 'Observation' not in app_orchestration_config.model_config.stop:
app_orchestration_config.model_config.stop.append('Observation') app_orchestration_config.model_config.stop.append('Observation')
# override inputs # override inputs
...@@ -202,6 +203,7 @@ class AssistantCotApplicationRunner(BaseAssistantApplicationRunner): ...@@ -202,6 +203,7 @@ class AssistantCotApplicationRunner(BaseAssistantApplicationRunner):
) )
) )
scratchpad.thought = scratchpad.thought.strip() or 'I am thinking about how to help you'
agent_scratchpad.append(scratchpad) agent_scratchpad.append(scratchpad)
# get llm usage # get llm usage
...@@ -255,9 +257,15 @@ class AssistantCotApplicationRunner(BaseAssistantApplicationRunner): ...@@ -255,9 +257,15 @@ class AssistantCotApplicationRunner(BaseAssistantApplicationRunner):
# invoke tool # invoke tool
error_response = None error_response = None
try: try:
if isinstance(tool_call_args, str):
try:
tool_call_args = json.loads(tool_call_args)
except json.JSONDecodeError:
pass
tool_response = tool_instance.invoke( tool_response = tool_instance.invoke(
user_id=self.user_id, user_id=self.user_id,
tool_parameters=tool_call_args if isinstance(tool_call_args, dict) else json.loads(tool_call_args) tool_parameters=tool_call_args
) )
# transform tool response to llm friendly response # transform tool response to llm friendly response
tool_response = self.transform_tool_invoke_messages(tool_response) tool_response = self.transform_tool_invoke_messages(tool_response)
...@@ -466,7 +474,7 @@ class AssistantCotApplicationRunner(BaseAssistantApplicationRunner): ...@@ -466,7 +474,7 @@ class AssistantCotApplicationRunner(BaseAssistantApplicationRunner):
if isinstance(message, AssistantPromptMessage): if isinstance(message, AssistantPromptMessage):
current_scratchpad = AgentScratchpadUnit( current_scratchpad = AgentScratchpadUnit(
agent_response=message.content, agent_response=message.content,
thought=message.content, thought=message.content or 'I am thinking about how to help you',
action_str='', action_str='',
action=None, action=None,
observation=None, observation=None,
...@@ -546,7 +554,8 @@ class AssistantCotApplicationRunner(BaseAssistantApplicationRunner): ...@@ -546,7 +554,8 @@ class AssistantCotApplicationRunner(BaseAssistantApplicationRunner):
result = '' result = ''
for scratchpad in agent_scratchpad: for scratchpad in agent_scratchpad:
result += scratchpad.thought + next_iteration.replace("{{observation}}", scratchpad.observation or '') + "\n" result += (scratchpad.thought or '') + (scratchpad.action_str or '') + \
next_iteration.replace("{{observation}}", scratchpad.observation or 'It seems that no response is available')
return result return result
...@@ -621,21 +630,24 @@ class AssistantCotApplicationRunner(BaseAssistantApplicationRunner): ...@@ -621,21 +630,24 @@ class AssistantCotApplicationRunner(BaseAssistantApplicationRunner):
)) ))
# add assistant message # add assistant message
if len(agent_scratchpad) > 0: if len(agent_scratchpad) > 0 and not self._is_first_iteration:
prompt_messages.append(AssistantPromptMessage( prompt_messages.append(AssistantPromptMessage(
content=(agent_scratchpad[-1].thought or '') content=(agent_scratchpad[-1].thought or '') + (agent_scratchpad[-1].action_str or ''),
)) ))
# add user message # add user message
if len(agent_scratchpad) > 0: if len(agent_scratchpad) > 0 and not self._is_first_iteration:
prompt_messages.append(UserPromptMessage( prompt_messages.append(UserPromptMessage(
content=(agent_scratchpad[-1].observation or ''), content=(agent_scratchpad[-1].observation or 'It seems that no response is available'),
)) ))
self._is_first_iteration = False
return prompt_messages return prompt_messages
elif mode == "completion": elif mode == "completion":
# parse agent scratchpad # parse agent scratchpad
agent_scratchpad_str = self._convert_scratchpad_list_to_str(agent_scratchpad) agent_scratchpad_str = self._convert_scratchpad_list_to_str(agent_scratchpad)
self._is_first_iteration = False
# parse prompt messages # parse prompt messages
return [UserPromptMessage( return [UserPromptMessage(
content=first_prompt.replace("{{instruction}}", instruction) content=first_prompt.replace("{{instruction}}", instruction)
......
...@@ -62,7 +62,8 @@ class IndexingRunner: ...@@ -62,7 +62,8 @@ class IndexingRunner:
text_docs = self._extract(index_processor, dataset_document, processing_rule.to_dict()) text_docs = self._extract(index_processor, dataset_document, processing_rule.to_dict())
# transform # transform
documents = self._transform(index_processor, dataset, text_docs, processing_rule.to_dict()) documents = self._transform(index_processor, dataset, text_docs, dataset_document.doc_language,
processing_rule.to_dict())
# save segment # save segment
self._load_segments(dataset, dataset_document, documents) self._load_segments(dataset, dataset_document, documents)
...@@ -120,7 +121,8 @@ class IndexingRunner: ...@@ -120,7 +121,8 @@ class IndexingRunner:
text_docs = self._extract(index_processor, dataset_document, processing_rule.to_dict()) text_docs = self._extract(index_processor, dataset_document, processing_rule.to_dict())
# transform # transform
documents = self._transform(index_processor, dataset, text_docs, processing_rule.to_dict()) documents = self._transform(index_processor, dataset, text_docs, dataset_document.doc_language,
processing_rule.to_dict())
# save segment # save segment
self._load_segments(dataset, dataset_document, documents) self._load_segments(dataset, dataset_document, documents)
...@@ -186,7 +188,7 @@ class IndexingRunner: ...@@ -186,7 +188,7 @@ class IndexingRunner:
first() first()
index_type = dataset_document.doc_form index_type = dataset_document.doc_form
index_processor = IndexProcessorFactory(index_type, processing_rule.to_dict()).init_index_processor() index_processor = IndexProcessorFactory(index_type).init_index_processor()
self._load( self._load(
index_processor=index_processor, index_processor=index_processor,
dataset=dataset, dataset=dataset,
...@@ -750,7 +752,7 @@ class IndexingRunner: ...@@ -750,7 +752,7 @@ class IndexingRunner:
index_processor.load(dataset, documents) index_processor.load(dataset, documents)
def _transform(self, index_processor: BaseIndexProcessor, dataset: Dataset, def _transform(self, index_processor: BaseIndexProcessor, dataset: Dataset,
text_docs: list[Document], process_rule: dict) -> list[Document]: text_docs: list[Document], doc_language: str, process_rule: dict) -> list[Document]:
# get embedding model instance # get embedding model instance
embedding_model_instance = None embedding_model_instance = None
if dataset.indexing_technique == 'high_quality': if dataset.indexing_technique == 'high_quality':
...@@ -768,7 +770,8 @@ class IndexingRunner: ...@@ -768,7 +770,8 @@ class IndexingRunner:
) )
documents = index_processor.transform(text_docs, embedding_model_instance=embedding_model_instance, documents = index_processor.transform(text_docs, embedding_model_instance=embedding_model_instance,
process_rule=process_rule) process_rule=process_rule, tenant_id=dataset.tenant_id,
doc_language=doc_language)
return documents return documents
......
...@@ -21,7 +21,7 @@ class AnthropicProvider(ModelProvider): ...@@ -21,7 +21,7 @@ class AnthropicProvider(ModelProvider):
# Use `claude-instant-1` model for validate, # Use `claude-instant-1` model for validate,
model_instance.validate_credentials( model_instance.validate_credentials(
model='claude-instant-1', model='claude-instant-1.2',
credentials=credentials credentials=credentials
) )
except CredentialsValidateFailedError as ex: except CredentialsValidateFailedError as ex:
......
...@@ -2,8 +2,8 @@ provider: anthropic ...@@ -2,8 +2,8 @@ provider: anthropic
label: label:
en_US: Anthropic en_US: Anthropic
description: description:
en_US: Anthropic’s powerful models, such as Claude 2 and Claude Instant. en_US: Anthropic’s powerful models, such as Claude 3.
zh_Hans: Anthropic 的强大模型,例如 Claude 2 和 Claude Instant zh_Hans: Anthropic 的强大模型,例如 Claude 3
icon_small: icon_small:
en_US: icon_s_en.svg en_US: icon_s_en.svg
icon_large: icon_large:
......
- claude-3-opus-20240229
- claude-3-sonnet-20240229
- claude-2.1
- claude-instant-1.2
- claude-2
- claude-instant-1
...@@ -34,3 +34,4 @@ pricing: ...@@ -34,3 +34,4 @@ pricing:
output: '24.00' output: '24.00'
unit: '0.000001' unit: '0.000001'
currency: USD currency: USD
deprecated: true
model: claude-3-opus-20240229
label:
en_US: claude-3-opus-20240229
model_type: llm
features:
- agent-thought
- vision
model_properties:
mode: chat
context_size: 200000
parameter_rules:
- name: temperature
use_template: temperature
- name: top_p
use_template: top_p
- name: top_k
label:
zh_Hans: 取样数量
en_US: Top k
type: int
help:
zh_Hans: 仅从每个后续标记的前 K 个选项中采样。
en_US: Only sample from the top K options for each subsequent token.
required: false
- name: max_tokens
use_template: max_tokens
required: true
default: 4096
min: 1
max: 4096
- name: response_format
use_template: response_format
pricing:
input: '15.00'
output: '75.00'
unit: '0.000001'
currency: USD
model: claude-3-sonnet-20240229
label:
en_US: claude-3-sonnet-20240229
model_type: llm
features:
- agent-thought
- vision
model_properties:
mode: chat
context_size: 200000
parameter_rules:
- name: temperature
use_template: temperature
- name: top_p
use_template: top_p
- name: top_k
label:
zh_Hans: 取样数量
en_US: Top k
type: int
help:
zh_Hans: 仅从每个后续标记的前 K 个选项中采样。
en_US: Only sample from the top K options for each subsequent token.
required: false
- name: max_tokens
use_template: max_tokens
required: true
default: 4096
min: 1
max: 4096
- name: response_format
use_template: response_format
pricing:
input: '3.00'
output: '15.00'
unit: '0.000001'
currency: USD
model: claude-instant-1.2
label:
en_US: claude-instant-1.2
model_type: llm
features: [ ]
model_properties:
mode: chat
context_size: 100000
parameter_rules:
- name: temperature
use_template: temperature
- name: top_p
use_template: top_p
- name: top_k
label:
zh_Hans: 取样数量
en_US: Top k
type: int
help:
zh_Hans: 仅从每个后续标记的前 K 个选项中采样。
en_US: Only sample from the top K options for each subsequent token.
required: false
- name: max_tokens
use_template: max_tokens
required: true
default: 4096
min: 1
max: 4096
- name: response_format
use_template: response_format
pricing:
input: '1.63'
output: '5.51'
unit: '0.000001'
currency: USD
...@@ -33,3 +33,4 @@ pricing: ...@@ -33,3 +33,4 @@ pricing:
output: '5.51' output: '5.51'
unit: '0.000001' unit: '0.000001'
currency: USD currency: USD
deprecated: true
import base64
import mimetypes
from collections.abc import Generator from collections.abc import Generator
from typing import Optional, Union from typing import Optional, Union, cast
import anthropic import anthropic
import requests
from anthropic import Anthropic, Stream from anthropic import Anthropic, Stream
from anthropic.types import Completion, completion_create_params from anthropic.types import (
ContentBlockDeltaEvent,
Message,
MessageDeltaEvent,
MessageStartEvent,
MessageStopEvent,
MessageStreamEvent,
completion_create_params,
)
from httpx import Timeout from httpx import Timeout
from core.model_runtime.callbacks.base_callback import Callback from core.model_runtime.callbacks.base_callback import Callback
from core.model_runtime.entities.llm_entities import LLMResult, LLMResultChunk, LLMResultChunkDelta from core.model_runtime.entities.llm_entities import LLMResult, LLMResultChunk, LLMResultChunkDelta
from core.model_runtime.entities.message_entities import ( from core.model_runtime.entities.message_entities import (
AssistantPromptMessage, AssistantPromptMessage,
ImagePromptMessageContent,
PromptMessage, PromptMessage,
PromptMessageContentType,
PromptMessageTool, PromptMessageTool,
SystemPromptMessage, SystemPromptMessage,
TextPromptMessageContent,
UserPromptMessage, UserPromptMessage,
) )
from core.model_runtime.errors.invoke import ( from core.model_runtime.errors.invoke import (
...@@ -35,6 +49,7 @@ if you are not sure about the structure. ...@@ -35,6 +49,7 @@ if you are not sure about the structure.
</instructions> </instructions>
""" """
class AnthropicLargeLanguageModel(LargeLanguageModel): class AnthropicLargeLanguageModel(LargeLanguageModel):
def _invoke(self, model: str, credentials: dict, def _invoke(self, model: str, credentials: dict,
prompt_messages: list[PromptMessage], model_parameters: dict, prompt_messages: list[PromptMessage], model_parameters: dict,
...@@ -55,54 +70,114 @@ class AnthropicLargeLanguageModel(LargeLanguageModel): ...@@ -55,54 +70,114 @@ class AnthropicLargeLanguageModel(LargeLanguageModel):
:return: full response or stream response chunk generator result :return: full response or stream response chunk generator result
""" """
# invoke model # invoke model
return self._generate(model, credentials, prompt_messages, model_parameters, stop, stream, user) return self._chat_generate(model, credentials, prompt_messages, model_parameters, stop, stream, user)
def _chat_generate(self, model: str, credentials: dict,
prompt_messages: list[PromptMessage], model_parameters: dict, stop: Optional[list[str]] = None,
stream: bool = True, user: Optional[str] = None) -> Union[LLMResult, Generator]:
"""
Invoke llm chat model
:param model: model name
:param credentials: credentials
:param prompt_messages: prompt messages
:param model_parameters: model parameters
:param stop: stop words
:param stream: is stream response
:param user: unique user id
:return: full response or stream response chunk generator result
"""
# transform credentials to kwargs for model instance
credentials_kwargs = self._to_credential_kwargs(credentials)
# transform model parameters from completion api of anthropic to chat api
if 'max_tokens_to_sample' in model_parameters:
model_parameters['max_tokens'] = model_parameters.pop('max_tokens_to_sample')
# init model client
client = Anthropic(**credentials_kwargs)
extra_model_kwargs = {}
if stop:
extra_model_kwargs['stop_sequences'] = stop
if user:
extra_model_kwargs['metadata'] = completion_create_params.Metadata(user_id=user)
system, prompt_message_dicts = self._convert_prompt_messages(prompt_messages)
if system:
extra_model_kwargs['system'] = system
# chat model
response = client.messages.create(
model=model,
messages=prompt_message_dicts,
stream=stream,
**model_parameters,
**extra_model_kwargs
)
if stream:
return self._handle_chat_generate_stream_response(model, credentials, response, prompt_messages)
return self._handle_chat_generate_response(model, credentials, response, prompt_messages)
def _code_block_mode_wrapper(self, model: str, credentials: dict, prompt_messages: list[PromptMessage], def _code_block_mode_wrapper(self, model: str, credentials: dict, prompt_messages: list[PromptMessage],
model_parameters: dict, tools: Optional[list[PromptMessageTool]] = None, model_parameters: dict, tools: Optional[list[PromptMessageTool]] = None,
stop: Optional[list[str]] = None, stream: bool = True, user: Optional[str] = None, stop: Optional[list[str]] = None, stream: bool = True, user: Optional[str] = None,
callbacks: list[Callback] = None) -> Union[LLMResult, Generator]: callbacks: list[Callback] = None) -> Union[LLMResult, Generator]:
""" """
Code block mode wrapper for invoking large language model Code block mode wrapper for invoking large language model
""" """
if 'response_format' in model_parameters and model_parameters['response_format']: if 'response_format' in model_parameters and model_parameters['response_format']:
stop = stop or [] stop = stop or []
self._transform_json_prompts( # chat model
model, credentials, prompt_messages, model_parameters, tools, stop, stream, user, model_parameters['response_format'] self._transform_chat_json_prompts(
model=model,
credentials=credentials,
prompt_messages=prompt_messages,
model_parameters=model_parameters,
tools=tools,
stop=stop,
stream=stream,
user=user,
response_format=model_parameters['response_format']
) )
model_parameters.pop('response_format') model_parameters.pop('response_format')
return self._invoke(model, credentials, prompt_messages, model_parameters, tools, stop, stream, user) return self._invoke(model, credentials, prompt_messages, model_parameters, tools, stop, stream, user)
def _transform_json_prompts(self, model: str, credentials: dict, def _transform_chat_json_prompts(self, model: str, credentials: dict,
prompt_messages: list[PromptMessage], model_parameters: dict, prompt_messages: list[PromptMessage], model_parameters: dict,
tools: list[PromptMessageTool] | None = None, stop: list[str] | None = None, tools: list[PromptMessageTool] | None = None, stop: list[str] | None = None,
stream: bool = True, user: str | None = None, response_format: str = 'JSON') \ stream: bool = True, user: str | None = None, response_format: str = 'JSON') \
-> None: -> None:
""" """
Transform json prompts Transform json prompts
""" """
if "```\n" not in stop: if "```\n" not in stop:
stop.append("```\n") stop.append("```\n")
if "\n```" not in stop:
stop.append("\n```")
# check if there is a system message # check if there is a system message
if len(prompt_messages) > 0 and isinstance(prompt_messages[0], SystemPromptMessage): if len(prompt_messages) > 0 and isinstance(prompt_messages[0], SystemPromptMessage):
# override the system message # override the system message
prompt_messages[0] = SystemPromptMessage( prompt_messages[0] = SystemPromptMessage(
content=ANTHROPIC_BLOCK_MODE_PROMPT content=ANTHROPIC_BLOCK_MODE_PROMPT
.replace("{{instructions}}", prompt_messages[0].content) .replace("{{instructions}}", prompt_messages[0].content)
.replace("{{block}}", response_format) .replace("{{block}}", response_format)
) )
prompt_messages.append(AssistantPromptMessage(content=f"\n```{response_format}"))
else: else:
# insert the system message # insert the system message
prompt_messages.insert(0, SystemPromptMessage( prompt_messages.insert(0, SystemPromptMessage(
content=ANTHROPIC_BLOCK_MODE_PROMPT content=ANTHROPIC_BLOCK_MODE_PROMPT
.replace("{{instructions}}", f"Please output a valid {response_format} object.") .replace("{{instructions}}", f"Please output a valid {response_format} object.")
.replace("{{block}}", response_format) .replace("{{block}}", response_format)
)) ))
prompt_messages.append(AssistantPromptMessage(content=f"\n```{response_format}"))
prompt_messages.append(AssistantPromptMessage(
content=f"```{response_format}\n"
))
def get_num_tokens(self, model: str, credentials: dict, prompt_messages: list[PromptMessage], def get_num_tokens(self, model: str, credentials: dict, prompt_messages: list[PromptMessage],
tools: Optional[list[PromptMessageTool]] = None) -> int: tools: Optional[list[PromptMessageTool]] = None) -> int:
...@@ -129,7 +204,7 @@ class AnthropicLargeLanguageModel(LargeLanguageModel): ...@@ -129,7 +204,7 @@ class AnthropicLargeLanguageModel(LargeLanguageModel):
:return: :return:
""" """
try: try:
self._generate( self._chat_generate(
model=model, model=model,
credentials=credentials, credentials=credentials,
prompt_messages=[ prompt_messages=[
...@@ -137,58 +212,17 @@ class AnthropicLargeLanguageModel(LargeLanguageModel): ...@@ -137,58 +212,17 @@ class AnthropicLargeLanguageModel(LargeLanguageModel):
], ],
model_parameters={ model_parameters={
"temperature": 0, "temperature": 0,
"max_tokens_to_sample": 20, "max_tokens": 20,
}, },
stream=False stream=False
) )
except Exception as ex: except Exception as ex:
raise CredentialsValidateFailedError(str(ex)) raise CredentialsValidateFailedError(str(ex))
def _generate(self, model: str, credentials: dict, def _handle_chat_generate_response(self, model: str, credentials: dict, response: Message,
prompt_messages: list[PromptMessage], model_parameters: dict, prompt_messages: list[PromptMessage]) -> LLMResult:
stop: Optional[list[str]] = None, stream: bool = True,
user: Optional[str] = None) -> Union[LLMResult, Generator]:
""" """
Invoke large language model Handle llm chat response
:param model: model name
:param credentials: credentials kwargs
:param prompt_messages: prompt messages
:param model_parameters: model parameters
:param stop: stop words
:param stream: is stream response
:param user: unique user id
:return: full response or stream response chunk generator result
"""
# transform credentials to kwargs for model instance
credentials_kwargs = self._to_credential_kwargs(credentials)
client = Anthropic(**credentials_kwargs)
extra_model_kwargs = {}
if stop:
extra_model_kwargs['stop_sequences'] = stop
if user:
extra_model_kwargs['metadata'] = completion_create_params.Metadata(user_id=user)
response = client.completions.create(
model=model,
prompt=self._convert_messages_to_prompt_anthropic(prompt_messages),
stream=stream,
**model_parameters,
**extra_model_kwargs
)
if stream:
return self._handle_generate_stream_response(model, credentials, response, prompt_messages)
return self._handle_generate_response(model, credentials, response, prompt_messages)
def _handle_generate_response(self, model: str, credentials: dict, response: Completion,
prompt_messages: list[PromptMessage]) -> LLMResult:
"""
Handle llm response
:param model: model name :param model: model name
:param credentials: credentials :param credentials: credentials
...@@ -198,75 +232,89 @@ class AnthropicLargeLanguageModel(LargeLanguageModel): ...@@ -198,75 +232,89 @@ class AnthropicLargeLanguageModel(LargeLanguageModel):
""" """
# transform assistant message to prompt message # transform assistant message to prompt message
assistant_prompt_message = AssistantPromptMessage( assistant_prompt_message = AssistantPromptMessage(
content=response.completion content=response.content[0].text
) )
# calculate num tokens # calculate num tokens
prompt_tokens = self.get_num_tokens(model, credentials, prompt_messages) if response.usage:
completion_tokens = self.get_num_tokens(model, credentials, [assistant_prompt_message]) # transform usage
prompt_tokens = response.usage.input_tokens
completion_tokens = response.usage.output_tokens
else:
# calculate num tokens
prompt_tokens = self.get_num_tokens(model, credentials, prompt_messages)
completion_tokens = self.get_num_tokens(model, credentials, [assistant_prompt_message])
# transform usage # transform usage
usage = self._calc_response_usage(model, credentials, prompt_tokens, completion_tokens) usage = self._calc_response_usage(model, credentials, prompt_tokens, completion_tokens)
# transform response # transform response
result = LLMResult( response = LLMResult(
model=response.model, model=response.model,
prompt_messages=prompt_messages, prompt_messages=prompt_messages,
message=assistant_prompt_message, message=assistant_prompt_message,
usage=usage, usage=usage
) )
return result return response
def _handle_generate_stream_response(self, model: str, credentials: dict, response: Stream[Completion], def _handle_chat_generate_stream_response(self, model: str, credentials: dict,
prompt_messages: list[PromptMessage]) -> Generator: response: Stream[MessageStreamEvent],
prompt_messages: list[PromptMessage]) -> Generator:
""" """
Handle llm stream response Handle llm chat stream response
:param model: model name :param model: model name
:param credentials: credentials
:param response: response :param response: response
:param prompt_messages: prompt messages :param prompt_messages: prompt messages
:return: llm response chunk generator result :return: llm response chunk generator
""" """
index = -1 full_assistant_content = ''
return_model = None
input_tokens = 0
output_tokens = 0
finish_reason = None
index = 0
for chunk in response: for chunk in response:
content = chunk.completion if isinstance(chunk, MessageStartEvent):
if chunk.stop_reason is None and (content is None or content == ''): return_model = chunk.message.model
continue input_tokens = chunk.message.usage.input_tokens
elif isinstance(chunk, MessageDeltaEvent):
# transform assistant message to prompt message output_tokens = chunk.usage.output_tokens
assistant_prompt_message = AssistantPromptMessage( finish_reason = chunk.delta.stop_reason
content=content if content else '', elif isinstance(chunk, MessageStopEvent):
)
index += 1
if chunk.stop_reason is not None:
# calculate num tokens
prompt_tokens = self.get_num_tokens(model, credentials, prompt_messages)
completion_tokens = self.get_num_tokens(model, credentials, [assistant_prompt_message])
# transform usage # transform usage
usage = self._calc_response_usage(model, credentials, prompt_tokens, completion_tokens) usage = self._calc_response_usage(model, credentials, input_tokens, output_tokens)
yield LLMResultChunk( yield LLMResultChunk(
model=chunk.model, model=return_model,
prompt_messages=prompt_messages, prompt_messages=prompt_messages,
delta=LLMResultChunkDelta( delta=LLMResultChunkDelta(
index=index, index=index + 1,
message=assistant_prompt_message, message=AssistantPromptMessage(
finish_reason=chunk.stop_reason, content=''
),
finish_reason=finish_reason,
usage=usage usage=usage
) )
) )
else: elif isinstance(chunk, ContentBlockDeltaEvent):
chunk_text = chunk.delta.text if chunk.delta.text else ''
full_assistant_content += chunk_text
# transform assistant message to prompt message
assistant_prompt_message = AssistantPromptMessage(
content=chunk_text
)
index = chunk.index
yield LLMResultChunk( yield LLMResultChunk(
model=chunk.model, model=return_model,
prompt_messages=prompt_messages, prompt_messages=prompt_messages,
delta=LLMResultChunkDelta( delta=LLMResultChunkDelta(
index=index, index=chunk.index,
message=assistant_prompt_message message=assistant_prompt_message,
) )
) )
...@@ -289,6 +337,80 @@ class AnthropicLargeLanguageModel(LargeLanguageModel): ...@@ -289,6 +337,80 @@ class AnthropicLargeLanguageModel(LargeLanguageModel):
return credentials_kwargs return credentials_kwargs
def _convert_prompt_messages(self, prompt_messages: list[PromptMessage]) -> tuple[str, list[dict]]:
"""
Convert prompt messages to dict list and system
"""
system = ""
prompt_message_dicts = []
for message in prompt_messages:
if isinstance(message, SystemPromptMessage):
system += message.content + ("\n" if not system else "")
else:
prompt_message_dicts.append(self._convert_prompt_message_to_dict(message))
return system, prompt_message_dicts
def _convert_prompt_message_to_dict(self, message: PromptMessage) -> dict:
"""
Convert PromptMessage to dict
"""
if isinstance(message, UserPromptMessage):
message = cast(UserPromptMessage, message)
if isinstance(message.content, str):
message_dict = {"role": "user", "content": message.content}
else:
sub_messages = []
for message_content in message.content:
if message_content.type == PromptMessageContentType.TEXT:
message_content = cast(TextPromptMessageContent, message_content)
sub_message_dict = {
"type": "text",
"text": message_content.data
}
sub_messages.append(sub_message_dict)
elif message_content.type == PromptMessageContentType.IMAGE:
message_content = cast(ImagePromptMessageContent, message_content)
if not message_content.data.startswith("data:"):
# fetch image data from url
try:
image_content = requests.get(message_content.data).content
mime_type, _ = mimetypes.guess_type(message_content.data)
base64_data = base64.b64encode(image_content).decode('utf-8')
except Exception as ex:
raise ValueError(f"Failed to fetch image data from url {message_content.data}, {ex}")
else:
data_split = message_content.data.split(";base64,")
mime_type = data_split[0].replace("data:", "")
base64_data = data_split[1]
if mime_type not in ["image/jpeg", "image/png", "image/gif", "image/webp"]:
raise ValueError(f"Unsupported image type {mime_type}, "
f"only support image/jpeg, image/png, image/gif, and image/webp")
sub_message_dict = {
"type": "image",
"source": {
"type": "base64",
"media_type": mime_type,
"data": base64_data
}
}
sub_messages.append(sub_message_dict)
message_dict = {"role": "user", "content": sub_messages}
elif isinstance(message, AssistantPromptMessage):
message = cast(AssistantPromptMessage, message)
message_dict = {"role": "assistant", "content": message.content}
elif isinstance(message, SystemPromptMessage):
message = cast(SystemPromptMessage, message)
message_dict = {"role": "system", "content": message.content}
else:
raise ValueError(f"Got unknown type {message}")
return message_dict
def _convert_one_message_to_text(self, message: PromptMessage) -> str: def _convert_one_message_to_text(self, message: PromptMessage) -> str:
""" """
Convert a single message to a string. Convert a single message to a string.
......
...@@ -108,7 +108,7 @@ class BaichuanTextEmbeddingModel(TextEmbeddingModel): ...@@ -108,7 +108,7 @@ class BaichuanTextEmbeddingModel(TextEmbeddingModel):
try: try:
response = post(url, headers=headers, data=dumps(data)) response = post(url, headers=headers, data=dumps(data))
except Exception as e: except Exception as e:
raise InvokeConnectionError(e) raise InvokeConnectionError(str(e))
if response.status_code != 200: if response.status_code != 200:
try: try:
......
...@@ -57,7 +57,7 @@ class JinaTextEmbeddingModel(TextEmbeddingModel): ...@@ -57,7 +57,7 @@ class JinaTextEmbeddingModel(TextEmbeddingModel):
try: try:
response = post(url, headers=headers, data=dumps(data)) response = post(url, headers=headers, data=dumps(data))
except Exception as e: except Exception as e:
raise InvokeConnectionError(e) raise InvokeConnectionError(str(e))
if response.status_code != 200: if response.status_code != 200:
try: try:
......
...@@ -59,7 +59,7 @@ class LocalAITextEmbeddingModel(TextEmbeddingModel): ...@@ -59,7 +59,7 @@ class LocalAITextEmbeddingModel(TextEmbeddingModel):
try: try:
response = post(join(url, 'embeddings'), headers=headers, data=dumps(data), timeout=10) response = post(join(url, 'embeddings'), headers=headers, data=dumps(data), timeout=10)
except Exception as e: except Exception as e:
raise InvokeConnectionError(e) raise InvokeConnectionError(str(e))
if response.status_code != 200: if response.status_code != 200:
try: try:
......
...@@ -65,7 +65,7 @@ class MinimaxTextEmbeddingModel(TextEmbeddingModel): ...@@ -65,7 +65,7 @@ class MinimaxTextEmbeddingModel(TextEmbeddingModel):
try: try:
response = post(url, headers=headers, data=dumps(data)) response = post(url, headers=headers, data=dumps(data))
except Exception as e: except Exception as e:
raise InvokeConnectionError(e) raise InvokeConnectionError(str(e))
if response.status_code != 200: if response.status_code != 200:
raise InvokeServerUnavailableError(response.text) raise InvokeServerUnavailableError(response.text)
......
...@@ -34,7 +34,7 @@ class OpenAIText2SpeechModel(_CommonOpenAI, TTSModel): ...@@ -34,7 +34,7 @@ class OpenAIText2SpeechModel(_CommonOpenAI, TTSModel):
:return: text translated to audio file :return: text translated to audio file
""" """
audio_type = self._get_model_audio_type(model, credentials) audio_type = self._get_model_audio_type(model, credentials)
if not voice: if not voice or voice not in [d['value'] for d in self.get_tts_model_voices(model=model, credentials=credentials)]:
voice = self._get_model_default_voice(model, credentials) voice = self._get_model_default_voice(model, credentials)
if streaming: if streaming:
return Response(stream_with_context(self._tts_invoke_streaming(model=model, return Response(stream_with_context(self._tts_invoke_streaming(model=model,
......
...@@ -53,7 +53,7 @@ class OpenLLMTextEmbeddingModel(TextEmbeddingModel): ...@@ -53,7 +53,7 @@ class OpenLLMTextEmbeddingModel(TextEmbeddingModel):
# cloud not connect to the server # cloud not connect to the server
raise InvokeAuthorizationError(f"Invalid server URL: {e}") raise InvokeAuthorizationError(f"Invalid server URL: {e}")
except Exception as e: except Exception as e:
raise InvokeConnectionError(e) raise InvokeConnectionError(str(e))
if response.status_code != 200: if response.status_code != 200:
if response.status_code == 400: if response.status_code == 400:
......
...@@ -34,7 +34,7 @@ class TongyiText2SpeechModel(_CommonTongyi, TTSModel): ...@@ -34,7 +34,7 @@ class TongyiText2SpeechModel(_CommonTongyi, TTSModel):
:return: text translated to audio file :return: text translated to audio file
""" """
audio_type = self._get_model_audio_type(model, credentials) audio_type = self._get_model_audio_type(model, credentials)
if not voice or voice not in self.get_tts_model_voices(model=model, credentials=credentials): if not voice or voice not in [d['value'] for d in self.get_tts_model_voices(model=model, credentials=credentials)]:
voice = self._get_model_default_voice(model, credentials) voice = self._get_model_default_voice(model, credentials)
if streaming: if streaming:
return Response(stream_with_context(self._tts_invoke_streaming(model=model, return Response(stream_with_context(self._tts_invoke_streaming(model=model,
......
from os import path
from threading import Lock from threading import Lock
from time import time from time import time
from requests.adapters import HTTPAdapter from requests.adapters import HTTPAdapter
from requests.exceptions import ConnectionError, MissingSchema, Timeout from requests.exceptions import ConnectionError, MissingSchema, Timeout
from requests.sessions import Session from requests.sessions import Session
from yarl import URL
class XinferenceModelExtraParameter: class XinferenceModelExtraParameter:
...@@ -55,7 +55,10 @@ class XinferenceHelper: ...@@ -55,7 +55,10 @@ class XinferenceHelper:
get xinference model extra parameter like model_format and model_handle_type get xinference model extra parameter like model_format and model_handle_type
""" """
url = path.join(server_url, 'v1/models', model_uid) if not model_uid or not model_uid.strip() or not server_url or not server_url.strip():
raise RuntimeError('model_uid is empty')
url = str(URL(server_url) / 'v1' / 'models' / model_uid)
# this method is surrounded by a lock, and default requests may hang forever, so we just set a Adapter with max_retries=3 # this method is surrounded by a lock, and default requests may hang forever, so we just set a Adapter with max_retries=3
session = Session() session = Session()
...@@ -66,7 +69,6 @@ class XinferenceHelper: ...@@ -66,7 +69,6 @@ class XinferenceHelper:
response = session.get(url, timeout=10) response = session.get(url, timeout=10)
except (MissingSchema, ConnectionError, Timeout) as e: except (MissingSchema, ConnectionError, Timeout) as e:
raise RuntimeError(f'get xinference model extra parameter failed, url: {url}, error: {e}') raise RuntimeError(f'get xinference model extra parameter failed, url: {url}, error: {e}')
if response.status_code != 200: if response.status_code != 200:
raise RuntimeError(f'get xinference model extra parameter failed, status code: {response.status_code}, response: {response.text}') raise RuntimeError(f'get xinference model extra parameter failed, status code: {response.status_code}, response: {response.text}')
......
...@@ -3,6 +3,7 @@ import csv ...@@ -3,6 +3,7 @@ import csv
from typing import Optional from typing import Optional
from core.rag.extractor.extractor_base import BaseExtractor from core.rag.extractor.extractor_base import BaseExtractor
from core.rag.extractor.helpers import detect_file_encodings
from core.rag.models.document import Document from core.rag.models.document import Document
...@@ -36,7 +37,7 @@ class CSVExtractor(BaseExtractor): ...@@ -36,7 +37,7 @@ class CSVExtractor(BaseExtractor):
docs = self._read_from_file(csvfile) docs = self._read_from_file(csvfile)
except UnicodeDecodeError as e: except UnicodeDecodeError as e:
if self._autodetect_encoding: if self._autodetect_encoding:
detected_encodings = detect_filze_encodings(self._file_path) detected_encodings = detect_file_encodings(self._file_path)
for encoding in detected_encodings: for encoding in detected_encodings:
try: try:
with open(self._file_path, newline="", encoding=encoding.encoding) as csvfile: with open(self._file_path, newline="", encoding=encoding.encoding) as csvfile:
......
...@@ -7,7 +7,6 @@ from typing import Optional ...@@ -7,7 +7,6 @@ from typing import Optional
import pandas as pd import pandas as pd
from flask import Flask, current_app from flask import Flask, current_app
from flask_login import current_user
from werkzeug.datastructures import FileStorage from werkzeug.datastructures import FileStorage
from core.generator.llm_generator import LLMGenerator from core.generator.llm_generator import LLMGenerator
...@@ -31,7 +30,7 @@ class QAIndexProcessor(BaseIndexProcessor): ...@@ -31,7 +30,7 @@ class QAIndexProcessor(BaseIndexProcessor):
def transform(self, documents: list[Document], **kwargs) -> list[Document]: def transform(self, documents: list[Document], **kwargs) -> list[Document]:
splitter = self._get_splitter(processing_rule=kwargs.get('process_rule'), splitter = self._get_splitter(processing_rule=kwargs.get('process_rule'),
embedding_model_instance=None) embedding_model_instance=kwargs.get('embedding_model_instance'))
# Split the text documents into nodes. # Split the text documents into nodes.
all_documents = [] all_documents = []
...@@ -66,10 +65,10 @@ class QAIndexProcessor(BaseIndexProcessor): ...@@ -66,10 +65,10 @@ class QAIndexProcessor(BaseIndexProcessor):
for doc in sub_documents: for doc in sub_documents:
document_format_thread = threading.Thread(target=self._format_qa_document, kwargs={ document_format_thread = threading.Thread(target=self._format_qa_document, kwargs={
'flask_app': current_app._get_current_object(), 'flask_app': current_app._get_current_object(),
'tenant_id': current_user.current_tenant.id, 'tenant_id': kwargs.get('tenant_id'),
'document_node': doc, 'document_node': doc,
'all_qa_documents': all_qa_documents, 'all_qa_documents': all_qa_documents,
'document_language': kwargs.get('document_language', 'English')}) 'document_language': kwargs.get('doc_language', 'English')})
threads.append(document_format_thread) threads.append(document_format_thread)
document_format_thread.start() document_format_thread.start()
for thread in threads: for thread in threads:
......
...@@ -18,3 +18,4 @@ ...@@ -18,3 +18,4 @@
- vectorizer - vectorizer
- gaode - gaode
- wecom - wecom
- qrcode
<?xml version="1.0" encoding="utf-8"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M16 17v-1h-3v-3h3v2h2v2h-1v2h-2v2h-2v-3h2v-1h1zm5 4h-4v-2h2v-2h2v4zM3 3h8v8H3V3zm2 2v4h4V5H5zm8-2h8v8h-8V3zm2 2v4h4V5h-4zM3 13h8v8H3v-8zm2 2v4h4v-4H5zm13-2h3v2h-3v-2zM6 6h2v2H6V6zm0 10h2v2H6v-2zM16 6h2v2h-2V6z"/>
</g>
</svg>
\ No newline at end of file
from typing import Any
from core.tools.errors import ToolProviderCredentialValidationError
from core.tools.provider.builtin.qrcode.tools.qrcode_generator import QRCodeGeneratorTool
from core.tools.provider.builtin_tool_provider import BuiltinToolProviderController
class QRCodeProvider(BuiltinToolProviderController):
def _validate_credentials(self, credentials: dict[str, Any]) -> None:
try:
QRCodeGeneratorTool().invoke(user_id='',
tool_parameters={
'content': 'Dify 123 😊'
})
except Exception as e:
raise ToolProviderCredentialValidationError(str(e))
identity:
author: Bowen Liang
name: qrcode
label:
en_US: QRCode
zh_Hans: 二维码工具
pt_BR: QRCode
description:
en_US: A tool for generating QR code (quick-response code) image.
zh_Hans: 一个二维码工具
pt_BR: A tool for generating QR code (quick-response code) image.
icon: icon.svg
import io
import logging
from typing import Any, Union
import qrcode
from qrcode.image.pure import PyPNGImage
from core.tools.entities.tool_entities import ToolInvokeMessage
from core.tools.tool.builtin_tool import BuiltinTool
class QRCodeGeneratorTool(BuiltinTool):
def _invoke(self,
user_id: str,
tool_parameters: dict[str, Any],
) -> Union[ToolInvokeMessage, list[ToolInvokeMessage]]:
"""
invoke tools
"""
# get expression
content = tool_parameters.get('content', '')
if not content:
return self.create_text_message('Invalid parameter content')
try:
img = qrcode.make(data=content, image_factory=PyPNGImage)
byte_stream = io.BytesIO()
img.save(byte_stream)
byte_array = byte_stream.getvalue()
return self.create_blob_message(blob=byte_array,
meta={'mime_type': 'image/png'},
save_as=self.VARIABLE_KEY.IMAGE.value)
except Exception:
logging.exception(f'Failed to generate QR code for content: {content}')
return self.create_text_message('Failed to generate QR code')
identity:
name: qrcode_generator
author: Bowen Liang
label:
en_US: QR Code Generator
zh_Hans: 二维码生成器
pt_BR: QR Code Generator
description:
human:
en_US: A tool for generating QR code image
zh_Hans: 一个用于生成二维码的工具
pt_BR: A tool for generating QR code image
llm: A tool for generating QR code image
parameters:
- name: content
type: string
required: true
label:
en_US: content text for QR code
zh_Hans: 二维码文本内容
pt_BR: content text for QR code
human_description:
en_US: content text for QR code
zh_Hans: 二维码文本内容
pt_BR: 二维码文本内容
form: llm
from typing import Any
from core.tools.errors import ToolProviderCredentialValidationError
from core.tools.provider.builtin.tavily.tools.tavily_search import TavilySearchTool
from core.tools.provider.builtin_tool_provider import BuiltinToolProviderController
class TavilyProvider(BuiltinToolProviderController):
def _validate_credentials(self, credentials: dict[str, Any]) -> None:
try:
TavilySearchTool().fork_tool_runtime(
meta={
"credentials": credentials,
}
).invoke(
user_id='',
tool_parameters={
"query": "Sachin Tendulkar",
},
)
except Exception as e:
raise ToolProviderCredentialValidationError(str(e))
\ No newline at end of file
identity:
author: Yash Parmar
name: tavily
label:
en_US: Tavily
zh_Hans: Tavily
pt_BR: Tavily
description:
en_US: Tavily
zh_Hans: Tavily
pt_BR: Tavily
icon: icon.png
credentials_for_provider:
tavily_api_key:
type: secret-input
required: true
label:
en_US: Tavily API key
zh_Hans: Tavily API key
pt_BR: Tavily API key
placeholder:
en_US: Please input your Tavily API key
zh_Hans: 请输入你的 Tavily API key
pt_BR: Please input your Tavily API key
help:
en_US: Get your Tavily API key from Tavily
zh_Hans: 从 TavilyApi 获取您的 Tavily API key
pt_BR: Get your Tavily API key from Tavily
url: https://docs.tavily.com/docs/tavily-api/introduction
from typing import Any, Optional
import requests
from core.tools.entities.tool_entities import ToolInvokeMessage
from core.tools.tool.builtin_tool import BuiltinTool
TAVILY_API_URL = "https://api.tavily.com"
class TavilySearch:
"""
A class for performing search operations using the Tavily Search API.
Args:
api_key (str): The API key for accessing the Tavily Search API.
Methods:
raw_results: Retrieves raw search results from the Tavily Search API.
results: Retrieves cleaned search results from the Tavily Search API.
clean_results: Cleans the raw search results.
"""
def __init__(self, api_key: str) -> None:
self.api_key = api_key
def raw_results(
self,
query: str,
max_results: Optional[int] = 3,
search_depth: Optional[str] = "advanced",
include_domains: Optional[list[str]] = [],
exclude_domains: Optional[list[str]] = [],
include_answer: Optional[bool] = False,
include_raw_content: Optional[bool] = False,
include_images: Optional[bool] = False,
) -> dict:
"""
Retrieves raw search results from the Tavily Search API.
Args:
query (str): The search query.
max_results (int, optional): The maximum number of results to retrieve. Defaults to 3.
search_depth (str, optional): The search depth. Defaults to "advanced".
include_domains (List[str], optional): The domains to include in the search. Defaults to [].
exclude_domains (List[str], optional): The domains to exclude from the search. Defaults to [].
include_answer (bool, optional): Whether to include answer in the search results. Defaults to False.
include_raw_content (bool, optional): Whether to include raw content in the search results. Defaults to False.
include_images (bool, optional): Whether to include images in the search results. Defaults to False.
Returns:
dict: The raw search results.
"""
params = {
"api_key": self.api_key,
"query": query,
"max_results": max_results,
"search_depth": search_depth,
"include_domains": include_domains,
"exclude_domains": exclude_domains,
"include_answer": include_answer,
"include_raw_content": include_raw_content,
"include_images": include_images,
}
response = requests.post(f"{TAVILY_API_URL}/search", json=params)
response.raise_for_status()
return response.json()
def results(
self,
query: str,
max_results: Optional[int] = 3,
search_depth: Optional[str] = "advanced",
include_domains: Optional[list[str]] = [],
exclude_domains: Optional[list[str]] = [],
include_answer: Optional[bool] = False,
include_raw_content: Optional[bool] = False,
include_images: Optional[bool] = False,
) -> list[dict]:
"""
Retrieves cleaned search results from the Tavily Search API.
Args:
query (str): The search query.
max_results (int, optional): The maximum number of results to retrieve. Defaults to 3.
search_depth (str, optional): The search depth. Defaults to "advanced".
include_domains (List[str], optional): The domains to include in the search. Defaults to [].
exclude_domains (List[str], optional): The domains to exclude from the search. Defaults to [].
include_answer (bool, optional): Whether to include answer in the search results. Defaults to False.
include_raw_content (bool, optional): Whether to include raw content in the search results. Defaults to False.
include_images (bool, optional): Whether to include images in the search results. Defaults to False.
Returns:
list: The cleaned search results.
"""
raw_search_results = self.raw_results(
query,
max_results=max_results,
search_depth=search_depth,
include_domains=include_domains,
exclude_domains=exclude_domains,
include_answer=include_answer,
include_raw_content=include_raw_content,
include_images=include_images,
)
return self.clean_results(raw_search_results["results"])
def clean_results(self, results: list[dict]) -> list[dict]:
"""
Cleans the raw search results.
Args:
results (list): The raw search results.
Returns:
list: The cleaned search results.
"""
clean_results = []
for result in results:
clean_results.append(
{
"url": result["url"],
"content": result["content"],
}
)
# return clean results as a string
return "\n".join([f"{res['url']}\n{res['content']}" for res in clean_results])
class TavilySearchTool(BuiltinTool):
"""
A tool for searching Tavily using a given query.
"""
def _invoke(
self, user_id: str, tool_parameters: dict[str, Any]
) -> ToolInvokeMessage | list[ToolInvokeMessage]:
"""
Invokes the Tavily search tool with the given user ID and tool parameters.
Args:
user_id (str): The ID of the user invoking the tool.
tool_parameters (Dict[str, Any]): The parameters for the Tavily search tool.
Returns:
ToolInvokeMessage | list[ToolInvokeMessage]: The result of the Tavily search tool invocation.
"""
query = tool_parameters.get("query", "")
api_key = self.runtime.credentials["tavily_api_key"]
if not query:
return self.create_text_message("Please input query")
tavily_search = TavilySearch(api_key)
results = tavily_search.results(query)
print(results)
if not results:
return self.create_text_message(f"No results found for '{query}' in Tavily")
else:
return self.create_text_message(text=results)
identity:
name: tavily_search
author: Yash Parmar
label:
en_US: TavilySearch
zh_Hans: TavilySearch
pt_BR: TavilySearch
description:
human:
en_US: A tool for search engine built specifically for AI agents (LLMs), delivering real-time, accurate, and factual results at speed.
zh_Hans: 专为人工智能代理 (LLM) 构建的搜索引擎工具,可快速提供实时、准确和真实的结果。
pt_BR: A tool for search engine built specifically for AI agents (LLMs), delivering real-time, accurate, and factual results at speed.
llm: A tool for search engine built specifically for AI agents (LLMs), delivering real-time, accurate, and factual results at speed.
parameters:
- name: query
type: string
required: true
label:
en_US: Query string
zh_Hans: 查询语句
pt_BR: Query string
human_description:
en_US: used for searching
zh_Hans: 用于搜索网页内容
pt_BR: used for searching
llm_description: key words for searching
form: llm
<svg width="2500" height="2500" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid"><g fill="#CF272D"><path d="M127.86 222.304c-52.005 0-94.164-42.159-94.164-94.163 0-52.005 42.159-94.163 94.164-94.163 52.004 0 94.162 42.158 94.162 94.163 0 52.004-42.158 94.163-94.162 94.163zm0-222.023C57.245.281 0 57.527 0 128.141 0 198.756 57.245 256 127.86 256c70.614 0 127.859-57.244 127.859-127.859 0-70.614-57.245-127.86-127.86-127.86z"/><path d="M133.116 96.297c0-14.682 11.903-26.585 26.586-26.585 14.683 0 26.585 11.903 26.585 26.585 0 14.684-11.902 26.586-26.585 26.586-14.683 0-26.586-11.902-26.586-26.586M133.116 159.983c0-14.682 11.903-26.586 26.586-26.586 14.683 0 26.585 11.904 26.585 26.586 0 14.683-11.902 26.586-26.585 26.586-14.683 0-26.586-11.903-26.586-26.586M69.431 159.983c0-14.682 11.904-26.586 26.586-26.586 14.683 0 26.586 11.904 26.586 26.586 0 14.683-11.903 26.586-26.586 26.586-14.682 0-26.586-11.903-26.586-26.586M69.431 96.298c0-14.683 11.904-26.585 26.586-26.585 14.683 0 26.586 11.902 26.586 26.585 0 14.684-11.903 26.586-26.586 26.586-14.682 0-26.586-11.902-26.586-26.586"/></g></svg>
\ No newline at end of file
from typing import Any, Union
from langchain.utilities import TwilioAPIWrapper
from core.tools.entities.tool_entities import ToolInvokeMessage
from core.tools.tool.builtin_tool import BuiltinTool
class SendMessageTool(BuiltinTool):
"""
A tool for sending messages using Twilio API.
Args:
user_id (str): The ID of the user invoking the tool.
tool_parameters (Dict[str, Any]): The parameters required for sending the message.
Returns:
Union[ToolInvokeMessage, List[ToolInvokeMessage]]: The result of invoking the tool, which includes the status of the message sending operation.
"""
def _invoke(
self, user_id: str, tool_parameters: dict[str, Any]
) -> Union[ToolInvokeMessage, list[ToolInvokeMessage]]:
account_sid = self.runtime.credentials["account_sid"]
auth_token = self.runtime.credentials["auth_token"]
from_number = self.runtime.credentials["from_number"]
message = tool_parameters["message"]
to_number = tool_parameters["to_number"]
if to_number.startswith("whatsapp:"):
from_number = f"whatsapp: {from_number}"
twilio = TwilioAPIWrapper(
account_sid=account_sid, auth_token=auth_token, from_number=from_number
)
# Sending the message through Twilio
result = twilio.run(message, to_number)
return self.create_text_message(text="Message sent successfully.")
identity:
name: send_message
author: Yash Parmar
label:
en_US: SendMessage
zh_Hans: 发送消息
pt_BR: SendMessage
description:
human:
en_US: Send SMS or Twilio Messaging Channels messages.
zh_Hans: 发送SMS或Twilio消息通道消息。
pt_BR: Send SMS or Twilio Messaging Channels messages.
llm: Send SMS or Twilio Messaging Channels messages. Supports different channels including WhatsApp.
parameters:
- name: message
type: string
required: true
label:
en_US: Message
zh_Hans: 消息内容
pt_BR: Message
human_description:
en_US: The content of the message to be sent.
zh_Hans: 要发送的消息内容。
pt_BR: The content of the message to be sent.
llm_description: The content of the message to be sent.
form: llm
- name: to_number
type: string
required: true
label:
en_US: To Number
zh_Hans: 收信号码
pt_BR: Para Número
human_description:
en_US: The recipient's phone number. Prefix with 'whatsapp:' for WhatsApp messages, e.g., "whatsapp:+1234567890".
zh_Hans: 收件人的电话号码。WhatsApp消息前缀为'whatsapp:',例如,"whatsapp:+1234567890"。
pt_BR: The recipient's phone number. Prefix with 'whatsapp:' for WhatsApp messages, e.g., "whatsapp:+1234567890".
llm_description: The recipient's phone number. Prefix with 'whatsapp:' for WhatsApp messages, e.g., "whatsapp:+1234567890".
form: llm
from typing import Any
from core.tools.errors import ToolProviderCredentialValidationError
from core.tools.provider.builtin_tool_provider import BuiltinToolProviderController
class TwilioProvider(BuiltinToolProviderController):
def _validate_credentials(self, credentials: dict[str, Any]) -> None:
try:
"""
SendMessageTool().fork_tool_runtime(
meta={
"credentials": credentials,
}
).invoke(
user_id="",
tool_parameters={
"message": "Credential validation message",
"to_number": "+14846624384",
},
)
"""
pass
except Exception as e:
raise ToolProviderCredentialValidationError(str(e))
identity:
author: Yash Parmar
name: twilio
label:
en_US: Twilio
zh_Hans: Twilio
pt_BR: Twilio
description:
en_US: Send messages through SMS or Twilio Messaging Channels.
zh_Hans: 通过SMS或Twilio消息通道发送消息。
pt_BR: Send messages through SMS or Twilio Messaging Channels.
icon: icon.svg
credentials_for_provider:
account_sid:
type: secret-input
required: true
label:
en_US: Account SID
zh_Hans: 账户SID
pt_BR: Account SID
placeholder:
en_US: Please input your Twilio Account SID
zh_Hans: 请输入您的Twilio账户SID
pt_BR: Please input your Twilio Account SID
auth_token:
type: secret-input
required: true
label:
en_US: Auth Token
zh_Hans: 认证令牌
pt_BR: Auth Token
placeholder:
en_US: Please input your Twilio Auth Token
zh_Hans: 请输入您的Twilio认证令牌
pt_BR: Please input your Twilio Auth Token
from_number:
type: secret-input
required: true
label:
en_US: From Number
zh_Hans: 发信号码
pt_BR: De Número
placeholder:
en_US: Please input your Twilio phone number
zh_Hans: 请输入您的Twilio电话号码
pt_BR: Please input your Twilio phone number
...@@ -174,7 +174,18 @@ class Tool(BaseModel, ABC): ...@@ -174,7 +174,18 @@ class Tool(BaseModel, ABC):
return result return result
def invoke(self, user_id: str, tool_parameters: dict[str, Any]) -> list[ToolInvokeMessage]: def invoke(self, user_id: str, tool_parameters: Union[dict[str, Any], str]) -> list[ToolInvokeMessage]:
# check if tool_parameters is a string
if isinstance(tool_parameters, str):
# check if this tool has only one parameter
parameters = [parameter for parameter in self.parameters if parameter.form == ToolParameter.ToolParameterForm.LLM]
if parameters and len(parameters) == 1:
tool_parameters = {
parameters[0].name: tool_parameters
}
else:
raise ValueError(f"tool_parameters should be a dict, but got a string: {tool_parameters}")
# update tool_parameters # update tool_parameters
if self.runtime.runtime_parameters: if self.runtime.runtime_parameters:
tool_parameters.update(self.runtime.runtime_parameters) tool_parameters.update(self.runtime.runtime_parameters)
......
from flask import Flask
def init_app(app: Flask):
if app.config.get('API_COMPRESSION_ENABLED', False):
from flask_compress import Compress
compress = Compress()
compress.init_app(app)
...@@ -3,6 +3,7 @@ beautifulsoup4==4.12.2 ...@@ -3,6 +3,7 @@ beautifulsoup4==4.12.2
flask~=3.0.1 flask~=3.0.1
Flask-SQLAlchemy~=3.0.5 Flask-SQLAlchemy~=3.0.5
SQLAlchemy~=1.4.28 SQLAlchemy~=1.4.28
Flask-Compress~=1.14
flask-login~=0.6.3 flask-login~=0.6.3
flask-migrate~=4.0.5 flask-migrate~=4.0.5
flask-restful~=0.3.10 flask-restful~=0.3.10
...@@ -35,7 +36,7 @@ docx2txt==0.8 ...@@ -35,7 +36,7 @@ docx2txt==0.8
pypdfium2==4.16.0 pypdfium2==4.16.0
resend~=0.7.0 resend~=0.7.0
pyjwt~=2.8.0 pyjwt~=2.8.0
anthropic~=0.7.7 anthropic~=0.17.0
newspaper3k==0.2.8 newspaper3k==0.2.8
google-api-python-client==2.90.0 google-api-python-client==2.90.0
wikipedia==1.4.0 wikipedia==1.4.0
...@@ -67,4 +68,7 @@ pydub~=0.25.1 ...@@ -67,4 +68,7 @@ pydub~=0.25.1
gmpy2~=2.1.5 gmpy2~=2.1.5
numexpr~=2.9.0 numexpr~=2.9.0
duckduckgo-search==4.4.3 duckduckgo-search==4.4.3
arxiv==2.1.0 arxiv==2.1.0
\ No newline at end of file yarl~=1.9.4
twilio==9.0.0
qrcode~=7.4.2
import os import os
from time import sleep from time import sleep
from typing import Any, Generator, List, Literal, Union from typing import Any, Literal, Union, Iterable
from anthropic.resources import Messages
from anthropic.types.message_delta_event import Delta
import anthropic import anthropic
import pytest import pytest
from _pytest.monkeypatch import MonkeyPatch from _pytest.monkeypatch import MonkeyPatch
from anthropic import Anthropic from anthropic import Anthropic, Stream
from anthropic._types import NOT_GIVEN, Body, Headers, NotGiven, Query from anthropic.types import MessageParam, Message, MessageStreamEvent, \
from anthropic.resources.completions import Completions ContentBlock, MessageStartEvent, Usage, TextDelta, MessageDeltaEvent, MessageStopEvent, ContentBlockDeltaEvent, \
from anthropic.types import Completion, completion_create_params MessageDeltaUsage
MOCK = os.getenv('MOCK_SWITCH', 'false') == 'true' MOCK = os.getenv('MOCK_SWITCH', 'false') == 'true'
class MockAnthropicClass(object): class MockAnthropicClass(object):
@staticmethod @staticmethod
def mocked_anthropic_chat_create_sync(model: str) -> Completion: def mocked_anthropic_chat_create_sync(model: str) -> Message:
return Completion( return Message(
completion='hello, I\'m a chatbot from anthropic', id='msg-123',
type='message',
role='assistant',
content=[ContentBlock(text='hello, I\'m a chatbot from anthropic', type='text')],
model=model, model=model,
stop_reason='stop_sequence' stop_reason='stop_sequence',
usage=Usage(
input_tokens=1,
output_tokens=1
)
) )
@staticmethod @staticmethod
def mocked_anthropic_chat_create_stream(model: str) -> Generator[Completion, None, None]: def mocked_anthropic_chat_create_stream(model: str) -> Stream[MessageStreamEvent]:
full_response_text = "hello, I'm a chatbot from anthropic" full_response_text = "hello, I'm a chatbot from anthropic"
for i in range(0, len(full_response_text) + 1): yield MessageStartEvent(
sleep(0.1) type='message_start',
if i == len(full_response_text): message=Message(
yield Completion( id='msg-123',
completion='', content=[],
model=model, role='assistant',
stop_reason='stop_sequence' model=model,
) stop_reason=None,
else: type='message',
yield Completion( usage=Usage(
completion=full_response_text[i], input_tokens=1,
model=model, output_tokens=1
stop_reason=''
) )
)
)
index = 0
for i in range(0, len(full_response_text)):
sleep(0.1)
yield ContentBlockDeltaEvent(
type='content_block_delta',
delta=TextDelta(text=full_response_text[i], type='text_delta'),
index=index
)
index += 1
yield MessageDeltaEvent(
type='message_delta',
delta=Delta(
stop_reason='stop_sequence'
),
usage=MessageDeltaUsage(
output_tokens=1
)
)
yield MessageStopEvent(type='message_stop')
def mocked_anthropic(self: Completions, *, def mocked_anthropic(self: Messages, *,
max_tokens_to_sample: int, max_tokens: int,
model: Union[str, Literal["claude-2.1", "claude-instant-1"]], messages: Iterable[MessageParam],
prompt: str, model: str,
stream: Literal[True], stream: Literal[True],
**kwargs: Any **kwargs: Any
) -> Union[Completion, Generator[Completion, None, None]]: ) -> Union[Message, Stream[MessageStreamEvent]]:
if len(self._client.api_key) < 18: if len(self._client.api_key) < 18:
raise anthropic.AuthenticationError('Invalid API key') raise anthropic.AuthenticationError('Invalid API key')
...@@ -55,12 +90,13 @@ class MockAnthropicClass(object): ...@@ -55,12 +90,13 @@ class MockAnthropicClass(object):
else: else:
return MockAnthropicClass.mocked_anthropic_chat_create_sync(model=model) return MockAnthropicClass.mocked_anthropic_chat_create_sync(model=model)
@pytest.fixture @pytest.fixture
def setup_anthropic_mock(request, monkeypatch: MonkeyPatch): def setup_anthropic_mock(request, monkeypatch: MonkeyPatch):
if MOCK: if MOCK:
monkeypatch.setattr(Completions, 'create', MockAnthropicClass.mocked_anthropic) monkeypatch.setattr(Messages, 'create', MockAnthropicClass.mocked_anthropic)
yield yield
if MOCK: if MOCK:
monkeypatch.undo() monkeypatch.undo()
\ No newline at end of file
...@@ -32,68 +32,70 @@ class MockXinferenceClass(object): ...@@ -32,68 +32,70 @@ class MockXinferenceClass(object):
response = Response() response = Response()
if 'v1/models/' in url: if 'v1/models/' in url:
# get model uid # get model uid
model_uid = url.split('/')[-1] model_uid = url.split('/')[-1] or ''
if not re.match(r'[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}', model_uid) and \ if not re.match(r'[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}', model_uid) and \
model_uid not in ['generate', 'chat', 'embedding', 'rerank']: model_uid not in ['generate', 'chat', 'embedding', 'rerank']:
response.status_code = 404 response.status_code = 404
response._content = b'{}'
return response return response
# check if url is valid # check if url is valid
if not re.match(r'^(https?):\/\/[^\s\/$.?#].[^\s]*$', url): if not re.match(r'^(https?):\/\/[^\s\/$.?#].[^\s]*$', url):
response.status_code = 404 response.status_code = 404
response._content = b'{}'
return response return response
if model_uid in ['generate', 'chat']: if model_uid in ['generate', 'chat']:
response.status_code = 200 response.status_code = 200
response._content = b'''{ response._content = b'''{
"model_type": "LLM", "model_type": "LLM",
"address": "127.0.0.1:43877", "address": "127.0.0.1:43877",
"accelerators": [ "accelerators": [
"0", "0",
"1" "1"
], ],
"model_name": "chatglm3-6b", "model_name": "chatglm3-6b",
"model_lang": [ "model_lang": [
"en" "en"
], ],
"model_ability": [ "model_ability": [
"generate", "generate",
"chat" "chat"
], ],
"model_description": "latest chatglm3", "model_description": "latest chatglm3",
"model_format": "pytorch", "model_format": "pytorch",
"model_size_in_billions": 7, "model_size_in_billions": 7,
"quantization": "none", "quantization": "none",
"model_hub": "huggingface", "model_hub": "huggingface",
"revision": null, "revision": null,
"context_length": 2048, "context_length": 2048,
"replica": 1 "replica": 1
}''' }'''
return response return response
elif model_uid == 'embedding': elif model_uid == 'embedding':
response.status_code = 200 response.status_code = 200
response._content = b'''{ response._content = b'''{
"model_type": "embedding", "model_type": "embedding",
"address": "127.0.0.1:43877", "address": "127.0.0.1:43877",
"accelerators": [ "accelerators": [
"0", "0",
"1" "1"
], ],
"model_name": "bge", "model_name": "bge",
"model_lang": [ "model_lang": [
"en" "en"
], ],
"revision": null, "revision": null,
"max_tokens": 512 "max_tokens": 512
}''' }'''
return response return response
elif 'v1/cluster/auth' in url: elif 'v1/cluster/auth' in url:
response.status_code = 200 response.status_code = 200
response._content = b'''{ response._content = b'''{
"auth": true "auth": true
}''' }'''
return response return response
def _check_cluster_authenticated(self): def _check_cluster_authenticated(self):
......
...@@ -15,14 +15,14 @@ def test_validate_credentials(setup_anthropic_mock): ...@@ -15,14 +15,14 @@ def test_validate_credentials(setup_anthropic_mock):
with pytest.raises(CredentialsValidateFailedError): with pytest.raises(CredentialsValidateFailedError):
model.validate_credentials( model.validate_credentials(
model='claude-instant-1', model='claude-instant-1.2',
credentials={ credentials={
'anthropic_api_key': 'invalid_key' 'anthropic_api_key': 'invalid_key'
} }
) )
model.validate_credentials( model.validate_credentials(
model='claude-instant-1', model='claude-instant-1.2',
credentials={ credentials={
'anthropic_api_key': os.environ.get('ANTHROPIC_API_KEY') 'anthropic_api_key': os.environ.get('ANTHROPIC_API_KEY')
} }
...@@ -33,7 +33,7 @@ def test_invoke_model(setup_anthropic_mock): ...@@ -33,7 +33,7 @@ def test_invoke_model(setup_anthropic_mock):
model = AnthropicLargeLanguageModel() model = AnthropicLargeLanguageModel()
response = model.invoke( response = model.invoke(
model='claude-instant-1', model='claude-instant-1.2',
credentials={ credentials={
'anthropic_api_key': os.environ.get('ANTHROPIC_API_KEY'), 'anthropic_api_key': os.environ.get('ANTHROPIC_API_KEY'),
'anthropic_api_url': os.environ.get('ANTHROPIC_API_URL') 'anthropic_api_url': os.environ.get('ANTHROPIC_API_URL')
...@@ -49,7 +49,7 @@ def test_invoke_model(setup_anthropic_mock): ...@@ -49,7 +49,7 @@ def test_invoke_model(setup_anthropic_mock):
model_parameters={ model_parameters={
'temperature': 0.0, 'temperature': 0.0,
'top_p': 1.0, 'top_p': 1.0,
'max_tokens_to_sample': 10 'max_tokens': 10
}, },
stop=['How'], stop=['How'],
stream=False, stream=False,
...@@ -64,7 +64,7 @@ def test_invoke_stream_model(setup_anthropic_mock): ...@@ -64,7 +64,7 @@ def test_invoke_stream_model(setup_anthropic_mock):
model = AnthropicLargeLanguageModel() model = AnthropicLargeLanguageModel()
response = model.invoke( response = model.invoke(
model='claude-instant-1', model='claude-instant-1.2',
credentials={ credentials={
'anthropic_api_key': os.environ.get('ANTHROPIC_API_KEY') 'anthropic_api_key': os.environ.get('ANTHROPIC_API_KEY')
}, },
...@@ -78,7 +78,7 @@ def test_invoke_stream_model(setup_anthropic_mock): ...@@ -78,7 +78,7 @@ def test_invoke_stream_model(setup_anthropic_mock):
], ],
model_parameters={ model_parameters={
'temperature': 0.0, 'temperature': 0.0,
'max_tokens_to_sample': 100 'max_tokens': 100
}, },
stream=True, stream=True,
user="abc-123" user="abc-123"
...@@ -97,7 +97,7 @@ def test_get_num_tokens(): ...@@ -97,7 +97,7 @@ def test_get_num_tokens():
model = AnthropicLargeLanguageModel() model = AnthropicLargeLanguageModel()
num_tokens = model.get_num_tokens( num_tokens = model.get_num_tokens(
model='claude-instant-1', model='claude-instant-1.2',
credentials={ credentials={
'anthropic_api_key': os.environ.get('ANTHROPIC_API_KEY') 'anthropic_api_key': os.environ.get('ANTHROPIC_API_KEY')
}, },
......
...@@ -2,7 +2,7 @@ version: '3.1' ...@@ -2,7 +2,7 @@ version: '3.1'
services: services:
# API service # API service
api: api:
image: langgenius/dify-api:0.5.7 image: langgenius/dify-api:0.5.8
restart: always restart: always
environment: environment:
# Startup mode, 'api' starts the API server. # Startup mode, 'api' starts the API server.
...@@ -135,7 +135,7 @@ services: ...@@ -135,7 +135,7 @@ services:
# worker service # worker service
# The Celery worker for processing the queue. # The Celery worker for processing the queue.
worker: worker:
image: langgenius/dify-api:0.5.7 image: langgenius/dify-api:0.5.8
restart: always restart: always
environment: environment:
# Startup mode, 'worker' starts the Celery worker for processing the queue. # Startup mode, 'worker' starts the Celery worker for processing the queue.
...@@ -206,7 +206,7 @@ services: ...@@ -206,7 +206,7 @@ services:
# Frontend web application. # Frontend web application.
web: web:
image: langgenius/dify-web:0.5.7 image: langgenius/dify-web:0.5.8
restart: always restart: always
environment: environment:
EDITION: SELF_HOSTED EDITION: SELF_HOSTED
......
...@@ -40,6 +40,7 @@ const TextToSpeech: FC = () => { ...@@ -40,6 +40,7 @@ const TextToSpeech: FC = () => {
{ languageInfo?.example && ( { languageInfo?.example && (
<AudioBtn <AudioBtn
value={languageInfo?.example} value={languageInfo?.example}
voice={voiceItem?.value}
isAudition={true} isAudition={true}
/> />
)} )}
......
...@@ -9,12 +9,14 @@ import { textToAudio } from '@/service/share' ...@@ -9,12 +9,14 @@ import { textToAudio } from '@/service/share'
type AudioBtnProps = { type AudioBtnProps = {
value: string value: string
voice?: string
className?: string className?: string
isAudition?: boolean isAudition?: boolean
} }
const AudioBtn = ({ const AudioBtn = ({
value, value,
voice,
className, className,
isAudition, isAudition,
}: AudioBtnProps) => { }: AudioBtnProps) => {
...@@ -27,13 +29,16 @@ const AudioBtn = ({ ...@@ -27,13 +29,16 @@ const AudioBtn = ({
const pathname = usePathname() const pathname = usePathname()
const removeCodeBlocks = (inputText: any) => { const removeCodeBlocks = (inputText: any) => {
const codeBlockRegex = /```[\s\S]*?```/g const codeBlockRegex = /```[\s\S]*?```/g
return inputText.replace(codeBlockRegex, '') if (inputText)
return inputText.replace(codeBlockRegex, '')
return ''
} }
const playAudio = async () => { const playAudio = async () => {
const formData = new FormData() const formData = new FormData()
if (value !== '') { if (value !== '') {
formData.append('text', removeCodeBlocks(value)) formData.append('text', removeCodeBlocks(value))
formData.append('voice', removeCodeBlocks(voice))
let url = '' let url = ''
let isPublic = false let isPublic = false
...@@ -56,13 +61,14 @@ const AudioBtn = ({ ...@@ -56,13 +61,14 @@ const AudioBtn = ({
const audioUrl = URL.createObjectURL(blob) const audioUrl = URL.createObjectURL(blob)
const audio = new Audio(audioUrl) const audio = new Audio(audioUrl)
audioRef.current = audio audioRef.current = audio
audio.play().then(() => { audio.play().then(() => {}).catch(() => {
setIsPlaying(true)
}).catch(() => {
setIsPlaying(false) setIsPlaying(false)
URL.revokeObjectURL(audioUrl) URL.revokeObjectURL(audioUrl)
}) })
audio.onended = () => setHasEnded(true) audio.onended = () => {
setHasEnded(true)
setIsPlaying(false)
}
} }
catch (error) { catch (error) {
setIsPlaying(false) setIsPlaying(false)
...@@ -70,24 +76,34 @@ const AudioBtn = ({ ...@@ -70,24 +76,34 @@ const AudioBtn = ({
} }
} }
} }
const togglePlayPause = () => { const togglePlayPause = () => {
if (audioRef.current) { if (audioRef.current) {
if (isPlaying) { if (isPlaying) {
setPause(true) if (!hasEnded) {
audioRef.current.pause() setPause(false)
} audioRef.current.play()
else if (!hasEnded) { }
setPause(false) if (!isPause) {
audioRef.current.play() setPause(true)
audioRef.current.pause()
}
} }
else if (!isPlaying) { else if (!isPlaying) {
playAudio().then() if (isPause) {
setPause(false)
audioRef.current.play()
}
else {
setHasEnded(false)
playAudio().then()
}
} }
setIsPlaying(prevIsPlaying => !prevIsPlaying) setIsPlaying(prevIsPlaying => !prevIsPlaying)
} }
else { else {
playAudio().then() setIsPlaying(true)
if (!isPlaying)
playAudio().then()
} }
} }
...@@ -102,7 +118,7 @@ const AudioBtn = ({ ...@@ -102,7 +118,7 @@ const AudioBtn = ({
className={`box-border p-0.5 flex items-center justify-center cursor-pointer ${isAudition || 'rounded-md bg-white'}`} className={`box-border p-0.5 flex items-center justify-center cursor-pointer ${isAudition || 'rounded-md bg-white'}`}
style={{ boxShadow: !isAudition ? '0px 4px 8px -2px rgba(16, 24, 40, 0.1), 0px 2px 4px -2px rgba(16, 24, 40, 0.06)' : '' }} style={{ boxShadow: !isAudition ? '0px 4px 8px -2px rgba(16, 24, 40, 0.1), 0px 2px 4px -2px rgba(16, 24, 40, 0.06)' : '' }}
onClick={togglePlayPause}> onClick={togglePlayPause}>
<div className={`w-6 h-6 rounded-md ${!isAudition ? 'hover:bg-gray-200' : 'hover:bg-gray-50'} ${!isPause ? ((isPlaying && !hasEnded) ? s.playIcon : s.stopIcon) : s.pauseIcon}`}></div> <div className={`w-6 h-6 rounded-md ${!isAudition ? 'hover:bg-gray-200' : 'hover:bg-gray-50'} ${(isPlaying && !hasEnded) ? s.pauseIcon : s.playIcon}`}></div>
</div> </div>
</Tooltip> </Tooltip>
</div> </div>
......
...@@ -8,9 +8,3 @@ ...@@ -8,9 +8,3 @@
background-position: center; background-position: center;
background-repeat: no-repeat; background-repeat: no-repeat;
} }
.stopIcon {
background-position: center;
background-repeat: no-repeat;
background-image: url(~@/app/components/develop/secret-key/assets/stop.svg);
}
\ No newline at end of file
...@@ -77,6 +77,7 @@ const Operation: FC<OperationProps> = ({ ...@@ -77,6 +77,7 @@ const Operation: FC<OperationProps> = ({
{(!isOpeningStatement && config?.text_to_speech?.enabled) && ( {(!isOpeningStatement && config?.text_to_speech?.enabled) && (
<AudioBtn <AudioBtn
value={content} value={content}
voice={config?.text_to_speech?.voice}
className='hidden group-hover:block' className='hidden group-hover:block'
/> />
)} )}
......
...@@ -9,6 +9,7 @@ import { ...@@ -9,6 +9,7 @@ import {
} from 'react' } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useThrottleEffect } from 'ahooks' import { useThrottleEffect } from 'ahooks'
import { debounce } from 'lodash-es'
import type { import type {
ChatConfig, ChatConfig,
ChatItem, ChatItem,
...@@ -81,16 +82,24 @@ const Chat: FC<ChatProps> = ({ ...@@ -81,16 +82,24 @@ const Chat: FC<ChatProps> = ({
chatContainerRef.current.scrollTop = chatContainerRef.current.scrollHeight chatContainerRef.current.scrollTop = chatContainerRef.current.scrollHeight
} }
useThrottleEffect(() => { const handleWindowResize = () => {
handleScrolltoBottom()
if (chatContainerRef.current && chatFooterRef.current) if (chatContainerRef.current && chatFooterRef.current)
chatFooterRef.current.style.width = `${chatContainerRef.current.clientWidth}px` chatFooterRef.current.style.width = `${chatContainerRef.current.clientWidth}px`
if (chatContainerInnerRef.current && chatFooterInnerRef.current) if (chatContainerInnerRef.current && chatFooterInnerRef.current)
chatFooterInnerRef.current.style.width = `${chatContainerInnerRef.current.clientWidth}px` chatFooterInnerRef.current.style.width = `${chatContainerInnerRef.current.clientWidth}px`
}
useThrottleEffect(() => {
handleScrolltoBottom()
handleWindowResize()
}, [chatList], { wait: 500 }) }, [chatList], { wait: 500 })
useEffect(() => {
window.addEventListener('resize', debounce(handleWindowResize))
return () => window.removeEventListener('resize', handleWindowResize)
}, [])
useEffect(() => { useEffect(() => {
if (chatFooterRef.current && chatContainerRef.current) { if (chatFooterRef.current && chatContainerRef.current) {
const resizeObserver = new ResizeObserver((entries) => { const resizeObserver = new ResizeObserver((entries) => {
......
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_129_107)"> <g clip-path="url(#clip0_129_107)">
<path d="M7.99991 14.6666C11.6819 14.6666 14.6666 11.6819 14.6666 7.99998C14.6666 4.31808 11.6819 1.33331 7.99998 1.33331C4.31808 1.33331 1.33331 4.31808 1.33331 7.99998C1.33331 11.6819 4.31808 14.6666 7.99998 14.6666Z" stroke="#155EEF" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> <path d="M7.99998 14.6666C11.6819 14.6666 14.6666 11.6819 14.6666 7.99998C14.6666 4.31808 11.6819 1.33331 7.99998 1.33331C4.31808 1.33331 1.33331 4.31808 1.33331 7.99998C1.33331 11.6819 4.31808 14.6666 7.99998 14.6666Z" stroke="#667085" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.66665 5.33331L10.6666 7.99998L6.66665 10.6666V5.33331Z" stroke="#155EEF" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> <path d="M6.66665 5.33331L10.6666 7.99998L6.66665 10.6666V5.33331Z" stroke="#667085" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</g> </g>
<defs> <defs>
<clipPath id="clip0_129_107"> <clipPath id="clip0_129_107">
......
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_129_107)">
<path d="M7.99998 14.6666C11.6819 14.6666 14.6666 11.6819 14.6666 7.99998C14.6666 4.31808 11.6819 1.33331 7.99998 1.33331C4.31808 1.33331 1.33331 4.31808 1.33331 7.99998C1.33331 11.6819 4.31808 14.6666 7.99998 14.6666Z" stroke="#667085" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.66665 5.33331L10.6666 7.99998L6.66665 10.6666V5.33331Z" stroke="#667085" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_129_107">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>
...@@ -71,8 +71,8 @@ Chat applications support session persistence, allowing previous chat history to ...@@ -71,8 +71,8 @@ Chat applications support session persistence, allowing previous chat history to
- `upload_file_id` (string) Uploaded file ID, which must be obtained by uploading through the File Upload API in advance (when the transfer method is `local_file`) - `upload_file_id` (string) Uploaded file ID, which must be obtained by uploading through the File Upload API in advance (when the transfer method is `local_file`)
</Property> </Property>
<Property name='auto_generate_name' type='bool' key='auto_generate_name'> <Property name='auto_generate_name' type='bool' key='auto_generate_name'>
Auto-generate title, default is `false`. Auto-generate title, default is `true`.
Can achieve async title generation by calling the conversation rename API and setting `auto_generate` to true. If set to `false`, can achieve async title generation by calling the conversation rename API and setting `auto_generate` to `true`.
</Property> </Property>
</Properties> </Properties>
......
...@@ -71,7 +71,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx' ...@@ -71,7 +71,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
- `upload_file_id` 上传文件 ID。(仅当传递方式为 `local_file `时)。 - `upload_file_id` 上传文件 ID。(仅当传递方式为 `local_file `时)。
</Property> </Property>
<Property name='auto_generate_name' type='bool' key='auto_generate_name'> <Property name='auto_generate_name' type='bool' key='auto_generate_name'>
(选填)自动生成标题,默认 `false`。 可通过调用会话重命名接口并设置 `auto_generate` 为 `true` 实现异步生成标题。 (选填)自动生成标题,默认 `true`。 若设置为 `false`,则可通过调用会话重命名接口并设置 `auto_generate` 为 `true` 实现异步生成标题。
</Property> </Property>
</Properties> </Properties>
......
{ {
"name": "dify-web", "name": "dify-web",
"version": "0.5.7", "version": "0.5.8",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment