Merge branch 'refs/heads/main' into feat/workflow-parallel-support

# Conflicts:
#	api/core/workflow/entities/variable_pool.py
#	api/core/workflow/nodes/iteration/iteration_node.py
#	api/core/workflow/workflow_engine_manager.py
This commit is contained in:
takatost 2024-07-31 02:25:31 +08:00
commit c9bb366e1a
184 changed files with 3427 additions and 930 deletions

View File

@ -3,8 +3,8 @@
cd web && npm install
pipx install poetry
echo 'alias start-api="cd /workspaces/dify/api && flask run --host 0.0.0.0 --port=5001 --debug"' >> ~/.bashrc
echo 'alias start-worker="cd /workspaces/dify/api && celery -A app.celery worker -P gevent -c 1 --loglevel INFO -Q dataset,generation,mail,ops_trace,app_deletion"' >> ~/.bashrc
echo 'alias start-api="cd /workspaces/dify/api && poetry run python -m flask run --host 0.0.0.0 --port=5001 --debug"' >> ~/.bashrc
echo 'alias start-worker="cd /workspaces/dify/api && poetry run python -m celery -A app.celery worker -P gevent -c 1 --loglevel INFO -Q dataset,generation,mail,ops_trace,app_deletion"' >> ~/.bashrc
echo 'alias start-web="cd /workspaces/dify/web && npm run dev"' >> ~/.bashrc
echo 'alias start-containers="cd /workspaces/dify/docker && docker-compose -f docker-compose.middleware.yaml -p dify up -d"' >> ~/.bashrc

View File

