Commit c8757ef0 authored by crazywoola's avatar crazywoola

Merge branch 'fix-explore-apps-search-params-issue' into deploy/dev

parents 6df111a6 5aae559b
...@@ -41,6 +41,8 @@ jobs: ...@@ -41,6 +41,8 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup NodeJS - name: Setup NodeJS
uses: actions/setup-node@v4 uses: actions/setup-node@v4
...@@ -60,11 +62,10 @@ jobs: ...@@ -60,11 +62,10 @@ jobs:
yarn run lint yarn run lint
- name: Super-linter - name: Super-linter
uses: super-linter/super-linter/slim@v5 uses: super-linter/super-linter/slim@v6
env: env:
BASH_SEVERITY: warning BASH_SEVERITY: warning
DEFAULT_BRANCH: main DEFAULT_BRANCH: main
ERROR_ON_MISSING_EXEC_BIT: true
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
IGNORE_GENERATED_FILES: true IGNORE_GENERATED_FILES: true
IGNORE_GITIGNORED_FILES: true IGNORE_GITIGNORED_FILES: true
......
...@@ -82,7 +82,7 @@ UPLOAD_IMAGE_FILE_SIZE_LIMIT=10 ...@@ -82,7 +82,7 @@ UPLOAD_IMAGE_FILE_SIZE_LIMIT=10
MULTIMODAL_SEND_IMAGE_FORMAT=base64 MULTIMODAL_SEND_IMAGE_FORMAT=base64
# Mail configuration, support: resend, smtp # Mail configuration, support: resend, smtp
MAIL_TYPE=resend MAIL_TYPE=
MAIL_DEFAULT_SEND_FROM=no-reply <no-reply@dify.ai> MAIL_DEFAULT_SEND_FROM=no-reply <no-reply@dify.ai>
RESEND_API_KEY= RESEND_API_KEY=
RESEND_API_URL=https://api.resend.com RESEND_API_URL=https://api.resend.com
...@@ -131,4 +131,4 @@ UNSTRUCTURED_API_URL= ...@@ -131,4 +131,4 @@ UNSTRUCTURED_API_URL=
SSRF_PROXY_HTTP_URL= SSRF_PROXY_HTTP_URL=
SSRF_PROXY_HTTPS_URL= SSRF_PROXY_HTTPS_URL=
BATCH_UPLOAD_LIMIT=10 BATCH_UPLOAD_LIMIT=10
\ No newline at end of file
...@@ -131,6 +131,8 @@ def vdb_migrate(): ...@@ -131,6 +131,8 @@ def vdb_migrate():
""" """
click.echo(click.style('Start migrate vector db.', fg='green')) click.echo(click.style('Start migrate vector db.', fg='green'))
create_count = 0 create_count = 0
skipped_count = 0
total_count = 0
config = current_app.config config = current_app.config
vector_type = config.get('VECTOR_STORE') vector_type = config.get('VECTOR_STORE')
page = 1 page = 1
...@@ -143,14 +145,19 @@ def vdb_migrate(): ...@@ -143,14 +145,19 @@ def vdb_migrate():
page += 1 page += 1
for dataset in datasets: for dataset in datasets:
total_count = total_count + 1
click.echo(f'Processing the {total_count} dataset {dataset.id}. '
+ f'{create_count} created, ${skipped_count} skipped.')
try: try:
click.echo('Create dataset vdb index: {}'.format(dataset.id)) click.echo('Create dataset vdb index: {}'.format(dataset.id))
if dataset.index_struct_dict: if dataset.index_struct_dict:
if dataset.index_struct_dict['type'] == vector_type: if dataset.index_struct_dict['type'] == vector_type:
skipped_count = skipped_count + 1
continue continue
collection_name = ''
if vector_type == "weaviate": if vector_type == "weaviate":
dataset_id = dataset.id dataset_id = dataset.id
collection_name = "Vector_index_" + dataset_id.replace("-", "_") + '_Node' collection_name = Dataset.gen_collection_name_by_id(dataset_id)
index_struct_dict = { index_struct_dict = {
"type": 'weaviate', "type": 'weaviate',
"vector_store": {"class_prefix": collection_name} "vector_store": {"class_prefix": collection_name}
...@@ -167,7 +174,7 @@ def vdb_migrate(): ...@@ -167,7 +174,7 @@ def vdb_migrate():
raise ValueError('Dataset Collection Bindings is not exist!') raise ValueError('Dataset Collection Bindings is not exist!')
else: else:
dataset_id = dataset.id dataset_id = dataset.id
collection_name = "Vector_index_" + dataset_id.replace("-", "_") + '_Node' collection_name = Dataset.gen_collection_name_by_id(dataset_id)
index_struct_dict = { index_struct_dict = {
"type": 'qdrant', "type": 'qdrant',
"vector_store": {"class_prefix": collection_name} "vector_store": {"class_prefix": collection_name}
...@@ -176,7 +183,7 @@ def vdb_migrate(): ...@@ -176,7 +183,7 @@ def vdb_migrate():
elif vector_type == "milvus": elif vector_type == "milvus":
dataset_id = dataset.id dataset_id = dataset.id
collection_name = "Vector_index_" + dataset_id.replace("-", "_") + '_Node' collection_name = Dataset.gen_collection_name_by_id(dataset_id)
index_struct_dict = { index_struct_dict = {
"type": 'milvus', "type": 'milvus',
"vector_store": {"class_prefix": collection_name} "vector_store": {"class_prefix": collection_name}
...@@ -186,11 +193,17 @@ def vdb_migrate(): ...@@ -186,11 +193,17 @@ def vdb_migrate():
raise ValueError(f"Vector store {config.get('VECTOR_STORE')} is not supported.") raise ValueError(f"Vector store {config.get('VECTOR_STORE')} is not supported.")
vector = Vector(dataset) vector = Vector(dataset)
click.echo(f"vdb_migrate {dataset.id}") click.echo(f"Start to migrate dataset {dataset.id}.")
try: try:
vector.delete() vector.delete()
click.echo(
click.style(f'Successfully delete vector index {collection_name} for dataset {dataset.id}.',
fg='green'))
except Exception as e: except Exception as e:
click.echo(
click.style(f'Failed to delete vector index {collection_name} for dataset {dataset.id}.',
fg='red'))
raise e raise e
dataset_documents = db.session.query(DatasetDocument).filter( dataset_documents = db.session.query(DatasetDocument).filter(
...@@ -201,6 +214,7 @@ def vdb_migrate(): ...@@ -201,6 +214,7 @@ def vdb_migrate():
).all() ).all()
documents = [] documents = []
segments_count = 0
for dataset_document in dataset_documents: for dataset_document in dataset_documents:
segments = db.session.query(DocumentSegment).filter( segments = db.session.query(DocumentSegment).filter(
DocumentSegment.document_id == dataset_document.id, DocumentSegment.document_id == dataset_document.id,
...@@ -220,15 +234,22 @@ def vdb_migrate(): ...@@ -220,15 +234,22 @@ def vdb_migrate():
) )
documents.append(document) documents.append(document)
segments_count = segments_count + 1
if documents: if documents:
try: try:
click.echo(click.style(
f'Start to created vector index with {len(documents)} documents of {segments_count} segments for dataset {dataset.id}.',
fg='green'))
vector.create(documents) vector.create(documents)
click.echo(
click.style(f'Successfully created vector index for dataset {dataset.id}.', fg='green'))
except Exception as e: except Exception as e:
click.echo(click.style(f'Failed to created vector index for dataset {dataset.id}.', fg='red'))
raise e raise e
click.echo(f"Dataset {dataset.id} create successfully.")
db.session.add(dataset) db.session.add(dataset)
db.session.commit() db.session.commit()
click.echo(f'Successfully migrated dataset {dataset.id}.')
create_count += 1 create_count += 1
except Exception as e: except Exception as e:
db.session.rollback() db.session.rollback()
...@@ -237,7 +258,9 @@ def vdb_migrate(): ...@@ -237,7 +258,9 @@ def vdb_migrate():
fg='red')) fg='red'))
continue continue
click.echo(click.style('Congratulations! Create {} dataset indexes.'.format(create_count), fg='green')) click.echo(
click.style(f'Congratulations! Create {create_count} dataset indexes, and skipped {skipped_count} datasets.',
fg='green'))
def register_commands(app): def register_commands(app):
......
from collections.abc import Generator from collections.abc import Generator
from os.path import join
from typing import cast from typing import cast
from urllib.parse import urljoin
from httpx import Timeout from httpx import Timeout
from openai import ( from openai import (
...@@ -313,10 +313,13 @@ class LocalAILarguageModel(LargeLanguageModel): ...@@ -313,10 +313,13 @@ class LocalAILarguageModel(LargeLanguageModel):
:param credentials: credentials dict :param credentials: credentials dict
:return: client kwargs :return: client kwargs
""" """
if not credentials['server_url'].endswith('/'):
credentials['server_url'] += '/'
client_kwargs = { client_kwargs = {
"timeout": Timeout(315.0, read=300.0, write=10.0, connect=5.0), "timeout": Timeout(315.0, read=300.0, write=10.0, connect=5.0),
"api_key": "1", "api_key": "1",
"base_url": join(credentials['server_url'], 'v1'), "base_url": urljoin(credentials['server_url'], 'v1'),
} }
return client_kwargs return client_kwargs
......
...@@ -39,7 +39,7 @@ class Vector: ...@@ -39,7 +39,7 @@ class Vector:
collection_name = class_prefix collection_name = class_prefix
else: else:
dataset_id = self._dataset.id dataset_id = self._dataset.id
collection_name = "Vector_index_" + dataset_id.replace("-", "_") + '_Node' collection_name = Dataset.gen_collection_name_by_id(dataset_id)
index_struct_dict = { index_struct_dict = {
"type": 'weaviate', "type": 'weaviate',
"vector_store": {"class_prefix": collection_name} "vector_store": {"class_prefix": collection_name}
...@@ -70,7 +70,7 @@ class Vector: ...@@ -70,7 +70,7 @@ class Vector:
collection_name = class_prefix collection_name = class_prefix
else: else:
dataset_id = self._dataset.id dataset_id = self._dataset.id
collection_name = "Vector_index_" + dataset_id.replace("-", "_") + '_Node' collection_name = Dataset.gen_collection_name_by_id(dataset_id)
if not self._dataset.index_struct_dict: if not self._dataset.index_struct_dict:
index_struct_dict = { index_struct_dict = {
...@@ -96,7 +96,7 @@ class Vector: ...@@ -96,7 +96,7 @@ class Vector:
collection_name = class_prefix collection_name = class_prefix
else: else:
dataset_id = self._dataset.id dataset_id = self._dataset.id
collection_name = "Vector_index_" + dataset_id.replace("-", "_") + '_Node' collection_name = Dataset.gen_collection_name_by_id(dataset_id)
index_struct_dict = { index_struct_dict = {
"type": 'milvus', "type": 'milvus',
"vector_store": {"class_prefix": collection_name} "vector_store": {"class_prefix": collection_name}
......
...@@ -70,7 +70,7 @@ class WeaviateVector(BaseVector): ...@@ -70,7 +70,7 @@ class WeaviateVector(BaseVector):
return class_prefix return class_prefix
dataset_id = dataset.id dataset_id = dataset.id
return "Vector_index_" + dataset_id.replace("-", "_") + '_Node' return Dataset.gen_collection_name_by_id(dataset_id)
def to_index_struct(self) -> dict: def to_index_struct(self) -> dict:
return { return {
......
<svg id="logomark" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 17.732 24.269"><g id="tiny"><path d="M573.549,280.916l2.266,2.738,6.674-7.84c.353-.47.52-.717.353-1.117a1.218,1.218,0,0,0-1.061-.748h0a.953.953,0,0,0-.712.262Z" transform="translate(-566.984 -271.548)" fill="#bdb9b4"/><path d="M579.525,282.225l-10.606-10.174a1.413,1.413,0,0,0-.834-.5,1.09,1.09,0,0,0-1.027.66c-.167.4-.047.681.319,1.206l8.44,10.242h0l-6.282,7.716a1.336,1.336,0,0,0-.323,1.3,1.114,1.114,0,0,0,1.04.69A.992.992,0,0,0,571,293l8.519-7.92A1.924,1.924,0,0,0,579.525,282.225Z" transform="translate(-566.984 -271.548)" fill="#b31b1b"/><path d="M584.32,293.912l-8.525-10.275,0,0L573.53,280.9l-1.389,1.254a2.063,2.063,0,0,0,0,2.965l10.812,10.419a.925.925,0,0,0,.742.282,1.039,1.039,0,0,0,.953-.667A1.261,1.261,0,0,0,584.32,293.912Z" transform="translate(-566.984 -271.548)" fill="#bdb9b4"/></g></svg>
\ No newline at end of file
from core.tools.errors import ToolProviderCredentialValidationError
from core.tools.provider.builtin.arxiv.tools.arxiv_search import ArxivSearchTool
from core.tools.provider.builtin_tool_provider import BuiltinToolProviderController
class ArxivProvider(BuiltinToolProviderController):
def _validate_credentials(self, credentials: dict) -> None:
try:
ArxivSearchTool().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
identity:
author: Yash Parmar
name: arxiv
label:
en_US: ArXiv
zh_Hans: ArXiv
description:
en_US: Access to a vast repository of scientific papers and articles in various fields of research.
zh_Hans: 访问各个研究领域大量科学论文和文章的存储库。
icon: icon.svg
from typing import Any
from langchain.utilities import ArxivAPIWrapper
from pydantic import BaseModel, Field
from core.tools.entities.tool_entities import ToolInvokeMessage
from core.tools.tool.builtin_tool import BuiltinTool
class ArxivSearchInput(BaseModel):
query: str = Field(..., description="Search query.")
class ArxivSearchTool(BuiltinTool):
"""
A tool for searching articles on Arxiv.
"""
def _invoke(self, user_id: str, tool_parameters: dict[str, Any]) -> ToolInvokeMessage | list[ToolInvokeMessage]:
"""
Invokes the Arxiv 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 tool, including the 'query' parameter.
Returns:
ToolInvokeMessage | list[ToolInvokeMessage]: The result of the tool invocation, which can be a single message or a list of messages.
"""
query = tool_parameters.get('query', '')
if not query:
return self.create_text_message('Please input query')
arxiv = ArxivAPIWrapper()
response = arxiv.run(query)
return self.create_text_message(self.summary(user_id=user_id, content=response))
identity:
name: arxiv_search
author: Yash Parmar
label:
en_US: Arxiv Search
zh_Hans: Arxiv 搜索
description:
human:
en_US: A tool for searching scientific papers and articles from the Arxiv repository. Input can be an Arxiv ID or an author's name.
zh_Hans: 一个用于从Arxiv存储库搜索科学论文和文章的工具。 输入可以是Arxiv ID或作者姓名。
llm: A tool for searching scientific papers and articles from the Arxiv repository. Input can be an Arxiv ID or an author's name.
parameters:
- name: query
type: string
required: true
label:
en_US: Query string
zh_Hans: 查询字符串
human_description:
en_US: The Arxiv ID or author's name used for searching.
zh_Hans: 用于搜索的Arxiv ID或作者姓名。
llm_description: The Arxiv ID or author's name used for searching.
form: llm
...@@ -54,4 +54,4 @@ class GaodeRepositoriesTool(BuiltinTool): ...@@ -54,4 +54,4 @@ class GaodeRepositoriesTool(BuiltinTool):
s.close() s.close()
return self.create_text_message(f'No weather information for {city} was found.') return self.create_text_message(f'No weather information for {city} was found.')
except Exception as e: except Exception as e:
return self.create_text_message("Github API Key and Api Version is invalid. {}".format(e)) return self.create_text_message("Gaode API Key and Api Version is invalid. {}".format(e))
...@@ -67,9 +67,9 @@ class ApiTool(Tool): ...@@ -67,9 +67,9 @@ class ApiTool(Tool):
if 'api_key_header_prefix' in credentials: if 'api_key_header_prefix' in credentials:
api_key_header_prefix = credentials['api_key_header_prefix'] api_key_header_prefix = credentials['api_key_header_prefix']
if api_key_header_prefix == 'basic': if api_key_header_prefix == 'basic' and credentials['api_key_value']:
credentials['api_key_value'] = f'Basic {credentials["api_key_value"]}' credentials['api_key_value'] = f'Basic {credentials["api_key_value"]}'
elif api_key_header_prefix == 'bearer': elif api_key_header_prefix == 'bearer' and credentials['api_key_value']:
credentials['api_key_value'] = f'Bearer {credentials["api_key_value"]}' credentials['api_key_value'] = f'Bearer {credentials["api_key_value"]}'
elif api_key_header_prefix == 'custom': elif api_key_header_prefix == 'custom':
pass pass
...@@ -184,21 +184,7 @@ class ApiTool(Tool): ...@@ -184,21 +184,7 @@ class ApiTool(Tool):
for name, property in properties.items(): for name, property in properties.items():
if name in parameters: if name in parameters:
# convert type # convert type
try: body[name] = self._convert_body_property_type(property, parameters[name])
value = parameters[name]
if property['type'] == 'integer':
value = int(value)
elif property['type'] == 'number':
# check if it is a float
if '.' in value:
value = float(value)
else:
value = int(value)
elif property['type'] == 'boolean':
value = bool(value)
body[name] = value
except ValueError as e:
body[name] = parameters[name]
elif name in required: elif name in required:
raise ToolProviderCredentialValidationError( raise ToolProviderCredentialValidationError(
f"Missing required parameter {name} in operation {self.api_bundle.operation_id}" f"Missing required parameter {name} in operation {self.api_bundle.operation_id}"
...@@ -228,10 +214,6 @@ class ApiTool(Tool): ...@@ -228,10 +214,6 @@ class ApiTool(Tool):
elif method == 'put': elif method == 'put':
response = ssrf_proxy.put(url, params=params, headers=headers, cookies=cookies, data=body, timeout=10, follow_redirects=True) response = ssrf_proxy.put(url, params=params, headers=headers, cookies=cookies, data=body, timeout=10, follow_redirects=True)
elif method == 'delete': elif method == 'delete':
"""
request body data is unsupported for DELETE method in standard http protocol
however, OpenAPI 3.0 supports request body data for DELETE method, so we support it here by using requests
"""
response = ssrf_proxy.delete(url, params=params, headers=headers, cookies=cookies, data=body, timeout=10, allow_redirects=True) response = ssrf_proxy.delete(url, params=params, headers=headers, cookies=cookies, data=body, timeout=10, allow_redirects=True)
elif method == 'patch': elif method == 'patch':
response = ssrf_proxy.patch(url, params=params, headers=headers, cookies=cookies, data=body, timeout=10, follow_redirects=True) response = ssrf_proxy.patch(url, params=params, headers=headers, cookies=cookies, data=body, timeout=10, follow_redirects=True)
...@@ -243,6 +225,66 @@ class ApiTool(Tool): ...@@ -243,6 +225,66 @@ class ApiTool(Tool):
raise ValueError(f'Invalid http method {method}') raise ValueError(f'Invalid http method {method}')
return response return response
def _convert_body_property_any_of(self, property: dict[str, Any], value: Any, any_of: list[dict[str, Any]], max_recursive=10) -> Any:
if max_recursive <= 0:
raise Exception("Max recursion depth reached")
for option in any_of or []:
try:
if 'type' in option:
# Attempt to convert the value based on the type.
if option['type'] == 'integer' or option['type'] == 'int':
return int(value)
elif option['type'] == 'number':
if '.' in str(value):
return float(value)
else:
return int(value)
elif option['type'] == 'string':
return str(value)
elif option['type'] == 'boolean':
if str(value).lower() in ['true', '1']:
return True
elif str(value).lower() in ['false', '0']:
return False
else:
continue # Not a boolean, try next option
elif option['type'] == 'null' and not value:
return None
else:
continue # Unsupported type, try next option
elif 'anyOf' in option and isinstance(option['anyOf'], list):
# Recursive call to handle nested anyOf
return self._convert_body_property_any_of(property, value, option['anyOf'], max_recursive - 1)
except ValueError:
continue # Conversion failed, try next option
# If no option succeeded, you might want to return the value as is or raise an error
return value # or raise ValueError(f"Cannot convert value '{value}' to any specified type in anyOf")
def _convert_body_property_type(self, property: dict[str, Any], value: Any) -> Any:
try:
if 'type' in property:
if property['type'] == 'integer' or property['type'] == 'int':
return int(value)
elif property['type'] == 'number':
# check if it is a float
if '.' in value:
return float(value)
else:
return int(value)
elif property['type'] == 'string':
return str(value)
elif property['type'] == 'boolean':
return bool(value)
elif property['type'] == 'null':
if value is None:
return None
else:
raise ValueError(f"Invalid type {property['type']} for property {property}")
elif 'anyOf' in property and isinstance(property['anyOf'], list):
return self._convert_body_property_any_of(property, value, property['anyOf'])
except ValueError as e:
return value
def _invoke(self, user_id: str, tool_parameters: dict[str, Any]) -> ToolInvokeMessage | list[ToolInvokeMessage]: def _invoke(self, user_id: str, tool_parameters: dict[str, Any]) -> ToolInvokeMessage | list[ToolInvokeMessage]:
""" """
......
...@@ -146,7 +146,8 @@ class ApiBasedToolSchemaParser: ...@@ -146,7 +146,8 @@ class ApiBasedToolSchemaParser:
bundles.append(ApiBasedToolBundle( bundles.append(ApiBasedToolBundle(
server_url=server_url + interface['path'], server_url=server_url + interface['path'],
method=interface['method'], method=interface['method'],
summary=interface['operation']['summary'] if 'summary' in interface['operation'] else None, summary=interface['operation']['description'] if 'description' in interface['operation'] else
interface['operation']['summary'] if 'summary' in interface['operation'] else None,
operation_id=interface['operation']['operationId'], operation_id=interface['operation']['operationId'],
parameters=parameters, parameters=parameters,
author='', author='',
...@@ -249,12 +250,10 @@ class ApiBasedToolSchemaParser: ...@@ -249,12 +250,10 @@ class ApiBasedToolSchemaParser:
if 'operationId' not in operation: if 'operationId' not in operation:
raise ToolApiSchemaError(f'No operationId found in operation {method} {path}.') raise ToolApiSchemaError(f'No operationId found in operation {method} {path}.')
if 'summary' not in operation or len(operation['summary']) == 0: if ('summary' not in operation or len(operation['summary']) == 0) and \
warning['missing_summary'] = f'No summary found in operation {method} {path}.' ('description' not in operation or len(operation['description']) == 0):
warning['missing_summary'] = f'No summary or description found in operation {method} {path}.'
if 'description' not in operation or len(operation['description']) == 0:
warning['missing_description'] = f'No description found in operation {method} {path}.'
openapi['paths'][path][method] = { openapi['paths'][path][method] = {
'operationId': operation['operationId'], 'operationId': operation['operationId'],
'summary': operation.get('summary', ''), 'summary': operation.get('summary', ''),
......
...@@ -116,6 +116,10 @@ class Dataset(db.Model): ...@@ -116,6 +116,10 @@ class Dataset(db.Model):
} }
return self.retrieval_model if self.retrieval_model else default_retrieval_model return self.retrieval_model if self.retrieval_model else default_retrieval_model
@staticmethod
def gen_collection_name_by_id(dataset_id: str) -> str:
normalized_dataset_id = dataset_id.replace("-", "_")
return f'Vector_index_{normalized_dataset_id}_Node'
class DatasetProcessRule(db.Model): class DatasetProcessRule(db.Model):
__tablename__ = 'dataset_process_rules' __tablename__ = 'dataset_process_rules'
......
...@@ -66,4 +66,5 @@ yfinance~=0.2.35 ...@@ -66,4 +66,5 @@ yfinance~=0.2.35
pydub~=0.25.1 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
\ No newline at end of file arxiv==2.1.0
\ No newline at end of file
...@@ -1255,7 +1255,7 @@ class DatasetCollectionBindingService: ...@@ -1255,7 +1255,7 @@ class DatasetCollectionBindingService:
dataset_collection_binding = DatasetCollectionBinding( dataset_collection_binding = DatasetCollectionBinding(
provider_name=provider_name, provider_name=provider_name,
model_name=model_name, model_name=model_name,
collection_name="Vector_index_" + str(uuid.uuid4()).replace("-", "_") + '_Node', collection_name=Dataset.gen_collection_name_by_id(str(uuid.uuid4())),
type=collection_type type=collection_type
) )
db.session.add(dataset_collection_binding) db.session.add(dataset_collection_binding)
......
...@@ -209,8 +209,8 @@ class ToolManageService: ...@@ -209,8 +209,8 @@ class ToolManageService:
# extra info like description will be set here # extra info like description will be set here
tool_bundles, schema_type = ToolManageService.convert_schema_to_tool_bundles(schema, extra_info) tool_bundles, schema_type = ToolManageService.convert_schema_to_tool_bundles(schema, extra_info)
if len(tool_bundles) > 10: if len(tool_bundles) > 100:
raise ValueError('the number of apis should be less than 10') raise ValueError('the number of apis should be less than 100')
# create db provider # create db provider
db_provider = ApiToolProvider( db_provider = ApiToolProvider(
......
...@@ -153,18 +153,18 @@ const AgentTools: FC = () => { ...@@ -153,18 +153,18 @@ const AgentTools: FC = () => {
) )
: ( : (
<div className='hidden group-hover:flex items-center'> <div className='hidden group-hover:flex items-center'>
{item.provider_type === CollectionType.builtIn && ( {/* {item.provider_type === CollectionType.builtIn && ( */}
<TooltipPlus <TooltipPlus
popupContent={t('tools.setBuiltInTools.infoAndSetting')} popupContent={t('tools.setBuiltInTools.infoAndSetting')}
> >
<div className='mr-1 p-1 rounded-md hover:bg-black/5 cursor-pointer' onClick={() => { <div className='mr-1 p-1 rounded-md hover:bg-black/5 cursor-pointer' onClick={() => {
setCurrentTool(item) setCurrentTool(item)
setIsShowSettingTool(true) setIsShowSettingTool(true)
}}> }}>
<InfoCircle className='w-4 h-4 text-gray-500' /> <InfoCircle className='w-4 h-4 text-gray-500' />
</div> </div>
</TooltipPlus> </TooltipPlus>
)} {/* )} */}
<div className='p-1 rounded-md hover:bg-black/5 cursor-pointer' onClick={() => { <div className='p-1 rounded-md hover:bg-black/5 cursor-pointer' onClick={() => {
const newModelConfig = produce(modelConfig, (draft) => { const newModelConfig = produce(modelConfig, (draft) => {
...@@ -209,6 +209,7 @@ const AgentTools: FC = () => { ...@@ -209,6 +209,7 @@ const AgentTools: FC = () => {
toolName={currentTool?.tool_name as string} toolName={currentTool?.tool_name as string}
setting={currentTool?.tool_parameters as any} setting={currentTool?.tool_parameters as any}
collection={currentTool?.collection as Collection} collection={currentTool?.collection as Collection}
isBuiltIn={currentTool?.collection?.type === CollectionType.builtIn}
onSave={handleToolSettingChange} onSave={handleToolSettingChange}
onHide={() => setIsShowSettingTool(false)} onHide={() => setIsShowSettingTool(false)}
/>) />)
......
...@@ -8,14 +8,17 @@ import Drawer from '@/app/components/base/drawer-plus' ...@@ -8,14 +8,17 @@ import Drawer from '@/app/components/base/drawer-plus'
import Form from '@/app/components/header/account-setting/model-provider-page/model-modal/Form' import Form from '@/app/components/header/account-setting/model-provider-page/model-modal/Form'
import { addDefaultValue, toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema' import { addDefaultValue, toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
import type { Collection, Tool } from '@/app/components/tools/types' import type { Collection, Tool } from '@/app/components/tools/types'
import { fetchBuiltInToolList } from '@/service/tools' import { fetchBuiltInToolList, fetchCustomToolList } from '@/service/tools'
import I18n from '@/context/i18n' import I18n from '@/context/i18n'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import Loading from '@/app/components/base/loading' import Loading from '@/app/components/base/loading'
import { DiagonalDividingLine } from '@/app/components/base/icons/src/public/common' import { DiagonalDividingLine } from '@/app/components/base/icons/src/public/common'
import { getLanguage } from '@/i18n/language' import { getLanguage } from '@/i18n/language'
import AppIcon from '@/app/components/base/app-icon'
type Props = { type Props = {
collection: Collection collection: Collection
isBuiltIn?: boolean
toolName: string toolName: string
setting?: Record<string, any> setting?: Record<string, any>
readonly?: boolean readonly?: boolean
...@@ -25,6 +28,7 @@ type Props = { ...@@ -25,6 +28,7 @@ type Props = {
const SettingBuiltInTool: FC<Props> = ({ const SettingBuiltInTool: FC<Props> = ({
collection, collection,
isBuiltIn = true,
toolName, toolName,
setting = {}, setting = {},
readonly, readonly,
...@@ -52,7 +56,7 @@ const SettingBuiltInTool: FC<Props> = ({ ...@@ -52,7 +56,7 @@ const SettingBuiltInTool: FC<Props> = ({
(async () => { (async () => {
setIsLoading(true) setIsLoading(true)
try { try {
const list = await fetchBuiltInToolList(collection.name) const list = isBuiltIn ? await fetchBuiltInToolList(collection.name) : await fetchCustomToolList(collection.name)
setTools(list) setTools(list)
const currTool = list.find(tool => tool.name === toolName) const currTool = list.find(tool => tool.name === toolName)
if (currTool) { if (currTool) {
...@@ -135,12 +139,24 @@ const SettingBuiltInTool: FC<Props> = ({ ...@@ -135,12 +139,24 @@ const SettingBuiltInTool: FC<Props> = ({
onHide={onHide} onHide={onHide}
title={( title={(
<div className='flex'> <div className='flex'>
<div {collection.icon === 'string'
className='w-6 h-6 bg-cover bg-center rounded-md' ? (
style={{ <div
backgroundImage: `url(${collection.icon})`, className='w-6 h-6 bg-cover bg-center rounded-md'
}} style={{
></div> backgroundImage: `url(${collection.icon})`,
}}
></div>
)
: (
<AppIcon
className='rounded-md'
size='tiny'
icon={(collection.icon as any)?.content}
background={(collection.icon as any)?.background}
/>
)}
<div className='ml-2 leading-6 text-base font-semibold text-gray-900'>{currTool?.label[language]}</div> <div className='ml-2 leading-6 text-base font-semibold text-gray-900'>{currTool?.label[language]}</div>
{(hasSetting && !readonly) && (<> {(hasSetting && !readonly) && (<>
<DiagonalDividingLine className='mx-4' /> <DiagonalDividingLine className='mx-4' />
......
...@@ -26,7 +26,8 @@ const Apps: FC = () => { ...@@ -26,7 +26,8 @@ const Apps: FC = () => {
const { isCurrentWorkspaceManager } = useAppContext() const { isCurrentWorkspaceManager } = useAppContext()
const router = useRouter() const router = useRouter()
const { hasEditPermission } = useContext(ExploreContext) const { hasEditPermission } = useContext(ExploreContext)
const allCategoriesEn = t('explore.apps.allCategories') const allCategoriesEn = t('explore.apps.allCategories', { lng: 'en' })
const [currCategory, setCurrCategory] = useTabSearchParams({ const [currCategory, setCurrCategory] = useTabSearchParams({
defaultTab: allCategoriesEn, defaultTab: allCategoriesEn,
}) })
...@@ -47,11 +48,10 @@ const Apps: FC = () => { ...@@ -47,11 +48,10 @@ const Apps: FC = () => {
}, },
) )
const currList = (() => { const currList
if (currCategory === allCategoriesEn) = currCategory === allCategoriesEn
return allList ? allList
return allList.filter(item => item.category === currCategory) : allList.filter(item => item.category === currCategory)
})()
const [currApp, setCurrApp] = React.useState<App | null>(null) const [currApp, setCurrApp] = React.useState<App | null>(null)
const [isShowCreateModal, setIsShowCreateModal] = React.useState(false) const [isShowCreateModal, setIsShowCreateModal] = React.useState(false)
...@@ -112,6 +112,7 @@ const Apps: FC = () => { ...@@ -112,6 +112,7 @@ const Apps: FC = () => {
list={categories} list={categories}
value={currCategory} value={currCategory}
onChange={setCurrCategory} onChange={setCurrCategory}
allCategoriesEn={allCategoriesEn}
/> />
<div className="relative flex flex-1 mt-6 pb-6 flex-col overflow-auto bg-gray-100 shrink-0 grow"> <div className="relative flex flex-1 mt-6 pb-6 flex-col overflow-auto bg-gray-100 shrink-0 grow">
<nav <nav
......
...@@ -12,7 +12,11 @@ export type ICategoryProps = { ...@@ -12,7 +12,11 @@ export type ICategoryProps = {
className?: string className?: string
list: AppCategory[] list: AppCategory[]
value: string value: string
onChange: (value: AppCategory | '') => void onChange: (value: AppCategory | string) => void
/**
* default value for searchparam 'category' in en
*/
allCategoriesEn: string
} }
const Category: FC<ICategoryProps> = ({ const Category: FC<ICategoryProps> = ({
...@@ -20,17 +24,24 @@ const Category: FC<ICategoryProps> = ({ ...@@ -20,17 +24,24 @@ const Category: FC<ICategoryProps> = ({
list, list,
value, value,
onChange, onChange,
allCategoriesEn,
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const isAllCategories = !list.includes(value)
const itemClassName = (isSelected: boolean) =>
cn(
isSelected
? 'bg-white text-primary-600 border-gray-200 font-semibold shadow-[0px_1px_2px_rgba(16,24,40,0.05)]'
: 'border-transparent font-medium',
'flex items-center h-7 px-3 border cursor-pointer rounded-lg',
)
const itemClassName = (isSelected: boolean) => cn(isSelected ? 'bg-white text-primary-600 border-gray-200 font-semibold' : 'border-transparent font-medium', 'flex items-center h-7 px-3 border cursor-pointer rounded-lg')
const itemStyle = (isSelected: boolean) => isSelected ? { boxShadow: '0px 1px 2px rgba(16, 24, 40, 0.05)' } : {}
return ( return (
<div className={cn(className, 'flex space-x-1 text-[13px] flex-wrap')}> <div className={cn(className, 'flex space-x-1 text-[13px] flex-wrap')}>
<div <div
className={itemClassName(value === '')} className={itemClassName(isAllCategories)}
style={itemStyle(value === '')} onClick={() => onChange(allCategoriesEn)}
onClick={() => onChange('')}
> >
{t('explore.apps.allCategories')} {t('explore.apps.allCategories')}
</div> </div>
...@@ -38,7 +49,6 @@ const Category: FC<ICategoryProps> = ({ ...@@ -38,7 +49,6 @@ const Category: FC<ICategoryProps> = ({
<div <div
key={name} key={name}
className={itemClassName(name === value)} className={itemClassName(name === value)}
style={itemStyle(name === value)}
onClick={() => onChange(name)} onClick={() => onChange(name)}
> >
{categoryI18n[name] ? t(`explore.category.${name}`) : name} {categoryI18n[name] ? t(`explore.category.${name}`) : name}
...@@ -47,4 +57,5 @@ const Category: FC<ICategoryProps> = ({ ...@@ -47,4 +57,5 @@ const Category: FC<ICategoryProps> = ({
</div> </div>
) )
} }
export default React.memo(Category) export default React.memo(Category)
...@@ -23,9 +23,9 @@ export const AppInfo: FC<{ siteInfo: SiteInfo }> = ({ siteInfo }) => { ...@@ -23,9 +23,9 @@ export const AppInfo: FC<{ siteInfo: SiteInfo }> = ({ siteInfo }) => {
export const PromptTemplate: FC<{ html: string }> = ({ html }) => { export const PromptTemplate: FC<{ html: string }> = ({ html }) => {
return ( return (
<div <div
className={' box-border text-sm text-gray-700'} className={'box-border text-sm text-gray-700'}
dangerouslySetInnerHTML={{ __html: html }} dangerouslySetInnerHTML={{ __html: html }}
></div> />
) )
} }
......
...@@ -118,6 +118,13 @@ const EditCustomCollectionModal: FC<Props> = ({ ...@@ -118,6 +118,13 @@ const EditCustomCollectionModal: FC<Props> = ({
const handleSave = () => { const handleSave = () => {
const postData = clone(customCollection) const postData = clone(customCollection)
delete postData.tools delete postData.tools
if (postData.credentials.auth_type === AuthType.none) {
delete postData.credentials.api_key_header
delete postData.credentials.api_key_header_prefix
delete postData.credentials.api_key_value
}
if (isAdd) { if (isAdd) {
onAdd?.(postData) onAdd?.(postData)
return return
......
...@@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next' ...@@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector' import { useContext } from 'use-context-selector'
import { Settings01 } from '../../base/icons/src/vender/line/general' import { Settings01 } from '../../base/icons/src/vender/line/general'
import ConfigCredentials from './config-credentials' import ConfigCredentials from './config-credentials'
import type { Credential, CustomCollectionBackend, CustomParamSchema } from '@/app/components/tools/types' import { AuthType, type Credential, type CustomCollectionBackend, type CustomParamSchema } from '@/app/components/tools/types'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import Drawer from '@/app/components/base/drawer-plus' import Drawer from '@/app/components/base/drawer-plus'
import I18n from '@/context/i18n' import I18n from '@/context/i18n'
...@@ -34,9 +34,16 @@ const TestApi: FC<Props> = ({ ...@@ -34,9 +34,16 @@ const TestApi: FC<Props> = ({
const { operation_id: toolName, parameters } = tool const { operation_id: toolName, parameters } = tool
const [parametersValue, setParametersValue] = useState<Record<string, string>>({}) const [parametersValue, setParametersValue] = useState<Record<string, string>>({})
const handleTest = async () => { const handleTest = async () => {
// clone test schema
const credentials = JSON.parse(JSON.stringify(tempCredential)) as Credential
if (credentials.auth_type === AuthType.none) {
delete credentials.api_key_header_prefix
delete credentials.api_key_header
delete credentials.api_key_value
}
const data = { const data = {
tool_name: toolName, tool_name: toolName,
credentials: tempCredential, credentials,
schema_type: customCollection.schema_type, schema_type: customCollection.schema_type,
schema: customCollection.schema, schema: customCollection.schema,
parameters: parametersValue, parameters: parametersValue,
......
...@@ -69,9 +69,22 @@ const Tools: FC<Props> = ({ ...@@ -69,9 +69,22 @@ const Tools: FC<Props> = ({
})() })()
const [query, setQuery] = useState('') const [query, setQuery] = useState('')
const [collectionType, setCollectionType] = useTabSearchParams({ const [toolPageCollectionType, setToolPageCollectionType] = useTabSearchParams({
defaultTab: collectionTypeOptions[0].value, defaultTab: collectionTypeOptions[0].value,
}) })
const [appPageCollectionType, setAppPageCollectionType] = useState(collectionTypeOptions[0].value)
const { collectionType, setCollectionType } = (() => {
if (isInToolsPage) {
return {
collectionType: toolPageCollectionType,
setCollectionType: setToolPageCollectionType,
}
}
return {
collectionType: appPageCollectionType,
setCollectionType: setAppPageCollectionType,
}
})()
const showCollectionList = (() => { const showCollectionList = (() => {
let typeFilteredList: Collection[] = [] let typeFilteredList: Collection[] = []
......
...@@ -35,9 +35,10 @@ const Item: FC<Props> = ({ ...@@ -35,9 +35,10 @@ const Item: FC<Props> = ({
const language = getLanguage(locale) const language = getLanguage(locale)
const isBuiltIn = collection.type === CollectionType.builtIn const isBuiltIn = collection.type === CollectionType.builtIn
const canShowDetail = !isBuiltIn || (isBuiltIn && isInToolsPage) const canShowDetail = isInToolsPage
const [showDetail, setShowDetail] = useState(false) const [showDetail, setShowDetail] = useState(false)
const addBtn = <Button className='shrink-0 flex items-center h-7 !px-3 !text-xs !font-medium !text-gray-700' disabled={added || !collection.is_team_authorization} onClick={() => onAdd?.(payload)}>{t(`common.operation.${added ? 'added' : 'add'}`)}</Button> const addBtn = <Button className='shrink-0 flex items-center h-7 !px-3 !text-xs !font-medium !text-gray-700' disabled={added || !collection.is_team_authorization} onClick={() => onAdd?.(payload)}>{t(`common.operation.${added ? 'added' : 'add'}`)}</Button>
return ( return (
<> <>
<div <div
...@@ -63,7 +64,7 @@ const Item: FC<Props> = ({ ...@@ -63,7 +64,7 @@ const Item: FC<Props> = ({
)} )}
</div> </div>
</div> </div>
{showDetail && isBuiltIn && ( {showDetail && (
<SettingBuiltInTool <SettingBuiltInTool
collection={collection} collection={collection}
toolName={payload.name} toolName={payload.name}
...@@ -71,6 +72,7 @@ const Item: FC<Props> = ({ ...@@ -71,6 +72,7 @@ const Item: FC<Props> = ({
onHide={() => { onHide={() => {
setShowDetail(false) setShowDetail(false)
}} }}
isBuiltIn={isBuiltIn}
/> />
)} )}
</> </>
......
// TODO: maybe use faker.js to randomize the data
export const mockApps = {
recommended_apps: [
{
app: {
id: 'b82da4c0-2887-48cc-a7d6-7edc0bdd6002',
name: 'AI 前端面试官',
mode: 'chat',
icon: '🤖',
icon_background: null,
},
app_id: 'b82da4c0-2887-48cc-a7d6-7edc0bdd6002',
description:
'一个模拟的前端面试官,通过提问的方式对前端开发的技能水平进行检验。',
copyright: null,
privacy_policy: null,
category: 'HR',
position: 20,
is_listed: true,
install_count: 0,
installed: false,
editable: true,
is_agent: false,
},
{
app: {
id: '1fa25f89-2883-41ac-877e-c372274020a4',
name: '扁平风插画生成',
mode: 'chat',
icon: '🖼️',
icon_background: '#D5F5F6',
},
app_id: '1fa25f89-2883-41ac-877e-c372274020a4',
description: '输入相关元素,为你生成扁平插画风格的封面图片',
copyright: null,
privacy_policy: null,
category: '绘画',
position: 10,
is_listed: true,
install_count: 0,
installed: false,
editable: true,
is_agent: true,
},
{
app: {
id: '94b509ad-4225-4924-8b50-5c25c2bd7e3c',
name: '文章翻译助理 ',
mode: 'completion',
icon: '🤖',
icon_background: null,
},
app_id: '94b509ad-4225-4924-8b50-5c25c2bd7e3c',
description:
'一个多语言翻译器,提供多种语言翻译能力,输入你需要翻译的文本,选择目标语言即可。提示词来自宝玉。',
copyright: null,
privacy_policy: null,
category: 'Assistant',
position: 10,
is_listed: true,
install_count: 0,
installed: false,
editable: true,
is_agent: false,
},
{
app: {
id: 'c8003ab3-9bb7-4693-9249-e603d48e58a6',
name: 'SQL 生成器',
mode: 'completion',
icon: '🤖',
icon_background: null,
},
app_id: 'c8003ab3-9bb7-4693-9249-e603d48e58a6',
description:
'我将帮助你把自然语言转化成指定的数据库查询 SQL 语句,请在下方输入你需要查询的条件,并选择目标数据库类型。',
copyright: null,
privacy_policy: null,
category: 'Programming',
position: 12,
is_listed: true,
install_count: 3142,
installed: false,
editable: true,
is_agent: false,
},
{
app: {
id: 'dad6a1e0-0fe9-47e1-91a9-e16de48f1276',
name: '代码解释器',
mode: 'chat',
icon: 'eye-in-speech-bubble',
icon_background: '#FFEAD5',
},
app_id: 'dad6a1e0-0fe9-47e1-91a9-e16de48f1276',
description: '阐明代码的语法和语义。',
copyright: 'Copyright 2023 Dify',
privacy_policy: 'https://dify.ai',
category: 'Programming',
position: 2,
is_listed: true,
install_count: 2344,
installed: false,
editable: true,
is_agent: false,
},
{
app: {
id: 'fae3e7ac-8ccc-4d43-8986-7c61d2bdde4f',
name: '赛博朋克插画生成',
mode: 'chat',
icon: '🖼️',
icon_background: '#FFEAD5',
},
app_id: 'fae3e7ac-8ccc-4d43-8986-7c61d2bdde4f',
description: '输入相关元素,为你生成赛博朋克风格的插画',
copyright: null,
privacy_policy: null,
category: '绘画',
position: 10,
is_listed: true,
install_count: 0,
installed: false,
editable: true,
is_agent: true,
},
{
app: {
id: '4e57bc83-ab95-4f8a-a955-70796b4804a0',
name: 'SEO 文章生成专家',
mode: 'completion',
icon: '🤖',
icon_background: '#FFEAD5',
},
app_id: '4e57bc83-ab95-4f8a-a955-70796b4804a0',
description:
'我是一名SEO专家,可以根据您提供的标题、关键词、相关信息来批量生成SEO文章。',
copyright: null,
privacy_policy: null,
category: 'Assistant',
position: 10,
is_listed: true,
install_count: 0,
installed: false,
editable: true,
is_agent: false,
},
{
app: {
id: '6786ce62-fa85-4ea7-a4d1-5dbe3e3ff59f',
name: '会议纪要',
mode: 'chat',
icon: 'clipboard',
icon_background: '#D1E0FF',
},
app_id: '6786ce62-fa85-4ea7-a4d1-5dbe3e3ff59f',
description: '帮你重新组织和输出混乱复杂的会议纪要。',
copyright: 'Copyright 2023 Dify',
privacy_policy: 'https://dify.ai',
category: 'Writing',
position: 6,
is_listed: true,
install_count: 1542,
installed: false,
editable: true,
is_agent: false,
},
{
app: {
id: '73dd96bb-49b7-4791-acbd-9ef2ef506900',
name: '美股投资分析助手',
mode: 'chat',
icon: '🤑',
icon_background: '#E4FBCC',
},
app_id: '73dd96bb-49b7-4791-acbd-9ef2ef506900',
description:
'欢迎使用您的个性化美股投资分析助手,在这里我们深入的进行股票分析,为您提供全面的洞察。',
copyright: 'Dify.AI',
privacy_policy: null,
category: '智能助理',
position: 0,
is_listed: true,
install_count: 2,
installed: false,
editable: true,
is_agent: true,
},
{
app: {
id: '93ca3c2c-3a47-4658-b230-d5a6cc61ff01',
name: 'SVG Logo 设计',
mode: 'chat',
icon: '🎨',
icon_background: '#E4FBCC',
},
app_id: '93ca3c2c-3a47-4658-b230-d5a6cc61ff01',
description:
'您好,我是您的创意伙伴,将帮助您将想法生动地实现!我可以协助您利用DALL·E 3的能力创造出令人惊叹的设计。',
copyright: 'Dify.AI',
privacy_policy: null,
category: '智能助理',
position: 4,
is_listed: true,
install_count: 6,
installed: false,
editable: true,
is_agent: true,
},
{
app: {
id: '59924f26-963f-4b4b-90cf-978bbfcddc49',
name: '中英文互译',
mode: 'chat',
icon: 'speaking_head_in_silhouette',
icon_background: '#FBE8FF',
},
app_id: '59924f26-963f-4b4b-90cf-978bbfcddc49',
description: '翻译专家:提供中英文互译',
copyright: 'Copyright 2023 Dify',
privacy_policy: 'https://dify.ai',
category: 'Translate',
position: 4,
is_listed: true,
install_count: 1662,
installed: false,
editable: true,
is_agent: false,
},
{
app: {
id: '89ad1e65-6711-4c80-b469-a71a434e2dbd',
name: '个人学习导师',
mode: 'chat',
icon: '🤖',
icon_background: '#FFEAD5',
},
app_id: '89ad1e65-6711-4c80-b469-a71a434e2dbd',
description: '您的私人学习导师,帮您制定学习计划并辅导',
copyright: 'Copyright 2023 Dify',
privacy_policy: 'https://dify.ai',
category: 'Assistant',
position: 26,
is_listed: true,
install_count: 1441,
installed: true,
editable: true,
is_agent: false,
},
{
app: {
id: 'ff551444-a3ff-4fd8-b297-f38581c98b4a',
name: '文献综述写作',
mode: 'completion',
icon: 'female-student',
icon_background: '#FBE8FF',
},
app_id: 'ff551444-a3ff-4fd8-b297-f38581c98b4a',
description: '帮你撰写论文文献综述',
copyright: 'Copyright 2023 Dify',
privacy_policy: 'https://dify.ai',
category: 'Writing',
position: 7,
is_listed: true,
install_count: 1651,
installed: false,
editable: true,
is_agent: false,
},
{
app: {
id: '79227a52-11f1-4cf9-8c49-0bd86f9be813',
name: 'Youtube 频道数据分析',
mode: 'chat',
icon: '🔢',
icon_background: '#E4FBCC',
},
app_id: '79227a52-11f1-4cf9-8c49-0bd86f9be813',
description:
'你好,告诉我您想分析的 YouTube 频道,我将为您整理一份完整的数据分析报告。',
copyright: null,
privacy_policy: null,
category: '智能助理',
position: 0,
is_listed: true,
install_count: 2,
installed: false,
editable: true,
is_agent: true,
},
{
app: {
id: '609f4a7f-36f7-4791-96a7-4ccbe6f8dfbb',
name: '旅行规划助手',
mode: 'chat',
icon: '✈️',
icon_background: '#E4FBCC',
},
app_id: '609f4a7f-36f7-4791-96a7-4ccbe6f8dfbb',
description:
'欢迎使用您的个性化旅行服务顾问!🌍✈️ 准备好踏上一段充满冒险与放松的旅程了吗?让我们一起深入打造您难忘的旅行体验吧。',
copyright: null,
privacy_policy: null,
category: '智能助理',
position: 0,
is_listed: true,
install_count: 3,
installed: false,
editable: true,
is_agent: true,
},
],
categories: [
'绘画',
'HR',
'Programming',
'Translate',
'Assistant',
'Writing',
'智能助理',
],
}
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