mirror of
https://git.mirrors.martin98.com/https://github.com/langgenius/dify.git
synced 2025-08-20 00:49:05 +08:00
Merge branch 'main' into feat/attachments
This commit is contained in:
commit
0451c5590c
@ -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
|
||||
|
||||
|
@ -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) にコントリビューターとして紹介されます。
|
||||
|
||||
## ヘルプを得る
|
||||
|
||||
|
@ -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=
|
||||
|
@ -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)
|
||||
|
@ -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:
|
||||
|
@ -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',
|
||||
]
|
||||
|
@ -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}')
|
||||
|
@ -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)
|
||||
|
@ -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]
|
||||
|
@ -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
|
||||
|
||||
|
||||
@ -57,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]
|
||||
|
@ -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'
|
||||
|
@ -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,59 +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)
|
||||
|
||||
def to_object(self):
|
||||
return {k: v.to_object() for k, v in self.value.items()}
|
||||
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])
|
||||
|
||||
def to_object(self):
|
||||
return [v.to_object() for v 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):
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -34,3 +34,8 @@ parameter_rules:
|
||||
min: -2
|
||||
max: 2
|
||||
default: 0
|
||||
pricing:
|
||||
input: '0.0027'
|
||||
output: '0.0027'
|
||||
unit: '0.0001'
|
||||
currency: USD
|
||||
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -34,3 +34,8 @@ parameter_rules:
|
||||
min: -2
|
||||
max: 2
|
||||
default: 0
|
||||
pricing:
|
||||
input: '0.0051'
|
||||
output: '0.0074'
|
||||
unit: '0.0001'
|
||||
currency: USD
|
||||
|
@ -34,3 +34,8 @@ parameter_rules:
|
||||
min: -2
|
||||
max: 2
|
||||
default: 0
|
||||
pricing:
|
||||
input: '0.00063'
|
||||
output: '0.00063'
|
||||
unit: '0.0001'
|
||||
currency: USD
|
||||
|
@ -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
|
@ -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
|
@ -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
|
@ -34,3 +34,8 @@ parameter_rules:
|
||||
min: -2
|
||||
max: 2
|
||||
default: 0
|
||||
pricing:
|
||||
input: '0.0058'
|
||||
output: '0.0078'
|
||||
unit: '0.0001'
|
||||
currency: USD
|
||||
|
@ -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
|
@ -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
|
@ -34,3 +34,8 @@ parameter_rules:
|
||||
min: -2
|
||||
max: 2
|
||||
default: 0
|
||||
pricing:
|
||||
input: '0.00119'
|
||||
output: '0.00119'
|
||||
unit: '0.0001'
|
||||
currency: USD
|
||||
|
@ -34,3 +34,8 @@ parameter_rules:
|
||||
min: -2
|
||||
max: 2
|
||||
default: 0
|
||||
pricing:
|
||||
input: '0.0017'
|
||||
output: '0.0017'
|
||||
unit: '0.0001'
|
||||
currency: USD
|
||||
|
@ -34,3 +34,8 @@ parameter_rules:
|
||||
min: -2
|
||||
max: 2
|
||||
default: 0
|
||||
pricing:
|
||||
input: '0.0017'
|
||||
output: '0.0017'
|
||||
unit: '0.0001'
|
||||
currency: USD
|
||||
|
@ -34,3 +34,8 @@ parameter_rules:
|
||||
min: -2
|
||||
max: 2
|
||||
default: 0
|
||||
pricing:
|
||||
input: '0.0064'
|
||||
output: '0.0064'
|
||||
unit: '0.0001'
|
||||
currency: USD
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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({
|
||||
|
@ -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
|
||||
@ -76,7 +77,7 @@ 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 = int(dify_config.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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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'
|
||||
|
20
api/core/tools/provider/builtin/firecrawl/tools/crawl_job.py
Normal file
20
api/core/tools/provider/builtin/firecrawl/tools/crawl_job.py
Normal 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)
|
@ -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
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -4,7 +4,7 @@ from typing import Any, Union
|
||||
|
||||
from typing_extensions import deprecated
|
||||
|
||||
from core.app.segments import Variable, factory
|
||||
from core.app.segments import Segment, Variable, factory
|
||||
from core.file.file_obj import FileVar
|
||||
from core.workflow.entities.node_entities import SystemVariable
|
||||
|
||||
@ -33,7 +33,7 @@ class VariablePool:
|
||||
# The first element of the selector is the node id, it's the first-level key in the dictionary.
|
||||
# Other elements of the selector are the keys in the second-level dictionary. To get the key, we hash the
|
||||
# elements of the selector except the first one.
|
||||
self._variable_dictionary: dict[str, dict[int, Variable]] = defaultdict(dict)
|
||||
self._variable_dictionary: dict[str, dict[int, Segment]] = defaultdict(dict)
|
||||
|
||||
# TODO: This user inputs is not used for pool.
|
||||
self.user_inputs = user_inputs
|
||||
@ -67,15 +67,15 @@ class VariablePool:
|
||||
if value is None:
|
||||
return
|
||||
|
||||
if not isinstance(value, Variable):
|
||||
v = factory.build_anonymous_variable(value)
|
||||
else:
|
||||
if isinstance(value, Segment):
|
||||
v = value
|
||||
else:
|
||||
v = factory.build_segment(value)
|
||||
|
||||
hash_key = hash(tuple(selector[1:]))
|
||||
self._variable_dictionary[selector[0]][hash_key] = v
|
||||
|
||||
def get(self, selector: Sequence[str], /) -> Variable | None:
|
||||
def get(self, selector: Sequence[str], /) -> Segment | None:
|
||||
"""
|
||||
Retrieves the value from the variable pool based on the given selector.
|
||||
|
||||
|
@ -115,7 +115,7 @@ class BaseNode(ABC):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def extract_variable_selector_to_variable_mapping(cls, config: dict) -> dict[str, list[str]]:
|
||||
def extract_variable_selector_to_variable_mapping(cls, config: dict):
|
||||
"""
|
||||
Extract variable selector to variable mapping
|
||||
:param config: node config
|
||||
@ -125,14 +125,13 @@ class BaseNode(ABC):
|
||||
return cls._extract_variable_selector_to_variable_mapping(node_data)
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def _extract_variable_selector_to_variable_mapping(cls, node_data: BaseNodeData) -> dict[str, list[str]]:
|
||||
def _extract_variable_selector_to_variable_mapping(cls, node_data: BaseNodeData) -> Mapping[str, Sequence[str]]:
|
||||
"""
|
||||
Extract variable selector to variable mapping
|
||||
:param node_data: node data
|
||||
:return:
|
||||
"""
|
||||
raise NotImplementedError
|
||||
return {}
|
||||
|
||||
@classmethod
|
||||
def get_default_config(cls, filters: Optional[dict] = None) -> dict:
|
||||
|
@ -126,6 +126,7 @@ class ToolNode(BaseNode):
|
||||
else:
|
||||
tool_input = node_data.tool_parameters[parameter_name]
|
||||
if tool_input.type == 'variable':
|
||||
# TODO: check if the variable exists in the variable pool
|
||||
parameter_value = variable_pool.get(tool_input.value).value
|
||||
else:
|
||||
segment_group = parser.convert_template(
|
||||
|
@ -389,11 +389,10 @@ class WorkflowEngineManager:
|
||||
environment_variables=workflow.environment_variables,
|
||||
)
|
||||
|
||||
if node_cls is None:
|
||||
raise ValueError('Node class not found')
|
||||
# variable selector to variable mapping
|
||||
try:
|
||||
variable_mapping = node_cls.extract_variable_selector_to_variable_mapping(node_config)
|
||||
except NotImplementedError:
|
||||
variable_mapping = {}
|
||||
variable_mapping = node_cls.extract_variable_selector_to_variable_mapping(node_config)
|
||||
|
||||
self._mapping_user_inputs_to_variable_pool(
|
||||
variable_mapping=variable_mapping,
|
||||
@ -473,10 +472,9 @@ class WorkflowEngineManager:
|
||||
for node_config in iteration_nested_nodes:
|
||||
# mapping user inputs to variable pool
|
||||
node_cls = node_classes.get(NodeType.value_of(node_config.get('data', {}).get('type')))
|
||||
try:
|
||||
variable_mapping = node_cls.extract_variable_selector_to_variable_mapping(node_config)
|
||||
except NotImplementedError:
|
||||
variable_mapping = {}
|
||||
if node_cls is None:
|
||||
raise ValueError('Node class not found')
|
||||
variable_mapping = node_cls.extract_variable_selector_to_variable_mapping(node_config)
|
||||
|
||||
# remove iteration variables
|
||||
variable_mapping = {
|
||||
@ -942,7 +940,7 @@ class WorkflowEngineManager:
|
||||
return new_value
|
||||
|
||||
def _mapping_user_inputs_to_variable_pool(self,
|
||||
variable_mapping: dict,
|
||||
variable_mapping: Mapping[str, Sequence[str]],
|
||||
user_inputs: dict,
|
||||
variable_pool: VariablePool,
|
||||
tenant_id: str,
|
||||
|
@ -0,0 +1,53 @@
|
||||
"""increase max model_name length
|
||||
|
||||
Revision ID: eeb2e349e6ac
|
||||
Revises: 53bf8af60645
|
||||
Create Date: 2024-07-26 12:02:00.750358
|
||||
|
||||
"""
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
import models as models
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'eeb2e349e6ac'
|
||||
down_revision = '53bf8af60645'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('dataset_collection_bindings', schema=None) as batch_op:
|
||||
batch_op.alter_column('model_name',
|
||||
existing_type=sa.VARCHAR(length=40),
|
||||
type_=sa.String(length=255),
|
||||
existing_nullable=False)
|
||||
|
||||
with op.batch_alter_table('embeddings', schema=None) as batch_op:
|
||||
batch_op.alter_column('model_name',
|
||||
existing_type=sa.VARCHAR(length=40),
|
||||
type_=sa.String(length=255),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text("'text-embedding-ada-002'::character varying"))
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('embeddings', schema=None) as batch_op:
|
||||
batch_op.alter_column('model_name',
|
||||
existing_type=sa.String(length=255),
|
||||
type_=sa.VARCHAR(length=40),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text("'text-embedding-ada-002'::character varying"))
|
||||
|
||||
with op.batch_alter_table('dataset_collection_bindings', schema=None) as batch_op:
|
||||
batch_op.alter_column('model_name',
|
||||
existing_type=sa.String(length=255),
|
||||
type_=sa.VARCHAR(length=40),
|
||||
existing_nullable=False)
|
||||
|
||||
# ### end Alembic commands ###
|
@ -635,7 +635,7 @@ class Embedding(db.Model):
|
||||
)
|
||||
|
||||
id = db.Column(StringUUID, primary_key=True, server_default=db.text('uuid_generate_v4()'))
|
||||
model_name = db.Column(db.String(40), nullable=False,
|
||||
model_name = db.Column(db.String(255), nullable=False,
|
||||
server_default=db.text("'text-embedding-ada-002'::character varying"))
|
||||
hash = db.Column(db.String(64), nullable=False)
|
||||
embedding = db.Column(db.LargeBinary, nullable=False)
|
||||
@ -660,7 +660,7 @@ class DatasetCollectionBinding(db.Model):
|
||||
|
||||
id = db.Column(StringUUID, primary_key=True, server_default=db.text('uuid_generate_v4()'))
|
||||
provider_name = db.Column(db.String(40), nullable=False)
|
||||
model_name = db.Column(db.String(40), nullable=False)
|
||||
model_name = db.Column(db.String(255), nullable=False)
|
||||
type = db.Column(db.String(40), server_default=db.text("'dataset'::character varying"), nullable=False)
|
||||
collection_name = db.Column(db.String(64), nullable=False)
|
||||
created_at = db.Column(db.DateTime, nullable=False, server_default=db.text('CURRENT_TIMESTAMP(0)'))
|
||||
|
2
api/poetry.lock
generated
2
api/poetry.lock
generated
@ -9543,4 +9543,4 @@ cffi = ["cffi (>=1.11)"]
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.10"
|
||||
content-hash = "6b7d8b1333ae9c71ba2e1c5800eecf1535ed3945cd55ebb1e253b7a29ba09559"
|
||||
content-hash = "9619ddabdd67710981c13dcfa3ddae0a48497c9f694afc81b820e882440c1265"
|
||||
|
@ -177,6 +177,7 @@ xinference-client = "0.9.4"
|
||||
yarl = "~1.9.4"
|
||||
zhipuai = "1.0.7"
|
||||
rank-bm25 = "~0.2.2"
|
||||
openpyxl = "^3.1.5"
|
||||
############################################################
|
||||
# Tool dependencies required by tool implementations
|
||||
############################################################
|
||||
|
307
api/tests/unit_tests/core/app/segments/test_factory.py
Normal file
307
api/tests/unit_tests/core/app/segments/test_factory.py
Normal file
@ -0,0 +1,307 @@
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
|
||||
from core.app.segments import (
|
||||
ArrayFileVariable,
|
||||
ArrayNumberVariable,
|
||||
ArrayObjectVariable,
|
||||
ArrayStringVariable,
|
||||
FileVariable,
|
||||
FloatVariable,
|
||||
IntegerVariable,
|
||||
NoneSegment,
|
||||
ObjectSegment,
|
||||
SecretVariable,
|
||||
StringVariable,
|
||||
factory,
|
||||
)
|
||||
|
||||
|
||||
def test_string_variable():
|
||||
test_data = {'value_type': 'string', 'name': 'test_text', 'value': 'Hello, World!'}
|
||||
result = factory.build_variable_from_mapping(test_data)
|
||||
assert isinstance(result, StringVariable)
|
||||
|
||||
|
||||
def test_integer_variable():
|
||||
test_data = {'value_type': 'number', 'name': 'test_int', 'value': 42}
|
||||
result = factory.build_variable_from_mapping(test_data)
|
||||
assert isinstance(result, IntegerVariable)
|
||||
|
||||
|
||||
def test_float_variable():
|
||||
test_data = {'value_type': 'number', 'name': 'test_float', 'value': 3.14}
|
||||
result = factory.build_variable_from_mapping(test_data)
|
||||
assert isinstance(result, FloatVariable)
|
||||
|
||||
|
||||
def test_secret_variable():
|
||||
test_data = {'value_type': 'secret', 'name': 'test_secret', 'value': 'secret_value'}
|
||||
result = factory.build_variable_from_mapping(test_data)
|
||||
assert isinstance(result, SecretVariable)
|
||||
|
||||
|
||||
def test_invalid_value_type():
|
||||
test_data = {'value_type': 'unknown', 'name': 'test_invalid', 'value': 'value'}
|
||||
with pytest.raises(ValueError):
|
||||
factory.build_variable_from_mapping(test_data)
|
||||
|
||||
|
||||
def test_build_a_blank_string():
|
||||
result = factory.build_variable_from_mapping(
|
||||
{
|
||||
'value_type': 'string',
|
||||
'name': 'blank',
|
||||
'value': '',
|
||||
}
|
||||
)
|
||||
assert isinstance(result, StringVariable)
|
||||
assert result.value == ''
|
||||
|
||||
|
||||
def test_build_a_object_variable_with_none_value():
|
||||
var = factory.build_segment(
|
||||
{
|
||||
'key1': None,
|
||||
}
|
||||
)
|
||||
assert isinstance(var, ObjectSegment)
|
||||
assert isinstance(var.value['key1'], NoneSegment)
|
||||
|
||||
|
||||
def test_object_variable():
|
||||
mapping = {
|
||||
'id': str(uuid4()),
|
||||
'value_type': 'object',
|
||||
'name': 'test_object',
|
||||
'description': 'Description of the variable.',
|
||||
'value': {
|
||||
'key1': {
|
||||
'id': str(uuid4()),
|
||||
'value_type': 'string',
|
||||
'name': 'text',
|
||||
'value': 'text',
|
||||
'description': 'Description of the variable.',
|
||||
},
|
||||
'key2': {
|
||||
'id': str(uuid4()),
|
||||
'value_type': 'number',
|
||||
'name': 'number',
|
||||
'value': 1,
|
||||
'description': 'Description of the variable.',
|
||||
},
|
||||
},
|
||||
}
|
||||
variable = factory.build_variable_from_mapping(mapping)
|
||||
assert isinstance(variable, ObjectSegment)
|
||||
assert isinstance(variable.value['key1'], StringVariable)
|
||||
assert isinstance(variable.value['key2'], IntegerVariable)
|
||||
|
||||
|
||||
def test_array_string_variable():
|
||||
mapping = {
|
||||
'id': str(uuid4()),
|
||||
'value_type': 'array[string]',
|
||||
'name': 'test_array',
|
||||
'description': 'Description of the variable.',
|
||||
'value': [
|
||||
{
|
||||
'id': str(uuid4()),
|
||||
'value_type': 'string',
|
||||
'name': 'text',
|
||||
'value': 'text',
|
||||
'description': 'Description of the variable.',
|
||||
},
|
||||
{
|
||||
'id': str(uuid4()),
|
||||
'value_type': 'string',
|
||||
'name': 'text',
|
||||
'value': 'text',
|
||||
'description': 'Description of the variable.',
|
||||
},
|
||||
],
|
||||
}
|
||||
variable = factory.build_variable_from_mapping(mapping)
|
||||
assert isinstance(variable, ArrayStringVariable)
|
||||
assert isinstance(variable.value[0], StringVariable)
|
||||
assert isinstance(variable.value[1], StringVariable)
|
||||
|
||||
|
||||
def test_array_number_variable():
|
||||
mapping = {
|
||||
'id': str(uuid4()),
|
||||
'value_type': 'array[number]',
|
||||
'name': 'test_array',
|
||||
'description': 'Description of the variable.',
|
||||
'value': [
|
||||
{
|
||||
'id': str(uuid4()),
|
||||
'value_type': 'number',
|
||||
'name': 'number',
|
||||
'value': 1,
|
||||
'description': 'Description of the variable.',
|
||||
},
|
||||
{
|
||||
'id': str(uuid4()),
|
||||
'value_type': 'number',
|
||||
'name': 'number',
|
||||
'value': 2.0,
|
||||
'description': 'Description of the variable.',
|
||||
},
|
||||
],
|
||||
}
|
||||
variable = factory.build_variable_from_mapping(mapping)
|
||||
assert isinstance(variable, ArrayNumberVariable)
|
||||
assert isinstance(variable.value[0], IntegerVariable)
|
||||
assert isinstance(variable.value[1], FloatVariable)
|
||||
|
||||
|
||||
def test_array_object_variable():
|
||||
mapping = {
|
||||
'id': str(uuid4()),
|
||||
'value_type': 'array[object]',
|
||||
'name': 'test_array',
|
||||
'description': 'Description of the variable.',
|
||||
'value': [
|
||||
{
|
||||
'id': str(uuid4()),
|
||||
'value_type': 'object',
|
||||
'name': 'object',
|
||||
'description': 'Description of the variable.',
|
||||
'value': {
|
||||
'key1': {
|
||||
'id': str(uuid4()),
|
||||
'value_type': 'string',
|
||||
'name': 'text',
|
||||
'value': 'text',
|
||||
'description': 'Description of the variable.',
|
||||
},
|
||||
'key2': {
|
||||
'id': str(uuid4()),
|
||||
'value_type': 'number',
|
||||
'name': 'number',
|
||||
'value': 1,
|
||||
'description': 'Description of the variable.',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
'id': str(uuid4()),
|
||||
'value_type': 'object',
|
||||
'name': 'object',
|
||||
'description': 'Description of the variable.',
|
||||
'value': {
|
||||
'key1': {
|
||||
'id': str(uuid4()),
|
||||
'value_type': 'string',
|
||||
'name': 'text',
|
||||
'value': 'text',
|
||||
'description': 'Description of the variable.',
|
||||
},
|
||||
'key2': {
|
||||
'id': str(uuid4()),
|
||||
'value_type': 'number',
|
||||
'name': 'number',
|
||||
'value': 1,
|
||||
'description': 'Description of the variable.',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
variable = factory.build_variable_from_mapping(mapping)
|
||||
assert isinstance(variable, ArrayObjectVariable)
|
||||
assert isinstance(variable.value[0], ObjectSegment)
|
||||
assert isinstance(variable.value[1], ObjectSegment)
|
||||
assert isinstance(variable.value[0].value['key1'], StringVariable)
|
||||
assert isinstance(variable.value[0].value['key2'], IntegerVariable)
|
||||
assert isinstance(variable.value[1].value['key1'], StringVariable)
|
||||
assert isinstance(variable.value[1].value['key2'], IntegerVariable)
|
||||
|
||||
|
||||
def test_file_variable():
|
||||
mapping = {
|
||||
'id': str(uuid4()),
|
||||
'value_type': 'file',
|
||||
'name': 'test_file',
|
||||
'description': 'Description of the variable.',
|
||||
'value': {
|
||||
'id': str(uuid4()),
|
||||
'tenant_id': 'tenant_id',
|
||||
'type': 'image',
|
||||
'transfer_method': 'local_file',
|
||||
'url': 'url',
|
||||
'related_id': 'related_id',
|
||||
'extra_config': {
|
||||
'image_config': {
|
||||
'width': 100,
|
||||
'height': 100,
|
||||
},
|
||||
},
|
||||
'filename': 'filename',
|
||||
'extension': 'extension',
|
||||
'mime_type': 'mime_type',
|
||||
},
|
||||
}
|
||||
variable = factory.build_variable_from_mapping(mapping)
|
||||
assert isinstance(variable, FileVariable)
|
||||
|
||||
|
||||
def test_array_file_variable():
|
||||
mapping = {
|
||||
'id': str(uuid4()),
|
||||
'value_type': 'array[file]',
|
||||
'name': 'test_array_file',
|
||||
'description': 'Description of the variable.',
|
||||
'value': [
|
||||
{
|
||||
'id': str(uuid4()),
|
||||
'name': 'file',
|
||||
'value_type': 'file',
|
||||
'value': {
|
||||
'id': str(uuid4()),
|
||||
'tenant_id': 'tenant_id',
|
||||
'type': 'image',
|
||||
'transfer_method': 'local_file',
|
||||
'url': 'url',
|
||||
'related_id': 'related_id',
|
||||
'extra_config': {
|
||||
'image_config': {
|
||||
'width': 100,
|
||||
'height': 100,
|
||||
},
|
||||
},
|
||||
'filename': 'filename',
|
||||
'extension': 'extension',
|
||||
'mime_type': 'mime_type',
|
||||
},
|
||||
},
|
||||
{
|
||||
'id': str(uuid4()),
|
||||
'name': 'file',
|
||||
'value_type': 'file',
|
||||
'value': {
|
||||
'id': str(uuid4()),
|
||||
'tenant_id': 'tenant_id',
|
||||
'type': 'image',
|
||||
'transfer_method': 'local_file',
|
||||
'url': 'url',
|
||||
'related_id': 'related_id',
|
||||
'extra_config': {
|
||||
'image_config': {
|
||||
'width': 100,
|
||||
'height': 100,
|
||||
},
|
||||
},
|
||||
'filename': 'filename',
|
||||
'extension': 'extension',
|
||||
'mime_type': 'mime_type',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
variable = factory.build_variable_from_mapping(mapping)
|
||||
assert isinstance(variable, ArrayFileVariable)
|
||||
assert isinstance(variable.value[0], FileVariable)
|
||||
assert isinstance(variable.value[1], FileVariable)
|
@ -1,4 +1,4 @@
|
||||
from core.app.segments import SecretVariable, parser
|
||||
from core.app.segments import SecretVariable, StringSegment, parser
|
||||
from core.helper import encrypter
|
||||
from core.workflow.entities.node_entities import SystemVariable
|
||||
from core.workflow.entities.variable_pool import VariablePool
|
||||
@ -51,3 +51,4 @@ def test_convert_variable_to_segment_group():
|
||||
segments_group = parser.convert_template(template=template, variable_pool=variable_pool)
|
||||
assert segments_group.text == 'fake-user-id'
|
||||
assert segments_group.log == 'fake-user-id'
|
||||
assert segments_group.value == [StringSegment(value='fake-user-id')]
|
@ -2,48 +2,16 @@ import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from core.app.segments import (
|
||||
ArrayVariable,
|
||||
ArrayAnyVariable,
|
||||
FloatVariable,
|
||||
IntegerVariable,
|
||||
NoneVariable,
|
||||
ObjectVariable,
|
||||
SecretVariable,
|
||||
SegmentType,
|
||||
StringVariable,
|
||||
factory,
|
||||
)
|
||||
|
||||
|
||||
def test_string_variable():
|
||||
test_data = {'value_type': 'string', 'name': 'test_text', 'value': 'Hello, World!'}
|
||||
result = factory.build_variable_from_mapping(test_data)
|
||||
assert isinstance(result, StringVariable)
|
||||
|
||||
|
||||
def test_integer_variable():
|
||||
test_data = {'value_type': 'number', 'name': 'test_int', 'value': 42}
|
||||
result = factory.build_variable_from_mapping(test_data)
|
||||
assert isinstance(result, IntegerVariable)
|
||||
|
||||
|
||||
def test_float_variable():
|
||||
test_data = {'value_type': 'number', 'name': 'test_float', 'value': 3.14}
|
||||
result = factory.build_variable_from_mapping(test_data)
|
||||
assert isinstance(result, FloatVariable)
|
||||
|
||||
|
||||
def test_secret_variable():
|
||||
test_data = {'value_type': 'secret', 'name': 'test_secret', 'value': 'secret_value'}
|
||||
result = factory.build_variable_from_mapping(test_data)
|
||||
assert isinstance(result, SecretVariable)
|
||||
|
||||
|
||||
def test_invalid_value_type():
|
||||
test_data = {'value_type': 'unknown', 'name': 'test_invalid', 'value': 'value'}
|
||||
with pytest.raises(ValueError):
|
||||
factory.build_variable_from_mapping(test_data)
|
||||
|
||||
|
||||
def test_frozen_variables():
|
||||
var = StringVariable(name='text', value='text')
|
||||
with pytest.raises(ValidationError):
|
||||
@ -64,34 +32,22 @@ def test_frozen_variables():
|
||||
|
||||
def test_variable_value_type_immutable():
|
||||
with pytest.raises(ValidationError):
|
||||
StringVariable(value_type=SegmentType.ARRAY, name='text', value='text')
|
||||
StringVariable(value_type=SegmentType.ARRAY_ANY, name='text', value='text')
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
StringVariable.model_validate({'value_type': 'not text', 'name': 'text', 'value': 'text'})
|
||||
|
||||
var = IntegerVariable(name='integer', value=42)
|
||||
with pytest.raises(ValidationError):
|
||||
IntegerVariable(value_type=SegmentType.ARRAY, name=var.name, value=var.value)
|
||||
IntegerVariable(value_type=SegmentType.ARRAY_ANY, name=var.name, value=var.value)
|
||||
|
||||
var = FloatVariable(name='float', value=3.14)
|
||||
with pytest.raises(ValidationError):
|
||||
FloatVariable(value_type=SegmentType.ARRAY, name=var.name, value=var.value)
|
||||
FloatVariable(value_type=SegmentType.ARRAY_ANY, name=var.name, value=var.value)
|
||||
|
||||
var = SecretVariable(name='secret', value='secret_value')
|
||||
with pytest.raises(ValidationError):
|
||||
SecretVariable(value_type=SegmentType.ARRAY, name=var.name, value=var.value)
|
||||
|
||||
|
||||
def test_build_a_blank_string():
|
||||
result = factory.build_variable_from_mapping(
|
||||
{
|
||||
'value_type': 'string',
|
||||
'name': 'blank',
|
||||
'value': '',
|
||||
}
|
||||
)
|
||||
assert isinstance(result, StringVariable)
|
||||
assert result.value == ''
|
||||
SecretVariable(value_type=SegmentType.ARRAY_ANY, name=var.name, value=var.value)
|
||||
|
||||
|
||||
def test_object_variable_to_object():
|
||||
@ -104,7 +60,7 @@ def test_object_variable_to_object():
|
||||
'key2': StringVariable(name='key2', value='value2'),
|
||||
},
|
||||
),
|
||||
'key2': ArrayVariable(
|
||||
'key2': ArrayAnyVariable(
|
||||
name='array',
|
||||
value=[
|
||||
StringVariable(name='key5_1', value='value5_1'),
|
||||
@ -136,13 +92,3 @@ def test_variable_to_object():
|
||||
assert var.to_object() == 3.14
|
||||
var = SecretVariable(name='secret', value='secret_value')
|
||||
assert var.to_object() == 'secret_value'
|
||||
|
||||
|
||||
def test_build_a_object_variable_with_none_value():
|
||||
var = factory.build_anonymous_variable(
|
||||
{
|
||||
'key1': None,
|
||||
}
|
||||
)
|
||||
assert isinstance(var, ObjectVariable)
|
||||
assert isinstance(var.value['key1'], NoneVariable)
|
@ -166,8 +166,8 @@ const ExtraInfo = ({ isMobile, relatedApps }: IExtraInfoProps) => {
|
||||
className='inline-flex items-center text-xs text-primary-600 mt-2 cursor-pointer'
|
||||
href={
|
||||
locale === LanguagesSupported[1]
|
||||
? 'https://docs.dify.ai/v/zh-hans/guides/application-design/prompt-engineering'
|
||||
: 'https://docs.dify.ai/user-guide/creating-dify-apps/prompt-engineering'
|
||||
? 'https://docs.dify.ai/v/zh-hans/guides/knowledge-base/integrate_knowledge_within_application'
|
||||
: 'https://docs.dify.ai/guides/knowledge-base/integrate-knowledge-within-application'
|
||||
}
|
||||
target='_blank' rel='noopener noreferrer'
|
||||
>
|
||||
|
@ -31,12 +31,12 @@ const VoiceParamConfig: FC = () => {
|
||||
|
||||
let languageItem = languages.find(item => item.value === textToSpeechConfig.language)
|
||||
const localLanguagePlaceholder = languageItem?.name || t('common.placeholder.select')
|
||||
if (languages && !languageItem)
|
||||
if (languages && !languageItem && languages.length > 0)
|
||||
languageItem = languages[0]
|
||||
const language = languageItem?.value
|
||||
const voiceItems = useSWR({ appId, language }, fetchAppVoices).data
|
||||
let voiceItem = voiceItems?.find(item => item.value === textToSpeechConfig.voice)
|
||||
if (voiceItems && !voiceItem)
|
||||
if (voiceItems && !voiceItem && voiceItems.length > 0)
|
||||
voiceItem = voiceItems[0]
|
||||
|
||||
const localVoicePlaceholder = voiceItem?.name || t('common.placeholder.select')
|
||||
@ -125,9 +125,11 @@ const VoiceParamConfig: FC = () => {
|
||||
<div
|
||||
className='mb-2 leading-[18px] text-[13px] font-semibold text-gray-800'>{t('appDebug.voice.voiceSettings.voice')}</div>
|
||||
<Listbox
|
||||
value={voiceItem}
|
||||
value={voiceItem ?? {}}
|
||||
disabled={!languageItem}
|
||||
onChange={(value: Item) => {
|
||||
if (!value.value)
|
||||
return
|
||||
setTextToSpeechConfig({
|
||||
...textToSpeechConfig,
|
||||
voice: String(value.value),
|
||||
|
@ -41,6 +41,7 @@ const TextToSpeech: FC = () => {
|
||||
<AudioBtn
|
||||
value={languageInfo?.example}
|
||||
isAudition
|
||||
voice={textToSpeechConfig.voice}
|
||||
noCache
|
||||
/>
|
||||
)}
|
||||
|
@ -495,7 +495,7 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo
|
||||
>
|
||||
<Chat
|
||||
config={{
|
||||
app_id: appDetail?.id,
|
||||
appId: appDetail?.id,
|
||||
text_to_speech: {
|
||||
enabled: true,
|
||||
},
|
||||
|
@ -29,6 +29,7 @@ import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import WorkflowProcessItem from '@/app/components/base/chat/chat/answer/workflow-process'
|
||||
import type { WorkflowProcess } from '@/app/components/base/chat/types'
|
||||
import type { SiteInfo } from '@/models/share'
|
||||
import { useChatContext } from '@/app/components/base/chat/chat/context'
|
||||
|
||||
const MAX_DEPTH = 3
|
||||
|
||||
@ -127,6 +128,10 @@ const GenerationItem: FC<IGenerationItemProps> = ({
|
||||
const [childFeedback, setChildFeedback] = useState<Feedbacktype>({
|
||||
rating: null,
|
||||
})
|
||||
const {
|
||||
config,
|
||||
} = useChatContext()
|
||||
|
||||
const setCurrentLogItem = useAppStore(s => s.setCurrentLogItem)
|
||||
const setShowPromptLogModal = useAppStore(s => s.setShowPromptLogModal)
|
||||
|
||||
@ -430,6 +435,7 @@ const GenerationItem: FC<IGenerationItemProps> = ({
|
||||
<AudioBtn
|
||||
id={messageId!}
|
||||
className={'mr-1'}
|
||||
voice={config?.text_to_speech?.voice}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
@ -41,7 +41,7 @@ export class AudioPlayerManager {
|
||||
}
|
||||
|
||||
this.msgId = id
|
||||
this.audioPlayers = new AudioPlayer(url, isPublic, id, msgContent, callback)
|
||||
this.audioPlayers = new AudioPlayer(url, isPublic, id, msgContent, voice, callback)
|
||||
return this.audioPlayers
|
||||
}
|
||||
}
|
||||
|
@ -23,12 +23,13 @@ export default class AudioPlayer {
|
||||
isPublic: boolean
|
||||
callback: ((event: string) => {}) | null
|
||||
|
||||
constructor(streamUrl: string, isPublic: boolean, msgId: string | undefined, msgContent: string | null | undefined, callback: ((event: string) => {}) | null) {
|
||||
constructor(streamUrl: string, isPublic: boolean, msgId: string | undefined, msgContent: string | null | undefined, voice: string | undefined, callback: ((event: string) => {}) | null) {
|
||||
this.audioContext = new AudioContext()
|
||||
this.msgId = msgId
|
||||
this.msgContent = msgContent
|
||||
this.url = streamUrl
|
||||
this.isPublic = isPublic
|
||||
this.voice = voice
|
||||
this.callback = callback
|
||||
|
||||
// Compatible with iphone ios17 ManagedMediaSource
|
||||
@ -154,7 +155,6 @@ export default class AudioPlayer {
|
||||
this.mediaSource?.endOfStream()
|
||||
clearInterval(endTimer)
|
||||
}
|
||||
console.log('finishStream endOfStream endTimer')
|
||||
}, 10)
|
||||
}
|
||||
|
||||
@ -169,7 +169,6 @@ export default class AudioPlayer {
|
||||
const arrayBuffer = this.cacheBuffers.shift()!
|
||||
this.sourceBuffer?.appendBuffer(arrayBuffer)
|
||||
}
|
||||
console.log('finishStream timer')
|
||||
}, 10)
|
||||
}
|
||||
|
||||
|
@ -65,11 +65,11 @@ const AudioBtn = ({
|
||||
}
|
||||
const handleToggle = async () => {
|
||||
if (audioState === 'playing' || audioState === 'loading') {
|
||||
setAudioState('paused')
|
||||
setTimeout(() => setAudioState('paused'), 1)
|
||||
AudioPlayerManager.getInstance().getAudioPlayer(url, isPublic, id, value, voice, audio_finished_call).pauseAudio()
|
||||
}
|
||||
else {
|
||||
setAudioState('loading')
|
||||
setTimeout(() => setAudioState('loading'), 1)
|
||||
AudioPlayerManager.getInstance().getAudioPlayer(url, isPublic, id, value, voice, audio_finished_call).playAudio()
|
||||
}
|
||||
}
|
||||
|
@ -53,10 +53,11 @@ const Operation: FC<OperationProps> = ({
|
||||
content: messageContent,
|
||||
annotation,
|
||||
feedback,
|
||||
adminFeedback,
|
||||
agent_thoughts,
|
||||
} = item
|
||||
const hasAnnotation = !!annotation?.id
|
||||
const [localFeedback, setLocalFeedback] = useState(feedback)
|
||||
const [localFeedback, setLocalFeedback] = useState(config?.supportAnnotation ? adminFeedback : feedback)
|
||||
|
||||
const content = useMemo(() => {
|
||||
if (agent_thoughts?.length)
|
||||
@ -125,6 +126,7 @@ const Operation: FC<OperationProps> = ({
|
||||
id={id}
|
||||
value={content}
|
||||
noCache={false}
|
||||
voice={config?.text_to_speech?.voice}
|
||||
className='hidden group-hover:block'
|
||||
/>
|
||||
</>
|
||||
|
@ -149,7 +149,7 @@ const VoiceParamConfig = ({
|
||||
<div
|
||||
className='mb-2 leading-[18px] text-[13px] font-semibold text-gray-800'>{t('appDebug.voice.voiceSettings.voice')}</div>
|
||||
<Listbox
|
||||
value={voiceItem}
|
||||
value={voiceItem ?? {}}
|
||||
disabled={!languageItem}
|
||||
onChange={(value: Item) => {
|
||||
handleChange({
|
||||
|
@ -62,7 +62,7 @@ const translation = {
|
||||
run: 'Run',
|
||||
firecrawlTitle: 'Extract web content with 🔥Firecrawl',
|
||||
firecrawlDoc: 'Firecrawl docs',
|
||||
firecrawlDocLink: 'https://docs.dify.ai/guides/knowledge-base/sync_from_website',
|
||||
firecrawlDocLink: 'https://docs.dify.ai/guides/knowledge-base/sync-from-website',
|
||||
options: 'Options',
|
||||
crawlSubPage: 'Crawl sub-pages',
|
||||
limit: 'Limit',
|
||||
|
@ -60,7 +60,7 @@ const translation = {
|
||||
rerankSettings: 'Rerank Setting',
|
||||
weightedScore: {
|
||||
title: 'Weighted Score',
|
||||
description: 'By adjusting the weights assigned, this rerank strategy determines whether to prioritize semantic or keyword matching.',
|
||||
description: 'By adjusting the weights assigned, this rerank strategy determines whether to prioritize semantic or keyword matching.',
|
||||
semanticFirst: 'Semantic first',
|
||||
keywordFirst: 'Keyword first',
|
||||
customized: 'Customized',
|
||||
|
@ -62,7 +62,7 @@ const translation = {
|
||||
run: 'Ejecutar',
|
||||
firecrawlTitle: 'Extraer contenido web con 🔥Firecrawl',
|
||||
firecrawlDoc: 'Documentación de Firecrawl',
|
||||
firecrawlDocLink: 'https://docs.dify.ai/guides/knowledge-base/sync_from_website',
|
||||
firecrawlDocLink: 'https://docs.dify.ai/guides/knowledge-base/sync-from-website',
|
||||
options: 'Opciones',
|
||||
crawlSubPage: 'Rastrear subpáginas',
|
||||
limit: 'Límite',
|
||||
|
@ -23,7 +23,7 @@ const translation = {
|
||||
title: '注釈の返信を編集',
|
||||
queryName: 'ユーザーのクエリ',
|
||||
answerName: 'ストーリーテラーボット',
|
||||
yourAnswer: 'あなたの回答',
|
||||
yourAnswer: '貴方の回答',
|
||||
answerPlaceholder: 'ここに回答を入力してください',
|
||||
yourQuery: 'あなたのクエリ',
|
||||
queryPlaceholder: 'ここにクエリを入力してください',
|
||||
|
@ -9,6 +9,7 @@ const translation = {
|
||||
play: '再生',
|
||||
pause: '一時停止',
|
||||
playing: '再生中',
|
||||
loading: '読み込み中',
|
||||
merMaind: {
|
||||
rerender: '再レンダリング',
|
||||
},
|
||||
|
@ -38,7 +38,6 @@ const translation = {
|
||||
description: 'LLMプロバイダーキーが設定されていません。デバッグする前に設定する必要があります。',
|
||||
settingBtn: '設定に移動',
|
||||
},
|
||||
|
||||
trailUseGPT4Info: {
|
||||
title: '現在、gpt-4はサポートされていません',
|
||||
description: 'gpt-4を使用するには、APIキーを設定してください。',
|
||||
@ -161,7 +160,6 @@ const translation = {
|
||||
toolbox: {
|
||||
title: 'ツールボックス',
|
||||
},
|
||||
|
||||
moderation: {
|
||||
title: 'コンテンツのモデレーション',
|
||||
description: 'モデレーションAPIを使用するか、機密語リストを維持することで、モデルの出力を安全にします。',
|
||||
@ -200,7 +198,59 @@ const translation = {
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
},
|
||||
generate: {
|
||||
title: 'プロンプト生成器',
|
||||
description: 'プロンプト生成器は、設定済みのモデルを使って、高品質で構造的に優れたプロンプトを作成するための最適化を行います。具体的で詳細な指示をお書きください。',
|
||||
tryIt: '試してみる',
|
||||
instruction: '指示',
|
||||
instructionPlaceHolder: '具体的で明確な指示を入力してください。',
|
||||
generate: '生成',
|
||||
resTitle: '生成されたプロンプト',
|
||||
noDataLine1: '左側に使用例を記入してください,',
|
||||
noDataLine2: 'オーケストレーションのプレビューがこちらに表示されます。',
|
||||
apply: '適用',
|
||||
loading: 'アプリケーションを処理中です',
|
||||
overwriteTitle: '既存の設定を上書きしますか?',
|
||||
overwriteMessage: 'このプロンプトを適用すると、既存の設定が上書きされます。',
|
||||
template: {
|
||||
pythonDebugger: {
|
||||
name: 'Python デバッガー',
|
||||
instruction: '指示に従ってコードを生成し、デバッグを行うボット',
|
||||
},
|
||||
translation: {
|
||||
name: '翻訳',
|
||||
instruction: '複数言語に対応した翻訳機能',
|
||||
},
|
||||
professionalAnalyst: {
|
||||
name: '専門アナリスト',
|
||||
instruction: '長文のレポートから洞察を引き出し、リスクを特定し、重要情報をまとめる',
|
||||
},
|
||||
excelFormulaExpert: {
|
||||
name: 'エクセル式エキスパート',
|
||||
instruction: 'ユーザーの指示に基づき、エクセル式の理解、使用、作成をサポートするチャットボット',
|
||||
},
|
||||
travelPlanning: {
|
||||
name: '旅行計画',
|
||||
instruction: 'ユーザーが簡単に旅行計画を立てられるように設計されたツール',
|
||||
},
|
||||
SQLSorcerer: {
|
||||
name: 'SQLソーサラー',
|
||||
instruction: '日常言語をSQLクエリに変換する',
|
||||
},
|
||||
GitGud: {
|
||||
name: 'Git gud',
|
||||
instruction: 'ユーザーが記述したバージョン管理アクションに対応するGitコマンドを生成する',
|
||||
},
|
||||
meetingTakeaways: {
|
||||
name: '会議の要点',
|
||||
instruction: '議題、重要点、行動項目を含む要約を作成する',
|
||||
},
|
||||
writingsPolisher: {
|
||||
name: 'ライティングポリッシャー',
|
||||
instruction: '文章を改善するための高度な編集技法を用いる',
|
||||
},
|
||||
},
|
||||
},
|
||||
resetConfig: {
|
||||
title: 'リセットを確認しますか?',
|
||||
@ -264,6 +314,7 @@ const translation = {
|
||||
'varName': '変数名',
|
||||
'labelName': 'ラベル名',
|
||||
'inputPlaceholder': '入力してください',
|
||||
'content': 'コンテンツ',
|
||||
'required': '必須',
|
||||
'errorMsg': {
|
||||
varNameRequired: '変数名は必須です',
|
||||
@ -362,7 +413,6 @@ const translation = {
|
||||
score_thresholdTip: 'チャンクフィルタリングの類似性閾値を設定するために使用されます。',
|
||||
retrieveChangeTip: 'インデックスモードとリトリーバルモードを変更すると、このナレッジに関連付けられたアプリケーションに影響を与える可能性があります。',
|
||||
},
|
||||
|
||||
debugAsSingleModel: '単一モデルでデバッグ',
|
||||
debugAsMultipleModel: '複数モデルでデバッグ',
|
||||
duplicateModel: '複製',
|
||||
|
@ -3,7 +3,7 @@ const translation = {
|
||||
firstStepTip: 'はじめるには、',
|
||||
enterKeyTip: '以下にOpenAI APIキーを入力してください',
|
||||
getKeyTip: 'OpenAIダッシュボードからAPIキーを取得してください',
|
||||
placeholder: 'あなたのOpenAI APIキー(例:sk-xxxx)',
|
||||
placeholder: 'あなた様のOpenAI APIキー(例:sk-xxxx)',
|
||||
},
|
||||
apiKeyInfo: {
|
||||
cloud: {
|
||||
@ -104,7 +104,7 @@ const translation = {
|
||||
},
|
||||
apiInfo: {
|
||||
title: 'バックエンドサービスAPI',
|
||||
explanation: 'あなたのアプリケーションに簡単に統合できます',
|
||||
explanation: 'あなた様のアプリケーションに簡単に統合できます',
|
||||
accessibleAddress: 'サービスAPIエンドポイント',
|
||||
doc: 'APIリファレンス',
|
||||
},
|
||||
|
@ -13,9 +13,13 @@ const translation = {
|
||||
exportFailed: 'DSL のエクスポートに失敗しました。',
|
||||
importDSL: 'DSL ファイルをインポート',
|
||||
createFromConfigFile: 'DSL ファイルから作成する',
|
||||
importFromDSL: 'DSLからインポート',
|
||||
importFromDSLFile: 'DSLファイルから',
|
||||
importFromDSLUrl: 'URLから',
|
||||
importFromDSLUrlPlaceholder: 'DSLリンクをここに貼り付けます',
|
||||
deleteAppConfirmTitle: 'このアプリを削除しますか?',
|
||||
deleteAppConfirmContent:
|
||||
'アプリを削除すると、元に戻すことはできません。ユーザーはもはやあなたのアプリにアクセスできず、すべてのプロンプトの設定とログが永久に削除されます。',
|
||||
'アプリを削除すると、元に戻すことはできません。ユーザーはもはやあなた様のアプリにアクセスできず、すべてのプロンプトの設定とログが永久に削除されます。',
|
||||
appDeleted: 'アプリが削除されました',
|
||||
appDeleteFailed: 'アプリの削除に失敗しました',
|
||||
join: 'コミュニティに参加する',
|
||||
@ -86,6 +90,42 @@ const translation = {
|
||||
workflow: 'ワークフロー',
|
||||
completion: 'テキスト生成',
|
||||
},
|
||||
tracing: {
|
||||
title: 'アプリのパフォーマンスの追跡',
|
||||
description: 'サードパーティのLLMOpsサービスとトレースアプリケーションのパフォーマンス設定を行います。',
|
||||
config: '設定',
|
||||
collapse: 'Collapse',
|
||||
expand: '展開',
|
||||
tracing: '追跡',
|
||||
disabled: '無効しました',
|
||||
disabledTip: 'まずはサービスの設定から始めましょう。',
|
||||
enabled: '有効しました',
|
||||
tracingDescription: 'LLMの呼び出し、コンテキスト、プロンプト、HTTPリクエストなど、アプリケーション実行の全ての文脈をサードパーティのトレースプラットフォームで取り込みます。',
|
||||
configProviderTitle: {
|
||||
configured: '設定しました',
|
||||
notConfigured: 'トレース機能を有効化するためには、サービスの設定が必要です。',
|
||||
moreProvider: 'その他のプロバイダー',
|
||||
},
|
||||
langsmith: {
|
||||
title: 'LangSmith',
|
||||
description: 'LLMを利用したアプリケーションのライフサイクル全段階を支援する、オールインワンの開発者向けプラットフォームです。',
|
||||
},
|
||||
langfuse: {
|
||||
title: 'Langfuse',
|
||||
description: 'トレース、評価、プロンプトの管理、そしてメトリクスを駆使して、LLMアプリケーションのデバッグや改善に役立てます。',
|
||||
},
|
||||
inUse: '使用中',
|
||||
configProvider: {
|
||||
title: '配置 ',
|
||||
placeholder: 'あなた様の{{key}}を入力しでください',
|
||||
project: 'プロジェクト',
|
||||
publicKey: '公開キー',
|
||||
secretKey: '秘密キー',
|
||||
viewDocsLink: '{{key}}のドキュメントを見る',
|
||||
removeConfirmTitle: '{{key}}の設定を削除しますか?',
|
||||
removeConfirmContent: '現在の設定は使用中です。これを削除すると、トレース機能が無効になります。',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
@ -8,7 +8,7 @@ const translation = {
|
||||
viewBilling: '請求とサブスクリプションの管理',
|
||||
buyPermissionDeniedTip: 'サブスクリプションするには、エンタープライズ管理者に連絡してください',
|
||||
plansCommon: {
|
||||
title: 'あなたに合ったプランを選択してください',
|
||||
title: 'あなた様に合ったプランを選択してください',
|
||||
yearlyTip: '年間購読で2か月無料!',
|
||||
mostPopular: '最も人気のある',
|
||||
planRange: {
|
||||
@ -28,10 +28,12 @@ const translation = {
|
||||
talkToSales: '営業と話す',
|
||||
modelProviders: 'モデルプロバイダー',
|
||||
teamMembers: 'チームメンバー',
|
||||
annotationQuota: 'アノテーション・クォータ',
|
||||
buildApps: 'アプリを作成する',
|
||||
vectorSpace: 'ベクトルスペース',
|
||||
vectorSpaceBillingTooltip: '1MBあたり約120万文字のベクトル化データを保存できます(OpenAI Embeddingsを使用して推定され、モデルによって異なります)。',
|
||||
vectorSpaceTooltip: 'ベクトルスペースは、LLMがデータを理解するために必要な長期記憶システムです。',
|
||||
documentsUploadQuota: 'ドキュメント・アップロード・クォータ',
|
||||
documentProcessingPriority: 'ドキュメント処理の優先度',
|
||||
documentProcessingPriorityTip: 'より高いドキュメント処理の優先度をご希望の場合は、プランをアップグレードしてください。',
|
||||
documentProcessingPriorityUpgrade: 'より高い精度と高速な速度でデータを処理します。',
|
||||
@ -56,8 +58,11 @@ const translation = {
|
||||
dedicatedAPISupport: '専用APIサポート',
|
||||
customIntegration: 'カスタム統合とサポート',
|
||||
ragAPIRequest: 'RAG APIリクエスト',
|
||||
bulkUpload: 'ドキュメントの一括アップロード',
|
||||
agentMode: 'エージェントモード',
|
||||
workflow: 'ワークフロー',
|
||||
llmLoadingBalancing: 'LLMロードバランシング',
|
||||
llmLoadingBalancingTooltip: 'APIレート制限を効果的に回避するために、モデルに複数のAPIキーを追加する。',
|
||||
},
|
||||
comingSoon: '近日公開',
|
||||
member: 'メンバー',
|
||||
|
@ -12,6 +12,7 @@ const translation = {
|
||||
cancel: 'キャンセル',
|
||||
clear: 'クリア',
|
||||
save: '保存',
|
||||
saveAndEnable: '保存 & 有効に',
|
||||
edit: '編集',
|
||||
add: '追加',
|
||||
added: '追加済み',
|
||||
@ -37,6 +38,10 @@ const translation = {
|
||||
duplicate: '重複',
|
||||
rename: '名前の変更',
|
||||
},
|
||||
errorMsg: {
|
||||
fieldRequired: '{{field}}は必要です',
|
||||
urlError: 'URL は http:// または https:// で始まる必要があります',
|
||||
},
|
||||
placeholder: {
|
||||
input: '入力してください',
|
||||
select: '選択してください',
|
||||
@ -173,17 +178,21 @@ const translation = {
|
||||
adminTip: 'アプリの構築およびチーム設定の管理ができます',
|
||||
normal: '通常',
|
||||
normalTip: 'アプリの使用のみが可能で、アプリの構築はできません',
|
||||
builder: 'ビルダー',
|
||||
builderTip: '独自のアプリを作成・編集できる',
|
||||
editor: 'エディター',
|
||||
editorTip: 'アプリの構築ができますが、チーム設定の管理はできません',
|
||||
datasetOperator: 'ナレッジ管理員',
|
||||
datasetOperatorTip: 'ナレッジベースのみを管理できる',
|
||||
inviteTeamMember: 'チームメンバーを招待する',
|
||||
inviteTeamMemberTip: '彼らはサインイン後、直接あなたのチームデータにアクセスできます。',
|
||||
inviteTeamMemberTip: '彼らはサインイン後、直接あなた様のチームデータにアクセスできます。',
|
||||
email: 'メール',
|
||||
emailInvalid: '無効なメール形式',
|
||||
emailPlaceholder: 'メールを入力してください',
|
||||
sendInvite: '招待を送る',
|
||||
invitedAsRole: '{{role}}ユーザーとして招待されました',
|
||||
invitationSent: '招待が送信されました',
|
||||
invitationSentTip: '招待が送信され、彼らはDifyにサインインしてあなたのチームデータにアクセスできます。',
|
||||
invitationSentTip: '招待が送信され、彼らはDifyにサインインしてあなた様のチームデータにアクセスできます。',
|
||||
invitationLink: '招待リンク',
|
||||
failedinvitationEmails: '以下のユーザーは正常に招待されませんでした',
|
||||
ok: 'OK',
|
||||
@ -191,10 +200,11 @@ const translation = {
|
||||
removeFromTeamTip: 'チームへのアクセスが削除されます',
|
||||
setAdmin: '管理者に設定',
|
||||
setMember: '通常のメンバーに設定',
|
||||
setBuilder: 'ビルダーに設定',
|
||||
setEditor: 'エディターに設定',
|
||||
disinvite: '招待をキャンセル',
|
||||
deleteMember: 'メンバーを削除',
|
||||
you: '(あなた)',
|
||||
you: '(あなた様)',
|
||||
},
|
||||
integrations: {
|
||||
connected: '接続済み',
|
||||
@ -285,6 +295,7 @@ const translation = {
|
||||
key: '再ランクモデル',
|
||||
tip: '再ランクモデルは、ユーザークエリとの意味的一致に基づいて候補文書リストを再配置し、意味的ランキングの結果を向上させます。',
|
||||
},
|
||||
apiKey: 'API-キー',
|
||||
quota: 'クォータ',
|
||||
searchModel: '検索モデル',
|
||||
noModelFound: '{{model}}に対するモデルが見つかりません',
|
||||
@ -303,18 +314,91 @@ const translation = {
|
||||
quotaExhausted: 'クォータが使い果たされました',
|
||||
callTimes: '通話回数',
|
||||
tokens: 'トークン',
|
||||
fee: '費用',
|
||||
feeToken: 'トークン毎の費用',
|
||||
basicSettings: '基本設定',
|
||||
advancedSettings: '高度な設定',
|
||||
comingSoon: '近日公開',
|
||||
installPlugin: 'プラグインをインストール',
|
||||
buyQuota: 'クォータを購入',
|
||||
priorityUse: '優先利用',
|
||||
removeKey: 'APIキーを削除',
|
||||
tip: '有料クォータは優先して使用されます。有料クォータを使用し終えた後、トライアルクォータが利用されます。',
|
||||
},
|
||||
item: {
|
||||
deleteDesc: '{{modelName}}はシステムが推測するモデルとして利用されています。削除すると、一部の機能が使用不可能になる可能性があります。ご確認ください。',
|
||||
freeQuota: '無料のクォータ',
|
||||
},
|
||||
addApiKey: 'APIキーを追加',
|
||||
invalidApiKey: '無効なAPIキー',
|
||||
encrypted: {
|
||||
front: 'APIキーは',
|
||||
back: ' の技術で暗号化されて保存されます。',
|
||||
},
|
||||
freeQuota: {
|
||||
howToEarn: '獲得方法',
|
||||
},
|
||||
addMoreModelProvider: 'モデルプロバイダを追加',
|
||||
addModel: 'モデルを追加',
|
||||
modelsNum: '{{num}}のモデル',
|
||||
showModels: 'モデルの表示',
|
||||
showModelsNum: '{{num}}のモデルを表示',
|
||||
collapse: '折り畳み',
|
||||
config: '設定',
|
||||
modelAndParameters: 'モデルとパラメータ',
|
||||
model: 'モデル',
|
||||
featureSupported: '{{feature}}に対応',
|
||||
callTimes: '呼び出し回数',
|
||||
credits: 'クレジット',
|
||||
buyQuota: 'クォータ購入',
|
||||
getFreeTokens: '無料トークンを獲得',
|
||||
priorityUsing: '優先利用',
|
||||
deprecated: '廃止予定',
|
||||
confirmDelete: '削除を確認',
|
||||
quotaTip: '残りの無料トークン',
|
||||
loadPresets: 'プリセットの読み込み',
|
||||
parameters: 'パラメータ',
|
||||
loadBalancing: '負荷分散',
|
||||
loadBalancingDescription: '複数の認証情報を使って負荷を分散させます。',
|
||||
loadBalancingHeadline: '負荷分散',
|
||||
configLoadBalancing: '負荷分散の設定',
|
||||
modelHasBeenDeprecated: 'このモデルは廃止予定です',
|
||||
providerManaged: 'プロバイダ管理',
|
||||
providerManagedDescription: 'モデルプロバイダによって提供される認証情報を使用します。',
|
||||
defaultConfig: 'デフォルトの設定',
|
||||
apiKeyStatusNormal: 'APIキーの状態は正常',
|
||||
apiKeyRateLimit: 'レート制限に到達しました。{{seconds}}秒後に再度利用可能です',
|
||||
addConfig: '設定を追加',
|
||||
editConfig: '設定を編集',
|
||||
loadBalancingLeastKeyWarning: '負荷分散を利用するには、最低2つのキーを有効化する必要があります。',
|
||||
loadBalancingInfo: 'デフォルトでは、負荷分散はラウンドロビン方式を採用しています。レート制限が発生した場合、1分間のクールダウン期間が適用されます。',
|
||||
upgradeForLoadBalancing: '負荷分散を利用するには、プランのアップグレードが必要です。',
|
||||
},
|
||||
dataSource: {
|
||||
externalAPI: '外部API',
|
||||
webhooks: 'Webhook',
|
||||
custom: 'カスタム',
|
||||
add: 'データソースの追加',
|
||||
connect: '接続',
|
||||
configure: '設定',
|
||||
notion: {
|
||||
title: 'ノーション',
|
||||
description: '知識データソースとしてノーションを使用します。',
|
||||
connectedWorkspace: '接続済みワークスペース',
|
||||
addWorkspace: 'ワークスペースの追加',
|
||||
connected: '接続済み',
|
||||
disconnected: '接続解除',
|
||||
changeAuthorizedPages: '認証済みページの変更',
|
||||
pagesAuthorized: '認証済みページ',
|
||||
sync: '同期',
|
||||
remove: '削除',
|
||||
selector: {
|
||||
pageSelected: '選択済みページ',
|
||||
searchPages: 'ページ検索...',
|
||||
noSearchResult: '検索結果なし',
|
||||
addPages: 'ページの追加',
|
||||
preview: 'プレビュー',
|
||||
},
|
||||
},
|
||||
website: {
|
||||
title: 'ウェブサイト',
|
||||
description: 'ウェブクローラーを使ってウェブサイトからコンテンツを取り込みます。',
|
||||
with: 'による',
|
||||
configuredCrawlers: '設定済みクローラー',
|
||||
active: 'アクティブ',
|
||||
inactive: '非アクティブ',
|
||||
},
|
||||
},
|
||||
plugin: {
|
||||
serpapi: {
|
||||
@ -360,7 +444,7 @@ const translation = {
|
||||
},
|
||||
appMenus: {
|
||||
overview: '概要',
|
||||
promptEng: 'Orchestrate',
|
||||
promptEng: 'オーケストレート',
|
||||
apiAccess: 'APIアクセス',
|
||||
logAndAnn: 'ログ&アナウンス',
|
||||
logs: 'ログ',
|
||||
|
@ -11,6 +11,11 @@ const translation = {
|
||||
error: {
|
||||
unavailable: 'このナレッジは利用できません',
|
||||
},
|
||||
firecrawl: {
|
||||
configFirecrawl: '🔥Firecrawlの設定',
|
||||
apiKeyPlaceholder: 'firecrawl.devからのAPIキー',
|
||||
getApiKeyLinkText: 'firecrawl.devからAPIキーを取得する',
|
||||
},
|
||||
stepOne: {
|
||||
filePreview: 'ファイルプレビュー',
|
||||
pagePreview: 'ページプレビュー',
|
||||
@ -50,6 +55,30 @@ const translation = {
|
||||
confirmButton: '作成',
|
||||
failed: '作成に失敗しました',
|
||||
},
|
||||
website: {
|
||||
fireCrawlNotConfigured: 'Firecrawlが設定されていません',
|
||||
fireCrawlNotConfiguredDescription: 'Firecrawl を使用するには、Firecrawl の API キーを設定してください。',
|
||||
configure: '設定',
|
||||
run: '実行',
|
||||
firecrawlTitle: '🔥Firecrawlを使っでウエブコンテンツを抽出',
|
||||
firecrawlDoc: 'Firecrawlドキュメント',
|
||||
firecrawlDocLink: 'https://docs.dify.ai/guides/knowledge-base/sync-from-website',
|
||||
options: 'オプション',
|
||||
crawlSubPage: 'サブページをクロールする',
|
||||
limit: '制限',
|
||||
maxDepth: '最大深度',
|
||||
excludePaths: 'パスを除外する',
|
||||
includeOnlyPaths: 'パスのみを含める',
|
||||
extractOnlyMainContent: 'メインコンテンツのみを抽出する(ヘッダー、ナビ、フッターなどは抽出しない)',
|
||||
exceptionErrorTitle: 'Firecrawl ジョブの実行中に例外が発生しました:',
|
||||
unknownError: '不明なエラー',
|
||||
totalPageScraped: 'スクレイピングされた総ページ数:',
|
||||
selectAll: 'すべて選択',
|
||||
resetAll: 'すべてリセット',
|
||||
scrapTimeInfo: '{{time}} 秒以内に合計 {{total}} ページをスクレイピングしました',
|
||||
preview: 'プレビュー',
|
||||
maxDepthTooltip: '入力されたURLを基にしたクローリング作業での設定可能な最大深度について説明します。深度0は入力されたURL自体のページを対象としたスクレイピングを意味します。深度1では、元のURLの直下にあるページ(URLに続く最初の"/"以降の内容)もスクレイピングの対象になります。この深度は指定した数値まで増加させることができ、それに応じてスクレイピングの範囲も広がっていきます。',
|
||||
},
|
||||
},
|
||||
stepTwo: {
|
||||
segmentation: 'チャンク設定',
|
||||
@ -86,9 +115,11 @@ const translation = {
|
||||
calculating: '計算中...',
|
||||
fileSource: 'ドキュメントの前処理',
|
||||
notionSource: 'ページの前処理',
|
||||
websiteSource: 'ウエブサイドの前処理',
|
||||
other: 'その他',
|
||||
fileUnit: 'ファイル',
|
||||
notionUnit: 'ページ',
|
||||
webpageUnit: ' ページ',
|
||||
previousStep: '前のステップ',
|
||||
nextStep: '保存して処理',
|
||||
save: '保存して処理',
|
||||
|
@ -4,6 +4,7 @@ const translation = {
|
||||
desc: 'ナレッジのすべてのファイルがここに表示され、ナレッジ全体がDifyの引用やチャットプラグインを介してリンクされるか、インデックス化されることができます。',
|
||||
addFile: 'ファイルを追加',
|
||||
addPages: 'ページを追加',
|
||||
addUrl: 'URLを追加',
|
||||
table: {
|
||||
header: {
|
||||
fileName: 'ファイル名',
|
||||
@ -13,6 +14,8 @@ const translation = {
|
||||
status: 'ステータス',
|
||||
action: 'アクション',
|
||||
},
|
||||
rename: '名前を変更',
|
||||
name: '名前',
|
||||
},
|
||||
action: {
|
||||
uploadFile: '新しいファイルをアップロード',
|
||||
|
@ -12,6 +12,8 @@ const translation = {
|
||||
permissions: '権限',
|
||||
permissionsOnlyMe: '自分のみ',
|
||||
permissionsAllMember: 'すべてのチームメンバー',
|
||||
permissionsInvitedMembers: '一部のチームメンバー',
|
||||
me: '(あなた様)',
|
||||
indexMethod: 'インデックス方法',
|
||||
indexMethodHighQuality: '高品質',
|
||||
indexMethodHighQualityTip: 'ユーザーがクエリを実行する際により高い精度を提供するために、Embeddingモデルを呼び出して処理を行う。',
|
||||
|
@ -1,18 +1,18 @@
|
||||
const translation = {
|
||||
knowledge: '知識',
|
||||
knowledge: 'ナレッジ',
|
||||
documentCount: ' ドキュメント',
|
||||
wordCount: ' k 単語',
|
||||
appCount: ' リンクされたアプリ',
|
||||
createDataset: '知識を作成',
|
||||
createDataset: 'ナレッジを作成',
|
||||
createDatasetIntro: '独自のテキストデータをインポートするか、LLMコンテキストの強化のためにWebhookを介してリアルタイムでデータを書き込むことができます。',
|
||||
deleteDatasetConfirmTitle: 'この知識を削除しますか?',
|
||||
deleteDatasetConfirmTitle: 'このナレッジを削除しますか?',
|
||||
deleteDatasetConfirmContent:
|
||||
'知識を削除すると元に戻すことはできません。ユーザーはもはやあなたの知識にアクセスできず、すべてのプロンプトの設定とログが永久に削除されます。',
|
||||
datasetUsedByApp: 'この知識は一部のアプリによって使用されています。アプリはこの知識を使用できなくなり、すべてのプロンプト設定とログは永久に削除されます。',
|
||||
datasetDeleted: '知識が削除されました',
|
||||
datasetDeleteFailed: '知識の削除に失敗しました',
|
||||
'ナレッジを削除すると元に戻すことはできません。ユーザーはもはやあなた様のナレッジにアクセスできず、すべてのプロンプトの設定とログが永久に削除されます。',
|
||||
datasetUsedByApp: 'このナレッジは一部のアプリによって使用されています。アプリはこのナレッジを使用できなくなり、すべてのプロンプト設定とログは永久に削除されます。',
|
||||
datasetDeleted: 'ナレッジが削除されました',
|
||||
datasetDeleteFailed: 'ナレッジの削除に失敗しました',
|
||||
didYouKnow: 'ご存知ですか?',
|
||||
intro1: '知識はDifyアプリケーションに統合することができます',
|
||||
intro1: 'ナレッジはDifyアプリケーションに統合することができます',
|
||||
intro2: 'コンテキストとして',
|
||||
intro3: '、',
|
||||
intro4: 'または',
|
||||
@ -20,7 +20,7 @@ const translation = {
|
||||
intro6: '単体のChatGPTインデックスプラグインとして公開するために',
|
||||
unavailable: '利用不可',
|
||||
unavailableTip: '埋め込みモデルが利用できません。デフォルトの埋め込みモデルを設定する必要があります',
|
||||
datasets: '知識',
|
||||
datasets: 'ナレッジ',
|
||||
datasetsApi: 'API',
|
||||
retrieval: {
|
||||
semantic_search: {
|
||||
@ -45,6 +45,29 @@ const translation = {
|
||||
},
|
||||
docsFailedNotice: 'ドキュメントのインデックスに失敗しました',
|
||||
retry: '再試行',
|
||||
indexingTechnique: {
|
||||
high_quality: '高品質',
|
||||
economy: '経済',
|
||||
},
|
||||
indexingMethod: {
|
||||
semantic_search: 'ベクトル検索',
|
||||
full_text_search: 'フルテキスト検索',
|
||||
hybrid_search: 'ハイブリッド検索',
|
||||
},
|
||||
mixtureHighQualityAndEconomicTip: '高品質なナレッジベースと経済的なナレッジベースを混在させるには、Rerankモデルを構成する必要がある。',
|
||||
inconsistentEmbeddingModelTip: '選択されたナレッジベースが一貫性のない埋め込みモデルで構成されている場合、Rerankモデルの構成が必要です。',
|
||||
retrievalSettings: '設定を回収',
|
||||
rerankSettings: 'Rerank設定',
|
||||
weightedScore: {
|
||||
title: 'ウェイト設定',
|
||||
description: '割り当てられた重みを調整することで、並べ替え戦略はセマンティックマッチングとキーワードマッチングのどちらを優先するかを決定します。',
|
||||
semanticFirst: 'セマンティック優先',
|
||||
keywordFirst: 'キーワード優先',
|
||||
customized: 'カスタマイズ',
|
||||
semantic: 'セマンティクス',
|
||||
keyword: 'キーワード',
|
||||
},
|
||||
nTo1RetrievalLegacy: '製品計画によると、N To 1 Retrievalは9月に正式に廃止される予定です。それまでは通常通り使用できます。',
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
@ -9,6 +9,7 @@ const translation = {
|
||||
namePlaceholder: 'ユーザー名を入力してください',
|
||||
forget: 'パスワードをお忘れですか?',
|
||||
signBtn: 'サインイン',
|
||||
sso: 'SSOに続ける',
|
||||
installBtn: 'セットアップ',
|
||||
setAdminAccount: '管理者アカウントの設定',
|
||||
setAdminAccountDesc: 'アプリケーションの作成やLLMプロバイダの管理など、管理者アカウントの最大権限を設定します。',
|
||||
@ -52,6 +53,7 @@ const translation = {
|
||||
emailInValid: '有効なメールアドレスを入力してください',
|
||||
nameEmpty: '名前は必須です',
|
||||
passwordEmpty: 'パスワードは必須です',
|
||||
passwordLengthInValid: 'パスワードは8文字以上でなければなりません',
|
||||
passwordInvalid: 'パスワードは文字と数字を含み、長さは8以上である必要があります',
|
||||
},
|
||||
license: {
|
||||
@ -59,11 +61,11 @@ const translation = {
|
||||
link: 'オープンソースライセンス',
|
||||
},
|
||||
join: '参加する',
|
||||
joinTipStart: 'あなたを招待します',
|
||||
joinTipStart: 'あなた様を招待します',
|
||||
joinTipEnd: 'チームに参加する',
|
||||
invalid: 'リンクの有効期限が切れています',
|
||||
explore: 'Difyを探索する',
|
||||
activatedTipStart: 'あなたは',
|
||||
activatedTipStart: 'あなた様は',
|
||||
activatedTipEnd: 'チームに参加しました',
|
||||
activated: '今すぐサインイン',
|
||||
adminInitPassword: '管理者初期化パスワード',
|
||||
|
@ -1,10 +1,12 @@
|
||||
const translation = {
|
||||
title: 'ツール',
|
||||
createCustomTool: 'カスタムツールを作成する',
|
||||
customToolTip: 'Difyカスタムツールの詳細',
|
||||
type: {
|
||||
all: 'すべて',
|
||||
builtIn: '組み込み',
|
||||
custom: 'カスタム',
|
||||
workflow: 'ワークフロー',
|
||||
},
|
||||
contribute: {
|
||||
line1: '私は',
|
||||
@ -21,12 +23,26 @@ const translation = {
|
||||
},
|
||||
includeToolNum: '{{num}}個のツールが含まれています',
|
||||
addTool: 'ツールを追加する',
|
||||
addToolModal: {
|
||||
type: 'タイプ',
|
||||
category: 'カテゴリー',
|
||||
add: '追加',
|
||||
added: '追加されだ',
|
||||
manageInTools: 'ツールリストに移動して管理する',
|
||||
emptyTitle: '利用可能なワークフローツールはありません',
|
||||
emptyTip: '追加するには、「ワークフロー -> ツールとして公開 」に移動する',
|
||||
},
|
||||
createTool: {
|
||||
title: 'カスタムツールを作成する',
|
||||
editAction: '設定',
|
||||
editTitle: 'カスタムツールを編集する',
|
||||
name: '名前',
|
||||
toolNamePlaceHolder: 'ツール名を入力してください',
|
||||
nameForToolCall: 'ツールコールの名前',
|
||||
nameForToolCallPlaceHolder: '機械認識に使用される名前, 例えば、getCurrentWeather、list_pets',
|
||||
nameForToolCallTip: '数字、文字、アンダースコアのみがサポートされます。',
|
||||
description: 'ツールの説明',
|
||||
descriptionPlaceholder: 'ツールの使い方の簡単な説明。例えば、特定の場所の温度を知るためなど。',
|
||||
schema: 'スキーマ',
|
||||
schemaPlaceHolder: 'ここにOpenAPIスキーマを入力してください',
|
||||
viewSchemaSpec: 'OpenAPI-Swagger仕様を表示する',
|
||||
@ -71,10 +87,26 @@ const translation = {
|
||||
},
|
||||
privacyPolicy: 'プライバシーポリシー',
|
||||
privacyPolicyPlaceholder: 'プライバシーポリシーを入力してください',
|
||||
toolInput: {
|
||||
title: 'ツール入力',
|
||||
name: '名前',
|
||||
required: '必須',
|
||||
method: 'メソッド',
|
||||
methodSetting: '設定',
|
||||
methodSettingTip: 'ユーザーがツール設定を入力する',
|
||||
methodParameter: 'LLM入力',
|
||||
methodParameterTip: 'LLM は推論中に入力されます',
|
||||
label: 'ラベル',
|
||||
labelPlaceholder: 'ラベルを選択します(オプション)',
|
||||
description: '説明',
|
||||
descriptionPlaceholder: 'パラメータの意味の説明',
|
||||
},
|
||||
customDisclaimer: 'カスタム免責事項',
|
||||
customDisclaimerPlaceholder: 'カスタム免責事項を入力してください',
|
||||
confirmTitle: '保存しますか?',
|
||||
confirmTip: '新しバージョン保存すると、このツールを使用されているアプリは影響を受けます',
|
||||
deleteToolConfirmTitle: 'このツールを削除しますか?',
|
||||
deleteToolConfirmContent: 'ツールの削除は取り消しできません。ユーザーはもうあなたのツールにアクセスできません。',
|
||||
deleteToolConfirmContent: 'ツールの削除は取り消しできません。ユーザーはもうあなた様のツールにアクセスできません。',
|
||||
},
|
||||
test: {
|
||||
title: 'テスト',
|
||||
@ -114,6 +146,8 @@ const translation = {
|
||||
toolRemoved: 'ツールが削除されました',
|
||||
notAuthorized: 'ツールが認可されていません',
|
||||
howToGet: '取得方法',
|
||||
openInStudio: 'スタジオで開く',
|
||||
toolNameUsageTip: 'ツール呼び出し名、エージェントの推論とプロンプトの単語に使用されます',
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
@ -62,7 +62,7 @@ const translation = {
|
||||
run: '运行',
|
||||
firecrawlTitle: '使用 🔥Firecrawl 提取网页内容',
|
||||
firecrawlDoc: 'Firecrawl 文档',
|
||||
firecrawlDocLink: 'https://docs.dify.ai/v/zh-hans/guides/knowledge-base/sync_from_website',
|
||||
firecrawlDocLink: 'https://docs.dify.ai/v/zh-hans/guides/knowledge-base/sync-from-website',
|
||||
options: '选项',
|
||||
crawlSubPage: '爬取子页面',
|
||||
limit: '限制数量',
|
||||
|
Loading…
x
Reference in New Issue
Block a user