@ -9,7 +9,7 @@ body:
required: true
- label: I confirm that I am using English to submit this report (我已阅读并同意 [Language Policy](https://github.com/langgenius/dify/issues/1542)).
required: true
- label: "请务必使用英文提交 Issue否则会被关闭。谢谢:"
- label: "[FOR CHINESE USERS] 请务必使用英文提交 Issue否则会被关闭。谢谢:"
required: true
- label: "Please do not modify this template :) and fill in all the required fields."
required: true

View File

@ -9,7 +9,7 @@ body:
required: true
- label: I confirm that I am using English to submit this report (我已阅读并同意 [Language Policy](https://github.com/langgenius/dify/issues/1542)).
required: true
- label: "请务必使用英文提交 Issue否则会被关闭。谢谢:"
- label: "[FOR CHINESE USERS] 请务必使用英文提交 Issue否则会被关闭。谢谢:"
required: true
- label: "Please do not modify this template :) and fill in all the required fields."
required: true

View File

@ -9,7 +9,7 @@ body:
required: true
- label: I confirm that I am using English to submit this report (我已阅读并同意 [Language Policy](https://github.com/langgenius/dify/issues/1542)).
required: true
- label: "请务必使用英文提交 Issue否则会被关闭。谢谢:"
- label: "[FOR CHINESE USERS] 请务必使用英文提交 Issue否则会被关闭。谢谢:"
required: true
- label: "Please do not modify this template :) and fill in all the required fields."
required: true

View File

@ -14,7 +14,7 @@ body:
required: true
- label: I confirm that I am using English to submit this report (我已阅读并同意 [Language Policy](https://github.com/langgenius/dify/issues/1542)).
required: true
- label: "请务必使用英文提交 Issue否则会被关闭。谢谢:"
- label: "[FOR CHINESE USERS] 请务必使用英文提交 Issue否则会被关闭。谢谢:"
required: true
- label: "Please do not modify this template :) and fill in all the required fields."
required: true
@ -22,7 +22,6 @@ body:
- type: input
attributes:
label: Dify version
placeholder: 0.6.11
description: See about section in Dify console
validations:
required: true

View File

@ -12,7 +12,7 @@ body:
required: true
- label: I confirm that I am using English to submit report (我已阅读并同意 [Language Policy](https://github.com/langgenius/dify/issues/1542)).
required: true
- label: "请务必使用英文提交 Issue否则会被关闭。谢谢:"
- label: "[FOR CHINESE USERS] 请务必使用英文提交 Issue否则会被关闭。谢谢:"
required: true
- label: "Please do not modify this template :) and fill in all the required fields."
required: true

View File

@ -12,7 +12,7 @@ body:
required: true
- label: I confirm that I am using English to submit this report (我已阅读并同意 [Language Policy](https://github.com/langgenius/dify/issues/1542)).
required: true
- label: "请务必使用英文提交 Issue否则会被关闭。谢谢:"
- label: "[FOR CHINESE USERS] 请务必使用英文提交 Issue否则会被关闭。谢谢:"
required: true
- label: "Please do not modify this template :) and fill in all the required fields."
required: true

View File

@ -12,14 +12,13 @@ body:
required: true
- label: I confirm that I am using English to submit this report (我已阅读并同意 [Language Policy](https://github.com/langgenius/dify/issues/1542)).
required: true
- label: "请务必使用英文提交 Issue否则会被关闭。谢谢:"
- label: "[FOR CHINESE USERS] 请务必使用英文提交 Issue否则会被关闭。谢谢:"
required: true
- label: "Please do not modify this template :) and fill in all the required fields."
required: true
- type: input
attributes:
label: Dify version
placeholder: 0.3.21
description: Hover over system tray icon or look at Settings
validations:
required: true

View File

@ -1,7 +1,7 @@
Dify にコントリビュートしたいとお考えなのですね。それは素晴らしいことです。
私たちは、LLM アプリケーションの構築と管理のための最も直感的なワークフローを設計するという壮大な野望を持っています。人数も資金も限られている新興企業として、コミュニティからの支援は本当に重要です。
私たちは現状を鑑み、機敏かつ迅速に開発をする必要がありますが、同時にあなたのようなコントリビューターの方々に、可能な限りスムーズな貢献体験をしていただきたいと思っています。そのためにこのコントリビュートガイドを作成しました。
私たちは現状を鑑み、機敏かつ迅速に開発をする必要がありますが、同時にあなたのようなコントリビューターの方々に、可能な限りスムーズな貢献体験をしていただきたいと思っています。そのためにこのコントリビュートガイドを作成しました。
コードベースやコントリビュータの方々と私たちがどのように仕事をしているのかに慣れていただき、楽しいパートにすぐに飛び込めるようにすることが目的です。
このガイドは Dify そのものと同様に、継続的に改善されています。実際のプロジェクトに遅れをとることがあるかもしれませんが、ご理解のほどよろしくお願いいたします。
@ -14,13 +14,13 @@ Dify にコントリビュートしたいとお考えなのですね。それは
### 機能リクエスト
* 新しい機能要望を出す場合は、提案する機能が何を実現するものなのかを説明し、可能な限り多くのコンテキストを含めてください。[@perzeusss](https://github.com/perzeuss)は、あなたの要望を書き出すのに役立つ [Feature Request Copilot](https://udify.app/chat/MK2kVSnw1gakVwMX) を作ってくれました。気軽に試してみてください。
* 新しい機能要望を出す場合は、提案する機能が何を実現するものなのかを説明し、可能な限り多くのコンテキストを含めてください。[@perzeusss](https://github.com/perzeuss)は、あなたの要望を書き出すのに役立つ [Feature Request Copilot](https://udify.app/chat/MK2kVSnw1gakVwMX) を作ってくれました。気軽に試してみてください。
* 既存の課題から 1 つ選びたい場合は、その下にコメントを書いてください。
関連する方向で作業しているチームメンバーが参加します。すべてが良好であれば、コーディングを開始する許可が与えられます。私たちが変更を提案した場合にあなたの作業が無駄になることがないよう、それまでこの機能の作業を控えていただくようお願いいたします。
関連する方向で作業しているチームメンバーが参加します。すべてが良好であれば、コーディングを開始する許可が与えられます。私たちが変更を提案した場合にあなたの作業が無駄になることがないよう、それまでこの機能の作業を控えていただくようお願いいたします。
提案された機能がどの分野に属するかによって、あなたは異なるチーム・メンバーと話をするかもしれません。以下は、各チームメンバーが現在取り組んでいる分野の概要です。
提案された機能がどの分野に属するかによって、あなたは異なるチーム・メンバーと話をするかもしれません。以下は、各チームメンバーが現在取り組んでいる分野の概要です。
| Member | Scope |
| --------------------------------------------------------------------------------------- | ------------------------------------ |
@ -153,7 +153,7 @@ Dify のバックエンドは[Flask](https://flask.palletsprojects.com/en/3.0.x/
いよいよ、私たちのリポジトリにプルリクエスト (PR) を提出する時が来ました。主要な機能については、まず `deploy/dev` ブランチにマージしてテストしてから `main` ブランチにマージします。
マージ競合などの問題が発生した場合、またはプル リクエストを開く方法がわからない場合は、[GitHub's pull request tutorial](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests) をチェックしてみてください。
これで完了です!あなたの PR がマージされると、[README](https://github.com/langgenius/dify/blob/main/README.md) にコントリビューターとして紹介されます。
これで完了です!あなたの PR がマージされると、[README](https://github.com/langgenius/dify/blob/main/README.md) にコントリビューターとして紹介されます。
## ヘルプを得る

View File

@ -183,6 +183,7 @@ UPLOAD_IMAGE_FILE_SIZE_LIMIT=10
# Model Configuration
MULTIMODAL_SEND_IMAGE_FORMAT=base64
PROMPT_GENERATION_MAX_TOKENS=512
# Mail configuration, support: resend, smtp
MAIL_TYPE=
@ -216,6 +217,7 @@ UNSTRUCTURED_API_KEY=
SSRF_PROXY_HTTP_URL=
SSRF_PROXY_HTTPS_URL=
SSRF_DEFAULT_MAX_RETRIES=3
BATCH_UPLOAD_LIMIT=10
KEYWORD_DATA_SOURCE_TYPE=database

View File

@ -261,6 +261,7 @@ def after_request(response):
@app.route('/health')
def health():
return Response(json.dumps({
'pid': os.getpid(),
'status': 'ok',
'version': app.config['CURRENT_VERSION']
}), status=200, content_type="application/json")
@ -284,6 +285,7 @@ def threads():
})
return {
'pid': os.getpid(),
'thread_num': num_threads,
'threads': thread_list
}
@ -293,6 +295,7 @@ def threads():
def pool_stat():
engine = db.engine
return {
'pid': os.getpid(),
'pool_size': engine.pool.size(),
'checked_in_connections': engine.pool.checkedin(),
'checked_out_connections': engine.pool.checkedout(),

View File

@ -249,8 +249,7 @@ def migrate_knowledge_vector_database():
create_count = 0
skipped_count = 0
total_count = 0
config = current_app.config
vector_type = config.get('VECTOR_STORE')
vector_type = dify_config.VECTOR_STORE
page = 1
while True:
try:
@ -484,8 +483,7 @@ def convert_to_agent_apps():
@click.option('--field', default='metadata.doc_id', prompt=False, help='index field , default is metadata.doc_id.')
def add_qdrant_doc_id_index(field: str):
click.echo(click.style('Start add qdrant doc_id index.', fg='green'))
config = current_app.config
vector_type = config.get('VECTOR_STORE')
vector_type = dify_config.VECTOR_STORE
if vector_type != "qdrant":
click.echo(click.style('Sorry, only support qdrant vector store.', fg='red'))
return
@ -502,13 +500,15 @@ def add_qdrant_doc_id_index(field: str):
from core.rag.datasource.vdb.qdrant.qdrant_vector import QdrantConfig
for binding in bindings:
if dify_config.QDRANT_URL is None:
raise ValueError('Qdrant url is required.')
qdrant_config = QdrantConfig(
endpoint=config.get('QDRANT_URL'),
api_key=config.get('QDRANT_API_KEY'),
endpoint=dify_config.QDRANT_URL,
api_key=dify_config.QDRANT_API_KEY,
root_path=current_app.root_path,
timeout=config.get('QDRANT_CLIENT_TIMEOUT'),
grpc_port=config.get('QDRANT_GRPC_PORT'),
prefer_grpc=config.get('QDRANT_GRPC_ENABLED')
timeout=dify_config.QDRANT_CLIENT_TIMEOUT,
grpc_port=dify_config.QDRANT_GRPC_PORT,
prefer_grpc=dify_config.QDRANT_GRPC_ENABLED
)
try:
client = qdrant_client.QdrantClient(**qdrant_config.to_qdrant_params())

View File

@ -64,4 +64,6 @@ class DifyConfig(
return f'{self.HTTP_REQUEST_NODE_MAX_TEXT_SIZE / 1024 / 1024:.2f}MB'
SSRF_PROXY_HTTP_URL: str | None = None
SSRF_PROXY_HTTPS_URL: str | None = None
SSRF_PROXY_HTTPS_URL: str | None = None
MODERATION_BUFFER_SIZE: int = Field(default=300, description='The buffer size for moderation.')

View File

@ -1,4 +1,5 @@
from typing import Any, Optional
from urllib.parse import quote_plus
from pydantic import Field, NonNegativeInt, PositiveInt, computed_field
from pydantic_settings import BaseSettings
@ -104,7 +105,7 @@ class DatabaseConfig:
).strip("&")
db_extras = f"?{db_extras}" if db_extras else ""
return (f"{self.SQLALCHEMY_DATABASE_URI_SCHEME}://"
f"{self.DB_USERNAME}:{self.DB_PASSWORD}@{self.DB_HOST}:{self.DB_PORT}/{self.DB_DATABASE}"
f"{quote_plus(self.DB_USERNAME)}:{quote_plus(self.DB_PASSWORD)}@{self.DB_HOST}:{self.DB_PORT}/{self.DB_DATABASE}"
f"{db_extras}")
SQLALCHEMY_POOL_SIZE: NonNegativeInt = Field(

View File

@ -1,3 +1,5 @@
import os
from flask_login import current_user
from flask_restful import Resource, reqparse
@ -28,13 +30,15 @@ class RuleGenerateApi(Resource):
args = parser.parse_args()
account = current_user
PROMPT_GENERATION_MAX_TOKENS = int(os.getenv('PROMPT_GENERATION_MAX_TOKENS', '512'))
try:
rules = LLMGenerator.generate_rule_config(
tenant_id=account.current_tenant_id,
instruction=args['instruction'],
model_config=args['model_config'],
no_variable=args['no_variable']
no_variable=args['no_variable'],
rule_config_max_tokens=PROMPT_GENERATION_MAX_TOKENS
)
except ProviderTokenNotInitError as ex:
raise ProviderNotInitializeError(ex.description)

View File

@ -71,7 +71,7 @@ class ResetPasswordApi(Resource):
# AccountService.update_password(account, new_password)
# todo: Send email
# MAILCHIMP_API_KEY = current_app.config['MAILCHIMP_TRANSACTIONAL_API_KEY']
# MAILCHIMP_API_KEY = dify_config.MAILCHIMP_TRANSACTIONAL_API_KEY
# mailchimp = MailchimpTransactional(MAILCHIMP_API_KEY)
# message = {
@ -92,7 +92,7 @@ class ResetPasswordApi(Resource):
# 'message': message,
# # required for transactional email
# ' settings': {
# 'sandbox_mode': current_app.config['MAILCHIMP_SANDBOX_MODE'],
# 'sandbox_mode': dify_config.MAILCHIMP_SANDBOX_MODE,
# },
# })

View File

@ -29,22 +29,21 @@ from services.app_generate_service import AppGenerateService
logger = logging.getLogger(__name__)
workflow_run_fields = {
'id': fields.String,
'workflow_id': fields.String,
'status': fields.String,
'inputs': fields.Raw,
'outputs': fields.Raw,
'error': fields.String,
'total_steps': fields.Integer,
'total_tokens': fields.Integer,
'created_at': fields.DateTime,
'finished_at': fields.DateTime,
'elapsed_time': fields.Float,
}
class WorkflowRunApi(Resource):
workflow_run_fields = {
'id': fields.String,
'workflow_id': fields.String,
'status': fields.String,
'inputs': fields.Raw,
'outputs': fields.Raw,
'error': fields.String,
'total_steps': fields.Integer,
'total_tokens': fields.Integer,
'created_at': fields.DateTime,
'finished_at': fields.DateTime,
'elapsed_time': fields.Float,
}
class WorkflowRunDetailApi(Resource):
@validate_app_token
@marshal_with(workflow_run_fields)
def get(self, app_model: App, workflow_id: str):
@ -57,7 +56,7 @@ class WorkflowRunApi(Resource):
workflow_run = db.session.query(WorkflowRun).filter(WorkflowRun.id == workflow_id).first()
return workflow_run
class WorkflowRunApi(Resource):
@validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.JSON, required=True))
def post(self, app_model: App, end_user: EndUser):
"""
@ -117,5 +116,6 @@ class WorkflowTaskStopApi(Resource):
}
api.add_resource(WorkflowRunApi, '/workflows/run/<string:workflow_id>', '/workflows/run')
api.add_resource(WorkflowRunApi, '/workflows/run')
api.add_resource(WorkflowRunDetailApi, '/workflows/run/<string:workflow_id>')
api.add_resource(WorkflowTaskStopApi, '/workflows/tasks/<string:task_id>/stop')

View File

@ -5,9 +5,9 @@ from collections.abc import Generator
from enum import Enum
from typing import Any
from flask import current_app
from sqlalchemy.orm import DeclarativeMeta
from configs import dify_config
from core.app.entities.app_invoke_entities import InvokeFrom
from core.app.entities.queue_entities import (
AppQueueEvent,
@ -48,7 +48,7 @@ class AppQueueManager:
:return:
"""
# wait for APP_MAX_EXECUTION_TIME seconds to stop listen
listen_timeout = current_app.config.get("APP_MAX_EXECUTION_TIME")
listen_timeout = dify_config.APP_MAX_EXECUTION_TIME
start_time = time.time()
last_ping_time = 0
while True:

View File

@ -1,8 +1,21 @@
from .segment_group import SegmentGroup
from .segments import NoneSegment, Segment
from .segments import (
ArrayAnySegment,
FileSegment,
FloatSegment,
IntegerSegment,
NoneSegment,
ObjectSegment,
Segment,
StringSegment,
)
from .types import SegmentType
from .variables import (
ArrayVariable,
ArrayAnyVariable,
ArrayFileVariable,
ArrayNumberVariable,
ArrayObjectVariable,
ArrayStringVariable,
FileVariable,
FloatVariable,
IntegerVariable,
@ -20,11 +33,21 @@ __all__ = [
'SecretVariable',
'FileVariable',
'StringVariable',
'ArrayVariable',
'ArrayAnyVariable',
'Variable',
'SegmentType',
'SegmentGroup',
'Segment',
'NoneSegment',
'NoneVariable',
'IntegerSegment',
'FloatSegment',
'ObjectSegment',
'ArrayAnySegment',
'FileSegment',
'StringSegment',
'ArrayStringVariable',
'ArrayNumberVariable',
'ArrayObjectVariable',
'ArrayFileVariable',
]

View File

@ -3,14 +3,25 @@ from typing import Any
from core.file.file_obj import FileVar
from .segments import Segment, StringSegment
from .segments import (
ArrayAnySegment,
FileSegment,
FloatSegment,
IntegerSegment,
NoneSegment,
ObjectSegment,
Segment,
StringSegment,
)
from .types import SegmentType
from .variables import (
ArrayVariable,
ArrayFileVariable,
ArrayNumberVariable,
ArrayObjectVariable,
ArrayStringVariable,
FileVariable,
FloatVariable,
IntegerVariable,
NoneVariable,
ObjectVariable,
SecretVariable,
StringVariable,
@ -28,40 +39,48 @@ def build_variable_from_mapping(m: Mapping[str, Any], /) -> Variable:
match value_type:
case SegmentType.STRING:
return StringVariable.model_validate(m)
case SegmentType.SECRET:
return SecretVariable.model_validate(m)
case SegmentType.NUMBER if isinstance(value, int):
return IntegerVariable.model_validate(m)
case SegmentType.NUMBER if isinstance(value, float):
return FloatVariable.model_validate(m)
case SegmentType.SECRET:
return SecretVariable.model_validate(m)
case SegmentType.NUMBER if not isinstance(value, float | int):
raise ValueError(f'invalid number value {value}')
case SegmentType.FILE:
return FileVariable.model_validate(m)
case SegmentType.OBJECT if isinstance(value, dict):
return ObjectVariable.model_validate(
{**m, 'value': {k: build_variable_from_mapping(v) for k, v in value.items()}}
)
case SegmentType.ARRAY_STRING if isinstance(value, list):
return ArrayStringVariable.model_validate({**m, 'value': [build_variable_from_mapping(v) for v in value]})
case SegmentType.ARRAY_NUMBER if isinstance(value, list):
return ArrayNumberVariable.model_validate({**m, 'value': [build_variable_from_mapping(v) for v in value]})
case SegmentType.ARRAY_OBJECT if isinstance(value, list):
return ArrayObjectVariable.model_validate({**m, 'value': [build_variable_from_mapping(v) for v in value]})
case SegmentType.ARRAY_FILE if isinstance(value, list):
return ArrayFileVariable.model_validate({**m, 'value': [build_variable_from_mapping(v) for v in value]})
raise ValueError(f'not supported value type {value_type}')
def build_anonymous_variable(value: Any, /) -> Variable:
if value is None:
return NoneVariable(name='anonymous')
if isinstance(value, str):
return StringVariable(name='anonymous', value=value)
if isinstance(value, int):
return IntegerVariable(name='anonymous', value=value)
if isinstance(value, float):
return FloatVariable(name='anonymous', value=value)
if isinstance(value, dict):
# TODO: Limit the depth of the object
obj = {k: build_anonymous_variable(v) for k, v in value.items()}
return ObjectVariable(name='anonymous', value=obj)
if isinstance(value, list):
# TODO: Limit the depth of the array
elements = [build_anonymous_variable(v) for v in value]
return ArrayVariable(name='anonymous', value=elements)
if isinstance(value, FileVar):
return FileVariable(name='anonymous', value=value)
raise ValueError(f'not supported value {value}')
def build_segment(value: Any, /) -> Segment:
if value is None:
return NoneSegment()
if isinstance(value, str):
return StringSegment(value=value)
if isinstance(value, int):
return IntegerSegment(value=value)
if isinstance(value, float):
return FloatSegment(value=value)
if isinstance(value, dict):
# TODO: Limit the depth of the object
obj = {k: build_segment(v) for k, v in value.items()}
return ObjectSegment(value=obj)
if isinstance(value, list):
# TODO: Limit the depth of the array
elements = [build_segment(v) for v in value]
return ArrayAnySegment(value=elements)
if isinstance(value, FileVar):
return FileSegment(value=value)
raise ValueError(f'not supported value {value}')

View File

@ -1,17 +1,18 @@
import re
from core.app.segments import SegmentGroup, factory
from core.workflow.entities.variable_pool import VariablePool
from . import SegmentGroup, factory
VARIABLE_PATTERN = re.compile(r'\{\{#([a-zA-Z0-9_]{1,50}(?:\.[a-zA-Z_][a-zA-Z0-9_]{0,29}){1,10})#\}\}')
def convert_template(*, template: str, variable_pool: VariablePool):
parts = re.split(VARIABLE_PATTERN, template)
segments = []
for part in parts:
for part in filter(lambda x: x, parts):
if '.' in part and (value := variable_pool.get(part.split('.'))):
segments.append(value)
else:
segments.append(factory.build_segment(part))
return SegmentGroup(segments=segments)
return SegmentGroup(value=segments)

View File

@ -1,19 +1,22 @@
from pydantic import BaseModel
from .segments import Segment
from .types import SegmentType
class SegmentGroup(BaseModel):
segments: list[Segment]
class SegmentGroup(Segment):
value_type: SegmentType = SegmentType.GROUP
value: list[Segment]
@property
def text(self):
return ''.join([segment.text for segment in self.segments])
return ''.join([segment.text for segment in self.value])
@property
def log(self):
return ''.join([segment.log for segment in self.segments])
return ''.join([segment.log for segment in self.value])
@property
def markdown(self):
return ''.join([segment.markdown for segment in self.segments])
return ''.join([segment.markdown for segment in self.value])
def to_object(self):
return [segment.to_object() for segment in self.value]

View File

@ -1,7 +1,11 @@
import json
from collections.abc import Mapping, Sequence
from typing import Any
from pydantic import BaseModel, ConfigDict, field_validator
from core.file.file_obj import FileVar
from .types import SegmentType
@ -34,12 +38,6 @@ class Segment(BaseModel):
return str(self.value)
def to_object(self) -> Any:
if isinstance(self.value, Segment):
return self.value.to_object()
if isinstance(self.value, list):
return [v.to_object() for v in self.value]
if isinstance(self.value, dict):
return {k: v.to_object() for k, v in self.value.items()}
return self.value
@ -63,3 +61,80 @@ class NoneSegment(Segment):
class StringSegment(Segment):
value_type: SegmentType = SegmentType.STRING
value: str
class FloatSegment(Segment):
value_type: SegmentType = SegmentType.NUMBER
value: float
class IntegerSegment(Segment):
value_type: SegmentType = SegmentType.NUMBER
value: int
class FileSegment(Segment):
value_type: SegmentType = SegmentType.FILE
# TODO: embed FileVar in this model.
value: FileVar
@property
def markdown(self) -> str:
return self.value.to_markdown()
class ObjectSegment(Segment):
value_type: SegmentType = SegmentType.OBJECT
value: Mapping[str, Segment]
@property
def text(self) -> str:
# TODO: Process variables.
return json.dumps(self.model_dump()['value'], ensure_ascii=False)
@property
def log(self) -> str:
# TODO: Process variables.
return json.dumps(self.model_dump()['value'], ensure_ascii=False, indent=2)
@property
def markdown(self) -> str:
# TODO: Use markdown code block
return json.dumps(self.model_dump()['value'], ensure_ascii=False, indent=2)
def to_object(self):
return {k: v.to_object() for k, v in self.value.items()}
class ArraySegment(Segment):
@property
def markdown(self) -> str:
return '\n'.join(['- ' + item.markdown for item in self.value])
def to_object(self):
return [v.to_object() for v in self.value]
class ArrayAnySegment(ArraySegment):
value_type: SegmentType = SegmentType.ARRAY_ANY
value: Sequence[Segment]
class ArrayStringSegment(ArraySegment):
value_type: SegmentType = SegmentType.ARRAY_STRING
value: Sequence[StringSegment]
class ArrayNumberSegment(ArraySegment):
value_type: SegmentType = SegmentType.ARRAY_NUMBER
value: Sequence[FloatSegment | IntegerSegment]
class ArrayObjectSegment(ArraySegment):
value_type: SegmentType = SegmentType.ARRAY_OBJECT
value: Sequence[ObjectSegment]
class ArrayFileSegment(ArraySegment):
value_type: SegmentType = SegmentType.ARRAY_FILE
value: Sequence[FileSegment]

View File

@ -6,6 +6,12 @@ class SegmentType(str, Enum):
NUMBER = 'number'
STRING = 'string'
SECRET = 'secret'
ARRAY = 'array'
ARRAY_ANY = 'array[any]'
ARRAY_STRING = 'array[string]'
ARRAY_NUMBER = 'array[number]'
ARRAY_OBJECT = 'array[object]'
ARRAY_FILE = 'array[file]'
OBJECT = 'object'
FILE = 'file'
GROUP = 'group'

View File

@ -1,12 +1,21 @@
import json
from collections.abc import Mapping, Sequence
from pydantic import Field
from core.file.file_obj import FileVar
from core.helper import encrypter
from .segments import NoneSegment, Segment, StringSegment
from .segments import (
ArrayAnySegment,
ArrayFileSegment,
ArrayNumberSegment,
ArrayObjectSegment,
ArrayStringSegment,
FileSegment,
FloatSegment,
IntegerSegment,
NoneSegment,
ObjectSegment,
Segment,
StringSegment,
)
from .types import SegmentType
@ -27,53 +36,40 @@ class StringVariable(StringSegment, Variable):
pass
class FloatVariable(Variable):
value_type: SegmentType = SegmentType.NUMBER
value: float
class FloatVariable(FloatSegment, Variable):
pass
class IntegerVariable(Variable):
value_type: SegmentType = SegmentType.NUMBER
value: int
class IntegerVariable(IntegerSegment, Variable):
pass
class ObjectVariable(Variable):
value_type: SegmentType = SegmentType.OBJECT
value: Mapping[str, Variable]
@property
def text(self) -> str:
# TODO: Process variables.
return json.dumps(self.model_dump()['value'], ensure_ascii=False)
@property
def log(self) -> str:
# TODO: Process variables.
return json.dumps(self.model_dump()['value'], ensure_ascii=False, indent=2)
@property
def markdown(self) -> str:
# TODO: Use markdown code block
return json.dumps(self.model_dump()['value'], ensure_ascii=False, indent=2)
class FileVariable(FileSegment, Variable):
pass
class ArrayVariable(Variable):
value_type: SegmentType = SegmentType.ARRAY
value: Sequence[Variable]
@property
def markdown(self) -> str:
return '\n'.join(['- ' + item.markdown for item in self.value])
class ObjectVariable(ObjectSegment, Variable):
pass
class FileVariable(Variable):
value_type: SegmentType = SegmentType.FILE
# TODO: embed FileVar in this model.
value: FileVar
class ArrayAnyVariable(ArrayAnySegment, Variable):
pass
@property
def markdown(self) -> str:
return self.value.to_markdown()
class ArrayStringVariable(ArrayStringSegment, Variable):
pass
class ArrayNumberVariable(ArrayNumberSegment, Variable):
pass
class ArrayObjectVariable(ArrayObjectSegment, Variable):
pass
class ArrayFileVariable(ArrayFileSegment, Variable):
pass
class SecretVariable(StringVariable):

View File

@ -6,8 +6,7 @@ import os
import time
from typing import Optional
from flask import current_app
from configs import dify_config
from extensions.ext_storage import storage
IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'png', 'webp', 'gif', 'svg']
@ -23,7 +22,7 @@ class UploadFileParser:
if upload_file.extension not in IMAGE_EXTENSIONS:
return None
if current_app.config['MULTIMODAL_SEND_IMAGE_FORMAT'] == 'url' or force_url:
if dify_config.MULTIMODAL_SEND_IMAGE_FORMAT == 'url' or force_url:
return cls.get_signed_temp_image_url(upload_file.id)
else:
# get image file base64
@ -44,13 +43,13 @@ class UploadFileParser:
:param upload_file: UploadFile object
:return:
"""
base_url = current_app.config.get('FILES_URL')
base_url = dify_config.FILES_URL
image_preview_url = f'{base_url}/files/{upload_file_id}/image-preview'
timestamp = str(int(time.time()))
nonce = os.urandom(16).hex()
data_to_sign = f"image-preview|{upload_file_id}|{timestamp}|{nonce}"
secret_key = current_app.config['SECRET_KEY'].encode()
secret_key = dify_config.SECRET_KEY.encode()
sign = hmac.new(secret_key, data_to_sign.encode(), hashlib.sha256).digest()
encoded_sign = base64.urlsafe_b64encode(sign).decode()
@ -68,7 +67,7 @@ class UploadFileParser:
:return:
"""
data_to_sign = f"image-preview|{upload_file_id}|{timestamp}|{nonce}"
secret_key = current_app.config['SECRET_KEY'].encode()
secret_key = dify_config.SECRET_KEY.encode()
recalculated_sign = hmac.new(secret_key, data_to_sign.encode(), hashlib.sha256).digest()
recalculated_encoded_sign = base64.urlsafe_b64encode(recalculated_sign).decode()
@ -77,4 +76,4 @@ class UploadFileParser:
return False
current_time = int(time.time())
return current_time - int(timestamp) <= current_app.config.get('FILES_ACCESS_TIMEOUT')
return current_time - int(timestamp) <= dify_config.FILES_ACCESS_TIMEOUT

View File

@ -13,18 +13,10 @@ def get_position_map(folder_path: str, *, file_name: str = "_position.yaml") ->
:param file_name: the YAML file name, default to '_position.yaml'
:return: a dict with name as key and index as value
"""
position_file_name = os.path.join(folder_path, file_name)
if not position_file_name or not os.path.exists(position_file_name):
return {}
positions = load_yaml_file(position_file_name, ignore_error=True)
position_map = {}
index = 0
for _, name in enumerate(positions):
if name and isinstance(name, str):
position_map[name.strip()] = index
index += 1
return position_map
position_file_path = os.path.join(folder_path, file_name)
yaml_content = load_yaml_file(file_path=position_file_path, default_value=[])
positions = [item.strip() for item in yaml_content if item and isinstance(item, str) and item.strip()]
return {name: index for index, name in enumerate(positions)}
def sort_by_position_map(

View File

@ -1,48 +1,75 @@
"""
Proxy requests to avoid SSRF
"""
import logging
import os
import time
import httpx
SSRF_PROXY_ALL_URL = os.getenv('SSRF_PROXY_ALL_URL', '')
SSRF_PROXY_HTTP_URL = os.getenv('SSRF_PROXY_HTTP_URL', '')
SSRF_PROXY_HTTPS_URL = os.getenv('SSRF_PROXY_HTTPS_URL', '')
SSRF_DEFAULT_MAX_RETRIES = int(os.getenv('SSRF_DEFAULT_MAX_RETRIES', '3'))
proxies = {
'http://': SSRF_PROXY_HTTP_URL,
'https://': SSRF_PROXY_HTTPS_URL
} if SSRF_PROXY_HTTP_URL and SSRF_PROXY_HTTPS_URL else None
BACKOFF_FACTOR = 0.5
STATUS_FORCELIST = [429, 500, 502, 503, 504]
def make_request(method, url, **kwargs):
if SSRF_PROXY_ALL_URL:
return httpx.request(method=method, url=url, proxy=SSRF_PROXY_ALL_URL, **kwargs)
elif proxies:
return httpx.request(method=method, url=url, proxies=proxies, **kwargs)
else:
return httpx.request(method=method, url=url, **kwargs)
def make_request(method, url, max_retries=SSRF_DEFAULT_MAX_RETRIES, **kwargs):
if "allow_redirects" in kwargs:
allow_redirects = kwargs.pop("allow_redirects")
if "follow_redirects" not in kwargs:
kwargs["follow_redirects"] = allow_redirects
retries = 0
while retries <= max_retries:
try:
if SSRF_PROXY_ALL_URL:
response = httpx.request(method=method, url=url, proxy=SSRF_PROXY_ALL_URL, **kwargs)
elif proxies:
response = httpx.request(method=method, url=url, proxies=proxies, **kwargs)
else:
response = httpx.request(method=method, url=url, **kwargs)
if response.status_code not in STATUS_FORCELIST:
return response
else:
logging.warning(f"Received status code {response.status_code} for URL {url} which is in the force list")
except httpx.RequestError as e:
logging.warning(f"Request to URL {url} failed on attempt {retries + 1}: {e}")
retries += 1
if retries <= max_retries:
time.sleep(BACKOFF_FACTOR * (2 ** (retries - 1)))
raise Exception(f"Reached maximum retries ({max_retries}) for URL {url}")
def get(url, **kwargs):
return make_request('GET', url, **kwargs)
def get(url, max_retries=SSRF_DEFAULT_MAX_RETRIES, **kwargs):
return make_request('GET', url, max_retries=max_retries, **kwargs)
def post(url, **kwargs):
return make_request('POST', url, **kwargs)
def post(url, max_retries=SSRF_DEFAULT_MAX_RETRIES, **kwargs):
return make_request('POST', url, max_retries=max_retries, **kwargs)
def put(url, **kwargs):
return make_request('PUT', url, **kwargs)
def put(url, max_retries=SSRF_DEFAULT_MAX_RETRIES, **kwargs):
return make_request('PUT', url, max_retries=max_retries, **kwargs)
def patch(url, **kwargs):
return make_request('PATCH', url, **kwargs)
def patch(url, max_retries=SSRF_DEFAULT_MAX_RETRIES, **kwargs):
return make_request('PATCH', url, max_retries=max_retries, **kwargs)
def delete(url, **kwargs):
return make_request('DELETE', url, **kwargs)
def delete(url, max_retries=SSRF_DEFAULT_MAX_RETRIES, **kwargs):
return make_request('DELETE', url, max_retries=max_retries, **kwargs)
def head(url, **kwargs):
return make_request('HEAD', url, **kwargs)
def head(url, max_retries=SSRF_DEFAULT_MAX_RETRIES, **kwargs):
return make_request('HEAD', url, max_retries=max_retries, **kwargs)

View File

@ -12,6 +12,7 @@ from flask import Flask, current_app
from flask_login import current_user
from sqlalchemy.orm.exc import ObjectDeletedError
from configs import dify_config
from core.errors.error import ProviderTokenNotInitError
from core.llm_generator.llm_generator import LLMGenerator
from core.model_manager import ModelInstance, ModelManager
@ -224,7 +225,7 @@ class IndexingRunner:
features = FeatureService.get_features(tenant_id)
if features.billing.enabled:
count = len(extract_settings)
batch_upload_limit = int(current_app.config['BATCH_UPLOAD_LIMIT'])
batch_upload_limit = dify_config.BATCH_UPLOAD_LIMIT
if count > batch_upload_limit:
raise ValueError(f"You have reached the batch upload limit of {batch_upload_limit}.")
@ -427,7 +428,7 @@ class IndexingRunner:
# The user-defined segmentation rule
rules = json.loads(processing_rule.rules)
segmentation = rules["segmentation"]
max_segmentation_tokens_length = int(current_app.config['INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH'])
max_segmentation_tokens_length = dify_config.INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH
if segmentation["max_tokens"] < 50 or segmentation["max_tokens"] > max_segmentation_tokens_length:
raise ValueError(f"Custom segment length should be between 50 and {max_segmentation_tokens_length}.")

View File

@ -118,7 +118,7 @@ class LLMGenerator:
return questions
@classmethod
def generate_rule_config(cls, tenant_id: str, instruction: str, model_config: dict, no_variable: bool) -> dict:
def generate_rule_config(cls, tenant_id: str, instruction: str, model_config: dict, no_variable: bool, rule_config_max_tokens: int = 512) -> dict:
output_parser = RuleConfigGeneratorOutputParser()
error = ""
@ -130,7 +130,7 @@ class LLMGenerator:
"error": ""
}
model_parameters = {
"max_tokens": 512,
"max_tokens": rule_config_max_tokens,
"temperature": 0.01
}

View File

@ -162,7 +162,7 @@ class AIModel(ABC):
# traverse all model_schema_yaml_paths
for model_schema_yaml_path in model_schema_yaml_paths:
# read yaml data from yaml file
yaml_data = load_yaml_file(model_schema_yaml_path, ignore_error=True)
yaml_data = load_yaml_file(model_schema_yaml_path)
new_parameter_rules = []
for parameter_rule in yaml_data.get('parameter_rules', []):

View File

@ -44,7 +44,7 @@ class ModelProvider(ABC):
# read provider schema from yaml file
yaml_path = os.path.join(current_path, f'{provider_name}.yaml')
yaml_data = load_yaml_file(yaml_path, ignore_error=True)
yaml_data = load_yaml_file(yaml_path)
try:
# yaml_data to entity

View File

@ -375,6 +375,10 @@ class AzureOpenAILargeLanguageModel(_CommonAzureOpenAI, LargeLanguageModel):
continue
delta = chunk.choices[0]
# NOTE: For fix https://github.com/langgenius/dify/issues/5790
if delta.delta is None:
continue
# extract tool calls from response
self._update_tool_calls(tool_calls=tool_calls, tool_calls_response=delta.delta.tool_calls)

View File

@ -10,10 +10,14 @@
- cohere.command-text-v14
- cohere.command-r-plus-v1.0
- cohere.command-r-v1.0
- meta.llama3-1-8b-instruct-v1:0
- meta.llama3-1-70b-instruct-v1:0
- meta.llama3-1-405b-instruct-v1:0
- meta.llama3-8b-instruct-v1:0
- meta.llama3-70b-instruct-v1:0
- meta.llama2-13b-chat-v1
- meta.llama2-70b-chat-v1
- mistral.mistral-large-2407-v1:0
- mistral.mistral-small-2402-v1:0
- mistral.mistral-large-2402-v1:0
- mistral.mixtral-8x7b-instruct-v0:1

View File

@ -3,8 +3,7 @@ label:
en_US: Command R+
model_type: llm
features:
#- multi-tool-call
- agent-thought
- tool-call
#- stream-tool-call
model_properties:
mode: chat

View File

@ -3,9 +3,7 @@ label:
en_US: Command R
model_type: llm
features:
#- multi-tool-call
- agent-thought
#- stream-tool-call
- tool-call
model_properties:
mode: chat
context_size: 128000

View File

@ -17,7 +17,6 @@ from botocore.exceptions import (
ServiceNotInRegionError,
UnknownServiceError,
)
from cohere import ChatMessage
# local import
from core.model_runtime.entities.llm_entities import LLMResult, LLMResultChunk, LLMResultChunkDelta
@ -42,7 +41,6 @@ from core.model_runtime.errors.invoke import (
)
from core.model_runtime.errors.validate import CredentialsValidateFailedError
from core.model_runtime.model_providers.__base.large_language_model import LargeLanguageModel
from core.model_runtime.model_providers.cohere.llm.llm import CohereLargeLanguageModel
logger = logging.getLogger(__name__)
@ -59,6 +57,7 @@ class BedrockLargeLanguageModel(LargeLanguageModel):
{'prefix': 'mistral.mixtral-8x7b-instruct', 'support_system_prompts': False, 'support_tool_use': False},
{'prefix': 'mistral.mistral-large', 'support_system_prompts': True, 'support_tool_use': True},
{'prefix': 'mistral.mistral-small', 'support_system_prompts': True, 'support_tool_use': True},
{'prefix': 'cohere.command-r', 'support_system_prompts': True, 'support_tool_use': True},
{'prefix': 'amazon.titan', 'support_system_prompts': False, 'support_tool_use': False}
]
@ -94,86 +93,8 @@ class BedrockLargeLanguageModel(LargeLanguageModel):
model_info['model'] = model
# invoke models via boto3 converse API
return self._generate_with_converse(model_info, credentials, prompt_messages, model_parameters, stop, stream, user, tools)
# invoke Cohere models via boto3 client
if "cohere.command-r" in model:
return self._generate_cohere_chat(model, credentials, prompt_messages, model_parameters, stop, stream, user, tools)
# invoke other models via boto3 client
return self._generate(model, credentials, prompt_messages, model_parameters, stop, stream, user)
def _generate_cohere_chat(
self, model: str, credentials: dict, prompt_messages: list[PromptMessage], model_parameters: dict,
stop: Optional[list[str]] = None, stream: bool = True, user: Optional[str] = None,
tools: Optional[list[PromptMessageTool]] = None,) -> Union[LLMResult, Generator]:
cohere_llm = CohereLargeLanguageModel()
client_config = Config(
region_name=credentials["aws_region"]
)
runtime_client = boto3.client(
service_name='bedrock-runtime',
config=client_config,
aws_access_key_id=credentials["aws_access_key_id"],
aws_secret_access_key=credentials["aws_secret_access_key"]
)
extra_model_kwargs = {}
if stop:
extra_model_kwargs['stop_sequences'] = stop
if tools:
tools = cohere_llm._convert_tools(tools)
model_parameters['tools'] = tools
message, chat_histories, tool_results \
= cohere_llm._convert_prompt_messages_to_message_and_chat_histories(prompt_messages)
if tool_results:
model_parameters['tool_results'] = tool_results
payload = {
**model_parameters,
"message": message,
"chat_history": chat_histories,
}
# need workaround for ai21 models which doesn't support streaming
if stream:
invoke = runtime_client.invoke_model_with_response_stream
else:
invoke = runtime_client.invoke_model
def serialize(obj):
if isinstance(obj, ChatMessage):
return obj.__dict__
raise TypeError(f"Type {type(obj)} not serializable")
try:
body_jsonstr=json.dumps(payload, default=serialize)
response = invoke(
modelId=model,
contentType="application/json",
accept="*/*",
body=body_jsonstr
)
except ClientError as ex:
error_code = ex.response['Error']['Code']
full_error_msg = f"{error_code}: {ex.response['Error']['Message']}"
raise self._map_client_to_invoke_error(error_code, full_error_msg)
except (EndpointConnectionError, NoRegionError, ServiceNotInRegionError) as ex:
raise InvokeConnectionError(str(ex))
except UnknownServiceError as ex:
raise InvokeServerUnavailableError(str(ex))
except Exception as ex:
raise InvokeError(str(ex))
if stream:
return self._handle_generate_stream_response(model, credentials, response, prompt_messages)
return self._handle_generate_response(model, credentials, response, prompt_messages)
def _generate_with_converse(self, model_info: dict, credentials: dict, prompt_messages: list[PromptMessage], model_parameters: dict,
stop: Optional[list[str]] = None, stream: bool = True, user: Optional[str] = None, tools: Optional[list[PromptMessageTool]] = None,) -> Union[LLMResult, Generator]:
@ -208,14 +129,25 @@ class BedrockLargeLanguageModel(LargeLanguageModel):
if model_info['support_tool_use'] and tools:
parameters['toolConfig'] = self._convert_converse_tool_config(tools=tools)
try:
if stream:
response = bedrock_client.converse_stream(**parameters)
return self._handle_converse_stream_response(model_info['model'], credentials, response, prompt_messages)
else:
response = bedrock_client.converse(**parameters)
return self._handle_converse_response(model_info['model'], credentials, response, prompt_messages)
except ClientError as ex:
error_code = ex.response['Error']['Code']
full_error_msg = f"{error_code}: {ex.response['Error']['Message']}"
raise self._map_client_to_invoke_error(error_code, full_error_msg)
except (EndpointConnectionError, NoRegionError, ServiceNotInRegionError) as ex:
raise InvokeConnectionError(str(ex))
if stream:
response = bedrock_client.converse_stream(**parameters)
return self._handle_converse_stream_response(model_info['model'], credentials, response, prompt_messages)
else:
response = bedrock_client.converse(**parameters)
return self._handle_converse_response(model_info['model'], credentials, response, prompt_messages)
except UnknownServiceError as ex:
raise InvokeServerUnavailableError(str(ex))
except Exception as ex:
raise InvokeError(str(ex))
def _handle_converse_response(self, model: str, credentials: dict, response: dict,
prompt_messages: list[PromptMessage]) -> LLMResult:
"""
@ -558,7 +490,6 @@ class BedrockLargeLanguageModel(LargeLanguageModel):
except ClientError as ex:
error_code = ex.response['Error']['Code']
full_error_msg = f"{error_code}: {ex.response['Error']['Message']}"
raise CredentialsValidateFailedError(str(self._map_client_to_invoke_error(error_code, full_error_msg)))
except Exception as ex:
@ -571,38 +502,9 @@ class BedrockLargeLanguageModel(LargeLanguageModel):
:param message: PromptMessage to convert.
:return: String representation of the message.
"""
if model_prefix == "anthropic":
human_prompt_prefix = "\n\nHuman:"
human_prompt_postfix = ""
ai_prompt = "\n\nAssistant:"
elif model_prefix == "meta":
# LLAMA3
if model_name.startswith("llama3"):
human_prompt_prefix = "<|eot_id|><|start_header_id|>user<|end_header_id|>\n\n"
human_prompt_postfix = "<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n"
ai_prompt = "\n\nAssistant:"
else:
# LLAMA2
human_prompt_prefix = "\n[INST]"
human_prompt_postfix = "[\\INST]\n"
ai_prompt = ""
elif model_prefix == "mistral":
human_prompt_prefix = "<s>[INST]"
human_prompt_postfix = "[\\INST]\n"
ai_prompt = "\n\nAssistant:"
elif model_prefix == "amazon":
human_prompt_prefix = "\n\nUser:"
human_prompt_postfix = ""
ai_prompt = "\n\nBot:"
else:
human_prompt_prefix = ""
human_prompt_postfix = ""
ai_prompt = ""
human_prompt_prefix = ""
human_prompt_postfix = ""
ai_prompt = ""
content = message.content
@ -653,13 +555,7 @@ class BedrockLargeLanguageModel(LargeLanguageModel):
model_prefix = model.split('.')[0]
model_name = model.split('.')[1]
if model_prefix == "amazon":
payload["textGenerationConfig"] = { **model_parameters }
payload["textGenerationConfig"]["stopSequences"] = ["User:"]
payload["inputText"] = self._convert_messages_to_prompt(prompt_messages, model_prefix)
elif model_prefix == "ai21":
if model_prefix == "ai21":
payload["temperature"] = model_parameters.get("temperature")
payload["topP"] = model_parameters.get("topP")
payload["maxTokens"] = model_parameters.get("maxTokens")
@ -671,28 +567,12 @@ class BedrockLargeLanguageModel(LargeLanguageModel):
payload["frequencyPenalty"] = {model_parameters.get("frequencyPenalty")}
if model_parameters.get("countPenalty"):
payload["countPenalty"] = {model_parameters.get("countPenalty")}
elif model_prefix == "mistral":
payload["temperature"] = model_parameters.get("temperature")
payload["top_p"] = model_parameters.get("top_p")
payload["max_tokens"] = model_parameters.get("max_tokens")
payload["prompt"] = self._convert_messages_to_prompt(prompt_messages, model_prefix)
payload["stop"] = stop[:10] if stop else []
elif model_prefix == "anthropic":
payload = { **model_parameters }
payload["prompt"] = self._convert_messages_to_prompt(prompt_messages, model_prefix)
payload["stop_sequences"] = ["\n\nHuman:"] + (stop if stop else [])
elif model_prefix == "cohere":
payload = { **model_parameters }
payload["prompt"] = prompt_messages[0].content
payload["stream"] = stream
elif model_prefix == "meta":
payload = { **model_parameters }
payload["prompt"] = self._convert_messages_to_prompt(prompt_messages, model_prefix, model_name)
else:
raise ValueError(f"Got unknown model prefix {model_prefix}")
@ -783,36 +663,16 @@ class BedrockLargeLanguageModel(LargeLanguageModel):
# get output text and calculate num tokens based on model / provider
model_prefix = model.split('.')[0]
if model_prefix == "amazon":
output = response_body.get("results")[0].get("outputText").strip('\n')
prompt_tokens = response_body.get("inputTextTokenCount")
completion_tokens = response_body.get("results")[0].get("tokenCount")
elif model_prefix == "ai21":
if model_prefix == "ai21":
output = response_body.get('completions')[0].get('data').get('text')
prompt_tokens = len(response_body.get("prompt").get("tokens"))
completion_tokens = len(response_body.get('completions')[0].get('data').get('tokens'))
elif model_prefix == "anthropic":
output = response_body.get("completion")
prompt_tokens = self.get_num_tokens(model, credentials, prompt_messages)
completion_tokens = self.get_num_tokens(model, credentials, output if output else '')
elif model_prefix == "cohere":
output = response_body.get("generations")[0].get("text")
prompt_tokens = self.get_num_tokens(model, credentials, prompt_messages)
completion_tokens = self.get_num_tokens(model, credentials, output if output else '')
elif model_prefix == "meta":
output = response_body.get("generation").strip('\n')
prompt_tokens = response_body.get("prompt_token_count")
completion_tokens = response_body.get("generation_token_count")
elif model_prefix == "mistral":
output = response_body.get("outputs")[0].get("text")
prompt_tokens = response.get('ResponseMetadata').get('HTTPHeaders').get('x-amzn-bedrock-input-token-count')
completion_tokens = response.get('ResponseMetadata').get('HTTPHeaders').get('x-amzn-bedrock-output-token-count')
else:
raise ValueError(f"Got unknown model prefix {model_prefix} when handling block response")
@ -883,26 +743,10 @@ class BedrockLargeLanguageModel(LargeLanguageModel):
payload = json.loads(chunk.get('bytes').decode())
model_prefix = model.split('.')[0]
if model_prefix == "amazon":
content_delta = payload.get("outputText").strip('\n')
finish_reason = payload.get("completion_reason")
elif model_prefix == "anthropic":
content_delta = payload.get("completion")
finish_reason = payload.get("stop_reason")
elif model_prefix == "cohere":
if model_prefix == "cohere":
content_delta = payload.get("text")
finish_reason = payload.get("finish_reason")
elif model_prefix == "mistral":
content_delta = payload.get('outputs')[0].get("text")
finish_reason = payload.get('outputs')[0].get("stop_reason")
elif model_prefix == "meta":
content_delta = payload.get("generation").strip('\n')
finish_reason = payload.get("stop_reason")
else:
raise ValueError(f"Got unknown model prefix {model_prefix} when handling stream response")

View File

@ -0,0 +1,25 @@
model: meta.llama3-1-405b-instruct-v1:0
label:
en_US: Llama 3.1 405B Instruct
model_type: llm
model_properties:
mode: completion
context_size: 128000
parameter_rules:
- name: temperature
use_template: temperature
default: 0.5
- name: top_p
use_template: top_p
default: 0.9
- name: max_gen_len
use_template: max_tokens
required: true
default: 512
min: 1
max: 2048
pricing:
input: '0.00532'
output: '0.016'
unit: '0.001'
currency: USD

View File

@ -0,0 +1,25 @@
model: meta.llama3-1-70b-instruct-v1:0
label:
en_US: Llama 3.1 Instruct 70B
model_type: llm
model_properties:
mode: completion
context_size: 128000
parameter_rules:
- name: temperature
use_template: temperature
default: 0.5
- name: top_p
use_template: top_p
default: 0.9
- name: max_gen_len
use_template: max_tokens
required: true
default: 512
min: 1
max: 2048
pricing:
input: '0.00265'
output: '0.0035'
unit: '0.001'
currency: USD

View File

@ -0,0 +1,25 @@
model: meta.llama3-1-8b-instruct-v1:0
label:
en_US: Llama 3.1 Instruct 8B
model_type: llm
model_properties:
mode: completion
context_size: 128000
parameter_rules:
- name: temperature
use_template: temperature
default: 0.5
- name: top_p
use_template: top_p
default: 0.9
- name: max_gen_len
use_template: max_tokens
required: true
default: 512
min: 1
max: 2048
pricing:
input: '0.0003'
output: '0.0006'
unit: '0.001'
currency: USD

View File

@ -0,0 +1,29 @@
model: mistral.mistral-large-2407-v1:0
label:
en_US: Mistral Large 2 (24.07)
model_type: llm
features:
- tool-call
model_properties:
mode: completion
context_size: 128000
parameter_rules:
- name: temperature
use_template: temperature
required: false
default: 0.7
- name: top_p
use_template: top_p
required: false
default: 1
- name: max_tokens
use_template: max_tokens
required: true
default: 512
min: 1
max: 8192
pricing:
input: '0.003'
output: '0.009'
unit: '0.001'
currency: USD

View File

@ -23,7 +23,7 @@ parameter_rules:
type: int
default: 4096
min: 1
max: 4096
max: 8192
help:
zh_Hans: 指定生成结果长度的上限。如果生成结果截断,可以调大该参数。
en_US: Specifies the upper limit on the length of generated results. If the generated results are truncated, you can increase this parameter.
@ -57,6 +57,18 @@ parameter_rules:
help:
zh_Hans: 介于 -2.0 和 2.0 之间的数字。如果该值为正,那么新 token 会根据其在已有文本中的出现频率受到相应的惩罚,降低模型重复相同内容的可能性。
en_US: A number between -2.0 and 2.0. If the value is positive, new tokens are penalized based on their frequency of occurrence in existing text, reducing the likelihood that the model will repeat the same content.
- name: response_format
label:
zh_Hans: 回复格式
en_US: response_format
type: string
help:
zh_Hans: 指定模型必须输出的格式
en_US: specifying the format that the model must output
required: false
options:
- text
- json_object
pricing:
input: '1'
output: '2'

View File

@ -18,6 +18,7 @@ help:
en_US: https://console.cloud.tencent.com/cam/capi
supported_model_types:
- llm
- text-embedding
configurate_methods:
- predefined-model
provider_credential_schema:

View File

@ -14,6 +14,7 @@ from core.model_runtime.entities.message_entities import (
PromptMessage,
PromptMessageTool,
SystemPromptMessage,
ToolPromptMessage,
UserPromptMessage,
)
from core.model_runtime.errors.invoke import InvokeError
@ -44,6 +45,17 @@ class HunyuanLargeLanguageModel(LargeLanguageModel):
"Stream": stream,
**custom_parameters,
}
# add Tools and ToolChoice
if (tools and len(tools) > 0):
params['ToolChoice'] = "auto"
params['Tools'] = [{
"Type": "function",
"Function": {
"Name": tool.name,
"Description": tool.description,
"Parameters": json.dumps(tool.parameters)
}
} for tool in tools]
request.from_json_string(json.dumps(params))
response = client.ChatCompletions(request)
@ -89,9 +101,43 @@ class HunyuanLargeLanguageModel(LargeLanguageModel):
def _convert_prompt_messages_to_dicts(self, prompt_messages: list[PromptMessage]) -> list[dict]:
"""Convert a list of PromptMessage objects to a list of dictionaries with 'Role' and 'Content' keys."""
return [{"Role": message.role.value, "Content": message.content} for message in prompt_messages]
dict_list = []
for message in prompt_messages:
if isinstance(message, AssistantPromptMessage):
tool_calls = message.tool_calls
if (tool_calls and len(tool_calls) > 0):
dict_tool_calls = [
{
"Id": tool_call.id,
"Type": tool_call.type,
"Function": {
"Name": tool_call.function.name,
"Arguments": tool_call.function.arguments if (tool_call.function.arguments == "") else "{}"
}
} for tool_call in tool_calls]
dict_list.append({
"Role": message.role.value,
# fix set content = "" while tool_call request
# fix [hunyuan] None, [TencentCloudSDKException] code:InvalidParameter message:Messages Content and Contents not allowed empty at the same time.
"Content": " ", # message.content if (message.content is not None) else "",
"ToolCalls": dict_tool_calls
})
else:
dict_list.append({ "Role": message.role.value, "Content": message.content })
elif isinstance(message, ToolPromptMessage):
tool_execute_result = { "result": message.content }
content =json.dumps(tool_execute_result, ensure_ascii=False)
dict_list.append({ "Role": message.role.value, "Content": content, "ToolCallId": message.tool_call_id })
else:
dict_list.append({ "Role": message.role.value, "Content": message.content })
return dict_list
def _handle_stream_chat_response(self, model, credentials, prompt_messages, resp):
tool_call = None
tool_calls = []
for index, event in enumerate(resp):
logging.debug("_handle_stream_chat_response, event: %s", event)
@ -109,20 +155,54 @@ class HunyuanLargeLanguageModel(LargeLanguageModel):
usage = data.get('Usage', {})
prompt_tokens = usage.get('PromptTokens', 0)
completion_tokens = usage.get('CompletionTokens', 0)
usage = self._calc_response_usage(model, credentials, prompt_tokens, completion_tokens)
response_tool_calls = delta.get('ToolCalls')
if (response_tool_calls is not None):
new_tool_calls = self._extract_response_tool_calls(response_tool_calls)
if (len(new_tool_calls) > 0):
new_tool_call = new_tool_calls[0]
if (tool_call is None): tool_call = new_tool_call
elif (tool_call.id != new_tool_call.id):
tool_calls.append(tool_call)
tool_call = new_tool_call
else:
tool_call.function.name += new_tool_call.function.name
tool_call.function.arguments += new_tool_call.function.arguments
if (tool_call is not None and len(tool_call.function.name) > 0 and len(tool_call.function.arguments) > 0):
tool_calls.append(tool_call)
tool_call = None
assistant_prompt_message = AssistantPromptMessage(
content=message_content,
tool_calls=[]
)
# rewrite content = "" while tool_call to avoid show content on web page
if (len(tool_calls) > 0): assistant_prompt_message.content = ""
# add tool_calls to assistant_prompt_message
if (finish_reason == 'tool_calls'):
assistant_prompt_message.tool_calls = tool_calls
tool_call = None
tool_calls = []
delta_chunk = LLMResultChunkDelta(
index=index,
role=delta.get('Role', 'assistant'),
message=assistant_prompt_message,
usage=usage,
finish_reason=finish_reason,
)
if (len(finish_reason) > 0):
usage = self._calc_response_usage(model, credentials, prompt_tokens, completion_tokens)
delta_chunk = LLMResultChunkDelta(
index=index,
role=delta.get('Role', 'assistant'),
message=assistant_prompt_message,
usage=usage,
finish_reason=finish_reason,
)
tool_call = None
tool_calls = []
else:
delta_chunk = LLMResultChunkDelta(
index=index,
message=assistant_prompt_message,
)
yield LLMResultChunk(
model=model,
@ -177,12 +257,15 @@ class HunyuanLargeLanguageModel(LargeLanguageModel):
"""
human_prompt = "\n\nHuman:"
ai_prompt = "\n\nAssistant:"
tool_prompt = "\n\nTool:"
content = message.content
if isinstance(message, UserPromptMessage):
message_text = f"{human_prompt} {content}"
elif isinstance(message, AssistantPromptMessage):
message_text = f"{ai_prompt} {content}"
elif isinstance(message, ToolPromptMessage):
message_text = f"{tool_prompt} {content}"
elif isinstance(message, SystemPromptMessage):
message_text = content
else:
@ -203,3 +286,30 @@ class HunyuanLargeLanguageModel(LargeLanguageModel):
return {
InvokeError: [TencentCloudSDKException],
}
def _extract_response_tool_calls(self,
response_tool_calls: list[dict]) \
-> list[AssistantPromptMessage.ToolCall]:
"""
Extract tool calls from response
:param response_tool_calls: response tool calls
:return: list of tool calls
"""
tool_calls = []
if response_tool_calls:
for response_tool_call in response_tool_calls:
response_function = response_tool_call.get('Function', {})
function = AssistantPromptMessage.ToolCall.ToolCallFunction(
name=response_function.get('Name', ''),
arguments=response_function.get('Arguments', '')
)
tool_call = AssistantPromptMessage.ToolCall(
id=response_tool_call.get('Id', 0),
type='function',
function=function
)
tool_calls.append(tool_call)
return tool_calls

View File

@ -0,0 +1,5 @@
model: hunyuan-embedding
model_type: text-embedding
model_properties:
context_size: 1024
max_chunks: 1

View File

@ -0,0 +1,173 @@
import json
import logging
import time
from typing import Optional
from tencentcloud.common import credential
from tencentcloud.common.exception import TencentCloudSDKException
from tencentcloud.common.profile.client_profile import ClientProfile
from tencentcloud.common.profile.http_profile import HttpProfile
from tencentcloud.hunyuan.v20230901 import hunyuan_client, models
from core.model_runtime.entities.model_entities import PriceType
from core.model_runtime.entities.text_embedding_entities import EmbeddingUsage, TextEmbeddingResult
from core.model_runtime.errors.invoke import (
InvokeError,
)
from core.model_runtime.errors.validate import CredentialsValidateFailedError
from core.model_runtime.model_providers.__base.text_embedding_model import TextEmbeddingModel
logger = logging.getLogger(__name__)
class HunyuanTextEmbeddingModel(TextEmbeddingModel):
"""
Model class for Hunyuan text embedding model.
"""
def _invoke(self, model: str, credentials: dict,
texts: list[str], user: Optional[str] = None) \
-> TextEmbeddingResult:
"""
Invoke text embedding model
:param model: model name
:param credentials: model credentials
:param texts: texts to embed
:param user: unique user id
:return: embeddings result
"""
if model != 'hunyuan-embedding':
raise ValueError('Invalid model name')
client = self._setup_hunyuan_client(credentials)
embeddings = []
token_usage = 0
for input in texts:
request = models.GetEmbeddingRequest()
params = {
"Input": input
}
request.from_json_string(json.dumps(params))
response = client.GetEmbedding(request)
usage = response.Usage.TotalTokens
embeddings.extend([data.Embedding for data in response.Data])
token_usage += usage
result = TextEmbeddingResult(
model=model,
embeddings=embeddings,
usage=self._calc_response_usage(
model=model,
credentials=credentials,
tokens=token_usage
)
)
return result
def validate_credentials(self, model: str, credentials: dict) -> None:
"""
Validate credentials
"""
try:
client = self._setup_hunyuan_client(credentials)
req = models.ChatCompletionsRequest()
params = {
"Model": model,
"Messages": [{
"Role": "user",
"Content": "hello"
}],
"TopP": 1,
"Temperature": 0,
"Stream": False
}
req.from_json_string(json.dumps(params))
client.ChatCompletions(req)
except Exception as e:
raise CredentialsValidateFailedError(f'Credentials validation failed: {e}')
def _setup_hunyuan_client(self, credentials):
secret_id = credentials['secret_id']
secret_key = credentials['secret_key']
cred = credential.Credential(secret_id, secret_key)
httpProfile = HttpProfile()
httpProfile.endpoint = "hunyuan.tencentcloudapi.com"
clientProfile = ClientProfile()
clientProfile.httpProfile = httpProfile
client = hunyuan_client.HunyuanClient(cred, "", clientProfile)
return client
def _calc_response_usage(self, model: str, credentials: dict, tokens: int) -> EmbeddingUsage:
"""
Calculate response usage
:param model: model name
:param credentials: model credentials
:param tokens: input tokens
:return: usage
"""
# get input price info
input_price_info = self.get_price(
model=model,
credentials=credentials,
price_type=PriceType.INPUT,
tokens=tokens
)
# transform usage
usage = EmbeddingUsage(
tokens=tokens,
total_tokens=tokens,
unit_price=input_price_info.unit_price,
price_unit=input_price_info.unit,
total_price=input_price_info.total_amount,
currency=input_price_info.currency,
latency=time.perf_counter() - self.started_at
)
return usage
@property
def _invoke_error_mapping(self) -> dict[type[InvokeError], list[type[Exception]]]:
"""
Map model invoke error to unified error
The key is the error type thrown to the caller
The value is the error type thrown by the model,
which needs to be converted into a unified error type for the caller.
:return: Invoke error mapping
"""
return {
InvokeError: [TencentCloudSDKException],
}
def get_num_tokens(self, model: str, credentials: dict, texts: list[str]) -> int:
"""
Get number of tokens for given prompt messages
:param model: model name
:param credentials: model credentials
:param texts: texts to embed
:return:
"""
# client = self._setup_hunyuan_client(credentials)
num_tokens = 0
for text in texts:
num_tokens += self._get_num_tokens_by_gpt2(text)
# use client.GetTokenCount to get num tokens
# request = models.GetTokenCountRequest()
# params = {
# "Prompt": text
# }
# request.from_json_string(json.dumps(params))
# response = client.GetTokenCount(request)
# num_tokens += response.TokenCount
return num_tokens

View File

@ -34,3 +34,8 @@ parameter_rules:
min: -2
max: 2
default: 0
pricing:
input: '0.0027'
output: '0.0027'
unit: '0.0001'
currency: USD

View File

@ -0,0 +1,41 @@
model: jondurbin/airoboros-l2-70b
label:
zh_Hans: jondurbin/airoboros-l2-70b
en_US: jondurbin/airoboros-l2-70b
model_type: llm
features:
- agent-thought
model_properties:
mode: chat
context_size: 4096
parameter_rules:
- name: temperature
use_template: temperature
min: 0
max: 2
default: 1
- name: top_p
use_template: top_p
min: 0
max: 1
default: 1
- name: max_tokens
use_template: max_tokens
min: 1
max: 2048
default: 512
- name: frequency_penalty
use_template: frequency_penalty
min: -2
max: 2
default: 0
- name: presence_penalty
use_template: presence_penalty
min: -2
max: 2
default: 0
pricing:
input: '0.005'
output: '0.005'
unit: '0.0001'
currency: USD

View File

@ -0,0 +1,41 @@
model: cognitivecomputations/dolphin-mixtral-8x22b
label:
zh_Hans: cognitivecomputations/dolphin-mixtral-8x22b
en_US: cognitivecomputations/dolphin-mixtral-8x22b
model_type: llm
features:
- agent-thought
model_properties:
mode: chat
context_size: 16000
parameter_rules:
- name: temperature
use_template: temperature
min: 0
max: 2
default: 1
- name: top_p
use_template: top_p
min: 0
max: 1
default: 1
- name: max_tokens
use_template: max_tokens
min: 1
max: 2048
default: 512
- name: frequency_penalty
use_template: frequency_penalty
min: -2
max: 2
default: 0
- name: presence_penalty
use_template: presence_penalty
min: -2
max: 2
default: 0
pricing:
input: '0.009'
output: '0.009'
unit: '0.0001'
currency: USD

View File

@ -0,0 +1,41 @@
model: google/gemma-2-9b-it
label:
zh_Hans: google/gemma-2-9b-it
en_US: google/gemma-2-9b-it
model_type: llm
features:
- agent-thought
model_properties:
mode: chat
context_size: 8192
parameter_rules:
- name: temperature
use_template: temperature
min: 0
max: 2
default: 1
- name: top_p
use_template: top_p
min: 0
max: 1
default: 1
- name: max_tokens
use_template: max_tokens
min: 1
max: 2048
default: 512
- name: frequency_penalty
use_template: frequency_penalty
min: -2
max: 2
default: 0
- name: presence_penalty
use_template: presence_penalty
min: -2
max: 2
default: 0
pricing:
input: '0.0008'
output: '0.0008'
unit: '0.0001'
currency: USD

View File

@ -0,0 +1,41 @@
model: nousresearch/hermes-2-pro-llama-3-8b
label:
zh_Hans: nousresearch/hermes-2-pro-llama-3-8b
en_US: nousresearch/hermes-2-pro-llama-3-8b
model_type: llm
features:
- agent-thought
model_properties:
mode: chat
context_size: 8192
parameter_rules:
- name: temperature
use_template: temperature
min: 0
max: 2
default: 1
- name: top_p
use_template: top_p
min: 0
max: 1
default: 1
- name: max_tokens
use_template: max_tokens
min: 1
max: 2048
default: 512
- name: frequency_penalty
use_template: frequency_penalty
min: -2
max: 2
default: 0
- name: presence_penalty
use_template: presence_penalty
min: -2
max: 2
default: 0
pricing:
input: '0.0014'
output: '0.0014'
unit: '0.0001'
currency: USD

View File

@ -0,0 +1,41 @@
model: sao10k/l3-70b-euryale-v2.1
label:
zh_Hans: sao10k/l3-70b-euryale-v2.1
en_US: sao10k/l3-70b-euryale-v2.1
model_type: llm
features:
- agent-thought
model_properties:
mode: chat
context_size: 16000
parameter_rules:
- name: temperature
use_template: temperature
min: 0
max: 2
default: 1
- name: top_p
use_template: top_p
min: 0
max: 1
default: 1
- name: max_tokens
use_template: max_tokens
min: 1
max: 2048
default: 512
- name: frequency_penalty
use_template: frequency_penalty
min: -2
max: 2
default: 0
- name: presence_penalty
use_template: presence_penalty
min: -2
max: 2
default: 0
pricing:
input: '0.0148'
output: '0.0148'
unit: '0.0001'
currency: USD

View File

@ -34,3 +34,8 @@ parameter_rules:
min: -2
max: 2
default: 0
pricing:
input: '0.0051'
output: '0.0074'
unit: '0.0001'
currency: USD

View File

@ -34,3 +34,8 @@ parameter_rules:
min: -2
max: 2
default: 0
pricing:
input: '0.00063'
output: '0.00063'
unit: '0.0001'
currency: USD

View File

@ -0,0 +1,41 @@
model: meta-llama/llama-3.1-405b-instruct
label:
zh_Hans: meta-llama/llama-3.1-405b-instruct
en_US: meta-llama/llama-3.1-405b-instruct
model_type: llm
features:
- agent-thought
model_properties:
mode: chat
context_size: 32768
parameter_rules:
- name: temperature
use_template: temperature
min: 0
max: 2
default: 1
- name: top_p
use_template: top_p
min: 0
max: 1
default: 1
- name: max_tokens
use_template: max_tokens
min: 1
max: 2048
default: 512
- name: frequency_penalty
use_template: frequency_penalty
min: -2
max: 2
default: 0
- name: presence_penalty
use_template: presence_penalty
min: -2
max: 2
default: 0
pricing:
input: '0.03'
output: '0.05'
unit: '0.0001'
currency: USD

View File

@ -0,0 +1,41 @@
model: meta-llama/llama-3.1-70b-instruct
label:
zh_Hans: meta-llama/llama-3.1-70b-instruct
en_US: meta-llama/llama-3.1-70b-instruct
model_type: llm
features:
- agent-thought
model_properties:
mode: chat
context_size: 8192
parameter_rules:
- name: temperature
use_template: temperature
min: 0
max: 2
default: 1
- name: top_p
use_template: top_p
min: 0
max: 1
default: 1
- name: max_tokens
use_template: max_tokens
min: 1
max: 2048
default: 512
- name: frequency_penalty
use_template: frequency_penalty
min: -2
max: 2
default: 0
- name: presence_penalty
use_template: presence_penalty
min: -2
max: 2
default: 0
pricing:
input: '0.0055'
output: '0.0076'
unit: '0.0001'
currency: USD

View File

@ -0,0 +1,41 @@
model: meta-llama/llama-3.1-8b-instruct
label:
zh_Hans: meta-llama/llama-3.1-8b-instruct
en_US: meta-llama/llama-3.1-8b-instruct
model_type: llm
features:
- agent-thought
model_properties:
mode: chat
context_size: 8192
parameter_rules:
- name: temperature
use_template: temperature
min: 0
max: 2
default: 1
- name: top_p
use_template: top_p
min: 0
max: 1
default: 1
- name: max_tokens
use_template: max_tokens
min: 1
max: 2048
default: 512
- name: frequency_penalty
use_template: frequency_penalty
min: -2
max: 2
default: 0
- name: presence_penalty
use_template: presence_penalty
min: -2
max: 2
default: 0
pricing:
input: '0.001'
output: '0.001'
unit: '0.0001'
currency: USD

View File

@ -34,3 +34,8 @@ parameter_rules:
min: -2
max: 2
default: 0
pricing:
input: '0.0058'
output: '0.0078'
unit: '0.0001'
currency: USD

View File

@ -0,0 +1,41 @@
model: sophosympatheia/midnight-rose-70b
label:
zh_Hans: sophosympatheia/midnight-rose-70b
en_US: sophosympatheia/midnight-rose-70b
model_type: llm
features:
- agent-thought
model_properties:
mode: chat
context_size: 4096
parameter_rules:
- name: temperature
use_template: temperature
min: 0
max: 2
default: 1
- name: top_p
use_template: top_p
min: 0
max: 1
default: 1
- name: max_tokens
use_template: max_tokens
min: 1
max: 2048
default: 512
- name: frequency_penalty
use_template: frequency_penalty
min: -2
max: 2
default: 0
- name: presence_penalty
use_template: presence_penalty
min: -2
max: 2
default: 0
pricing:
input: '0.008'
output: '0.008'
unit: '0.0001'
currency: USD

View File

@ -0,0 +1,41 @@
model: mistralai/mistral-7b-instruct
label:
zh_Hans: mistralai/mistral-7b-instruct
en_US: mistralai/mistral-7b-instruct
model_type: llm
features:
- agent-thought
model_properties:
mode: chat
context_size: 32768
parameter_rules:
- name: temperature
use_template: temperature
min: 0
max: 2
default: 1
- name: top_p
use_template: top_p
min: 0
max: 1
default: 1
- name: max_tokens
use_template: max_tokens
min: 1
max: 2048
default: 512
- name: frequency_penalty
use_template: frequency_penalty
min: -2
max: 2
default: 0
- name: presence_penalty
use_template: presence_penalty
min: -2
max: 2
default: 0
pricing:
input: '0.00059'
output: '0.00059'
unit: '0.0001'
currency: USD

View File

@ -34,3 +34,8 @@ parameter_rules:
min: -2
max: 2
default: 0
pricing:
input: '0.00119'
output: '0.00119'
unit: '0.0001'
currency: USD

View File

@ -34,3 +34,8 @@ parameter_rules:
min: -2
max: 2
default: 0
pricing:
input: '0.0017'
output: '0.0017'
unit: '0.0001'
currency: USD

View File

@ -34,3 +34,8 @@ parameter_rules:
min: -2
max: 2
default: 0
pricing:
input: '0.0017'
output: '0.0017'
unit: '0.0001'
currency: USD

View File

@ -34,3 +34,8 @@ parameter_rules:
min: -2
max: 2
default: 0
pricing:
input: '0.0064'
output: '0.0064'
unit: '0.0001'
currency: USD

View File

@ -1,6 +1,9 @@
provider: novita
label:
en_US: novita.ai
description:
en_US: An LLM API that matches various application scenarios with high cost-effectiveness.
zh_Hans: 适配多种海外应用场景的高性价比 LLM API
icon_small:
en_US: icon_s_en.svg
icon_large:
@ -11,7 +14,7 @@ help:
en_US: Get your API key from novita.ai
zh_Hans: 从 novita.ai 获取 API Key
url:
en_US: https://novita.ai/dashboard/key?utm_source=dify
en_US: https://novita.ai/settings#key-management?utm_source=dify&utm_medium=ch&utm_campaign=api
supported_model_types:
- llm
configurate_methods:

View File

@ -114,7 +114,8 @@ class OpenAIText2SpeechModel(_CommonOpenAI, TTSModel):
# doc: https://platform.openai.com/docs/guides/text-to-speech
credentials_kwargs = self._to_credential_kwargs(credentials)
client = OpenAI(**credentials_kwargs)
if not voice or voice not in self.get_tts_model_voices(model=model, credentials=credentials):
model_support_voice = [x.get("value") for x in self.get_tts_model_voices(model=model, credentials=credentials)]
if not voice or voice not in model_support_voice:
voice = self._get_model_default_voice(model, credentials)
word_limit = self._get_model_word_limit(model, credentials)
if len(content_text) > word_limit:

View File

@ -0,0 +1,30 @@
model: deepseek-ai/DeepSeek-Coder-V2-Instruct
label:
en_US: deepseek-ai/DeepSeek-Coder-V2-Instruct
model_type: llm
features:
- agent-thought
model_properties:
mode: chat
context_size: 32768
parameter_rules:
- name: temperature
use_template: temperature
- name: max_tokens
use_template: max_tokens
type: int
default: 512
min: 1
max: 4096
help:
zh_Hans: 指定生成结果长度的上限。如果生成结果截断,可以调大该参数。
en_US: Specifies the upper limit on the length of generated results. If the generated results are truncated, you can increase this parameter.
- name: top_p
use_template: top_p
- name: frequency_penalty
use_template: frequency_penalty
pricing:
input: '1.33'
output: '1.33'
unit: '0.000001'
currency: RMB

View File

@ -1,11 +1,9 @@
model: deepseek-ai/deepseek-v2-chat
label:
en_US: deepseek-ai/deepseek-v2-chat
en_US: deepseek-ai/DeepSeek-V2-Chat
model_type: llm
features:
- multi-tool-call
- agent-thought
- stream-tool-call
model_properties:
mode: chat
context_size: 32768

View File

@ -1,11 +1,9 @@
model: zhipuai/glm4-9B-chat
label:
en_US: zhipuai/glm4-9B-chat
en_US: THUDM/glm-4-9b-chat
model_type: llm
features:
- multi-tool-call
- agent-thought
- stream-tool-call
model_properties:
mode: chat
context_size: 32768

View File

@ -1,11 +1,9 @@
model: alibaba/Qwen2-57B-A14B-Instruct
label:
en_US: alibaba/Qwen2-57B-A14B-Instruct
en_US: Qwen/Qwen2-57B-A14B-Instruct
model_type: llm
features:
- multi-tool-call
- agent-thought
- stream-tool-call
model_properties:
mode: chat
context_size: 32768

View File

@ -1,11 +1,9 @@
model: alibaba/Qwen2-72B-Instruct
label:
en_US: alibaba/Qwen2-72B-Instruct
en_US: Qwen/Qwen2-72B-Instruct
model_type: llm
features:
- multi-tool-call
- agent-thought
- stream-tool-call
model_properties:
mode: chat
context_size: 32768

View File

@ -1,11 +1,9 @@
model: alibaba/Qwen2-7B-Instruct
label:
en_US: alibaba/Qwen2-7B-Instruct
en_US: Qwen/Qwen2-7B-Instruct
model_type: llm
features:
- multi-tool-call
- agent-thought
- stream-tool-call
model_properties:
mode: chat
context_size: 32768

View File

@ -1,11 +1,9 @@
model: 01-ai/Yi-1.5-34B-Chat
label:
en_US: 01-ai/Yi-1.5-34B-Chat
en_US: 01-ai/Yi-1.5-34B-Chat-16K
model_type: llm
features:
- multi-tool-call
- agent-thought
- stream-tool-call
model_properties:
mode: chat
context_size: 16384

View File

@ -3,9 +3,7 @@ label:
en_US: 01-ai/Yi-1.5-6B-Chat
model_type: llm
features:
- multi-tool-call
- agent-thought
- stream-tool-call
model_properties:
mode: chat
context_size: 4096

View File

@ -1,11 +1,9 @@
model: 01-ai/Yi-1.5-9B-Chat
label:
en_US: 01-ai/Yi-1.5-9B-Chat
en_US: 01-ai/Yi-1.5-9B-Chat-16K
model_type: llm
features:
- multi-tool-call
- agent-thought
- stream-tool-call
model_properties:
mode: chat
context_size: 16384

View File

@ -19,7 +19,7 @@ class SiliconflowProvider(ModelProvider):
model_instance = self.get_model_instance(ModelType.LLM)
model_instance.validate_credentials(
model='deepseek-ai/deepseek-v2-chat',
model='deepseek-ai/DeepSeek-V2-Chat',
credentials=credentials
)
except CredentialsValidateFailedError as ex:

View File

@ -501,8 +501,7 @@ You should also complete the text started with ``` but not tell ``` directly.
'role': 'assistant',
'content': content if not rich_content else [{"text": content}],
'tool_calls': [tool_call.model_dump() for tool_call in
prompt_message.tool_calls] if prompt_message.tool_calls else []
prompt_message.tool_calls] if prompt_message.tool_calls else None
})
elif isinstance(prompt_message, ToolPromptMessage):
tongyi_messages.append({

View File

@ -6,6 +6,7 @@ from typing import Any, Optional
from flask import Flask, current_app
from pydantic import BaseModel, ConfigDict
from configs import dify_config
from core.app.apps.base_app_queue_manager import AppQueueManager, PublishFrom
from core.app.entities.queue_entities import QueueMessageReplaceEvent
from core.moderation.base import ModerationAction, ModerationOutputsResult
@ -20,8 +21,6 @@ class ModerationRule(BaseModel):
class OutputModeration(BaseModel):
DEFAULT_BUFFER_SIZE: int = 300
tenant_id: str
app_id: str
@ -76,10 +75,10 @@ class OutputModeration(BaseModel):
return final_output
def start_thread(self) -> threading.Thread:
buffer_size = int(current_app.config.get('MODERATION_BUFFER_SIZE', self.DEFAULT_BUFFER_SIZE))
buffer_size = dify_config.MODERATION_BUFFER_SIZE
thread = threading.Thread(target=self.worker, kwargs={
'flask_app': current_app._get_current_object(),
'buffer_size': buffer_size if buffer_size > 0 else self.DEFAULT_BUFFER_SIZE
'buffer_size': buffer_size if buffer_size > 0 else dify_config.MODERATION_BUFFER_SIZE
})
thread.start()

View File

@ -298,34 +298,29 @@ class TraceTask:
self.file_base_url = os.getenv("FILES_URL", "http://127.0.0.1:5001")
def execute(self):
method_name, trace_info = self.preprocess()
return trace_info
return self.preprocess()
def preprocess(self):
if self.trace_type == TraceTaskName.CONVERSATION_TRACE:
return TraceTaskName.CONVERSATION_TRACE, self.conversation_trace(**self.kwargs)
if self.trace_type == TraceTaskName.WORKFLOW_TRACE:
return TraceTaskName.WORKFLOW_TRACE, self.workflow_trace(self.workflow_run, self.conversation_id)
elif self.trace_type == TraceTaskName.MESSAGE_TRACE:
return TraceTaskName.MESSAGE_TRACE, self.message_trace(self.message_id)
elif self.trace_type == TraceTaskName.MODERATION_TRACE:
return TraceTaskName.MODERATION_TRACE, self.moderation_trace(self.message_id, self.timer, **self.kwargs)
elif self.trace_type == TraceTaskName.SUGGESTED_QUESTION_TRACE:
return TraceTaskName.SUGGESTED_QUESTION_TRACE, self.suggested_question_trace(
preprocess_map = {
TraceTaskName.CONVERSATION_TRACE: lambda: self.conversation_trace(**self.kwargs),
TraceTaskName.WORKFLOW_TRACE: lambda: self.workflow_trace(self.workflow_run, self.conversation_id),
TraceTaskName.MESSAGE_TRACE: lambda: self.message_trace(self.message_id),
TraceTaskName.MODERATION_TRACE: lambda: self.moderation_trace(
self.message_id, self.timer, **self.kwargs
)
elif self.trace_type == TraceTaskName.DATASET_RETRIEVAL_TRACE:
return TraceTaskName.DATASET_RETRIEVAL_TRACE, self.dataset_retrieval_trace(
),
TraceTaskName.SUGGESTED_QUESTION_TRACE: lambda: self.suggested_question_trace(
self.message_id, self.timer, **self.kwargs
)
elif self.trace_type == TraceTaskName.TOOL_TRACE:
return TraceTaskName.TOOL_TRACE, self.tool_trace(self.message_id, self.timer, **self.kwargs)
elif self.trace_type == TraceTaskName.GENERATE_NAME_TRACE:
return TraceTaskName.GENERATE_NAME_TRACE, self.generate_name_trace(
),
TraceTaskName.DATASET_RETRIEVAL_TRACE: lambda: self.dataset_retrieval_trace(
self.message_id, self.timer, **self.kwargs
),
TraceTaskName.TOOL_TRACE: lambda: self.tool_trace(self.message_id, self.timer, **self.kwargs),
TraceTaskName.GENERATE_NAME_TRACE: lambda: self.generate_name_trace(
self.conversation_id, self.timer, **self.kwargs
)
else:
return '', {}
),
}
return preprocess_map.get(self.trace_type, lambda: None)()
# process methods for different trace types
def conversation_trace(self, **kwargs):

View File

@ -2,9 +2,9 @@ import json
from collections import defaultdict
from typing import Any, Optional
from flask import current_app
from pydantic import BaseModel
from configs import dify_config
from core.rag.datasource.keyword.jieba.jieba_keyword_table_handler import JiebaKeywordTableHandler
from core.rag.datasource.keyword.keyword_base import BaseKeyword
from core.rag.models.document import Document
@ -139,7 +139,7 @@ class Jieba(BaseKeyword):
if keyword_table_dict:
return keyword_table_dict['__data__']['table']
else:
keyword_data_source_type = current_app.config['KEYWORD_DATA_SOURCE_TYPE']
keyword_data_source_type = dify_config.KEYWORD_DATA_SOURCE_TYPE
dataset_keyword_table = DatasetKeywordTable(
dataset_id=self.dataset.id,
keyword_table='',

View File

@ -5,6 +5,7 @@ from uuid import uuid4
from pydantic import BaseModel, model_validator
from pymilvus import MilvusClient, MilvusException, connections
from pymilvus.milvus_client import IndexParams
from configs import dify_config
from core.rag.datasource.entity.embedding import Embeddings
@ -250,11 +251,15 @@ class MilvusVector(BaseVector):
# Since primary field is auto-id, no need to track it
self._fields.remove(Field.PRIMARY_KEY.value)
# Create Index params for the collection
index_params_obj = IndexParams()
index_params_obj.add_index(field_name=Field.VECTOR.value, **index_params)
# Create the collection
collection_name = self._collection_name
self._client.create_collection_with_schema(collection_name=collection_name,
schema=schema, index_param=index_params,
consistency_level=self._consistency_level)
self._client.create_collection(collection_name=collection_name,
schema=schema, index_params=index_params_obj,
consistency_level=self._consistency_level)
redis_client.set(collection_exist_cache_key, 1, ex=3600)
def _init_client(self, config) -> MilvusClient:

View File

@ -55,7 +55,7 @@ CREATE TABLE IF NOT EXISTS {table_name} (
)
"""
SQL_CREATE_INDEX = """
CREATE INDEX idx_docs_{table_name} ON {table_name}(text)
CREATE INDEX IF NOT EXISTS idx_docs_{table_name} ON {table_name}(text)
INDEXTYPE IS CTXSYS.CONTEXT PARAMETERS
('FILTER CTXSYS.NULL_FILTER SECTION GROUP CTXSYS.HTML_SECTION_GROUP LEXER sys.my_chinese_vgram_lexer')
"""
@ -248,7 +248,7 @@ class OracleVector(BaseVector):
def delete(self) -> None:
with self._get_cursor() as cur:
cur.execute(f"DROP TABLE IF EXISTS {self.table_name}")
cur.execute(f"DROP TABLE IF EXISTS {self.table_name} cascade constraints")
def _create_collection(self, dimension: int):
cache_key = f"vector_indexing_{self._collection_name}"

View File

@ -3,6 +3,7 @@ import os
from typing import Optional
import pandas as pd
from openpyxl import load_workbook
from core.rag.extractor.extractor_base import BaseExtractor
from core.rag.models.document import Document
@ -28,26 +29,48 @@ class ExcelExtractor(BaseExtractor):
self._autodetect_encoding = autodetect_encoding
def extract(self) -> list[Document]:
""" Load from Excel file in xls or xlsx format using Pandas."""
""" Load from Excel file in xls or xlsx format using Pandas and openpyxl."""
documents = []
# Determine the file extension
file_extension = os.path.splitext(self._file_path)[-1].lower()
# Read each worksheet of an Excel file using Pandas
if file_extension == '.xlsx':
excel_file = pd.ExcelFile(self._file_path, engine='openpyxl')
wb = load_workbook(self._file_path, data_only=True)
for sheet_name in wb.sheetnames:
sheet = wb[sheet_name]
data = sheet.values
cols = next(data)
df = pd.DataFrame(data, columns=cols)
df.dropna(how='all', inplace=True)
for index, row in df.iterrows():
page_content = []
for col_index, (k, v) in enumerate(row.items()):
if pd.notna(v):
cell = sheet.cell(row=index + 2,
column=col_index + 1) # +2 to account for header and 1-based index
if cell.hyperlink:
value = f"[{v}]({cell.hyperlink.target})"
page_content.append(f'"{k}":"{value}"')
else:
page_content.append(f'"{k}":"{v}"')
documents.append(Document(page_content=';'.join(page_content),
metadata={'source': self._file_path}))
elif file_extension == '.xls':
excel_file = pd.ExcelFile(self._file_path, engine='xlrd')
for sheet_name in excel_file.sheet_names:
df = excel_file.parse(sheet_name=sheet_name)
df.dropna(how='all', inplace=True)
for _, row in df.iterrows():
page_content = []
for k, v in row.items():
if pd.notna(v):
page_content.append(f'"{k}":"{v}"')
documents.append(Document(page_content=';'.join(page_content),
metadata={'source': self._file_path}))
else:
raise ValueError(f"Unsupported file extension: {file_extension}")
for sheet_name in excel_file.sheet_names:
df: pd.DataFrame = excel_file.parse(sheet_name=sheet_name)
# filter out rows with all NaN values
df.dropna(how='all', inplace=True)
# transform each row into a Document
documents += [Document(page_content=';'.join(f'"{k}":"{v}"' for k, v in row.items() if pd.notna(v)),
metadata={'source': self._file_path},
) for _, row in df.iterrows()]
return documents

View File

@ -4,9 +4,8 @@ from pathlib import Path
from typing import Union
from urllib.parse import unquote
import requests
from configs import dify_config
from core.helper import ssrf_proxy
from core.rag.extractor.csv_extractor import CSVExtractor
from core.rag.extractor.entity.datasource_type import DatasourceType
from core.rag.extractor.entity.extract_setting import ExtractSetting
@ -51,7 +50,7 @@ class ExtractProcessor:
@classmethod
def load_from_url(cls, url: str, return_text: bool = False) -> Union[list[Document], str]:
response = requests.get(url, headers={
response = ssrf_proxy.get(url, headers={
"User-Agent": USER_AGENT
})

View File

@ -54,8 +54,16 @@ class MarkdownExtractor(BaseExtractor):
current_header = None
current_text = ""
code_block_flag = False
for line in lines:
if line.startswith("```"):
code_block_flag = not code_block_flag
current_text += line + "\n"
continue
if code_block_flag:
current_text += line + "\n"
continue
header_match = re.match(r"^#+\s", line)
if header_match:
if current_header is not None:

View File

@ -30,7 +30,6 @@ class CogView3Tool(BuiltinTool):
if not prompt:
return self.create_text_message('Please input prompt')
# get size
print(tool_parameters.get('prompt', 'square'))
size = size_mapping[tool_parameters.get('size', 'square')]
# get n
n = tool_parameters.get('n', 1)
@ -58,8 +57,9 @@ class CogView3Tool(BuiltinTool):
result = []
for image in response.data:
result.append(self.create_image_message(image=image.url))
result.append(self.create_text_message(
f'\nGenerate image source to Seed ID: {seed_id}'))
result.append(self.create_json_message({
"url": image.url,
}))
return result
@staticmethod

View File

@ -1,22 +1,19 @@
from core.tools.errors import ToolProviderCredentialValidationError
from core.tools.provider.builtin.firecrawl.tools.crawl import CrawlTool
from core.tools.provider.builtin.firecrawl.tools.scrape import ScrapeTool
from core.tools.provider.builtin_tool_provider import BuiltinToolProviderController
class FirecrawlProvider(BuiltinToolProviderController):
def _validate_credentials(self, credentials: dict) -> None:
try:
# Example validation using the Crawl tool
CrawlTool().fork_tool_runtime(
# Example validation using the ScrapeTool, only scraping title for minimize content
ScrapeTool().fork_tool_runtime(
runtime={"credentials": credentials}
).invoke(
user_id='',
tool_parameters={
"url": "https://example.com",
"includes": '',
"excludes": '',
"limit": 1,
"onlyMainContent": True,
"url": "https://google.com",
"onlyIncludeTags": 'title'
}
)
except Exception as e:

View File

@ -31,8 +31,5 @@ credentials_for_provider:
label:
en_US: Firecrawl server's Base URL
zh_Hans: Firecrawl服务器的API URL
pt_BR: Firecrawl server's Base URL
placeholder:
en_US: https://www.firecrawl.dev
zh_HansL: https://www.firecrawl.dev
pt_BR: https://www.firecrawl.dev
en_US: https://api.firecrawl.dev

View File

@ -1,3 +1,4 @@
import json
import logging
import time
from collections.abc import Mapping
@ -8,6 +9,7 @@ from requests.exceptions import HTTPError
logger = logging.getLogger(__name__)
class FirecrawlApp:
def __init__(self, api_key: str | None = None, base_url: str | None = None):
self.api_key = api_key
@ -25,14 +27,16 @@ class FirecrawlApp:
return headers
def _request(
self,
method: str,
url: str,
data: Mapping[str, Any] | None = None,
headers: Mapping[str, str] | None = None,
retries: int = 3,
backoff_factor: float = 0.3,
self,
method: str,
url: str,
data: Mapping[str, Any] | None = None,
headers: Mapping[str, str] | None = None,
retries: int = 3,
backoff_factor: float = 0.3,
) -> Mapping[str, Any] | None:
if not headers:
headers = self._prepare_headers()
for i in range(retries):
try:
response = requests.request(method, url, json=data, headers=headers)
@ -47,47 +51,51 @@ class FirecrawlApp:
def scrape_url(self, url: str, **kwargs):
endpoint = f'{self.base_url}/v0/scrape'
headers = self._prepare_headers()
data = {'url': url, **kwargs}
response = self._request('POST', endpoint, data, headers)
logger.debug(f"Sent request to {endpoint=} body={data}")
response = self._request('POST', endpoint, data)
if response is None:
raise HTTPError("Failed to scrape URL after multiple retries")
return response
def search(self, query: str, **kwargs):
endpoint = f'{self.base_url}/v0/search'
headers = self._prepare_headers()
data = {'query': query, **kwargs}
response = self._request('POST', endpoint, data, headers)
logger.debug(f"Sent request to {endpoint=} body={data}")
response = self._request('POST', endpoint, data)
if response is None:
raise HTTPError("Failed to perform search after multiple retries")
return response
def crawl_url(
self, url: str, wait: bool = False, poll_interval: int = 5, idempotency_key: str | None = None, **kwargs
self, url: str, wait: bool = True, poll_interval: int = 5, idempotency_key: str | None = None, **kwargs
):
endpoint = f'{self.base_url}/v0/crawl'
headers = self._prepare_headers(idempotency_key)
data = {'url': url, **kwargs['params']}
response = self._request('POST', endpoint, data, headers)
data = {'url': url, **kwargs}
logger.debug(f"Sent request to {endpoint=} body={data}")
response = self._request('POST', endpoint, data, headers)
if response is None:
raise HTTPError("Failed to initiate crawl after multiple retries")
job_id: str = response['jobId']
if wait:
return self._monitor_job_status(job_id=job_id, poll_interval=poll_interval)
return job_id
return response
def check_crawl_status(self, job_id: str):
endpoint = f'{self.base_url}/v0/crawl/status/{job_id}'
headers = self._prepare_headers()
response = self._request('GET', endpoint, headers=headers)
response = self._request('GET', endpoint)
if response is None:
raise HTTPError(f"Failed to check status for job {job_id} after multiple retries")
return response
def cancel_crawl_job(self, job_id: str):
endpoint = f'{self.base_url}/v0/crawl/cancel/{job_id}'
response = self._request('DELETE', endpoint)
if response is None:
raise HTTPError(f"Failed to cancel job {job_id} after multiple retries")
return response
def _monitor_job_status(self, job_id: str, poll_interval: int):
while True:
status = self.check_crawl_status(job_id)
@ -96,3 +104,21 @@ class FirecrawlApp:
elif status['status'] == 'failed':
raise HTTPError(f'Job {job_id} failed: {status["error"]}')
time.sleep(poll_interval)
def get_array_params(tool_parameters: dict[str, Any], key):
param = tool_parameters.get(key)
if param:
return param.split(',')
def get_json_params(tool_parameters: dict[str, Any], key):
param = tool_parameters.get(key)
if param:
try:
# support both single quotes and double quotes
param = param.replace("'", '"')
param = json.loads(param)
except:
raise ValueError(f"Invalid {key} format.")
return param

View File

@ -1,36 +1,48 @@
import json
from typing import Any, Union
from typing import Any
from core.tools.entities.tool_entities import ToolInvokeMessage
from core.tools.provider.builtin.firecrawl.firecrawl_appx import FirecrawlApp
from core.tools.provider.builtin.firecrawl.firecrawl_appx import FirecrawlApp, get_array_params, get_json_params
from core.tools.tool.builtin_tool import BuiltinTool
class CrawlTool(BuiltinTool):
def _invoke(self, user_id: str, tool_parameters: dict[str, Any]) -> Union[ToolInvokeMessage, list[ToolInvokeMessage]]:
app = FirecrawlApp(api_key=self.runtime.credentials['firecrawl_api_key'], base_url=self.runtime.credentials['base_url'])
def _invoke(self, user_id: str, tool_parameters: dict[str, Any]) -> ToolInvokeMessage:
"""
the crawlerOptions and pageOptions comes from doc here:
https://docs.firecrawl.dev/api-reference/endpoint/crawl
"""
app = FirecrawlApp(api_key=self.runtime.credentials['firecrawl_api_key'],
base_url=self.runtime.credentials['base_url'])
crawlerOptions = {}
pageOptions = {}
options = {
'crawlerOptions': {
'excludes': tool_parameters.get('excludes', '').split(',') if tool_parameters.get('excludes') else [],
'includes': tool_parameters.get('includes', '').split(',') if tool_parameters.get('includes') else [],
'limit': tool_parameters.get('limit', 5)
},
'pageOptions': {
'onlyMainContent': tool_parameters.get('onlyMainContent', False)
}
}
wait_for_results = tool_parameters.get('wait_for_results', True)
crawlerOptions['excludes'] = get_array_params(tool_parameters, 'excludes')
crawlerOptions['includes'] = get_array_params(tool_parameters, 'includes')
crawlerOptions['returnOnlyUrls'] = tool_parameters.get('returnOnlyUrls', False)
crawlerOptions['maxDepth'] = tool_parameters.get('maxDepth')
crawlerOptions['mode'] = tool_parameters.get('mode')
crawlerOptions['ignoreSitemap'] = tool_parameters.get('ignoreSitemap', False)
crawlerOptions['limit'] = tool_parameters.get('limit', 5)
crawlerOptions['allowBackwardCrawling'] = tool_parameters.get('allowBackwardCrawling', False)
crawlerOptions['allowExternalContentLinks'] = tool_parameters.get('allowExternalContentLinks', False)
pageOptions['headers'] = get_json_params(tool_parameters, 'headers')
pageOptions['includeHtml'] = tool_parameters.get('includeHtml', False)
pageOptions['includeRawHtml'] = tool_parameters.get('includeRawHtml', False)
pageOptions['onlyIncludeTags'] = get_array_params(tool_parameters, 'onlyIncludeTags')
pageOptions['removeTags'] = get_array_params(tool_parameters, 'removeTags')
pageOptions['onlyMainContent'] = tool_parameters.get('onlyMainContent', False)
pageOptions['replaceAllPathsWithAbsolutePaths'] = tool_parameters.get('replaceAllPathsWithAbsolutePaths', False)
pageOptions['screenshot'] = tool_parameters.get('screenshot', False)
pageOptions['waitFor'] = tool_parameters.get('waitFor', 0)
crawl_result = app.crawl_url(
url=tool_parameters['url'],
params=options,
wait=True
url=tool_parameters['url'],
wait=wait_for_results,
crawlerOptions=crawlerOptions,
pageOptions=pageOptions
)
if not isinstance(crawl_result, str):
crawl_result = json.dumps(crawl_result, ensure_ascii=False, indent=4)
if not crawl_result:
return self.create_text_message("Crawl request failed.")
return self.create_text_message(crawl_result)
return self.create_json_message(crawl_result)

View File

@ -3,76 +3,243 @@ identity:
author: Richards Tu
label:
en_US: Crawl
zh_Hans: 爬取
zh_Hans: 深度爬取
description:
human:
en_US: Extract data from a website by crawling through a URL.
zh_Hans: 通过URL从网站中提取数据
en_US: Recursively search through a urls subdomains, and gather the content.
zh_Hans: 递归爬取一个网址的子域名,并收集内容
llm: This tool initiates a web crawl to extract data from a specified URL. It allows configuring crawler options such as including or excluding URL patterns, generating alt text for images using LLMs (paid plan required), limiting the maximum number of pages to crawl, and returning only the main content of the page. The tool can return either a list of crawled documents or a list of URLs based on the provided options.
parameters:
- name: url
type: string
required: true
label:
en_US: URL to crawl
zh_Hans: 要爬取的URL
en_US: Start URL
zh_Hans: 起始URL
human_description:
en_US: The URL of the website to crawl and extract data from.
zh_Hans: 要爬取并提取数据的网站URL。
en_US: The base URL to start crawling from.
zh_Hans: 要爬取网站的起始URL。
llm_description: The URL of the website that needs to be crawled. This is a required parameter.
form: llm
- name: wait_for_results
type: boolean
default: true
label:
en_US: Wait For Results
zh_Hans: 等待爬取结果
human_description:
en_US: If you choose not to wait, it will directly return a job ID. You can use this job ID to check the crawling results or cancel the crawling task, which is usually very useful for a large-scale crawling task.
zh_Hans: 如果选择不等待则会直接返回一个job_id可以通过job_id查询爬取结果或取消爬取任务这通常对于一个大型爬取任务来说非常有用。
form: form
############## Crawl Options #######################
- name: includes
type: string
required: false
label:
en_US: URL patterns to include
zh_Hans: 要包含的URL模式
placeholder:
en_US: Use commas to separate multiple tags
zh_Hans: 多个标签时使用半角逗号分隔
human_description:
en_US: Specify URL patterns to include during the crawl. Only pages matching these patterns will be crawled, you can use ',' to separate multiple patterns.
zh_Hans: 指定爬取过程中要包含的URL模式。只有与这些模式匹配的页面才会被爬取。
en_US: |
Only pages matching these patterns will be crawled. Example: blog/*, about/*
zh_Hans: 只有与这些模式匹配的页面才会被爬取。示例blog/*, about/*
form: form
default: ''
- name: excludes
type: string
required: false
label:
en_US: URL patterns to exclude
zh_Hans: 要排除的URL模式
placeholder:
en_US: Use commas to separate multiple tags
zh_Hans: 多个标签时使用半角逗号分隔
human_description:
en_US: Specify URL patterns to exclude during the crawl. Pages matching these patterns will be skipped, you can use ',' to separate multiple patterns.
zh_Hans: 指定爬取过程中要排除的URL模式。匹配这些模式的页面将被跳过。
en_US: |
Pages matching these patterns will be skipped. Example: blog/*, about/*
zh_Hans: 匹配这些模式的页面将被跳过。示例blog/*, about/*
form: form
- name: returnOnlyUrls
type: boolean
default: false
label:
en_US: return Only Urls
zh_Hans: 仅返回URL
human_description:
en_US: |
If true, returns only the URLs as a list on the crawl status. Attention: the return response will be a list of URLs inside the data, not a list of documents.
zh_Hans: 只返回爬取到的网页链接,而不是网页内容本身。
form: form
- name: maxDepth
type: number
label:
en_US: Maximum crawl depth
zh_Hans: 爬取深度
human_description:
en_US: Maximum depth to crawl relative to the entered URL. A maxDepth of 0 scrapes only the entered URL. A maxDepth of 1 scrapes the entered URL and all pages one level deep. A maxDepth of 2 scrapes the entered URL and all pages up to two levels deep. Higher values follow the same pattern.
zh_Hans: 相对于输入的URL爬取的最大深度。maxDepth为0时仅抓取输入的URL。maxDepth为1时抓取输入的URL以及所有一级深层页面。maxDepth为2时抓取输入的URL以及所有两级深层页面。更高值遵循相同模式。
form: form
min: 0
- name: mode
type: select
required: false
form: form
options:
- value: default
label:
en_US: default
- value: fast
label:
en_US: fast
default: default
label:
en_US: Crawl Mode
zh_Hans: 爬取模式
human_description:
en_US: The crawling mode to use. Fast mode crawls 4x faster websites without sitemap, but may not be as accurate and shouldn't be used in heavy js-rendered websites.
zh_Hans: 使用fast模式将不会使用其站点地图比普通模式快4倍但是可能不够准确也不适用于大量js渲染的网站。
- name: ignoreSitemap
type: boolean
default: false
label:
en_US: ignore Sitemap
zh_Hans: 忽略站点地图
human_description:
en_US: Ignore the website sitemap when crawling.
zh_Hans: 爬取时忽略网站站点地图。
form: form
default: 'blog/*'
- name: limit
type: number
required: false
label:
en_US: Maximum number of pages to crawl
en_US: Maximum pages to crawl
zh_Hans: 最大爬取页面数
human_description:
en_US: Specify the maximum number of pages to crawl. The crawler will stop after reaching this limit.
zh_Hans: 指定要爬取的最大页面数。爬虫将在达到此限制后停止。
form: form
min: 1
max: 20
default: 5
- name: allowBackwardCrawling
type: boolean
default: false
label:
en_US: allow Backward Crawling
zh_Hans: 允许向后爬取
human_description:
en_US: Enables the crawler to navigate from a specific URL to previously linked pages. For instance, from 'example.com/product/123' back to 'example.com/product'
zh_Hans: 使爬虫能够从特定URL导航到之前链接的页面。例如从'example.com/product/123'返回到'example.com/product'
form: form
- name: allowExternalContentLinks
type: boolean
default: false
label:
en_US: allow External Content Links
zh_Hans: 允许爬取外链
human_description:
en_US: Allows the crawler to follow links to external websites.
zh_Hans:
form: form
############## Page Options #######################
- name: headers
type: string
label:
en_US: headers
zh_Hans: 请求头
human_description:
en_US: |
Headers to send with the request. Can be used to send cookies, user-agent, etc. Example: {"cookies": "testcookies"}
zh_Hans: |
随请求发送的头部。可以用来发送cookies、用户代理等。示例{"cookies": "testcookies"}
placeholder:
en_US: Please enter an object that can be serialized in JSON
zh_Hans: 请输入可以json序列化的对象
form: form
- name: includeHtml
type: boolean
default: false
label:
en_US: include Html
zh_Hans: 包含HTML
human_description:
en_US: Include the HTML version of the content on page. Will output a html key in the response.
zh_Hans: 返回中包含一个HTML版本的内容将以html键返回。
form: form
- name: includeRawHtml
type: boolean
default: false
label:
en_US: include Raw Html
zh_Hans: 包含原始HTML
human_description:
en_US: Include the raw HTML content of the page. Will output a rawHtml key in the response.
zh_Hans: 返回中包含一个原始HTML版本的内容将以rawHtml键返回。
form: form
- name: onlyIncludeTags
type: string
label:
en_US: only Include Tags
zh_Hans: 仅抓取这些标签
placeholder:
en_US: Use commas to separate multiple tags
zh_Hans: 多个标签时使用半角逗号分隔
human_description:
en_US: |
Only include tags, classes and ids from the page in the final output. Use comma separated values. Example: script, .ad, #footer
zh_Hans: |
仅在最终输出中包含HTML页面的这些标签可以通过标签名、类或ID来设定使用逗号分隔值。示例script, .ad, #footer
form: form
- name: onlyMainContent
type: boolean
required: false
default: false
label:
en_US: Only return the main content of the page
zh_Hans: 仅返回页面的主要内容
en_US: only Main Content
zh_Hans: 抓取主要内容
human_description:
en_US: If enabled, the crawler will only return the main content of the page, excluding headers, navigation, footers, etc.
zh_Hans: 如果启用,爬虫将仅返回页面的主要内容,不包括标题、导航、页脚等。
en_US: Only return the main content of the page excluding headers, navs, footers, etc.
zh_Hans: 只返回页面的主要内容,不包括头部、导航栏、尾部等。
form: form
- name: removeTags
type: string
label:
en_US: remove Tags
zh_Hans: 要移除这些标签
human_description:
en_US: |
Tags, classes and ids to remove from the page. Use comma separated values. Example: script, .ad, #footer
zh_Hans: |
要在最终输出中移除HTML页面的这些标签可以通过标签名、类或ID来设定使用逗号分隔值。示例script, .ad, #footer
placeholder:
en_US: Use commas to separate multiple tags
zh_Hans: 多个标签时使用半角逗号分隔
form: form
- name: replaceAllPathsWithAbsolutePaths
type: boolean
default: false
label:
en_US: All AbsolutePaths
zh_Hans: 使用绝对路径
human_description:
en_US: Replace all relative paths with absolute paths for images and links.
zh_Hans: 将所有图片和链接的相对路径替换为绝对路径。
form: form
- name: screenshot
type: boolean
default: false
label:
en_US: screenshot
zh_Hans: 截图
human_description:
en_US: Include a screenshot of the top of the page that you are scraping.
zh_Hans: 提供正在抓取的页面的顶部的截图。
form: form
- name: waitFor
type: number
min: 0
label:
en_US: wait For
zh_Hans: 等待时间
human_description:
en_US: Wait x amount of milliseconds for the page to load to fetch content.
zh_Hans: 等待x毫秒以使页面加载并获取内容。
form: form
options:
- value: 'true'
label:
en_US: 'Yes'
zh_Hans:
- value: 'false'
label:
en_US: 'No'
zh_Hans:
default: 'false'

View File

@ -0,0 +1,20 @@
from typing import Any
from core.tools.entities.tool_entities import ToolInvokeMessage
from core.tools.provider.builtin.firecrawl.firecrawl_appx import FirecrawlApp
from core.tools.tool.builtin_tool import BuiltinTool
class CrawlJobTool(BuiltinTool):
def _invoke(self, user_id: str, tool_parameters: dict[str, Any]) -> ToolInvokeMessage:
app = FirecrawlApp(api_key=self.runtime.credentials['firecrawl_api_key'],
base_url=self.runtime.credentials['base_url'])
operation = tool_parameters.get('operation', 'get')
if operation == 'get':
result = app.check_crawl_status(job_id=tool_parameters['job_id'])
elif operation == 'cancel':
result = app.cancel_crawl_job(job_id=tool_parameters['job_id'])
else:
raise ValueError(f'Invalid operation: {operation}')
return self.create_json_message(result)

View File

@ -0,0 +1,37 @@
identity:
name: crawl_job
author: hjlarry
label:
en_US: Crawl Job
zh_Hans: 爬取任务处理
description:
human:
en_US: Retrieve the scraping results based on the job ID, or cancel the scraping task.
zh_Hans: 根据爬取任务ID获取爬取结果或者取消爬取任务
llm: Retrieve the scraping results based on the job ID, or cancel the scraping task.
parameters:
- name: job_id
type: string
required: true
label:
en_US: Job ID
human_description:
en_US: Set wait_for_results to false in the Crawl tool can get the job ID.
zh_Hans: 在深度爬取工具中将等待爬取结果设置为否可以获取Job ID。
llm_description: Set wait_for_results to false in the Crawl tool can get the job ID.
form: llm
- name: operation
type: select
required: true
options:
- value: get
label:
en_US: get crawl status
- value: cancel
label:
en_US: cancel crawl job
label:
en_US: operation
zh_Hans: 操作
llm_description: choose the operation to perform. `get` is for getting the crawl status, `cancel` is for cancelling the crawl job.
form: llm

View File

@ -1,26 +1,39 @@
import json
from typing import Any, Union
from typing import Any
from core.tools.entities.tool_entities import ToolInvokeMessage
from core.tools.provider.builtin.firecrawl.firecrawl_appx import FirecrawlApp
from core.tools.provider.builtin.firecrawl.firecrawl_appx import FirecrawlApp, get_array_params, get_json_params
from core.tools.tool.builtin_tool import BuiltinTool
class ScrapeTool(BuiltinTool):
def _invoke(self, user_id: str, tool_parameters: dict[str, Any]) -> Union[ToolInvokeMessage, list[ToolInvokeMessage]]:
app = FirecrawlApp(api_key=self.runtime.credentials['firecrawl_api_key'], base_url=self.runtime.credentials['base_url'])
crawl_result = app.scrape_url(
url=tool_parameters['url'],
wait=True
)
def _invoke(self, user_id: str, tool_parameters: dict[str, Any]) -> ToolInvokeMessage:
"""
the pageOptions and extractorOptions comes from doc here:
https://docs.firecrawl.dev/api-reference/endpoint/scrape
"""
app = FirecrawlApp(api_key=self.runtime.credentials['firecrawl_api_key'],
base_url=self.runtime.credentials['base_url'])
if isinstance(crawl_result, dict):
result_message = json.dumps(crawl_result, ensure_ascii=False, indent=4)
else:
result_message = str(crawl_result)
pageOptions = {}
extractorOptions = {}
if not crawl_result:
return self.create_text_message("Scrape request failed.")
pageOptions['headers'] = get_json_params(tool_parameters, 'headers')
pageOptions['includeHtml'] = tool_parameters.get('includeHtml', False)
pageOptions['includeRawHtml'] = tool_parameters.get('includeRawHtml', False)
pageOptions['onlyIncludeTags'] = get_array_params(tool_parameters, 'onlyIncludeTags')
pageOptions['removeTags'] = get_array_params(tool_parameters, 'removeTags')
pageOptions['onlyMainContent'] = tool_parameters.get('onlyMainContent', False)
pageOptions['replaceAllPathsWithAbsolutePaths'] = tool_parameters.get('replaceAllPathsWithAbsolutePaths', False)
pageOptions['screenshot'] = tool_parameters.get('screenshot', False)
pageOptions['waitFor'] = tool_parameters.get('waitFor', 0)
return self.create_text_message(result_message)
extractorOptions['mode'] = tool_parameters.get('mode', '')
extractorOptions['extractionPrompt'] = tool_parameters.get('extractionPrompt', '')
extractorOptions['extractionSchema'] = get_json_params(tool_parameters, 'extractionSchema')
crawl_result = app.scrape_url(url=tool_parameters['url'],
pageOptions=pageOptions,
extractorOptions=extractorOptions)
return self.create_json_message(crawl_result)

View File

@ -3,7 +3,7 @@ identity:
author: ahasasjeb
label:
en_US: Scrape
zh_Hans: 抓取
zh_Hans: 单页面抓取
description:
human:
en_US: Extract data from a single URL.
@ -21,3 +21,160 @@ parameters:
zh_Hans: 要抓取并提取数据的网站URL。
llm_description: The URL of the website that needs to be crawled. This is a required parameter.
form: llm
############## Page Options #######################
- name: headers
type: string
label:
en_US: headers
zh_Hans: 请求头
human_description:
en_US: |
Headers to send with the request. Can be used to send cookies, user-agent, etc. Example: {"cookies": "testcookies"}
zh_Hans: |
随请求发送的头部。可以用来发送cookies、用户代理等。示例{"cookies": "testcookies"}
placeholder:
en_US: Please enter an object that can be serialized in JSON
zh_Hans: 请输入可以json序列化的对象
form: form
- name: includeHtml
type: boolean
default: false
label:
en_US: include Html
zh_Hans: 包含HTML
human_description:
en_US: Include the HTML version of the content on page. Will output a html key in the response.
zh_Hans: 返回中包含一个HTML版本的内容将以html键返回。
form: form
- name: includeRawHtml
type: boolean
default: false
label:
en_US: include Raw Html
zh_Hans: 包含原始HTML
human_description:
en_US: Include the raw HTML content of the page. Will output a rawHtml key in the response.
zh_Hans: 返回中包含一个原始HTML版本的内容将以rawHtml键返回。
form: form
- name: onlyIncludeTags
type: string
label:
en_US: only Include Tags
zh_Hans: 仅抓取这些标签
placeholder:
en_US: Use commas to separate multiple tags
zh_Hans: 多个标签时使用半角逗号分隔
human_description:
en_US: |
Only include tags, classes and ids from the page in the final output. Use comma separated values. Example: script, .ad, #footer
zh_Hans: |
仅在最终输出中包含HTML页面的这些标签可以通过标签名、类或ID来设定使用逗号分隔值。示例script, .ad, #footer
form: form
- name: onlyMainContent
type: boolean
default: false
label:
en_US: only Main Content
zh_Hans: 仅抓取主要内容
human_description:
en_US: Only return the main content of the page excluding headers, navs, footers, etc.
zh_Hans: 只返回页面的主要内容,不包括头部、导航栏、尾部等。
form: form
- name: removeTags
type: string
label:
en_US: remove Tags
zh_Hans: 要移除这些标签
human_description:
en_US: |
Tags, classes and ids to remove from the page. Use comma separated values. Example: script, .ad, #footer
zh_Hans: |
要在最终输出中移除HTML页面的这些标签可以通过标签名、类或ID来设定使用逗号分隔值。示例script, .ad, #footer
placeholder:
en_US: Use commas to separate multiple tags
zh_Hans: 多个标签时使用半角逗号分隔
form: form
- name: replaceAllPathsWithAbsolutePaths
type: boolean
default: false
label:
en_US: All AbsolutePaths
zh_Hans: 使用绝对路径
human_description:
en_US: Replace all relative paths with absolute paths for images and links.
zh_Hans: 将所有图片和链接的相对路径替换为绝对路径。
form: form
- name: screenshot
type: boolean
default: false
label:
en_US: screenshot
zh_Hans: 截图
human_description:
en_US: Include a screenshot of the top of the page that you are scraping.
zh_Hans: 提供正在抓取的页面的顶部的截图。
form: form
- name: waitFor
type: number
min: 0
label:
en_US: wait For
zh_Hans: 等待时间
human_description:
en_US: Wait x amount of milliseconds for the page to load to fetch content.
zh_Hans: 等待x毫秒以使页面加载并获取内容。
form: form
############## Extractor Options #######################
- name: mode
type: select
options:
- value: markdown
label:
en_US: markdown
- value: llm-extraction
label:
en_US: llm-extraction
- value: llm-extraction-from-raw-html
label:
en_US: llm-extraction-from-raw-html
- value: llm-extraction-from-markdown
label:
en_US: llm-extraction-from-markdown
label:
en_US: Extractor Mode
zh_Hans: 提取模式
human_description:
en_US: |
The extraction mode to use. 'markdown': Returns the scraped markdown content, does not perform LLM extraction. 'llm-extraction': Extracts information from the cleaned and parsed content using LLM.
zh_Hans: 使用的提取模式。“markdown”返回抓取的markdown内容不执行LLM提取。“llm-extractioin”使用LLM按Extractor Schema从内容中提取信息。
form: form
- name: extractionPrompt
type: string
label:
en_US: Extractor Prompt
zh_Hans: 提取时的提示词
human_description:
en_US: A prompt describing what information to extract from the page, applicable for LLM extraction modes.
zh_Hans: 当使用LLM提取模式时用于给LLM描述提取规则。
form: form
- name: extractionSchema
type: string
label:
en_US: Extractor Schema
zh_Hans: 提取时的结构
placeholder:
en_US: Please enter an object that can be serialized in JSON
human_description:
en_US: |
The schema for the data to be extracted, required only for LLM extraction modes. Example: {
"type": "object",
"properties": {"company_mission": {"type": "string"}},
"required": ["company_mission"]
}
zh_Hans: |
当使用LLM提取模式时使用该结构去提取示例{
"type": "object",
"properties": {"company_mission": {"type": "string"}},
"required": ["company_mission"]
}
form: form

View File

@ -1,5 +1,4 @@
import json
from typing import Any, Union
from typing import Any
from core.tools.entities.tool_entities import ToolInvokeMessage
from core.tools.provider.builtin.firecrawl.firecrawl_appx import FirecrawlApp
@ -7,20 +6,23 @@ from core.tools.tool.builtin_tool import BuiltinTool
class SearchTool(BuiltinTool):
def _invoke(self, user_id: str, tool_parameters: dict[str, Any]) -> Union[ToolInvokeMessage, list[ToolInvokeMessage]]:
app = FirecrawlApp(api_key=self.runtime.credentials['firecrawl_api_key'], base_url=self.runtime.credentials['base_url'])
crawl_result = app.search(
def _invoke(self, user_id: str, tool_parameters: dict[str, Any]) -> ToolInvokeMessage:
"""
the pageOptions and searchOptions comes from doc here:
https://docs.firecrawl.dev/api-reference/endpoint/search
"""
app = FirecrawlApp(api_key=self.runtime.credentials['firecrawl_api_key'],
base_url=self.runtime.credentials['base_url'])
pageOptions = {}
pageOptions['onlyMainContent'] = tool_parameters.get('onlyMainContent', False)
pageOptions['fetchPageContent'] = tool_parameters.get('fetchPageContent', True)
pageOptions['includeHtml'] = tool_parameters.get('includeHtml', False)
pageOptions['includeRawHtml'] = tool_parameters.get('includeRawHtml', False)
searchOptions = {'limit': tool_parameters.get('limit')}
search_result = app.search(
query=tool_parameters['keyword'],
wait=True
pageOptions=pageOptions,
searchOptions=searchOptions
)
if isinstance(crawl_result, dict):
result_message = json.dumps(crawl_result, ensure_ascii=False, indent=4)
else:
result_message = str(crawl_result)
if not crawl_result:
return self.create_text_message("Search request failed.")
return self.create_text_message(result_message)
return self.create_json_message(search_result)

View File

@ -21,3 +21,55 @@ parameters:
zh_Hans: 输入关键词即可使用Firecrawl API进行搜索。
llm_description: Efficiently extract keywords from user text.
form: llm
############## Page Options #######################
- name: onlyMainContent
type: boolean
default: false
label:
en_US: only Main Content
zh_Hans: 仅抓取主要内容
human_description:
en_US: Only return the main content of the page excluding headers, navs, footers, etc.
zh_Hans: 只返回页面的主要内容,不包括头部、导航栏、尾部等。
form: form
- name: fetchPageContent
type: boolean
default: true
label:
en_US: fetch Page Content
zh_Hans: 抓取页面内容
human_description:
en_US: Fetch the content of each page. If false, defaults to a basic fast serp API.
zh_Hans: 获取每个页面的内容。如果为否则使用基本的快速搜索结果页面API。
form: form
- name: includeHtml
type: boolean
default: false
label:
en_US: include Html
zh_Hans: 包含HTML
human_description:
en_US: Include the HTML version of the content on page. Will output a html key in the response.
zh_Hans: 返回中包含一个HTML版本的内容将以html键返回。
form: form
- name: includeRawHtml
type: boolean
default: false
label:
en_US: include Raw Html
zh_Hans: 包含原始HTML
human_description:
en_US: Include the raw HTML content of the page. Will output a rawHtml key in the response.
zh_Hans: 返回中包含一个原始HTML版本的内容将以rawHtml键返回。
form: form
############## Search Options #######################
- name: limit
type: number
min: 0
label:
en_US: Maximum results
zh_Hans: 最大结果数量
human_description:
en_US: Maximum number of results. Max is 20 during beta.
zh_Hans: 最大结果数量。在测试阶段最大为20。
form: form

View File

@ -60,11 +60,13 @@ class JinaReaderTool(BuiltinTool):
if tool_parameters.get('no_cache', False):
headers['X-No-Cache'] = 'true'
max_retries = tool_parameters.get('max_retries', 3)
response = ssrf_proxy.get(
str(URL(self._jina_reader_endpoint + url)),
headers=headers,
params=request_params,
timeout=(10, 60),
max_retries=max_retries
)
if tool_parameters.get('summary', False):

View File

@ -150,3 +150,17 @@ parameters:
pt_BR: Habilitar resumo para a saída
llm_description: enable summary
form: form
- name: max_retries
type: number
required: false
default: 3
label:
en_US: Retry
zh_Hans: 重试
pt_BR: Repetir
human_description:
en_US: Number of times to retry the request if it fails
zh_Hans: 请求失败时重试的次数
pt_BR: Número de vezes para repetir a solicitação se falhar
llm_description: Number of times to retry the request if it fails
form: form

Some files were not shown because too many files have changed in this diff Show More