merge main

This commit is contained in:
Joel 2024-10-25 11:25:04 +08:00
parent ae00211691
commit bdb990eb90
375 changed files with 18637 additions and 7426 deletions

View File

@ -0,0 +1,20 @@
from typing import Optional
from pydantic import Field
from pydantic_settings import BaseSettings
class UpstashConfig(BaseSettings):
"""
Configuration settings for Upstash vector database
"""
UPSTASH_VECTOR_URL: Optional[str] = Field(
description="URL of the upstash server (e.g., 'https://vector.upstash.io')",
default=None,
)
UPSTASH_VECTOR_TOKEN: Optional[str] = Field(
description="Token for authenticating with the upstash server",
default=None,
)

View File

@ -1,18 +0,0 @@
import re
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 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(value=segments)

View File

@ -1,29 +0,0 @@
import enum
from typing import Any
from pydantic import BaseModel
class PromptMessageFileType(enum.Enum):
IMAGE = "image"
@staticmethod
def value_of(value):
for member in PromptMessageFileType:
if member.value == value:
return member
raise ValueError(f"No matching enum found for value '{value}'")
class PromptMessageFile(BaseModel):
type: PromptMessageFileType
data: Any = None
class ImagePromptMessageFile(PromptMessageFile):
class DETAIL(enum.Enum):
LOW = "low"
HIGH = "high"
type: PromptMessageFileType = PromptMessageFileType.IMAGE
detail: DETAIL = DETAIL.LOW

View File

@ -0,0 +1 @@
FILE_MODEL_IDENTITY = "__dify__file__"

55
api/core/file/enums.py Normal file
View File

@ -0,0 +1,55 @@
from enum import Enum
class FileType(str, Enum):
IMAGE = "image"
DOCUMENT = "document"
AUDIO = "audio"
VIDEO = "video"
CUSTOM = "custom"
@staticmethod
def value_of(value):
for member in FileType:
if member.value == value:
return member
raise ValueError(f"No matching enum found for value '{value}'")
class FileTransferMethod(str, Enum):
REMOTE_URL = "remote_url"
LOCAL_FILE = "local_file"
TOOL_FILE = "tool_file"
@staticmethod
def value_of(value):
for member in FileTransferMethod:
if member.value == value:
return member
raise ValueError(f"No matching enum found for value '{value}'")
class FileBelongsTo(str, Enum):
USER = "user"
ASSISTANT = "assistant"
@staticmethod
def value_of(value):
for member in FileBelongsTo:
if member.value == value:
return member
raise ValueError(f"No matching enum found for value '{value}'")
class FileAttribute(str, Enum):
TYPE = "type"
SIZE = "size"
NAME = "name"
MIME_TYPE = "mime_type"
TRANSFER_METHOD = "transfer_method"
URL = "url"
EXTENSION = "extension"
class ArrayFileAttribute(str, Enum):
LENGTH = "length"

View File

@ -0,0 +1,156 @@
import base64
from configs import dify_config
from core.file import file_repository
from core.helper import ssrf_proxy
from core.model_runtime.entities import AudioPromptMessageContent, ImagePromptMessageContent
from extensions.ext_database import db
from extensions.ext_storage import storage
from . import helpers
from .enums import FileAttribute
from .models import File, FileTransferMethod, FileType
from .tool_file_parser import ToolFileParser
def get_attr(*, file: File, attr: FileAttribute):
match attr:
case FileAttribute.TYPE:
return file.type.value
case FileAttribute.SIZE:
return file.size
case FileAttribute.NAME:
return file.filename
case FileAttribute.MIME_TYPE:
return file.mime_type
case FileAttribute.TRANSFER_METHOD:
return file.transfer_method.value
case FileAttribute.URL:
return file.remote_url
case FileAttribute.EXTENSION:
return file.extension
case _:
raise ValueError(f"Invalid file attribute: {attr}")
def to_prompt_message_content(f: File, /):
"""
Convert a File object to an ImagePromptMessageContent object.
This function takes a File object and converts it to an ImagePromptMessageContent
object, which can be used as a prompt for image-based AI models.
Args:
file (File): The File object to convert. Must be of type FileType.IMAGE.
Returns:
ImagePromptMessageContent: An object containing the image data and detail level.
Raises:
ValueError: If the file is not an image or if the file data is missing.
Note:
The detail level of the image prompt is determined by the file's extra_config.
If not specified, it defaults to ImagePromptMessageContent.DETAIL.LOW.
"""
match f.type:
case FileType.IMAGE:
if dify_config.MULTIMODAL_SEND_IMAGE_FORMAT == "url":
data = _to_url(f)
else:
data = _to_base64_data_string(f)
if f._extra_config and f._extra_config.image_config and f._extra_config.image_config.detail:
detail = f._extra_config.image_config.detail
else:
detail = ImagePromptMessageContent.DETAIL.LOW
return ImagePromptMessageContent(data=data, detail=detail)
case FileType.AUDIO:
encoded_string = _file_to_encoded_string(f)
if f.extension is None:
raise ValueError("Missing file extension")
return AudioPromptMessageContent(data=encoded_string, format=f.extension.lstrip("."))
case _:
raise ValueError(f"file type {f.type} is not supported")
def download(f: File, /):
upload_file = file_repository.get_upload_file(session=db.session(), file=f)
return _download_file_content(upload_file.key)
def _download_file_content(path: str, /):
"""
Download and return the contents of a file as bytes.
This function loads the file from storage and ensures it's in bytes format.
Args:
path (str): The path to the file in storage.
Returns:
bytes: The contents of the file as a bytes object.
Raises:
ValueError: If the loaded file is not a bytes object.
"""
data = storage.load(path, stream=False)
if not isinstance(data, bytes):
raise ValueError(f"file {path} is not a bytes object")
return data
def _get_encoded_string(f: File, /):
match f.transfer_method:
case FileTransferMethod.REMOTE_URL:
response = ssrf_proxy.get(f.remote_url)
response.raise_for_status()
content = response.content
encoded_string = base64.b64encode(content).decode("utf-8")
return encoded_string
case FileTransferMethod.LOCAL_FILE:
upload_file = file_repository.get_upload_file(session=db.session(), file=f)
data = _download_file_content(upload_file.key)
encoded_string = base64.b64encode(data).decode("utf-8")
return encoded_string
case FileTransferMethod.TOOL_FILE:
tool_file = file_repository.get_tool_file(session=db.session(), file=f)
data = _download_file_content(tool_file.file_key)
encoded_string = base64.b64encode(data).decode("utf-8")
return encoded_string
case _:
raise ValueError(f"Unsupported transfer method: {f.transfer_method}")
def _to_base64_data_string(f: File, /):
encoded_string = _get_encoded_string(f)
return f"data:{f.mime_type};base64,{encoded_string}"
def _file_to_encoded_string(f: File, /):
match f.type:
case FileType.IMAGE:
return _to_base64_data_string(f)
case FileType.AUDIO:
return _get_encoded_string(f)
case _:
raise ValueError(f"file type {f.type} is not supported")
def _to_url(f: File, /):
if f.transfer_method == FileTransferMethod.REMOTE_URL:
if f.remote_url is None:
raise ValueError("Missing file remote_url")
return f.remote_url
elif f.transfer_method == FileTransferMethod.LOCAL_FILE:
if f.related_id is None:
raise ValueError("Missing file related_id")
return helpers.get_signed_file_url(upload_file_id=f.related_id)
elif f.transfer_method == FileTransferMethod.TOOL_FILE:
# add sign url
if f.related_id is None or f.extension is None:
raise ValueError("Missing file related_id or extension")
return ToolFileParser.get_tool_file_manager().sign_file(tool_file_id=f.related_id, extension=f.extension)
else:
raise ValueError(f"Unsupported transfer method: {f.transfer_method}")

View File

@ -1,145 +0,0 @@
import enum
from typing import Any, Optional
from pydantic import BaseModel
from core.file.tool_file_parser import ToolFileParser
from core.file.upload_file_parser import UploadFileParser
from core.model_runtime.entities.message_entities import ImagePromptMessageContent
from extensions.ext_database import db
class FileExtraConfig(BaseModel):
"""
File Upload Entity.
"""
image_config: Optional[dict[str, Any]] = None
class FileType(enum.Enum):
IMAGE = "image"
@staticmethod
def value_of(value):
for member in FileType:
if member.value == value:
return member
raise ValueError(f"No matching enum found for value '{value}'")
class FileTransferMethod(enum.Enum):
REMOTE_URL = "remote_url"
LOCAL_FILE = "local_file"
TOOL_FILE = "tool_file"
@staticmethod
def value_of(value):
for member in FileTransferMethod:
if member.value == value:
return member
raise ValueError(f"No matching enum found for value '{value}'")
class FileBelongsTo(enum.Enum):
USER = "user"
ASSISTANT = "assistant"
@staticmethod
def value_of(value):
for member in FileBelongsTo:
if member.value == value:
return member
raise ValueError(f"No matching enum found for value '{value}'")
class FileVar(BaseModel):
id: Optional[str] = None # message file id
tenant_id: str
type: FileType
transfer_method: FileTransferMethod
url: Optional[str] = None # remote url
related_id: Optional[str] = None
extra_config: Optional[FileExtraConfig] = None
filename: Optional[str] = None
extension: Optional[str] = None
mime_type: Optional[str] = None
def to_dict(self) -> dict:
return {
"__variant": self.__class__.__name__,
"tenant_id": self.tenant_id,
"type": self.type.value,
"transfer_method": self.transfer_method.value,
"url": self.preview_url,
"remote_url": self.url,
"related_id": self.related_id,
"filename": self.filename,
"extension": self.extension,
"mime_type": self.mime_type,
}
def to_markdown(self) -> str:
"""
Convert file to markdown
:return:
"""
preview_url = self.preview_url
if self.type == FileType.IMAGE:
text = f'![{self.filename or ""}]({preview_url})'
else:
text = f"[{self.filename or preview_url}]({preview_url})"
return text
@property
def data(self) -> Optional[str]:
"""
Get image data, file signed url or base64 data
depending on config MULTIMODAL_SEND_IMAGE_FORMAT
:return:
"""
return self._get_data()
@property
def preview_url(self) -> Optional[str]:
"""
Get signed preview url
:return:
"""
return self._get_data(force_url=True)
@property
def prompt_message_content(self) -> ImagePromptMessageContent:
if self.type == FileType.IMAGE:
image_config = self.extra_config.image_config
return ImagePromptMessageContent(
data=self.data,
detail=ImagePromptMessageContent.DETAIL.HIGH
if image_config.get("detail") == "high"
else ImagePromptMessageContent.DETAIL.LOW,
)
def _get_data(self, force_url: bool = False) -> Optional[str]:
from models.model import UploadFile
if self.type == FileType.IMAGE:
if self.transfer_method == FileTransferMethod.REMOTE_URL:
return self.url
elif self.transfer_method == FileTransferMethod.LOCAL_FILE:
upload_file = (
db.session.query(UploadFile)
.filter(UploadFile.id == self.related_id, UploadFile.tenant_id == self.tenant_id)
.first()
)
return UploadFileParser.get_image_data(upload_file=upload_file, force_url=force_url)
elif self.transfer_method == FileTransferMethod.TOOL_FILE:
extension = self.extension
# add sign url
return ToolFileParser.get_tool_file_manager().sign_file(
tool_file_id=self.related_id, extension=extension
)
return None

View File

@ -0,0 +1,32 @@
from sqlalchemy import select
from sqlalchemy.orm import Session
from models import ToolFile, UploadFile
from .models import File
def get_upload_file(*, session: Session, file: File):
if file.related_id is None:
raise ValueError("Missing file related_id")
stmt = select(UploadFile).filter(
UploadFile.id == file.related_id,
UploadFile.tenant_id == file.tenant_id,
)
record = session.scalar(stmt)
if not record:
raise ValueError(f"upload file {file.related_id} not found")
return record
def get_tool_file(*, session: Session, file: File):
if file.related_id is None:
raise ValueError("Missing file related_id")
stmt = select(ToolFile).filter(
ToolFile.id == file.related_id,
ToolFile.tenant_id == file.tenant_id,
)
record = session.scalar(stmt)
if not record:
raise ValueError(f"tool file {file.related_id} not found")
return record

48
api/core/file/helpers.py Normal file
View File

@ -0,0 +1,48 @@
import base64
import hashlib
import hmac
import os
import time
from configs import dify_config
def get_signed_file_url(upload_file_id: str) -> str:
url = f"{dify_config.FILES_URL}/files/{upload_file_id}/file-preview"
timestamp = str(int(time.time()))
nonce = os.urandom(16).hex()
key = dify_config.SECRET_KEY.encode()
msg = f"file-preview|{upload_file_id}|{timestamp}|{nonce}"
sign = hmac.new(key, msg.encode(), hashlib.sha256).digest()
encoded_sign = base64.urlsafe_b64encode(sign).decode()
return f"{url}?timestamp={timestamp}&nonce={nonce}&sign={encoded_sign}"
def verify_image_signature(*, upload_file_id: str, timestamp: str, nonce: str, sign: str) -> bool:
data_to_sign = f"image-preview|{upload_file_id}|{timestamp}|{nonce}"
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()
# verify signature
if sign != recalculated_encoded_sign:
return False
current_time = int(time.time())
return current_time - int(timestamp) <= dify_config.FILES_ACCESS_TIMEOUT
def verify_file_signature(*, upload_file_id: str, timestamp: str, nonce: str, sign: str) -> bool:
data_to_sign = f"file-preview|{upload_file_id}|{timestamp}|{nonce}"
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()
# verify signature
if sign != recalculated_encoded_sign:
return False
current_time = int(time.time())
return current_time - int(timestamp) <= dify_config.FILES_ACCESS_TIMEOUT

View File

@ -1,243 +0,0 @@
import re
from collections.abc import Mapping, Sequence
from typing import Any, Union
from urllib.parse import parse_qs, urlparse
import requests
from core.file.file_obj import FileBelongsTo, FileExtraConfig, FileTransferMethod, FileType, FileVar
from extensions.ext_database import db
from models.account import Account
from models.model import EndUser, MessageFile, UploadFile
from services.file_service import IMAGE_EXTENSIONS
class MessageFileParser:
def __init__(self, tenant_id: str, app_id: str) -> None:
self.tenant_id = tenant_id
self.app_id = app_id
def validate_and_transform_files_arg(
self, files: Sequence[Mapping[str, Any]], file_extra_config: FileExtraConfig, user: Union[Account, EndUser]
) -> list[FileVar]:
"""
validate and transform files arg
:param files:
:param file_extra_config:
:param user:
:return:
"""
for file in files:
if not isinstance(file, dict):
raise ValueError("Invalid file format, must be dict")
if not file.get("type"):
raise ValueError("Missing file type")
FileType.value_of(file.get("type"))
if not file.get("transfer_method"):
raise ValueError("Missing file transfer method")
FileTransferMethod.value_of(file.get("transfer_method"))
if file.get("transfer_method") == FileTransferMethod.REMOTE_URL.value:
if not file.get("url"):
raise ValueError("Missing file url")
if not file.get("url").startswith("http"):
raise ValueError("Invalid file url")
if file.get("transfer_method") == FileTransferMethod.LOCAL_FILE.value and not file.get("upload_file_id"):
raise ValueError("Missing file upload_file_id")
if file.get("transform_method") == FileTransferMethod.TOOL_FILE.value and not file.get("tool_file_id"):
raise ValueError("Missing file tool_file_id")
# transform files to file objs
type_file_objs = self._to_file_objs(files, file_extra_config)
# validate files
new_files = []
for file_type, file_objs in type_file_objs.items():
if file_type == FileType.IMAGE:
# parse and validate files
image_config = file_extra_config.image_config
# check if image file feature is enabled
if not image_config:
continue
# Validate number of files
if len(files) > image_config["number_limits"]:
raise ValueError(f"Number of image files exceeds the maximum limit {image_config['number_limits']}")
for file_obj in file_objs:
# Validate transfer method
if file_obj.transfer_method.value not in image_config["transfer_methods"]:
raise ValueError(f"Invalid transfer method: {file_obj.transfer_method.value}")
# Validate file type
if file_obj.type != FileType.IMAGE:
raise ValueError(f"Invalid file type: {file_obj.type}")
if file_obj.transfer_method == FileTransferMethod.REMOTE_URL:
# check remote url valid and is image
result, error = self._check_image_remote_url(file_obj.url)
if result is False:
raise ValueError(error)
elif file_obj.transfer_method == FileTransferMethod.LOCAL_FILE:
# get upload file from upload_file_id
upload_file = (
db.session.query(UploadFile)
.filter(
UploadFile.id == file_obj.related_id,
UploadFile.tenant_id == self.tenant_id,
UploadFile.created_by == user.id,
UploadFile.created_by_role == ("account" if isinstance(user, Account) else "end_user"),
UploadFile.extension.in_(IMAGE_EXTENSIONS),
)
.first()
)
# check upload file is belong to tenant and user
if not upload_file:
raise ValueError("Invalid upload file")
new_files.append(file_obj)
# return all file objs
return new_files
def transform_message_files(self, files: list[MessageFile], file_extra_config: FileExtraConfig):
"""
transform message files
:param files:
:param file_extra_config:
:return:
"""
# transform files to file objs
type_file_objs = self._to_file_objs(files, file_extra_config)
# return all file objs
return [file_obj for file_objs in type_file_objs.values() for file_obj in file_objs]
def _to_file_objs(
self, files: list[Union[dict, MessageFile]], file_extra_config: FileExtraConfig
) -> dict[FileType, list[FileVar]]:
"""
transform files to file objs
:param files:
:param file_extra_config:
:return:
"""
type_file_objs: dict[FileType, list[FileVar]] = {
# Currently only support image
FileType.IMAGE: []
}
if not files:
return type_file_objs
# group by file type and convert file args or message files to FileObj
for file in files:
if isinstance(file, MessageFile):
if file.belongs_to == FileBelongsTo.ASSISTANT.value:
continue
file_obj = self._to_file_obj(file, file_extra_config)
if file_obj.type not in type_file_objs:
continue
type_file_objs[file_obj.type].append(file_obj)
return type_file_objs
def _to_file_obj(self, file: Union[dict, MessageFile], file_extra_config: FileExtraConfig):
"""
transform file to file obj
:param file:
:return:
"""
if isinstance(file, dict):
transfer_method = FileTransferMethod.value_of(file.get("transfer_method"))
if transfer_method != FileTransferMethod.TOOL_FILE:
return FileVar(
tenant_id=self.tenant_id,
type=FileType.value_of(file.get("type")),
transfer_method=transfer_method,
url=file.get("url") if transfer_method == FileTransferMethod.REMOTE_URL else None,
related_id=file.get("upload_file_id") if transfer_method == FileTransferMethod.LOCAL_FILE else None,
extra_config=file_extra_config,
)
return FileVar(
tenant_id=self.tenant_id,
type=FileType.value_of(file.get("type")),
transfer_method=transfer_method,
url=None,
related_id=file.get("tool_file_id"),
extra_config=file_extra_config,
)
else:
return FileVar(
id=file.id,
tenant_id=self.tenant_id,
type=FileType.value_of(file.type),
transfer_method=FileTransferMethod.value_of(file.transfer_method),
url=file.url,
related_id=file.upload_file_id or None,
extra_config=file_extra_config,
)
def _check_image_remote_url(self, url):
try:
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko)"
" Chrome/91.0.4472.124 Safari/537.36"
}
def is_s3_presigned_url(url):
try:
parsed_url = urlparse(url)
if "amazonaws.com" not in parsed_url.netloc:
return False
query_params = parse_qs(parsed_url.query)
def check_presign_v2(query_params):
required_params = ["Signature", "Expires"]
for param in required_params:
if param not in query_params:
return False
if not query_params["Expires"][0].isdigit():
return False
signature = query_params["Signature"][0]
if not re.match(r"^[A-Za-z0-9+/]+={0,2}$", signature):
return False
return True
def check_presign_v4(query_params):
required_params = ["X-Amz-Signature", "X-Amz-Expires"]
for param in required_params:
if param not in query_params:
return False
if not query_params["X-Amz-Expires"][0].isdigit():
return False
signature = query_params["X-Amz-Signature"][0]
if not re.match(r"^[A-Za-z0-9+/]+={0,2}$", signature):
return False
return True
return check_presign_v4(query_params) or check_presign_v2(query_params)
except Exception:
return False
if is_s3_presigned_url(url):
response = requests.get(url, headers=headers, allow_redirects=True)
if response.status_code in {200, 304}:
return True, ""
response = requests.head(url, headers=headers, allow_redirects=True)
if response.status_code in {200, 304}:
return True, ""
else:
return False, "URL does not exist."
except requests.RequestException as e:
return False, f"Error checking URL: {e}"

140
api/core/file/models.py Normal file
View File

@ -0,0 +1,140 @@
from collections.abc import Mapping, Sequence
from typing import Optional
from pydantic import BaseModel, Field, model_validator
from core.model_runtime.entities.message_entities import ImagePromptMessageContent
from . import helpers
from .constants import FILE_MODEL_IDENTITY
from .enums import FileTransferMethod, FileType
from .tool_file_parser import ToolFileParser
class ImageConfig(BaseModel):
"""
NOTE: This part of validation is deprecated, but still used in app features "Image Upload".
"""
number_limits: int = 0
transfer_methods: Sequence[FileTransferMethod] = Field(default_factory=list)
detail: ImagePromptMessageContent.DETAIL | None = None
class FileExtraConfig(BaseModel):
"""
File Upload Entity.
"""
image_config: Optional[ImageConfig] = None
allowed_file_types: Sequence[FileType] = Field(default_factory=list)
allowed_extensions: Sequence[str] = Field(default_factory=list)
allowed_upload_methods: Sequence[FileTransferMethod] = Field(default_factory=list)
number_limits: int = 0
class File(BaseModel):
dify_model_identity: str = FILE_MODEL_IDENTITY
id: Optional[str] = None # message file id
tenant_id: str
type: FileType
transfer_method: FileTransferMethod
remote_url: Optional[str] = None # remote url
related_id: Optional[str] = None
filename: Optional[str] = None
extension: Optional[str] = Field(default=None, description="File extension, should contains dot")
mime_type: Optional[str] = None
size: int = -1
_extra_config: FileExtraConfig | None = None
def to_dict(self) -> Mapping[str, str | int | None]:
data = self.model_dump(mode="json")
return {
**data,
"url": self.generate_url(),
}
@property
def markdown(self) -> str:
url = self.generate_url()
if self.type == FileType.IMAGE:
text = f'![{self.filename or ""}]({url})'
else:
text = f"[{self.filename or url}]({url})"
return text
def generate_url(self) -> Optional[str]:
if self.type == FileType.IMAGE:
if self.transfer_method == FileTransferMethod.REMOTE_URL:
return self.remote_url
elif self.transfer_method == FileTransferMethod.LOCAL_FILE:
if self.related_id is None:
raise ValueError("Missing file related_id")
return helpers.get_signed_file_url(upload_file_id=self.related_id)
elif self.transfer_method == FileTransferMethod.TOOL_FILE:
assert self.related_id is not None
assert self.extension is not None
return ToolFileParser.get_tool_file_manager().sign_file(
tool_file_id=self.related_id, extension=self.extension
)
else:
if self.transfer_method == FileTransferMethod.REMOTE_URL:
return self.remote_url
elif self.transfer_method == FileTransferMethod.LOCAL_FILE:
if self.related_id is None:
raise ValueError("Missing file related_id")
return helpers.get_signed_file_url(upload_file_id=self.related_id)
elif self.transfer_method == FileTransferMethod.TOOL_FILE:
assert self.related_id is not None
assert self.extension is not None
return ToolFileParser.get_tool_file_manager().sign_file(
tool_file_id=self.related_id, extension=self.extension
)
@model_validator(mode="after")
def validate_after(self):
match self.transfer_method:
case FileTransferMethod.REMOTE_URL:
if not self.remote_url:
raise ValueError("Missing file url")
if not isinstance(self.remote_url, str) or not self.remote_url.startswith("http"):
raise ValueError("Invalid file url")
case FileTransferMethod.LOCAL_FILE:
if not self.related_id:
raise ValueError("Missing file related_id")
case FileTransferMethod.TOOL_FILE:
if not self.related_id:
raise ValueError("Missing file related_id")
# Validate the extra config.
if not self._extra_config:
return self
if self._extra_config.allowed_file_types:
if self.type not in self._extra_config.allowed_file_types and self.type != FileType.CUSTOM:
raise ValueError(f"Invalid file type: {self.type}")
if self._extra_config.allowed_extensions and self.extension not in self._extra_config.allowed_extensions:
raise ValueError(f"Invalid file extension: {self.extension}")
if (
self._extra_config.allowed_upload_methods
and self.transfer_method not in self._extra_config.allowed_upload_methods
):
raise ValueError(f"Invalid transfer method: {self.transfer_method}")
match self.type:
case FileType.IMAGE:
# NOTE: This part of validation is deprecated, but still used in app features "Image Upload".
if not self._extra_config.image_config:
return self
# TODO: skip check if transfer_methods is empty, because many test cases are not setting this field
if (
self._extra_config.image_config.transfer_methods
and self.transfer_method not in self._extra_config.image_config.transfer_methods
):
raise ValueError(f"Invalid transfer method: {self.transfer_method}")
return self

View File

@ -1,79 +0,0 @@
import base64
import hashlib
import hmac
import logging
import os
import time
from typing import Optional
from configs import dify_config
from extensions.ext_storage import storage
IMAGE_EXTENSIONS = ["jpg", "jpeg", "png", "webp", "gif", "svg"]
IMAGE_EXTENSIONS.extend([ext.upper() for ext in IMAGE_EXTENSIONS])
class UploadFileParser:
@classmethod
def get_image_data(cls, upload_file, force_url: bool = False) -> Optional[str]:
if not upload_file:
return None
if upload_file.extension not in IMAGE_EXTENSIONS:
return None
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
try:
data = storage.load(upload_file.key)
except FileNotFoundError:
logging.error(f"File not found: {upload_file.key}")
return None
encoded_string = base64.b64encode(data).decode("utf-8")
return f"data:{upload_file.mime_type};base64,{encoded_string}"
@classmethod
def get_signed_temp_image_url(cls, upload_file_id) -> str:
"""
get signed url from upload file
:param upload_file: UploadFile object
:return:
"""
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 = dify_config.SECRET_KEY.encode()
sign = hmac.new(secret_key, data_to_sign.encode(), hashlib.sha256).digest()
encoded_sign = base64.urlsafe_b64encode(sign).decode()
return f"{image_preview_url}?timestamp={timestamp}&nonce={nonce}&sign={encoded_sign}"
@classmethod
def verify_image_file_signature(cls, upload_file_id: str, timestamp: str, nonce: str, sign: str) -> bool:
"""
verify signature
:param upload_file_id: file id
:param timestamp: timestamp
:param nonce: nonce
:param sign: signature
:return:
"""
data_to_sign = f"image-preview|{upload_file_id}|{timestamp}|{nonce}"
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()
# verify signature
if sign != recalculated_encoded_sign:
return False
current_time = int(time.time())
return current_time - int(timestamp) <= dify_config.FILES_ACCESS_TIMEOUT

View File

@ -0,0 +1,39 @@
model: claude-3-5-sonnet-20241022
label:
en_US: claude-3-5-sonnet-20241022
model_type: llm
features:
- agent-thought
- vision
- tool-call
- stream-tool-call
model_properties:
mode: chat
context_size: 200000
parameter_rules:
- name: temperature
use_template: temperature
- name: top_p
use_template: top_p
- name: top_k
label:
zh_Hans: 取样数量
en_US: Top k
type: int
help:
zh_Hans: 仅从每个后续标记的前 K 个选项中采样。
en_US: Only sample from the top K options for each subsequent token.
required: false
- name: max_tokens
use_template: max_tokens
required: true
default: 8192
min: 1
max: 8192
- name: response_format
use_template: response_format
pricing:
input: '3.00'
output: '15.00'
unit: '0.000001'
currency: USD

View File

@ -0,0 +1,60 @@
model: anthropic.claude-3-5-sonnet-20241022-v2:0
label:
en_US: Claude 3.5 Sonnet V2
model_type: llm
features:
- agent-thought
- vision
- tool-call
- stream-tool-call
model_properties:
mode: chat
context_size: 200000
# docs: https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-anthropic-claude-messages.html
parameter_rules:
- name: max_tokens
use_template: max_tokens
required: true
type: int
default: 4096
min: 1
max: 4096
help:
zh_Hans: 停止前生成的最大令牌数。请注意Anthropic Claude 模型可能会在达到 max_tokens 的值之前停止生成令牌。不同的 Anthropic Claude 模型对此参数具有不同的最大值。
en_US: The maximum number of tokens to generate before stopping. Note that Anthropic Claude models might stop generating tokens before reaching the value of max_tokens. Different Anthropic Claude models have different maximum values for this parameter.
- name: temperature
use_template: temperature
required: false
type: float
default: 1
min: 0.0
max: 1.0
help:
zh_Hans: 生成内容的随机性。
en_US: The amount of randomness injected into the response.
- name: top_p
required: false
type: float
default: 0.999
min: 0.000
max: 1.000
help:
zh_Hans: 在核采样中Anthropic Claude 按概率递减顺序计算每个后续标记的所有选项的累积分布,并在达到 top_p 指定的特定概率时将其切断。您应该更改温度或top_p但不能同时更改两者。
en_US: In nucleus sampling, Anthropic Claude computes the cumulative distribution over all the options for each subsequent token in decreasing probability order and cuts it off once it reaches a particular probability specified by top_p. You should alter either temperature or top_p, but not both.
- name: top_k
required: false
type: int
default: 0
min: 0
# tip docs from aws has error, max value is 500
max: 500
help:
zh_Hans: 对于每个后续标记,仅从前 K 个选项中进行采样。使用 top_k 删除长尾低概率响应。
en_US: Only sample from the top K options for each subsequent token. Use top_k to remove long tail low probability responses.
- name: response_format
use_template: response_format
pricing:
input: '0.003'
output: '0.015'
unit: '0.001'
currency: USD

View File

@ -0,0 +1,60 @@
model: eu.anthropic.claude-3-5-sonnet-20241022-v2:0
label:
en_US: Claude 3.5 Sonnet V2(EU.Cross Region Inference)
model_type: llm
features:
- agent-thought
- vision
- tool-call
- stream-tool-call
model_properties:
mode: chat
context_size: 200000
# docs: https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-anthropic-claude-messages.html
parameter_rules:
- name: max_tokens
use_template: max_tokens
required: true
type: int
default: 4096
min: 1
max: 4096
help:
zh_Hans: 停止前生成的最大令牌数。请注意Anthropic Claude 模型可能会在达到 max_tokens 的值之前停止生成令牌。不同的 Anthropic Claude 模型对此参数具有不同的最大值。
en_US: The maximum number of tokens to generate before stopping. Note that Anthropic Claude models might stop generating tokens before reaching the value of max_tokens. Different Anthropic Claude models have different maximum values for this parameter.
- name: temperature
use_template: temperature
required: false
type: float
default: 1
min: 0.0
max: 1.0
help:
zh_Hans: 生成内容的随机性。
en_US: The amount of randomness injected into the response.
- name: top_p
required: false
type: float
default: 0.999
min: 0.000
max: 1.000
help:
zh_Hans: 在核采样中Anthropic Claude 按概率递减顺序计算每个后续标记的所有选项的累积分布,并在达到 top_p 指定的特定概率时将其切断。您应该更改温度或top_p但不能同时更改两者。
en_US: In nucleus sampling, Anthropic Claude computes the cumulative distribution over all the options for each subsequent token in decreasing probability order and cuts it off once it reaches a particular probability specified by top_p. You should alter either temperature or top_p, but not both.
- name: top_k
required: false
type: int
default: 0
min: 0
# tip docs from aws has error, max value is 500
max: 500
help:
zh_Hans: 对于每个后续标记,仅从前 K 个选项中进行采样。使用 top_k 删除长尾低概率响应。
en_US: Only sample from the top K options for each subsequent token. Use top_k to remove long tail low probability responses.
- name: response_format
use_template: response_format
pricing:
input: '0.003'
output: '0.015'
unit: '0.001'
currency: USD

View File

@ -0,0 +1,60 @@
model: us.anthropic.claude-3-5-sonnet-20241022-v2:0
label:
en_US: Claude 3.5 Sonnet V2(US.Cross Region Inference)
model_type: llm
features:
- agent-thought
- vision
- tool-call
- stream-tool-call
model_properties:
mode: chat
context_size: 200000
# docs: https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-anthropic-claude-messages.html
parameter_rules:
- name: max_tokens
use_template: max_tokens
required: true
type: int
default: 4096
min: 1
max: 4096
help:
zh_Hans: 停止前生成的最大令牌数。请注意Anthropic Claude 模型可能会在达到 max_tokens 的值之前停止生成令牌。不同的 Anthropic Claude 模型对此参数具有不同的最大值。
en_US: The maximum number of tokens to generate before stopping. Note that Anthropic Claude models might stop generating tokens before reaching the value of max_tokens. Different Anthropic Claude models have different maximum values for this parameter.
- name: temperature
use_template: temperature
required: false
type: float
default: 1
min: 0.0
max: 1.0
help:
zh_Hans: 生成内容的随机性。
en_US: The amount of randomness injected into the response.
- name: top_p
required: false
type: float
default: 0.999
min: 0.000
max: 1.000
help:
zh_Hans: 在核采样中Anthropic Claude 按概率递减顺序计算每个后续标记的所有选项的累积分布,并在达到 top_p 指定的特定概率时将其切断。您应该更改温度或top_p但不能同时更改两者。
en_US: In nucleus sampling, Anthropic Claude computes the cumulative distribution over all the options for each subsequent token in decreasing probability order and cuts it off once it reaches a particular probability specified by top_p. You should alter either temperature or top_p, but not both.
- name: top_k
required: false
type: int
default: 0
min: 0
# tip docs from aws has error, max value is 500
max: 500
help:
zh_Hans: 对于每个后续标记,仅从前 K 个选项中进行采样。使用 top_k 删除长尾低概率响应。
en_US: Only sample from the top K options for each subsequent token. Use top_k to remove long tail low probability responses.
- name: response_format
use_template: response_format
pricing:
input: '0.003'
output: '0.015'
unit: '0.001'
currency: USD

View File

@ -0,0 +1,26 @@
model: llama-3.2-11b-vision-preview
label:
zh_Hans: Llama 3.2 11B Vision (Preview)
en_US: Llama 3.2 11B Vision (Preview)
model_type: llm
features:
- agent-thought
- vision
model_properties:
mode: chat
context_size: 131072
parameter_rules:
- name: temperature
use_template: temperature
- name: top_p
use_template: top_p
- name: max_tokens
use_template: max_tokens
default: 512
min: 1
max: 8192
pricing:
input: '0.05'
output: '0.1'
unit: '0.000001'
currency: USD

View File

@ -0,0 +1,26 @@
model: llama-3.2-90b-vision-preview
label:
zh_Hans: Llama 3.2 90B Vision (Preview)
en_US: Llama 3.2 90B Vision (Preview)
model_type: llm
features:
- agent-thought
- vision
model_properties:
mode: chat
context_size: 131072
parameter_rules:
- name: temperature
use_template: temperature
- name: top_p
use_template: top_p
- name: max_tokens
use_template: max_tokens
default: 512
min: 1
max: 8192
pricing:
input: '0.05'
output: '0.1'
unit: '0.000001'
currency: USD

View File

@ -0,0 +1,5 @@
model: distil-whisper-large-v3-en
model_type: speech2text
model_properties:
file_upload_limit: 1
supported_file_extensions: flac,mp3,mp4,mpeg,mpga,m4a,ogg,wav,webm

View File

@ -0,0 +1,30 @@
from typing import IO, Optional
from core.model_runtime.model_providers.openai_api_compatible.speech2text.speech2text import OAICompatSpeech2TextModel
class GroqSpeech2TextModel(OAICompatSpeech2TextModel):
"""
Model class for Groq Speech to text model.
"""
def _invoke(self, model: str, credentials: dict, file: IO[bytes], user: Optional[str] = None) -> str:
"""
Invoke speech2text model
:param model: model name
:param credentials: model credentials
:param file: audio file
:param user: unique user id
:return: text for given audio file
"""
self._add_custom_parameters(credentials)
return super()._invoke(model, credentials, file)
def validate_credentials(self, model: str, credentials: dict) -> None:
self._add_custom_parameters(credentials)
return super().validate_credentials(model, credentials)
@classmethod
def _add_custom_parameters(cls, credentials: dict) -> None:
credentials["endpoint_url"] = "https://api.groq.com/openai/v1"

View File

@ -0,0 +1,5 @@
model: whisper-large-v3-turbo
model_type: speech2text
model_properties:
file_upload_limit: 1
supported_file_extensions: flac,mp3,mp4,mpeg,mpga,m4a,ogg,wav,webm

View File

@ -0,0 +1,5 @@
model: whisper-large-v3
model_type: speech2text
model_properties:
file_upload_limit: 1
supported_file_extensions: flac,mp3,mp4,mpeg,mpga,m4a,ogg,wav,webm

View File

@ -0,0 +1,44 @@
model: gpt-4o-audio-preview
label:
zh_Hans: gpt-4o-audio-preview
en_US: gpt-4o-audio-preview
model_type: llm
features:
- multi-tool-call
- agent-thought
- stream-tool-call
- vision
model_properties:
mode: chat
context_size: 128000
parameter_rules:
- name: temperature
use_template: temperature
- name: top_p
use_template: top_p
- name: presence_penalty
use_template: presence_penalty
- name: frequency_penalty
use_template: frequency_penalty
- name: max_tokens
use_template: max_tokens
default: 512
min: 1
max: 4096
- name: response_format
label:
zh_Hans: 回复格式
en_US: Response Format
type: string
help:
zh_Hans: 指定模型必须输出的格式
en_US: specifying the format that the model must output
required: false
options:
- text
- json_object
pricing:
input: '5.00'
output: '15.00'
unit: '0.000001'
currency: USD

View File

@ -0,0 +1,159 @@
from json import dumps
from typing import Optional
import httpx
from requests import post
from yarl import URL
from core.model_runtime.entities.common_entities import I18nObject
from core.model_runtime.entities.model_entities import AIModelEntity, FetchFrom, ModelType
from core.model_runtime.entities.rerank_entities import RerankDocument, RerankResult
from core.model_runtime.errors.invoke import (
InvokeAuthorizationError,
InvokeBadRequestError,
InvokeConnectionError,
InvokeError,
InvokeRateLimitError,
InvokeServerUnavailableError,
)
from core.model_runtime.errors.validate import CredentialsValidateFailedError
from core.model_runtime.model_providers.__base.rerank_model import RerankModel
class OAICompatRerankModel(RerankModel):
"""
rerank model API is compatible with Jina rerank model API. So copy the JinaRerankModel class code here.
we need enhance for llama.cpp , which return raw score, not normalize score 0~1. It seems Dify need it
"""
def _invoke(
self,
model: str,
credentials: dict,
query: str,
docs: list[str],
score_threshold: Optional[float] = None,
top_n: Optional[int] = None,
user: Optional[str] = None,
) -> RerankResult:
"""
Invoke rerank model
:param model: model name
:param credentials: model credentials
:param query: search query
:param docs: docs for reranking
:param score_threshold: score threshold
:param top_n: top n documents to return
:param user: unique user id
:return: rerank result
"""
if len(docs) == 0:
return RerankResult(model=model, docs=[])
server_url = credentials["endpoint_url"]
model_name = model
if not server_url:
raise CredentialsValidateFailedError("server_url is required")
if not model_name:
raise CredentialsValidateFailedError("model_name is required")
url = server_url
headers = {"Authorization": f"Bearer {credentials.get('api_key')}", "Content-Type": "application/json"}
# TODO: Do we need truncate docs to avoid llama.cpp return error?
data = {"model": model_name, "query": query, "documents": docs, "top_n": top_n}
try:
response = post(str(URL(url) / "rerank"), headers=headers, data=dumps(data), timeout=60)
response.raise_for_status()
results = response.json()
rerank_documents = []
scores = [result["relevance_score"] for result in results["results"]]
# Min-Max Normalization: Normalize scores to 0 ~ 1.0 range
min_score = min(scores)
max_score = max(scores)
score_range = max_score - min_score if max_score != min_score else 1.0 # Avoid division by zero
for result in results["results"]:
index = result["index"]
# Retrieve document text (fallback if llama.cpp rerank doesn't return it)
text = result.get("document", {}).get("text", docs[index])
# Normalize the score
normalized_score = (result["relevance_score"] - min_score) / score_range
# Create RerankDocument object with normalized score
rerank_document = RerankDocument(
index=index,
text=text,
score=normalized_score,
)
# Apply threshold (if defined)
if score_threshold is None or normalized_score >= score_threshold:
rerank_documents.append(rerank_document)
# Sort rerank_documents by normalized score in descending order
rerank_documents.sort(key=lambda doc: doc.score, reverse=True)
return RerankResult(model=model, docs=rerank_documents)
except httpx.HTTPStatusError as e:
raise InvokeServerUnavailableError(str(e))
def validate_credentials(self, model: str, credentials: dict) -> None:
"""
Validate model credentials
:param model: model name
:param credentials: model credentials
:return:
"""
try:
self._invoke(
model=model,
credentials=credentials,
query="What is the capital of the United States?",
docs=[
"Carson City is the capital city of the American state of Nevada. At the 2010 United States "
"Census, Carson City had a population of 55,274.",
"The Commonwealth of the Northern Mariana Islands is a group of islands in the Pacific Ocean that "
"are a political division controlled by the United States. Its capital is Saipan.",
],
score_threshold=0.8,
)
except Exception as ex:
raise CredentialsValidateFailedError(str(ex))
@property
def _invoke_error_mapping(self) -> dict[type[InvokeError], list[type[Exception]]]:
"""
Map model invoke error to unified error
"""
return {
InvokeConnectionError: [httpx.ConnectError],
InvokeServerUnavailableError: [httpx.RemoteProtocolError],
InvokeRateLimitError: [],
InvokeAuthorizationError: [httpx.HTTPStatusError],
InvokeBadRequestError: [httpx.RequestError],
}
def get_customizable_model_schema(self, model: str, credentials: dict) -> AIModelEntity:
"""
generate custom model entities from credentials
"""
entity = AIModelEntity(
model=model,
label=I18nObject(en_US=model),
model_type=ModelType.RERANK,
fetch_from=FetchFrom.CUSTOMIZABLE_MODEL,
model_properties={},
)
return entity

View File

@ -0,0 +1,55 @@
model: claude-3-5-sonnet-v2@20241022
label:
en_US: Claude 3.5 Sonnet v2
model_type: llm
features:
- agent-thought
- vision
model_properties:
mode: chat
context_size: 200000
parameter_rules:
- name: max_tokens
use_template: max_tokens
required: true
type: int
default: 4096
min: 1
max: 4096
help:
zh_Hans: 停止前生成的最大令牌数。请注意Anthropic Claude 模型可能会在达到 max_tokens 的值之前停止生成令牌。不同的 Anthropic Claude 模型对此参数具有不同的最大值。
en_US: The maximum number of tokens to generate before stopping. Note that Anthropic Claude models might stop generating tokens before reaching the value of max_tokens. Different Anthropic Claude models have different maximum values for this parameter.
- name: temperature
use_template: temperature
required: false
type: float
default: 1
min: 0.0
max: 1.0
help:
zh_Hans: 生成内容的随机性。
en_US: The amount of randomness injected into the response.
- name: top_p
required: false
type: float
default: 0.999
min: 0.000
max: 1.000
help:
zh_Hans: 在核采样中Anthropic Claude 按概率递减顺序计算每个后续标记的所有选项的累积分布,并在达到 top_p 指定的特定概率时将其切断。您应该更改温度或top_p但不能同时更改两者。
en_US: In nucleus sampling, Anthropic Claude computes the cumulative distribution over all the options for each subsequent token in decreasing probability order and cuts it off once it reaches a particular probability specified by top_p. You should alter either temperature or top_p, but not both.
- name: top_k
required: false
type: int
default: 0
min: 0
# tip docs from aws has error, max value is 500
max: 500
help:
zh_Hans: 对于每个后续标记,仅从前 K 个选项中进行采样。使用 top_k 删除长尾低概率响应。
en_US: Only sample from the top K options for each subsequent token. Use top_k to remove long tail low probability responses.
pricing:
input: '0.003'
output: '0.015'
unit: '0.001'
currency: USD

View File

@ -0,0 +1,129 @@
import json
from typing import Any
from uuid import uuid4
from pydantic import BaseModel, model_validator
from upstash_vector import Index, Vector
from configs import dify_config
from core.rag.datasource.vdb.vector_base import BaseVector
from core.rag.datasource.vdb.vector_factory import AbstractVectorFactory
from core.rag.datasource.vdb.vector_type import VectorType
from core.rag.embedding.embedding_base import Embeddings
from core.rag.models.document import Document
from models.dataset import Dataset
class UpstashVectorConfig(BaseModel):
url: str
token: str
@model_validator(mode="before")
@classmethod
def validate_config(cls, values: dict) -> dict:
if not values["url"]:
raise ValueError("Upstash URL is required")
if not values["token"]:
raise ValueError("Upstash Token is required")
return values
class UpstashVector(BaseVector):
def __init__(self, collection_name: str, config: UpstashVectorConfig):
super().__init__(collection_name)
self._table_name = collection_name
self.index = Index(url=config.url, token=config.token)
def _get_index_dimension(self) -> int:
index_info = self.index.info()
if index_info and index_info.dimension:
return index_info.dimension
else:
return 1536
def create(self, texts: list[Document], embeddings: list[list[float]], **kwargs):
self.add_texts(texts, embeddings)
def add_texts(self, documents: list[Document], embeddings: list[list[float]], **kwargs):
vectors = [
Vector(
id=str(uuid4()),
vector=embedding,
metadata=doc.metadata,
data=doc.page_content,
)
for doc, embedding in zip(documents, embeddings)
]
self.index.upsert(vectors=vectors)
def text_exists(self, id: str) -> bool:
response = self.get_ids_by_metadata_field("doc_id", id)
return len(response) > 0
def delete_by_ids(self, ids: list[str]) -> None:
item_ids = []
for doc_id in ids:
ids = self.get_ids_by_metadata_field("doc_id", doc_id)
if id:
item_ids += ids
self._delete_by_ids(ids=item_ids)
def _delete_by_ids(self, ids: list[str]) -> None:
if ids:
self.index.delete(ids=ids)
def get_ids_by_metadata_field(self, key: str, value: str) -> list[str]:
query_result = self.index.query(
vector=[1.001 * i for i in range(self._get_index_dimension())],
include_metadata=True,
top_k=1000,
filter=f"{key} = '{value}'",
)
return [result.id for result in query_result]
def delete_by_metadata_field(self, key: str, value: str) -> None:
ids = self.get_ids_by_metadata_field(key, value)
if ids:
self._delete_by_ids(ids)
def search_by_vector(self, query_vector: list[float], **kwargs: Any) -> list[Document]:
top_k = kwargs.get("top_k", 4)
result = self.index.query(vector=query_vector, top_k=top_k, include_metadata=True, include_data=True)
docs = []
score_threshold = float(kwargs.get("score_threshold") or 0.0)
for record in result:
metadata = record.metadata
text = record.data
score = record.score
metadata["score"] = score
if score > score_threshold:
docs.append(Document(page_content=text, metadata=metadata))
return docs
def search_by_full_text(self, query: str, **kwargs: Any) -> list[Document]:
return []
def delete(self) -> None:
self.index.reset()
def get_type(self) -> str:
return VectorType.UPSTASH
class UpstashVectorFactory(AbstractVectorFactory):
def init_vector(self, dataset: Dataset, attributes: list, embeddings: Embeddings) -> UpstashVector:
if dataset.index_struct_dict:
class_prefix: str = dataset.index_struct_dict["vector_store"]["class_prefix"]
collection_name = class_prefix.lower()
else:
dataset_id = dataset.id
collection_name = Dataset.gen_collection_name_by_id(dataset_id).lower()
dataset.index_struct = json.dumps(self.gen_index_struct_dict(VectorType.UPSTASH, collection_name))
return UpstashVector(
collection_name=collection_name,
config=UpstashVectorConfig(
url=dify_config.UPSTASH_VECTOR_URL,
token=dify_config.UPSTASH_VECTOR_TOKEN,
),
)

View File

@ -0,0 +1,47 @@
import logging
from core.rag.extractor.extractor_base import BaseExtractor
from core.rag.models.document import Document
logger = logging.getLogger(__name__)
class UnstructuredPDFExtractor(BaseExtractor):
"""Load pdf files.
Args:
file_path: Path to the file to load.
api_url: Unstructured API URL
api_key: Unstructured API Key
"""
def __init__(self, file_path: str, api_url: str, api_key: str):
"""Initialize with file path."""
self._file_path = file_path
self._api_url = api_url
self._api_key = api_key
def extract(self) -> list[Document]:
if self._api_url:
from unstructured.partition.api import partition_via_api
elements = partition_via_api(
filename=self._file_path, api_url=self._api_url, api_key=self._api_key, strategy="auto"
)
else:
from unstructured.partition.pdf import partition_pdf
elements = partition_pdf(filename=self._file_path, strategy="auto")
from unstructured.chunking.title import chunk_by_title
chunks = chunk_by_title(elements, max_characters=2000, combine_text_under_n_chars=2000)
documents = []
for chunk in chunks:
text = chunk.text.strip()
documents.append(Document(page_content=text))
return documents

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

@ -0,0 +1,19 @@
from core.tools.errors import ToolProviderCredentialValidationError
from core.tools.provider.builtin.aliyuque.tools.base import AliYuqueTool
from core.tools.provider.builtin_tool_provider import BuiltinToolProviderController
class AliYuqueProvider(BuiltinToolProviderController):
def _validate_credentials(self, credentials: dict) -> None:
token = credentials.get("token")
if not token:
raise ToolProviderCredentialValidationError("token is required")
try:
resp = AliYuqueTool.auth(token)
if resp and resp.get("data", {}).get("id"):
return
raise ToolProviderCredentialValidationError(resp)
except Exception as e:
raise ToolProviderCredentialValidationError(str(e))

View File

@ -0,0 +1,29 @@
identity:
author: 佐井
name: aliyuque
label:
en_US: yuque
zh_Hans: 语雀
pt_BR: yuque
description:
en_US: Yuque, https://www.yuque.com.
zh_Hans: 语雀https://www.yuque.com。
pt_BR: Yuque, https://www.yuque.com.
icon: icon.svg
tags:
- productivity
- search
credentials_for_provider:
token:
type: secret-input
required: true
label:
en_US: Yuque Team Token
zh_Hans: 语雀团队Token
placeholder:
en_US: Please input your Yuque team token
zh_Hans: 请输入你的语雀团队Token
help:
en_US: Get Alibaba Yuque team token
zh_Hans: 先获取语雀团队Token
url: https://www.yuque.com/settings/tokens

View File

@ -0,0 +1,50 @@
"""
语雀客户端
"""
__author__ = "佐井"
__created__ = "2024-06-01 09:45:20"
from typing import Any
import requests
class AliYuqueTool:
# yuque service url
server_url = "https://www.yuque.com"
@staticmethod
def auth(token):
session = requests.Session()
session.headers.update({"Accept": "application/json", "X-Auth-Token": token})
login = session.request("GET", AliYuqueTool.server_url + "/api/v2/user")
login.raise_for_status()
resp = login.json()
return resp
def request(self, method: str, token, tool_parameters: dict[str, Any], path: str) -> str:
if not token:
raise Exception("token is required")
session = requests.Session()
session.headers.update({"accept": "application/json", "X-Auth-Token": token})
new_params = {**tool_parameters}
# 找出需要替换的变量
replacements = {k: v for k, v in new_params.items() if f"{{{k}}}" in path}
# 替换 path 中的变量
for key, value in replacements.items():
path = path.replace(f"{{{key}}}", str(value))
del new_params[key] # 从 kwargs 中删除已经替换的变量
# 请求接口
if method.upper() in {"POST", "PUT"}:
session.headers.update(
{
"Content-Type": "application/json",
}
)
response = session.request(method.upper(), self.server_url + path, json=new_params)
else:
response = session.request(method, self.server_url + path, params=new_params)
response.raise_for_status()
return response.text

View File

@ -0,0 +1,22 @@
"""
创建文档
"""
__author__ = "佐井"
__created__ = "2024-06-01 10:45:20"
from typing import Any, Union
from core.tools.entities.tool_entities import ToolInvokeMessage
from core.tools.provider.builtin.aliyuque.tools.base import AliYuqueTool
from core.tools.tool.builtin_tool import BuiltinTool
class AliYuqueCreateDocumentTool(AliYuqueTool, BuiltinTool):
def _invoke(
self, user_id: str, tool_parameters: dict[str, Any]
) -> Union[ToolInvokeMessage, list[ToolInvokeMessage]]:
token = self.runtime.credentials.get("token", None)
if not token:
raise Exception("token is required")
return self.create_text_message(self.request("POST", token, tool_parameters, "/api/v2/repos/{book_id}/docs"))

View File

@ -0,0 +1,99 @@
identity:
name: aliyuque_create_document
author: 佐井
label:
en_US: Create Document
zh_Hans: 创建文档
icon: icon.svg
description:
human:
en_US: Creates a new document within a knowledge base without automatic addition to the table of contents. Requires a subsequent call to the "knowledge base directory update API". Supports setting visibility, format, and content. # 接口英文描述
zh_Hans: 在知识库中创建新文档,但不会自动加入目录,需额外调用“知识库目录更新接口”。允许设置公开性、格式及正文内容。
llm: Creates docs in a KB.
parameters:
- name: book_id
type: number
required: true
form: llm
label:
en_US: Knowledge Base ID
zh_Hans: 知识库ID
human_description:
en_US: The unique identifier of the knowledge base where the document will be created.
zh_Hans: 文档将被创建的知识库的唯一标识。
llm_description: ID of the target knowledge base.
- name: title
type: string
required: false
form: llm
label:
en_US: Title
zh_Hans: 标题
human_description:
en_US: The title of the document, defaults to 'Untitled' if not provided.
zh_Hans: 文档标题,默认为'无标题'如未提供。
llm_description: Title of the document, defaults to 'Untitled'.
- name: public
type: select
required: false
form: llm
options:
- value: 0
label:
en_US: Private
zh_Hans: 私密
- value: 1
label:
en_US: Public
zh_Hans: 公开
- value: 2
label:
en_US: Enterprise-only
zh_Hans: 企业内公开
label:
en_US: Visibility
zh_Hans: 公开性
human_description:
en_US: Document visibility (0 Private, 1 Public, 2 Enterprise-only).
zh_Hans: 文档可见性0 私密, 1 公开, 2 企业内公开)。
llm_description: Doc visibility options, 0-private, 1-public, 2-enterprise.
- name: format
type: select
required: false
form: llm
options:
- value: markdown
label:
en_US: markdown
zh_Hans: markdown
- value: html
label:
en_US: html
zh_Hans: html
- value: lake
label:
en_US: lake
zh_Hans: lake
label:
en_US: Content Format
zh_Hans: 内容格式
human_description:
en_US: Format of the document content (markdown, HTML, Lake).
zh_Hans: 文档内容格式markdown, HTML, Lake
llm_description: Content format choices, markdown, HTML, Lake.
- name: body
type: string
required: true
form: llm
label:
en_US: Body Content
zh_Hans: 正文内容
human_description:
en_US: The actual content of the document.
zh_Hans: 文档的实际内容。
llm_description: Content of the document.

View File

@ -0,0 +1,25 @@
#!/usr/bin/env python3
"""
删除文档
"""
__author__ = "佐井"
__created__ = "2024-09-17 22:04"
from typing import Any, Union
from core.tools.entities.tool_entities import ToolInvokeMessage
from core.tools.provider.builtin.aliyuque.tools.base import AliYuqueTool
from core.tools.tool.builtin_tool import BuiltinTool
class AliYuqueDeleteDocumentTool(AliYuqueTool, BuiltinTool):
def _invoke(
self, user_id: str, tool_parameters: dict[str, Any]
) -> Union[ToolInvokeMessage, list[ToolInvokeMessage]]:
token = self.runtime.credentials.get("token", None)
if not token:
raise Exception("token is required")
return self.create_text_message(
self.request("DELETE", token, tool_parameters, "/api/v2/repos/{book_id}/docs/{id}")
)

View File

@ -0,0 +1,37 @@
identity:
name: aliyuque_delete_document
author: 佐井
label:
en_US: Delete Document
zh_Hans: 删除文档
icon: icon.svg
description:
human:
en_US: Delete Document
zh_Hans: 根据id删除文档
llm: Delete document.
parameters:
- name: book_id
type: number
required: true
form: llm
label:
en_US: Knowledge Base ID
zh_Hans: 知识库ID
human_description:
en_US: The unique identifier of the knowledge base where the document will be created.
zh_Hans: 文档将被创建的知识库的唯一标识。
llm_description: ID of the target knowledge base.
- name: id
type: string
required: true
form: llm
label:
en_US: Document ID or Path
zh_Hans: 文档 ID or 路径
human_description:
en_US: Document ID or path.
zh_Hans: 文档 ID or 路径。
llm_description: Document ID or path.

View File

@ -0,0 +1,24 @@
"""
获取知识库首页
"""
__author__ = "佐井"
__created__ = "2024-06-01 22:57:14"
from typing import Any, Union
from core.tools.entities.tool_entities import ToolInvokeMessage
from core.tools.provider.builtin.aliyuque.tools.base import AliYuqueTool
from core.tools.tool.builtin_tool import BuiltinTool
class AliYuqueDescribeBookIndexPageTool(AliYuqueTool, BuiltinTool):
def _invoke(
self, user_id: str, tool_parameters: dict[str, Any]
) -> Union[ToolInvokeMessage, list[ToolInvokeMessage]]:
token = self.runtime.credentials.get("token", None)
if not token:
raise Exception("token is required")
return self.create_text_message(
self.request("GET", token, tool_parameters, "/api/v2/repos/{group_login}/{book_slug}/index_page")
)

View File

@ -0,0 +1,38 @@
identity:
name: aliyuque_describe_book_index_page
author: 佐井
label:
en_US: Get Repo Index Page
zh_Hans: 获取知识库首页
icon: icon.svg
description:
human:
en_US: Retrieves the homepage of a knowledge base within a group, supporting both book ID and group login with book slug access.
zh_Hans: 获取团队中知识库的首页信息可通过书籍ID或团队登录名与书籍路径访问。
llm: Fetches the knowledge base homepage using group and book identifiers with support for alternate access paths.
parameters:
- name: group_login
type: string
required: true
form: llm
label:
en_US: Group Login
zh_Hans: 团队登录名
human_description:
en_US: The login name of the group that owns the knowledge base.
zh_Hans: 拥有该知识库的团队登录名。
llm_description: Team login identifier for the knowledge base owner.
- name: book_slug
type: string
required: true
form: llm
label:
en_US: Book Slug
zh_Hans: 知识库路径
human_description:
en_US: The unique slug representing the path of the knowledge base.
zh_Hans: 知识库的唯一路径标识。
llm_description: Unique path identifier for the knowledge base.

View File

@ -0,0 +1,23 @@
#!/usr/bin/env python3
"""
获取知识库目录
"""
__author__ = "佐井"
__created__ = "2024-09-17 15:17:11"
from typing import Any, Union
from core.tools.entities.tool_entities import ToolInvokeMessage
from core.tools.provider.builtin.aliyuque.tools.base import AliYuqueTool
from core.tools.tool.builtin_tool import BuiltinTool
class YuqueDescribeBookTableOfContentsTool(AliYuqueTool, BuiltinTool):
def _invoke(
self, user_id: str, tool_parameters: dict[str, Any]
) -> (Union)[ToolInvokeMessage, list[ToolInvokeMessage]]:
token = self.runtime.credentials.get("token", None)
if not token:
raise Exception("token is required")
return self.create_text_message(self.request("GET", token, tool_parameters, "/api/v2/repos/{book_id}/toc"))

View File

@ -0,0 +1,25 @@
identity:
name: aliyuque_describe_book_table_of_contents
author: 佐井
label:
en_US: Get Book's Table of Contents
zh_Hans: 获取知识库的目录
icon: icon.svg
description:
human:
en_US: Get Book's Table of Contents.
zh_Hans: 获取知识库的目录。
llm: Get Book's Table of Contents.
parameters:
- name: book_id
type: number
required: true
form: llm
label:
en_US: Book ID
zh_Hans: 知识库 ID
human_description:
en_US: Book ID.
zh_Hans: 知识库 ID。
llm_description: Book ID.

View File

@ -0,0 +1,61 @@
"""
获取文档
"""
__author__ = "佐井"
__created__ = "2024-06-02 07:11:45"
import json
from typing import Any, Union
from urllib.parse import urlparse
from core.tools.entities.tool_entities import ToolInvokeMessage
from core.tools.provider.builtin.aliyuque.tools.base import AliYuqueTool
from core.tools.tool.builtin_tool import BuiltinTool
class AliYuqueDescribeDocumentContentTool(AliYuqueTool, BuiltinTool):
def _invoke(
self, user_id: str, tool_parameters: dict[str, Any]
) -> Union[ToolInvokeMessage, list[ToolInvokeMessage]]:
new_params = {**tool_parameters}
token = new_params.pop("token")
if not token or token.lower() == "none":
token = self.runtime.credentials.get("token", None)
if not token:
raise Exception("token is required")
new_params = {**tool_parameters}
url = new_params.pop("url")
if not url or not url.startswith("http"):
raise Exception("url is not valid")
parsed_url = urlparse(url)
path_parts = parsed_url.path.strip("/").split("/")
if len(path_parts) < 3:
raise Exception("url is not correct")
doc_id = path_parts[-1]
book_slug = path_parts[-2]
group_id = path_parts[-3]
# 1. 请求首页信息获取book_id
new_params["group_login"] = group_id
new_params["book_slug"] = book_slug
index_page = json.loads(
self.request("GET", token, new_params, "/api/v2/repos/{group_login}/{book_slug}/index_page")
)
book_id = index_page.get("data", {}).get("book", {}).get("id")
if not book_id:
raise Exception(f"can not parse book_id from {index_page}")
# 2. 获取文档内容
new_params["book_id"] = book_id
new_params["id"] = doc_id
data = self.request("GET", token, new_params, "/api/v2/repos/{book_id}/docs/{id}")
data = json.loads(data)
body_only = tool_parameters.get("body_only") or ""
if body_only.lower() == "true":
return self.create_text_message(data.get("data").get("body"))
else:
raw = data.get("data")
del raw["body_lake"]
del raw["body_html"]
return self.create_text_message(json.dumps(data))

View File

@ -0,0 +1,50 @@
identity:
name: aliyuque_describe_document_content
author: 佐井
label:
en_US: Fetch Document Content
zh_Hans: 获取文档内容
icon: icon.svg
description:
human:
en_US: Retrieves document content from Yuque based on the provided document URL, which can be a normal or shared link.
zh_Hans: 根据提供的语雀文档地址(支持正常链接或分享链接)获取文档内容。
llm: Fetches Yuque document content given a URL.
parameters:
- name: url
type: string
required: true
form: llm
label:
en_US: Document URL
zh_Hans: 文档地址
human_description:
en_US: The URL of the document to retrieve content from, can be normal or shared.
zh_Hans: 需要获取内容的文档地址,可以是正常链接或分享链接。
llm_description: URL of the Yuque document to fetch content.
- name: body_only
type: string
required: false
form: llm
label:
en_US: return body content only
zh_Hans: 仅返回body内容
human_description:
en_US: true:Body content only, false:Full response with metadata.
zh_Hans: true:仅返回body内容不返回其他元数据false:返回所有元数据。
llm_description: true:Body content only, false:Full response with metadata.
- name: token
type: secret-input
required: false
form: llm
label:
en_US: Yuque API Token
zh_Hans: 语雀接口Token
human_description:
en_US: The token for calling the Yuque API defaults to the Yuque token bound to the current tool if not provided.
zh_Hans: 调用语雀接口的token如果不传则默认为当前工具绑定的语雀Token。
llm_description: If the token for calling the Yuque API is not provided, it will default to the Yuque token bound to the current tool.

View File

@ -0,0 +1,24 @@
"""
获取文档
"""
__author__ = "佐井"
__created__ = "2024-06-01 10:45:20"
from typing import Any, Union
from core.tools.entities.tool_entities import ToolInvokeMessage
from core.tools.provider.builtin.aliyuque.tools.base import AliYuqueTool
from core.tools.tool.builtin_tool import BuiltinTool
class AliYuqueDescribeDocumentsTool(AliYuqueTool, BuiltinTool):
def _invoke(
self, user_id: str, tool_parameters: dict[str, Any]
) -> Union[ToolInvokeMessage, list[ToolInvokeMessage]]:
token = self.runtime.credentials.get("token", None)
if not token:
raise Exception("token is required")
return self.create_text_message(
self.request("GET", token, tool_parameters, "/api/v2/repos/{book_id}/docs/{id}")
)

View File

@ -0,0 +1,38 @@
identity:
name: aliyuque_describe_documents
author: 佐井
label:
en_US: Get Doc Detail
zh_Hans: 获取文档详情
icon: icon.svg
description:
human:
en_US: Retrieves detailed information of a specific document identified by its ID or path within a knowledge base.
zh_Hans: 根据知识库ID和文档ID或路径获取文档详细信息。
llm: Fetches detailed doc info using ID/path from a knowledge base; supports doc lookup in Yuque.
parameters:
- name: book_id
type: number
required: true
form: llm
label:
en_US: Knowledge Base ID
zh_Hans: 知识库 ID
human_description:
en_US: Identifier for the knowledge base where the document resides.
zh_Hans: 文档所属知识库的唯一标识。
llm_description: ID of the knowledge base holding the document.
- name: id
type: string
required: true
form: llm
label:
en_US: Document ID or Path
zh_Hans: 文档 ID 或路径
human_description:
en_US: The unique identifier or path of the document to retrieve.
zh_Hans: 需要获取的文档的ID或其在知识库中的路径。
llm_description: Unique doc ID or its path for retrieval.

View File

@ -0,0 +1,29 @@
#!/usr/bin/env python3
"""
获取知识库目录
"""
__author__ = "佐井"
__created__ = "2024-09-17 15:17:11"
from typing import Any, Union
from core.tools.entities.tool_entities import ToolInvokeMessage
from core.tools.provider.builtin.aliyuque.tools.base import AliYuqueTool
from core.tools.tool.builtin_tool import BuiltinTool
class YuqueDescribeBookTableOfContentsTool(AliYuqueTool, BuiltinTool):
def _invoke(
self, user_id: str, tool_parameters: dict[str, Any]
) -> (Union)[ToolInvokeMessage, list[ToolInvokeMessage]]:
token = self.runtime.credentials.get("token", None)
if not token:
raise Exception("token is required")
doc_ids = tool_parameters.get("doc_ids")
if doc_ids:
doc_ids = [int(doc_id.strip()) for doc_id in doc_ids.split(",")]
tool_parameters["doc_ids"] = doc_ids
return self.create_text_message(self.request("PUT", token, tool_parameters, "/api/v2/repos/{book_id}/toc"))

View File

@ -0,0 +1,222 @@
identity:
name: aliyuque_update_book_table_of_contents
author: 佐井
label:
en_US: Update Book's Table of Contents
zh_Hans: 更新知识库目录
icon: icon.svg
description:
human:
en_US: Update Book's Table of Contents.
zh_Hans: 更新知识库目录。
llm: Update Book's Table of Contents.
parameters:
- name: book_id
type: number
required: true
form: llm
label:
en_US: Book ID
zh_Hans: 知识库 ID
human_description:
en_US: Book ID.
zh_Hans: 知识库 ID。
llm_description: Book ID.
- name: action
type: select
required: true
form: llm
options:
- value: appendNode
label:
en_US: appendNode
zh_Hans: appendNode
pt_BR: appendNode
- value: prependNode
label:
en_US: prependNode
zh_Hans: prependNode
pt_BR: prependNode
- value: editNode
label:
en_US: editNode
zh_Hans: editNode
pt_BR: editNode
- value: editNode
label:
en_US: removeNode
zh_Hans: removeNode
pt_BR: removeNode
label:
en_US: Action Type
zh_Hans: 操作
human_description:
en_US: In the operation scenario, sibling node prepending is not supported, deleting a node doesn't remove associated documents, and node deletion has two modes, 'sibling' (delete current node) and 'child' (delete current node and its children).
zh_Hans: 操作,创建场景下不支持同级头插 prependNode删除节点不会删除关联文档删除节点时action_mode=sibling (删除当前节点), action_mode=child (删除当前节点及子节点)
llm_description: In the operation scenario, sibling node prepending is not supported, deleting a node doesn't remove associated documents, and node deletion has two modes, 'sibling' (delete current node) and 'child' (delete current node and its children).
- name: action_mode
type: select
required: false
form: llm
options:
- value: sibling
label:
en_US: sibling
zh_Hans: 同级
pt_BR: sibling
- value: child
label:
en_US: child
zh_Hans: 子集
pt_BR: child
label:
en_US: Action Type
zh_Hans: 操作
human_description:
en_US: Operation mode (sibling:same level, child:child level).
zh_Hans: 操作模式 (sibling:同级, child:子级)。
llm_description: Operation mode (sibling:same level, child:child level).
- name: target_uuid
type: string
required: false
form: llm
label:
en_US: Target node UUID
zh_Hans: 目标节点 UUID
human_description:
en_US: Target node UUID, defaults to root node if left empty.
zh_Hans: 目标节点 UUID, 不填默认为根节点。
llm_description: Target node UUID, defaults to root node if left empty.
- name: node_uuid
type: string
required: false
form: llm
label:
en_US: Node UUID
zh_Hans: 操作节点 UUID
human_description:
en_US: Operation node UUID [required for move/update/delete].
zh_Hans: 操作节点 UUID [移动/更新/删除必填]。
llm_description: Operation node UUID [required for move/update/delete].
- name: doc_ids
type: string
required: false
form: llm
label:
en_US: Document IDs
zh_Hans: 文档id列表
human_description:
en_US: Document IDs [required for creating documents], separate multiple IDs with ','.
zh_Hans: 文档 IDs [创建文档必填],多个用','分隔。
llm_description: Document IDs [required for creating documents], separate multiple IDs with ','.
- name: type
type: select
required: false
form: llm
default: DOC
options:
- value: DOC
label:
en_US: DOC
zh_Hans: 文档
pt_BR: DOC
- value: LINK
label:
en_US: LINK
zh_Hans: 链接
pt_BR: LINK
- value: TITLE
label:
en_US: TITLE
zh_Hans: 分组
pt_BR: TITLE
label:
en_US: Node type
zh_Hans: 操节点类型
human_description:
en_US: Node type [required for creation] (DOC:document, LINK:external link, TITLE:group).
zh_Hans: 操节点类型 [创建必填] (DOC:文档, LINK:外链, TITLE:分组)。
llm_description: Node type [required for creation] (DOC:document, LINK:external link, TITLE:group).
- name: title
type: string
required: false
form: llm
label:
en_US: Node Name
zh_Hans: 节点名称
human_description:
en_US: Node name [required for creating groups/external links].
zh_Hans: 节点名称 [创建分组/外链必填]。
llm_description: Node name [required for creating groups/external links].
- name: url
type: string
required: false
form: llm
label:
en_US: Node URL
zh_Hans: 节点URL
human_description:
en_US: Node URL [required for creating external links].
zh_Hans: 节点 URL [创建外链必填]。
llm_description: Node URL [required for creating external links].
- name: open_window
type: select
required: false
form: llm
default: 0
options:
- value: 0
label:
en_US: DOC
zh_Hans: Current Page
pt_BR: DOC
- value: 1
label:
en_US: LINK
zh_Hans: New Page
pt_BR: LINK
label:
en_US: Open in new window
zh_Hans: 是否新窗口打开
human_description:
en_US: Open in new window [optional for external links] (0:open in current page, 1:open in new window).
zh_Hans: 是否新窗口打开 [外链选填] (0:当前页打开, 1:新窗口打开)。
llm_description: Open in new window [optional for external links] (0:open in current page, 1:open in new window).
- name: visible
type: select
required: false
form: llm
default: 1
options:
- value: 0
label:
en_US: Invisible
zh_Hans: 隐藏
pt_BR: Invisible
- value: 1
label:
en_US: Visible
zh_Hans: 可见
pt_BR: Visible
label:
en_US: Visibility
zh_Hans: 是否可见
human_description:
en_US: Visibility (0:invisible, 1:visible).
zh_Hans: 是否可见 (0:不可见, 1:可见)。
llm_description: Visibility (0:invisible, 1:visible).

View File

@ -0,0 +1,24 @@
"""
更新文档
"""
__author__ = "佐井"
__created__ = "2024-06-19 16:50:07"
from typing import Any, Union
from core.tools.entities.tool_entities import ToolInvokeMessage
from core.tools.provider.builtin.aliyuque.tools.base import AliYuqueTool
from core.tools.tool.builtin_tool import BuiltinTool
class AliYuqueUpdateDocumentTool(AliYuqueTool, BuiltinTool):
def _invoke(
self, user_id: str, tool_parameters: dict[str, Any]
) -> Union[ToolInvokeMessage, list[ToolInvokeMessage]]:
token = self.runtime.credentials.get("token", None)
if not token:
raise Exception("token is required")
return self.create_text_message(
self.request("PUT", token, tool_parameters, "/api/v2/repos/{book_id}/docs/{id}")
)

View File

@ -0,0 +1,87 @@
identity:
name: aliyuque_update_document
author: 佐井
label:
en_US: Update Document
zh_Hans: 更新文档
icon: icon.svg
description:
human:
en_US: Update an existing document within a specified knowledge base by providing the document ID or path.
zh_Hans: 通过提供文档ID或路径更新指定知识库中的现有文档。
llm: Update doc in a knowledge base via ID/path.
parameters:
- name: book_id
type: number
required: true
form: llm
label:
en_US: Knowledge Base ID
zh_Hans: 知识库 ID
human_description:
en_US: The unique identifier of the knowledge base where the document resides.
zh_Hans: 文档所属知识库的ID。
llm_description: ID of the knowledge base holding the doc.
- name: id
type: string
required: true
form: llm
label:
en_US: Document ID or Path
zh_Hans: 文档 ID 或 路径
human_description:
en_US: The unique identifier or the path of the document to be updated.
zh_Hans: 要更新的文档的唯一ID或路径。
llm_description: Doc's ID or path for update.
- name: title
type: string
required: false
form: llm
label:
en_US: Title
zh_Hans: 标题
human_description:
en_US: The title of the document, defaults to 'Untitled' if not provided.
zh_Hans: 文档标题,默认为'无标题'如未提供。
llm_description: Title of the document, defaults to 'Untitled'.
- name: format
type: select
required: false
form: llm
options:
- value: markdown
label:
en_US: markdown
zh_Hans: markdown
pt_BR: markdown
- value: html
label:
en_US: html
zh_Hans: html
pt_BR: html
- value: lake
label:
en_US: lake
zh_Hans: lake
pt_BR: lake
label:
en_US: Content Format
zh_Hans: 内容格式
human_description:
en_US: Format of the document content (markdown, HTML, Lake).
zh_Hans: 文档内容格式markdown, HTML, Lake
llm_description: Content format choices, markdown, HTML, Lake.
- name: body
type: string
required: true
form: llm
label:
en_US: Body Content
zh_Hans: 正文内容
human_description:
en_US: The actual content of the document.
zh_Hans: 文档的实际内容。
llm_description: Content of the document.

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

@ -1,47 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="240px" height="240px" viewBox="0 0 240 240" enable-background="new 0 0 240 240" xml:space="preserve"> <image id="image0" width="240" height="240" x="0" y="0"
xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAPAAAADwCAMAAAAJixmgAAAAIGNIUk0AAHomAACAhAAA+gAAAIDo
AAB1MAAA6mAAADqYAAAXcJy6UTwAAAINUExURQAAAJ9g6qNg6qRd6KNe6aVg6J9g6p9g359a6qRd
66Ne66Re66Rd66Rd66Re7Z9g559g76Ne7aRe6qJd76Fe66Ne66Re66Vd7KNc6aRe66Nd66Re7KRd
66Nc6p9g36Re7KRf7KRe7KNe7KNc76Va6qVd66Ne66Re66Nd6qRb6KRe66Zd66Ve7KJd6aNe66Fe
7KRd6p9Y56Re66Je6qRb6qNe66Re7KNc56Jf6qNe6qRe6qFe6KNe6qRf66Rf66Ne7KNe6qRe6qRd
66Rd7KFe6aNc6qNe6qJd7Z9b6KNd659Z7KNd66Ne6aRe66Re7KNc66Nd66Ne66Jd6qNe6qNc6aJd
6qNd6qRe66Jd7KFe6qJe66Re6qNe66Jd66Jd6aRd6qRd66Va759V6qVd6qFe56Ne6KJd6KRe66Zj
67eB79Gu9eLM+OjX+uXS+dSz9bJ37s6p9LV87q9y7de49vn1/f///9q99qlo7NGu9Kxt7d/H+Pz5
/vz6/qdj68CQ8e7h+/Pq/Maa8rqG8NzC9/Pr/Myk8+DH+OXR+cyk9Pn1/ujW+u3g+7iB7+vc+r2L
8N3C9//+//bw/cmf87uG8OPM+ebR+axt7Muk8/Dm/Mif8/n0/c+p9MOV8fHm++7g+/n0/vbv/bqG
78CQ8Kxu7cCR8ePM+LqH8MCP8bV87+LM+bqF8Muk9OLL+NSy9bqF75VHsr4AAABndFJOUwAYSHCA
WDAQMJfn57+PVyAQb98/gO/mllDPr1/ebwiHn+6eLzCvf/63OL4/xkfOT58g9odfZ65Aj3fHT2+X
j3f3z7efgEjPRzi3KKd/7sZAv+6fri+P3rY3X77G5j9oxn4fGGCAbzcMFjqxAAAAAWJLR0R1qGqY
+wAAAAd0SU1FB+gGDQkfBmABjhYAAAXYSURBVHja7d35WxtFGAfwbUGDtRWKilAwFg+2VHtwqa31
qlfV1qta7/vYkDQhJFnTliQWhFYoDaVY8ECw3vffaHgefZ5CZvbZIbMz83a/35/ngfkwx+5m3iyW
hSAIgiAIgiAIgiAIgiAIQjAbNtbVa0vdNRuvjSjlNly3ydGd6zdvuaFRDbdpa7Nu7f9p3nJjU/De
m27W7bwyLbcEbW5t021cm7atQc7thm26fYw01wdHbjdmAa9Kx61NwXijt+mm8dLWHgi5cbtuGDed
t98RAPjOu3S7PBLEIBsNdjq7pIvNBjv2ju5wgR17593hAjvOPbtCBnZ27wkZWK6YAljqOqYAlrpX
kwA79t6ecIGd3r6mcIGdjv6QgaUtYypgaZOaDFjWpKYDlrRT0wE7LQMhA9v3yvgEhBBYziqmBJay
iimBpQwxKbDdFw0X2Nl8X8jAEuY0LbB9f80H58TAtT9C0AJLmNPEwL37QgaufRFTA29rCBfY2V/r
OQQ18O4HQgau+aGYGrjzQMjAdhfAVzm41idEcuBa7zwANjwAAwwwwKQCcIDg2GA8cXx1kqnBGK/5
UHpta16SqWETwZlszq1O7tMhZuv8CVZrXnKpk8aBh05xOptgDY+Y13VHsj7FysB8QSFerG5e+kzI
67qnRw0DDye4fT0+Vv3nyQp6XffzjFng0XH+Apyoaj10Rhh8dtAs8BeT3K4W0lWtx6aEwedKlMHT
Z4XB58tmgUvnuF0dr95vMjPC4IS/a7EysAeB1dV0QdB7IZs3C8zftWYvMloLz2nGVq8ZXJyYZfZ0
Ms4cmjmxbcvvZVjpvfTcl9UbV+FSmXMzPTwz6ZtbuDTntxNqn5bmp+dWZ3reo3VmzmemMzHfXcDj
IcAAA0wqAAMMMMCkAjDAAANMKgADDDDApAIwwBLB+XI6vjrp0kmB1rx4/RSN4PwC67BlfCHPbB0r
nRc4ePjq67zPXqgD82oeCuzqjIuzAl7X/eaET7E68ASvxiP3LaPGQ/BoiXyNRzEl6DXvuNSjxoNx
IB66Go91lDyErsbDNPDidyGb0h5jxqjxKMbp13hw5/RkmnGcLXxZWvI3o1XeeMSXmT0dSbGGpjjx
vZB3mV0pohPs5EeXLlTPxNOcW8viIqM1N7wbVK3gCvnywtrb/h/4RR4CDw+L/gpLlYNNCMAAAwww
qQAMMMAAkwrAAAMMMKkADDDAAJMKwIGCfxwcW5OMUGte/H9KqxIcKyerD4xyyZ+K7ObTP/s/Xiok
fzEPnE+zqzZy7OqMskhNC+dLyHrB3BKAcVZ1hkeFBDvGfWHaQ8D6SvyE2ItaXHckXvTVD0MPxNfx
0gPTDsQ96q6uztdaeNR4uPGq1ut5ccmvZoE9ypYYO+xvvwuDp/4wC+xRw8CoVlhHYZppLx/il6kU
WDUPpSVB8PKf/vqh7jrM3XjZLxDjVITwwq4U0Qp2MlmWYWSG84o4IfFyat5nL5Q+PAynT02tTiJ1
Oea/NS9//f2P7xd54PEQYIABJhWAAQYYYFIBWDSN23UTFIM3PKiboBgceUg3QTHYevgR3QbF4IFN
ug2Kwa2P6jYoBncf1G1QDO55TLdBMTj6OKldS8I/5G1v1o1QDKa1iCWAad16SADTmtMywKTmtAww
qX1aBth6Yr9uhmIwpW1LCtja86Ruh2IwoSGWA7aeIrOKJYGjXb26JWrB1qGndUsUg63+Dt0UxeBI
H41JLQ1sPfOsbotiMJGdWiKYxjKWCY4eaNHNUQu2IoeP6PaoBVMQywUTEEsGW5F9hq9j2eDK1ek5
WzdKLdg6tMNkcQBgsxey3ReVDras51+wdcO44K4AvCuD/KKh5I7+QMCW1fCSkWT76MsBgQ0lHzks
f8+6gvzKMcPI9quvBeitJPL6GyYNs/3mrmC9K+kZOPqWbQba3vl28N6VRBve2ftuRW1rhFd+97H3
3lfj/S8fdLd+WFevKx99/EkQdxwIgiAIgiAIgiAIgiAIgiBB519+T+5Fl+ldNwAAACV0RVh0ZGF0
ZTpjcmVhdGUAMjAyNC0wNi0xM1QwOTozMTowNiswMDowMPHqs70AAAAldEVYdGRhdGU6bW9kaWZ5
ADIwMjQtMDYtMTNUMDk6MzE6MDYrMDA6MDCAtwsBAAAAKHRFWHRkYXRlOnRpbWVzdGFtcAAyMDI0
LTA2LTEzVDA5OjMxOjA2KzAwOjAw16Iq3gAAAABJRU5ErkJggg==" />
</svg>

Before

Width:  |  Height:  |  Size: 3.7 KiB

View File

@ -1,56 +0,0 @@
import json
from typing import Any, Union
import httpx
from core.tools.entities.tool_entities import ToolInvokeMessage
from core.tools.tool.builtin_tool import BuiltinTool
class AddBaseRecordTool(BuiltinTool):
def _invoke(
self, user_id: str, tool_parameters: dict[str, Any]
) -> Union[ToolInvokeMessage, list[ToolInvokeMessage]]:
url = "https://open.feishu.cn/open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/records"
access_token = tool_parameters.get("Authorization", "")
if not access_token:
return self.create_text_message("Invalid parameter access_token")
app_token = tool_parameters.get("app_token", "")
if not app_token:
return self.create_text_message("Invalid parameter app_token")
table_id = tool_parameters.get("table_id", "")
if not table_id:
return self.create_text_message("Invalid parameter table_id")
fields = tool_parameters.get("fields", "")
if not fields:
return self.create_text_message("Invalid parameter fields")
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {access_token}",
}
params = {}
payload = {"fields": json.loads(fields)}
try:
res = httpx.post(
url.format(app_token=app_token, table_id=table_id),
headers=headers,
params=params,
json=payload,
timeout=30,
)
res_json = res.json()
if res.is_success:
return self.create_text_message(text=json.dumps(res_json))
else:
return self.create_text_message(
f"Failed to add base record, status code: {res.status_code}, response: {res.text}"
)
except Exception as e:
return self.create_text_message("Failed to add base record. {}".format(e))

View File

@ -1,66 +0,0 @@
identity:
name: add_base_record
author: Doug Lea
label:
en_US: Add Base Record
zh_Hans: 在多维表格数据表中新增一条记录
description:
human:
en_US: Add Base Record
zh_Hans: |
在多维表格数据表中新增一条记录详细请参考https://open.larkoffice.com/document/server-docs/docs/bitable-v1/app-table-record/create
llm: Add a new record in the multidimensional table data table.
parameters:
- name: Authorization
type: string
required: true
label:
en_US: token
zh_Hans: 凭证
human_description:
en_US: API access token parameter, tenant_access_token or user_access_token
zh_Hans: API 的访问凭证参数tenant_access_token 或 user_access_token
llm_description: API access token parameter, tenant_access_token or user_access_token
form: llm
- name: app_token
type: string
required: true
label:
en_US: app_token
zh_Hans: 多维表格
human_description:
en_US: bitable app token
zh_Hans: 多维表格的唯一标识符 app_token
llm_description: bitable app token
form: llm
- name: table_id
type: string
required: true
label:
en_US: table_id
zh_Hans: 多维表格的数据表
human_description:
en_US: bitable table id
zh_Hans: 多维表格数据表的唯一标识符 table_id
llm_description: bitable table id
form: llm
- name: fields
type: string
required: true
label:
en_US: fields
zh_Hans: 数据表的列字段内容
human_description:
en_US: The fields of the Base data table are the columns of the data table.
zh_Hans: |
要增加一行多维表格记录,字段结构拼接如下:{"多行文本":"多行文本内容","单选":"选项1","多选":["选项1","选项2"],"复选框":true,"人员":[{"id":"ou_2910013f1e6456f16a0ce75ede950a0a"}],"群组":[{"id":"oc_cd07f55f14d6f4a4f1b51504e7e97f48"}],"电话号码":"13026162666"}
当前接口支持的字段类型为:多行文本、单选、条码、多选、日期、人员、附件、复选框、超链接、数字、单向关联、双向关联、电话号码、地理位置。
不同类型字段的数据结构请参考数据结构概述https://open.larkoffice.com/document/server-docs/docs/bitable-v1/bitable-structure
llm_description: |
要增加一行多维表格记录,字段结构拼接如下:{"多行文本":"多行文本内容","单选":"选项1","多选":["选项1","选项2"],"复选框":true,"人员":[{"id":"ou_2910013f1e6456f16a0ce75ede950a0a"}],"群组":[{"id":"oc_cd07f55f14d6f4a4f1b51504e7e97f48"}],"电话号码":"13026162666"}
当前接口支持的字段类型为:多行文本、单选、条码、多选、日期、人员、附件、复选框、超链接、数字、单向关联、双向关联、电话号码、地理位置。
不同类型字段的数据结构请参考数据结构概述https://open.larkoffice.com/document/server-docs/docs/bitable-v1/bitable-structure
form: llm

View File

@ -0,0 +1,21 @@
from typing import Any
from core.tools.entities.tool_entities import ToolInvokeMessage
from core.tools.tool.builtin_tool import BuiltinTool
from core.tools.utils.feishu_api_utils import FeishuRequest
class AddRecordsTool(BuiltinTool):
def _invoke(self, user_id: str, tool_parameters: dict[str, Any]) -> ToolInvokeMessage:
app_id = self.runtime.credentials.get("app_id")
app_secret = self.runtime.credentials.get("app_secret")
client = FeishuRequest(app_id, app_secret)
app_token = tool_parameters.get("app_token")
table_id = tool_parameters.get("table_id")
table_name = tool_parameters.get("table_name")
records = tool_parameters.get("records")
user_id_type = tool_parameters.get("user_id_type", "open_id")
res = client.add_records(app_token, table_id, table_name, records, user_id_type)
return self.create_json_message(res)

View File

@ -0,0 +1,91 @@
identity:
name: add_records
author: Doug Lea
label:
en_US: Add Records
zh_Hans: 新增多条记录
description:
human:
en_US: Add Multiple Records to Multidimensional Table
zh_Hans: 在多维表格数据表中新增多条记录
llm: A tool for adding multiple records to a multidimensional table. (在多维表格数据表中新增多条记录)
parameters:
- name: app_token
type: string
required: true
label:
en_US: app_token
zh_Hans: app_token
human_description:
en_US: Unique identifier for the multidimensional table, supports inputting document URL.
zh_Hans: 多维表格的唯一标识符,支持输入文档 URL。
llm_description: 多维表格的唯一标识符,支持输入文档 URL。
form: llm
- name: table_id
type: string
required: false
label:
en_US: table_id
zh_Hans: table_id
human_description:
en_US: Unique identifier for the multidimensional table data, either table_id or table_name must be provided, cannot be empty simultaneously.
zh_Hans: 多维表格数据表的唯一标识符table_id 和 table_name 至少需要提供一个,不能同时为空。
llm_description: 多维表格数据表的唯一标识符table_id 和 table_name 至少需要提供一个,不能同时为空。
form: llm
- name: table_name
type: string
required: false
label:
en_US: table_name
zh_Hans: table_name
human_description:
en_US: Name of the multidimensional table data, either table_name or table_id must be provided, cannot be empty simultaneously.
zh_Hans: 多维表格数据表的名称table_name 和 table_id 至少需要提供一个,不能同时为空。
llm_description: 多维表格数据表的名称table_name 和 table_id 至少需要提供一个,不能同时为空。
form: llm
- name: records
type: string
required: true
label:
en_US: records
zh_Hans: 记录列表
human_description:
en_US: |
List of records to be added in this request. Example value: [{"multi-line-text":"text content","single_select":"option 1","date":1674206443000}]
For supported field types, refer to the integration guide (https://open.larkoffice.com/document/server-docs/docs/bitable-v1/notification). For data structures of different field types, refer to the data structure overview (https://open.larkoffice.com/document/server-docs/docs/bitable-v1/bitable-structure).
zh_Hans: |
本次请求将要新增的记录列表,示例值:[{"多行文本":"文本内容","单选":"选项 1","日期":1674206443000}]。
当前接口支持的字段类型请参考接入指南(https://open.larkoffice.com/document/server-docs/docs/bitable-v1/notification),不同类型字段的数据结构请参考数据结构概述(https://open.larkoffice.com/document/server-docs/docs/bitable-v1/bitable-structure)。
llm_description: |
本次请求将要新增的记录列表,示例值:[{"多行文本":"文本内容","单选":"选项 1","日期":1674206443000}]。
当前接口支持的字段类型请参考接入指南(https://open.larkoffice.com/document/server-docs/docs/bitable-v1/notification),不同类型字段的数据结构请参考数据结构概述(https://open.larkoffice.com/document/server-docs/docs/bitable-v1/bitable-structure)。
form: llm
- name: user_id_type
type: select
required: false
options:
- value: open_id
label:
en_US: open_id
zh_Hans: open_id
- value: union_id
label:
en_US: union_id
zh_Hans: union_id
- value: user_id
label:
en_US: user_id
zh_Hans: user_id
default: "open_id"
label:
en_US: user_id_type
zh_Hans: 用户 ID 类型
human_description:
en_US: User ID type, optional values are open_id, union_id, user_id, with a default value of open_id.
zh_Hans: 用户 ID 类型,可选值有 open_id、union_id、user_id默认值为 open_id。
llm_description: 用户 ID 类型,可选值有 open_id、union_id、user_id默认值为 open_id。
form: form

View File

@ -1,48 +0,0 @@
import json
from typing import Any, Union
import httpx
from core.tools.entities.tool_entities import ToolInvokeMessage
from core.tools.tool.builtin_tool import BuiltinTool
class CreateBaseTableTool(BuiltinTool):
def _invoke(
self, user_id: str, tool_parameters: dict[str, Any]
) -> Union[ToolInvokeMessage, list[ToolInvokeMessage]]:
url = "https://open.feishu.cn/open-apis/bitable/v1/apps/{app_token}/tables"
access_token = tool_parameters.get("Authorization", "")
if not access_token:
return self.create_text_message("Invalid parameter access_token")
app_token = tool_parameters.get("app_token", "")
if not app_token:
return self.create_text_message("Invalid parameter app_token")
name = tool_parameters.get("name", "")
fields = tool_parameters.get("fields", "")
if not fields:
return self.create_text_message("Invalid parameter fields")
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {access_token}",
}
params = {}
payload = {"table": {"name": name, "fields": json.loads(fields)}}
try:
res = httpx.post(url.format(app_token=app_token), headers=headers, params=params, json=payload, timeout=30)
res_json = res.json()
if res.is_success:
return self.create_text_message(text=json.dumps(res_json))
else:
return self.create_text_message(
f"Failed to create base table, status code: {res.status_code}, response: {res.text}"
)
except Exception as e:
return self.create_text_message("Failed to create base table. {}".format(e))

View File

@ -1,106 +0,0 @@
identity:
name: create_base_table
author: Doug Lea
label:
en_US: Create Base Table
zh_Hans: 多维表格新增一个数据表
description:
human:
en_US: Create base table
zh_Hans: |
多维表格新增一个数据表详细请参考https://open.larkoffice.com/document/server-docs/docs/bitable-v1/app-table/create
llm: A tool for add a new data table to the multidimensional table.
parameters:
- name: Authorization
type: string
required: true
label:
en_US: token
zh_Hans: 凭证
human_description:
en_US: API access token parameter, tenant_access_token or user_access_token
zh_Hans: API 的访问凭证参数tenant_access_token 或 user_access_token
llm_description: API access token parameter, tenant_access_token or user_access_token
form: llm
- name: app_token
type: string
required: true
label:
en_US: app_token
zh_Hans: 多维表格
human_description:
en_US: bitable app token
zh_Hans: 多维表格的唯一标识符 app_token
llm_description: bitable app token
form: llm
- name: name
type: string
required: false
label:
en_US: name
zh_Hans: name
human_description:
en_US: Multidimensional table data table name
zh_Hans: 多维表格数据表名称
llm_description: Multidimensional table data table name
form: llm
- name: fields
type: string
required: true
label:
en_US: fields
zh_Hans: fields
human_description:
en_US: Initial fields of the data table
zh_Hans: |
数据表的初始字段,格式为:[{"field_name":"多行文本","type":1},{"field_name":"数字","type":2},{"field_name":"单选","type":3},{"field_name":"多选","type":4},{"field_name":"日期","type":5}]。
field_name字段名
type: 字段类型;可选值有
1:多行文本
2:数字
3:单选
4:多选
5:日期
7:复选框
11:人员
13:电话号码
15:超链接
17:附件
18:单向关联
20:公式
21:双向关联
22:地理位置
23:群组
1001:创建时间
1002:最后更新时间
1003:创建人
1004:修改人
1005:自动编号
llm_description: |
数据表的初始字段,格式为:[{"field_name":"多行文本","type":1},{"field_name":"数字","type":2},{"field_name":"单选","type":3},{"field_name":"多选","type":4},{"field_name":"日期","type":5}]。
field_name字段名
type: 字段类型;可选值有
1:多行文本
2:数字
3:单选
4:多选
5:日期
7:复选框
11:人员
13:电话号码
15:超链接
17:附件
18:单向关联
20:公式
21:双向关联
22:地理位置
23:群组
1001:创建时间
1002:最后更新时间
1003:创建人
1004:修改人
1005:自动编号
form: llm

View File

@ -0,0 +1,20 @@
from typing import Any
from core.tools.entities.tool_entities import ToolInvokeMessage
from core.tools.tool.builtin_tool import BuiltinTool
from core.tools.utils.feishu_api_utils import FeishuRequest
class CreateTableTool(BuiltinTool):
def _invoke(self, user_id: str, tool_parameters: dict[str, Any]) -> ToolInvokeMessage:
app_id = self.runtime.credentials.get("app_id")
app_secret = self.runtime.credentials.get("app_secret")
client = FeishuRequest(app_id, app_secret)
app_token = tool_parameters.get("app_token")
table_name = tool_parameters.get("table_name")
default_view_name = tool_parameters.get("default_view_name")
fields = tool_parameters.get("fields")
res = client.create_table(app_token, table_name, default_view_name, fields)
return self.create_json_message(res)

View File

@ -0,0 +1,61 @@
identity:
name: create_table
author: Doug Lea
label:
en_US: Create Table
zh_Hans: 新增数据表
description:
human:
en_US: Add a Data Table to Multidimensional Table
zh_Hans: 在多维表格中新增一个数据表
llm: A tool for adding a data table to a multidimensional table. (在多维表格中新增一个数据表)
parameters:
- name: app_token
type: string
required: true
label:
en_US: app_token
zh_Hans: app_token
human_description:
en_US: Unique identifier for the multidimensional table, supports inputting document URL.
zh_Hans: 多维表格的唯一标识符,支持输入文档 URL。
llm_description: 多维表格的唯一标识符,支持输入文档 URL。
form: llm
- name: table_name
type: string
required: true
label:
en_US: Table Name
zh_Hans: 数据表名称
human_description:
en_US: |
The name of the data table, length range: 1 character to 100 characters.
zh_Hans: 数据表名称长度范围1 字符 100 字符。
llm_description: 数据表名称长度范围1 字符 100 字符。
form: llm
- name: default_view_name
type: string
required: false
label:
en_US: Default View Name
zh_Hans: 默认表格视图的名称
human_description:
en_US: The name of the default table view, defaults to "Table" if not filled.
zh_Hans: 默认表格视图的名称,不填则默认为"表格"。
llm_description: 默认表格视图的名称,不填则默认为"表格"。
form: llm
- name: fields
type: string
required: true
label:
en_US: Initial Fields
zh_Hans: 初始字段
human_description:
en_US: |
Initial fields of the data table, format: [ { "field_name": "Multi-line Text","type": 1 },{ "field_name": "Number","type": 2 },{ "field_name": "Single Select","type": 3 },{ "field_name": "Multiple Select","type": 4 },{ "field_name": "Date","type": 5 } ]. For field details, refer to: https://open.larkoffice.com/document/server-docs/docs/bitable-v1/app-table-field/guide
zh_Hans: 数据表的初始字段,格式为:[{"field_name":"多行文本","type":1},{"field_name":"数字","type":2},{"field_name":"单选","type":3},{"field_name":"多选","type":4},{"field_name":"日期","type":5}]。字段详情参考https://open.larkoffice.com/document/server-docs/docs/bitable-v1/app-table-field/guide
llm_description: 数据表的初始字段,格式为:[{"field_name":"多行文本","type":1},{"field_name":"数字","type":2},{"field_name":"单选","type":3},{"field_name":"多选","type":4},{"field_name":"日期","type":5}]。字段详情参考https://open.larkoffice.com/document/server-docs/docs/bitable-v1/app-table-field/guide
form: llm

View File

@ -1,56 +0,0 @@
import json
from typing import Any, Union
import httpx
from core.tools.entities.tool_entities import ToolInvokeMessage
from core.tools.tool.builtin_tool import BuiltinTool
class DeleteBaseRecordsTool(BuiltinTool):
def _invoke(
self, user_id: str, tool_parameters: dict[str, Any]
) -> Union[ToolInvokeMessage, list[ToolInvokeMessage]]:
url = "https://open.feishu.cn/open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/records/batch_delete"
access_token = tool_parameters.get("Authorization", "")
if not access_token:
return self.create_text_message("Invalid parameter access_token")
app_token = tool_parameters.get("app_token", "")
if not app_token:
return self.create_text_message("Invalid parameter app_token")
table_id = tool_parameters.get("table_id", "")
if not table_id:
return self.create_text_message("Invalid parameter table_id")
record_ids = tool_parameters.get("record_ids", "")
if not record_ids:
return self.create_text_message("Invalid parameter record_ids")
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {access_token}",
}
params = {}
payload = {"records": json.loads(record_ids)}
try:
res = httpx.post(
url.format(app_token=app_token, table_id=table_id),
headers=headers,
params=params,
json=payload,
timeout=30,
)
res_json = res.json()
if res.is_success:
return self.create_text_message(text=json.dumps(res_json))
else:
return self.create_text_message(
f"Failed to delete base records, status code: {res.status_code}, response: {res.text}"
)
except Exception as e:
return self.create_text_message("Failed to delete base records. {}".format(e))

View File

@ -1,60 +0,0 @@
identity:
name: delete_base_records
author: Doug Lea
label:
en_US: Delete Base Records
zh_Hans: 在多维表格数据表中删除多条记录
description:
human:
en_US: Delete base records
zh_Hans: |
该接口用于删除多维表格数据表中的多条记录,单次调用中最多删除 500 条记录。
llm: A tool for delete multiple records in a multidimensional table data table, up to 500 records can be deleted in a single call.
parameters:
- name: Authorization
type: string
required: true
label:
en_US: token
zh_Hans: 凭证
human_description:
en_US: API access token parameter, tenant_access_token or user_access_token
zh_Hans: API 的访问凭证参数tenant_access_token 或 user_access_token
llm_description: API access token parameter, tenant_access_token or user_access_token
form: llm
- name: app_token
type: string
required: true
label:
en_US: app_token
zh_Hans: 多维表格
human_description:
en_US: bitable app token
zh_Hans: 多维表格的唯一标识符 app_token
llm_description: bitable app token
form: llm
- name: table_id
type: string
required: true
label:
en_US: table_id
zh_Hans: 多维表格的数据表
human_description:
en_US: bitable table id
zh_Hans: 多维表格数据表的唯一标识符 table_id
llm_description: bitable table id
form: llm
- name: record_ids
type: string
required: true
label:
en_US: record_ids
zh_Hans: record_ids
human_description:
en_US: A list of multiple record IDs to be deleted, for example ["recwNXzPQv","recpCsf4ME"]
zh_Hans: 待删除的多条记录id列表示例为 ["recwNXzPQv","recpCsf4ME"]
llm_description: A list of multiple record IDs to be deleted, for example ["recwNXzPQv","recpCsf4ME"]
form: llm

View File

@ -1,46 +0,0 @@
import json
from typing import Any, Union
import httpx
from core.tools.entities.tool_entities import ToolInvokeMessage
from core.tools.tool.builtin_tool import BuiltinTool
class DeleteBaseTablesTool(BuiltinTool):
def _invoke(
self, user_id: str, tool_parameters: dict[str, Any]
) -> Union[ToolInvokeMessage, list[ToolInvokeMessage]]:
url = "https://open.feishu.cn/open-apis/bitable/v1/apps/{app_token}/tables/batch_delete"
access_token = tool_parameters.get("Authorization", "")
if not access_token:
return self.create_text_message("Invalid parameter access_token")
app_token = tool_parameters.get("app_token", "")
if not app_token:
return self.create_text_message("Invalid parameter app_token")
table_ids = tool_parameters.get("table_ids", "")
if not table_ids:
return self.create_text_message("Invalid parameter table_ids")
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {access_token}",
}
params = {}
payload = {"table_ids": json.loads(table_ids)}
try:
res = httpx.post(url.format(app_token=app_token), headers=headers, params=params, json=payload, timeout=30)
res_json = res.json()
if res.is_success:
return self.create_text_message(text=json.dumps(res_json))
else:
return self.create_text_message(
f"Failed to delete base tables, status code: {res.status_code}, response: {res.text}"
)
except Exception as e:
return self.create_text_message("Failed to delete base tables. {}".format(e))

View File

@ -1,48 +0,0 @@
identity:
name: delete_base_tables
author: Doug Lea
label:
en_US: Delete Base Tables
zh_Hans: 删除多维表格中的数据表
description:
human:
en_US: Delete base tables
zh_Hans: |
删除多维表格中的数据表
llm: A tool for deleting a data table in a multidimensional table
parameters:
- name: Authorization
type: string
required: true
label:
en_US: token
zh_Hans: 凭证
human_description:
en_US: API access token parameter, tenant_access_token or user_access_token
zh_Hans: API 的访问凭证参数tenant_access_token 或 user_access_token
llm_description: API access token parameter, tenant_access_token or user_access_token
form: llm
- name: app_token
type: string
required: true
label:
en_US: app_token
zh_Hans: 多维表格
human_description:
en_US: bitable app token
zh_Hans: 多维表格的唯一标识符 app_token
llm_description: bitable app token
form: llm
- name: table_ids
type: string
required: true
label:
en_US: table_ids
zh_Hans: table_ids
human_description:
en_US: The ID list of the data tables to be deleted. Currently, a maximum of 50 data tables can be deleted at a time. The example is ["tbl1TkhyTWDkSoZ3","tblsRc9GRRXKqhvW"]
zh_Hans: 待删除数据表的id列表当前一次操作最多支持50个数据表示例为 ["tbl1TkhyTWDkSoZ3","tblsRc9GRRXKqhvW"]
llm_description: The ID list of the data tables to be deleted. Currently, a maximum of 50 data tables can be deleted at a time. The example is ["tbl1TkhyTWDkSoZ3","tblsRc9GRRXKqhvW"]
form: llm

View File

@ -0,0 +1,20 @@
from typing import Any
from core.tools.entities.tool_entities import ToolInvokeMessage
from core.tools.tool.builtin_tool import BuiltinTool
from core.tools.utils.feishu_api_utils import FeishuRequest
class DeleteRecordsTool(BuiltinTool):
def _invoke(self, user_id: str, tool_parameters: dict[str, Any]) -> ToolInvokeMessage:
app_id = self.runtime.credentials.get("app_id")
app_secret = self.runtime.credentials.get("app_secret")
client = FeishuRequest(app_id, app_secret)
app_token = tool_parameters.get("app_token")
table_id = tool_parameters.get("table_id")
table_name = tool_parameters.get("table_name")
record_ids = tool_parameters.get("record_ids")
res = client.delete_records(app_token, table_id, table_name, record_ids)
return self.create_json_message(res)

View File

@ -0,0 +1,86 @@
identity:
name: delete_records
author: Doug Lea
label:
en_US: Delete Records
zh_Hans: 删除多条记录
description:
human:
en_US: Delete Multiple Records from Multidimensional Table
zh_Hans: 删除多维表格数据表中的多条记录
llm: A tool for deleting multiple records from a multidimensional table. (删除多维表格数据表中的多条记录)
parameters:
- name: app_token
type: string
required: true
label:
en_US: app_token
zh_Hans: app_token
human_description:
en_US: Unique identifier for the multidimensional table, supports inputting document URL.
zh_Hans: 多维表格的唯一标识符,支持输入文档 URL。
llm_description: 多维表格的唯一标识符,支持输入文档 URL。
form: llm
- name: table_id
type: string
required: false
label:
en_US: table_id
zh_Hans: table_id
human_description:
en_US: Unique identifier for the multidimensional table data, either table_id or table_name must be provided, cannot be empty simultaneously.
zh_Hans: 多维表格数据表的唯一标识符table_id 和 table_name 至少需要提供一个,不能同时为空。
llm_description: 多维表格数据表的唯一标识符table_id 和 table_name 至少需要提供一个,不能同时为空。
form: llm
- name: table_name
type: string
required: false
label:
en_US: table_name
zh_Hans: table_name
human_description:
en_US: Name of the multidimensional table data, either table_name or table_id must be provided, cannot be empty simultaneously.
zh_Hans: 多维表格数据表的名称table_name 和 table_id 至少需要提供一个,不能同时为空。
llm_description: 多维表格数据表的名称table_name 和 table_id 至少需要提供一个,不能同时为空。
form: llm
- name: record_ids
type: string
required: true
label:
en_US: Record IDs
zh_Hans: 记录 ID 列表
human_description:
en_US: |
List of IDs for the records to be deleted, example value: ["recwNXzPQv"].
zh_Hans: 删除的多条记录 ID 列表,示例值:["recwNXzPQv"]。
llm_description: 删除的多条记录 ID 列表,示例值:["recwNXzPQv"]。
form: llm
- name: user_id_type
type: select
required: false
options:
- value: open_id
label:
en_US: open_id
zh_Hans: open_id
- value: union_id
label:
en_US: union_id
zh_Hans: union_id
- value: user_id
label:
en_US: user_id
zh_Hans: user_id
default: "open_id"
label:
en_US: user_id_type
zh_Hans: 用户 ID 类型
human_description:
en_US: User ID type, optional values are open_id, union_id, user_id, with a default value of open_id.
zh_Hans: 用户 ID 类型,可选值有 open_id、union_id、user_id默认值为 open_id。
llm_description: 用户 ID 类型,可选值有 open_id、union_id、user_id默认值为 open_id。
form: form

View File

@ -0,0 +1,19 @@
from typing import Any
from core.tools.entities.tool_entities import ToolInvokeMessage
from core.tools.tool.builtin_tool import BuiltinTool
from core.tools.utils.feishu_api_utils import FeishuRequest
class DeleteTablesTool(BuiltinTool):
def _invoke(self, user_id: str, tool_parameters: dict[str, Any]) -> ToolInvokeMessage:
app_id = self.runtime.credentials.get("app_id")
app_secret = self.runtime.credentials.get("app_secret")
client = FeishuRequest(app_id, app_secret)
app_token = tool_parameters.get("app_token")
table_ids = tool_parameters.get("table_ids")
table_names = tool_parameters.get("table_names")
res = client.delete_tables(app_token, table_ids, table_names)
return self.create_json_message(res)

View File

@ -0,0 +1,49 @@
identity:
name: delete_tables
author: Doug Lea
label:
en_US: Delete Tables
zh_Hans: 删除数据表
description:
human:
en_US: Batch Delete Data Tables from Multidimensional Table
zh_Hans: 批量删除多维表格中的数据表
llm: A tool for batch deleting data tables from a multidimensional table. (批量删除多维表格中的数据表)
parameters:
- name: app_token
type: string
required: true
label:
en_US: app_token
zh_Hans: app_token
human_description:
en_US: Unique identifier for the multidimensional table, supports inputting document URL.
zh_Hans: 多维表格的唯一标识符,支持输入文档 URL。
llm_description: 多维表格的唯一标识符,支持输入文档 URL。
form: llm
- name: table_ids
type: string
required: false
label:
en_US: Table IDs
zh_Hans: 数据表 ID
human_description:
en_US: |
IDs of the tables to be deleted. Each operation supports deleting up to 50 tables. Example: ["tbl1TkhyTWDkSoZ3"]. Ensure that either table_ids or table_names is not empty.
zh_Hans: 待删除的数据表的 ID每次操作最多支持删除 50 个数据表。示例值:["tbl1TkhyTWDkSoZ3"]。请确保 table_ids 和 table_names 至少有一个不为空。
llm_description: 待删除的数据表的 ID每次操作最多支持删除 50 个数据表。示例值:["tbl1TkhyTWDkSoZ3"]。请确保 table_ids 和 table_names 至少有一个不为空。
form: llm
- name: table_names
type: string
required: false
label:
en_US: Table Names
zh_Hans: 数据表名称
human_description:
en_US: |
Names of the tables to be deleted. Each operation supports deleting up to 50 tables. Example: ["Table1", "Table2"]. Ensure that either table_names or table_ids is not empty.
zh_Hans: 待删除的数据表的名称,每次操作最多支持删除 50 个数据表。示例值:["数据表1", "数据表2"]。请确保 table_names 和 table_ids 至少有一个不为空。
llm_description: 待删除的数据表的名称,每次操作最多支持删除 50 个数据表。示例值:["数据表1", "数据表2"]。请确保 table_names 和 table_ids 至少有一个不为空。
form: llm

View File

@ -1,48 +0,0 @@
import json
from typing import Any, Union
import httpx
from core.tools.entities.tool_entities import ToolInvokeMessage
from core.tools.tool.builtin_tool import BuiltinTool
class GetTenantAccessTokenTool(BuiltinTool):
def _invoke(
self, user_id: str, tool_parameters: dict[str, Any]
) -> Union[ToolInvokeMessage, list[ToolInvokeMessage]]:
url = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal"
app_id = tool_parameters.get("app_id", "")
if not app_id:
return self.create_text_message("Invalid parameter app_id")
app_secret = tool_parameters.get("app_secret", "")
if not app_secret:
return self.create_text_message("Invalid parameter app_secret")
headers = {
"Content-Type": "application/json",
}
params = {}
payload = {"app_id": app_id, "app_secret": app_secret}
"""
{
"code": 0,
"msg": "ok",
"tenant_access_token": "t-caecc734c2e3328a62489fe0648c4b98779515d3",
"expire": 7200
}
"""
try:
res = httpx.post(url, headers=headers, params=params, json=payload, timeout=30)
res_json = res.json()
if res.is_success:
return self.create_text_message(text=json.dumps(res_json))
else:
return self.create_text_message(
f"Failed to get tenant access token, status code: {res.status_code}, response: {res.text}"
)
except Exception as e:
return self.create_text_message("Failed to get tenant access token. {}".format(e))

View File

@ -1,39 +0,0 @@
identity:
name: get_tenant_access_token
author: Doug Lea
label:
en_US: Get Tenant Access Token
zh_Hans: 获取飞书自建应用的 tenant_access_token
description:
human:
en_US: Get tenant access token
zh_Hans: |
获取飞书自建应用的 tenant_access_token响应体示例:
{"code":0,"msg":"ok","tenant_access_token":"t-caecc734c2e3328a62489fe0648c4b98779515d3","expire":7200}
tenant_access_token: 租户访问凭证;
expire: tenant_access_token 的过期时间,单位为秒;
llm: A tool for obtaining a tenant access token. The input parameters must include app_id and app_secret.
parameters:
- name: app_id
type: string
required: true
label:
en_US: app_id
zh_Hans: 应用唯一标识
human_description:
en_US: app_id is the unique identifier of the Lark Open Platform application
zh_Hans: app_id 是飞书开放平台应用的唯一标识
llm_description: app_id is the unique identifier of the Lark Open Platform application
form: llm
- name: app_secret
type: secret-input
required: true
label:
en_US: app_secret
zh_Hans: 应用秘钥
human_description:
en_US: app_secret is the secret key of the application
zh_Hans: app_secret 是应用的秘钥
llm_description: app_secret is the secret key of the application
form: llm

View File

@ -1,65 +0,0 @@
import json
from typing import Any, Union
import httpx
from core.tools.entities.tool_entities import ToolInvokeMessage
from core.tools.tool.builtin_tool import BuiltinTool
class ListBaseRecordsTool(BuiltinTool):
def _invoke(
self, user_id: str, tool_parameters: dict[str, Any]
) -> Union[ToolInvokeMessage, list[ToolInvokeMessage]]:
url = "https://open.feishu.cn/open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/records/search"
access_token = tool_parameters.get("Authorization", "")
if not access_token:
return self.create_text_message("Invalid parameter access_token")
app_token = tool_parameters.get("app_token", "")
if not app_token:
return self.create_text_message("Invalid parameter app_token")
table_id = tool_parameters.get("table_id", "")
if not table_id:
return self.create_text_message("Invalid parameter table_id")
page_token = tool_parameters.get("page_token", "")
page_size = tool_parameters.get("page_size", "")
sort_condition = tool_parameters.get("sort_condition", "")
filter_condition = tool_parameters.get("filter_condition", "")
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {access_token}",
}
params = {
"page_token": page_token,
"page_size": page_size,
}
payload = {"automatic_fields": True}
if sort_condition:
payload["sort"] = json.loads(sort_condition)
if filter_condition:
payload["filter"] = json.loads(filter_condition)
try:
res = httpx.post(
url.format(app_token=app_token, table_id=table_id),
headers=headers,
params=params,
json=payload,
timeout=30,
)
res_json = res.json()
if res.is_success:
return self.create_text_message(text=json.dumps(res_json))
else:
return self.create_text_message(
f"Failed to list base records, status code: {res.status_code}, response: {res.text}"
)
except Exception as e:
return self.create_text_message("Failed to list base records. {}".format(e))

View File

@ -1,108 +0,0 @@
identity:
name: list_base_records
author: Doug Lea
label:
en_US: List Base Records
zh_Hans: 查询多维表格数据表中的现有记录
description:
human:
en_US: List base records
zh_Hans: |
查询多维表格数据表中的现有记录,单次最多查询 500 行记录,支持分页获取。
llm: Query existing records in a multidimensional table data table. A maximum of 500 rows of records can be queried at a time, and paging retrieval is supported.
parameters:
- name: Authorization
type: string
required: true
label:
en_US: token
zh_Hans: 凭证
human_description:
en_US: API access token parameter, tenant_access_token or user_access_token
zh_Hans: API 的访问凭证参数tenant_access_token 或 user_access_token
llm_description: API access token parameter, tenant_access_token or user_access_token
form: llm
- name: app_token
type: string
required: true
label:
en_US: app_token
zh_Hans: 多维表格
human_description:
en_US: bitable app token
zh_Hans: 多维表格的唯一标识符 app_token
llm_description: bitable app token
form: llm
- name: table_id
type: string
required: true
label:
en_US: table_id
zh_Hans: 多维表格的数据表
human_description:
en_US: bitable table id
zh_Hans: 多维表格数据表的唯一标识符 table_id
llm_description: bitable table id
form: llm
- name: page_token
type: string
required: false
label:
en_US: page_token
zh_Hans: 分页标记
human_description:
en_US: Pagination mark. If it is not filled in the first request, it means to traverse from the beginning.
zh_Hans: 分页标记,第一次请求不填,表示从头开始遍历。
llm_description: 分页标记,第一次请求不填,表示从头开始遍历;分页查询结果还有更多项时会同时返回新的 page_token下次遍历可采用该 page_token 获取查询结果。
form: llm
- name: page_size
type: number
required: false
default: 20
label:
en_US: page_size
zh_Hans: 分页大小
human_description:
en_US: paging size
zh_Hans: 分页大小,默认值为 20最大值为 100。
llm_description: The default value of paging size is 20 and the maximum value is 100.
form: llm
- name: sort_condition
type: string
required: false
label:
en_US: sort_condition
zh_Hans: 排序条件
human_description:
en_US: sort condition
zh_Hans: |
排序条件,格式为:[{"field_name":"多行文本","desc":true}]。
field_name: 字段名称;
desc: 是否倒序排序;
llm_description: |
Sorting conditions, the format is: [{"field_name":"multi-line text","desc":true}].
form: llm
- name: filter_condition
type: string
required: false
label:
en_US: filter_condition
zh_Hans: 筛选条件
human_description:
en_US: filter condition
zh_Hans: |
筛选条件,格式为:{"conjunction":"and","conditions":[{"field_name":"字段1","operator":"is","value":["文本内容"]}]}。
conjunction条件逻辑连接词
conditions筛选条件集合
field_name筛选条件的左值值为字段的名称
operator条件运算符
value目标值
llm_description: |
The format of the filter condition is: {"conjunction":"and","conditions":[{"field_name":"Field 1","operator":"is","value":["text content"]}]}.
form: llm

View File

@ -1,47 +0,0 @@
import json
from typing import Any, Union
import httpx
from core.tools.entities.tool_entities import ToolInvokeMessage
from core.tools.tool.builtin_tool import BuiltinTool
class ListBaseTablesTool(BuiltinTool):
def _invoke(
self, user_id: str, tool_parameters: dict[str, Any]
) -> Union[ToolInvokeMessage, list[ToolInvokeMessage]]:
url = "https://open.feishu.cn/open-apis/bitable/v1/apps/{app_token}/tables"
access_token = tool_parameters.get("Authorization", "")
if not access_token:
return self.create_text_message("Invalid parameter access_token")
app_token = tool_parameters.get("app_token", "")
if not app_token:
return self.create_text_message("Invalid parameter app_token")
page_token = tool_parameters.get("page_token", "")
page_size = tool_parameters.get("page_size", "")
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {access_token}",
}
params = {
"page_token": page_token,
"page_size": page_size,
}
try:
res = httpx.get(url.format(app_token=app_token), headers=headers, params=params, timeout=30)
res_json = res.json()
if res.is_success:
return self.create_text_message(text=json.dumps(res_json))
else:
return self.create_text_message(
f"Failed to list base tables, status code: {res.status_code}, response: {res.text}"
)
except Exception as e:
return self.create_text_message("Failed to list base tables. {}".format(e))

View File

@ -1,65 +0,0 @@
identity:
name: list_base_tables
author: Doug Lea
label:
en_US: List Base Tables
zh_Hans: 根据 app_token 获取多维表格下的所有数据表
description:
human:
en_US: List base tables
zh_Hans: |
根据 app_token 获取多维表格下的所有数据表
llm: A tool for getting all data tables under a multidimensional table based on app_token.
parameters:
- name: Authorization
type: string
required: true
label:
en_US: token
zh_Hans: 凭证
human_description:
en_US: API access token parameter, tenant_access_token or user_access_token
zh_Hans: API 的访问凭证参数tenant_access_token 或 user_access_token
llm_description: API access token parameter, tenant_access_token or user_access_token
form: llm
- name: app_token
type: string
required: true
label:
en_US: app_token
zh_Hans: 多维表格
human_description:
en_US: bitable app token
zh_Hans: 多维表格的唯一标识符 app_token
llm_description: bitable app token
form: llm
- name: page_token
type: string
required: false
label:
en_US: page_token
zh_Hans: 分页标记
human_description:
en_US: Pagination mark. If it is not filled in the first request, it means to traverse from the beginning.
zh_Hans: 分页标记,第一次请求不填,表示从头开始遍历。
llm_description: |
Pagination token. If it is not filled in the first request, it means to start traversal from the beginning.
If there are more items in the pagination query result, a new page_token will be returned at the same time.
The page_token can be used to obtain the query result in the next traversal.
分页标记,第一次请求不填,表示从头开始遍历;分页查询结果还有更多项时会同时返回新的 page_token下次遍历可采用该 page_token 获取查询结果。
form: llm
- name: page_size
type: number
required: false
default: 20
label:
en_US: page_size
zh_Hans: 分页大小
human_description:
en_US: paging size
zh_Hans: 分页大小,默认值为 20最大值为 100。
llm_description: The default value of paging size is 20 and the maximum value is 100.
form: llm

View File

@ -0,0 +1,19 @@
from typing import Any
from core.tools.entities.tool_entities import ToolInvokeMessage
from core.tools.tool.builtin_tool import BuiltinTool
from core.tools.utils.feishu_api_utils import FeishuRequest
class ListTablesTool(BuiltinTool):
def _invoke(self, user_id: str, tool_parameters: dict[str, Any]) -> ToolInvokeMessage:
app_id = self.runtime.credentials.get("app_id")
app_secret = self.runtime.credentials.get("app_secret")
client = FeishuRequest(app_id, app_secret)
app_token = tool_parameters.get("app_token")
page_token = tool_parameters.get("page_token")
page_size = tool_parameters.get("page_size", 20)
res = client.list_tables(app_token, page_token, page_size)
return self.create_json_message(res)

View File

@ -0,0 +1,50 @@
identity:
name: list_tables
author: Doug Lea
label:
en_US: List Tables
zh_Hans: 列出数据表
description:
human:
en_US: Get All Data Tables under Multidimensional Table
zh_Hans: 获取多维表格下的所有数据表
llm: A tool for getting all data tables under a multidimensional table. (获取多维表格下的所有数据表)
parameters:
- name: app_token
type: string
required: true
label:
en_US: app_token
zh_Hans: app_token
human_description:
en_US: Unique identifier for the multidimensional table, supports inputting document URL.
zh_Hans: 多维表格的唯一标识符,支持输入文档 URL。
llm_description: 多维表格的唯一标识符,支持输入文档 URL。
form: llm
- name: page_size
type: number
required: false
default: 20
label:
en_US: page_size
zh_Hans: 分页大小
human_description:
en_US: |
Page size, default value: 20, maximum value: 100.
zh_Hans: 分页大小默认值20最大值100。
llm_description: 分页大小默认值20最大值100。
form: llm
- name: page_token
type: string
required: false
label:
en_US: page_token
zh_Hans: 分页标记
human_description:
en_US: |
Page token, leave empty for the first request to start from the beginning; a new page_token will be returned if there are more items in the paginated query results, which can be used for the next traversal. Example value: "tblsRc9GRRXKqhvW".
zh_Hans: 分页标记,第一次请求不填,表示从头开始遍历;分页查询结果还有更多项时会同时返回新的 page_token下次遍历可采用该 page_token 获取查询结果。示例值:"tblsRc9GRRXKqhvW"。
llm_description: 分页标记,第一次请求不填,表示从头开始遍历;分页查询结果还有更多项时会同时返回新的 page_token下次遍历可采用该 page_token 获取查询结果。示例值:"tblsRc9GRRXKqhvW"。
form: llm

View File

@ -1,49 +0,0 @@
import json
from typing import Any, Union
import httpx
from core.tools.entities.tool_entities import ToolInvokeMessage
from core.tools.tool.builtin_tool import BuiltinTool
class ReadBaseRecordTool(BuiltinTool):
def _invoke(
self, user_id: str, tool_parameters: dict[str, Any]
) -> Union[ToolInvokeMessage, list[ToolInvokeMessage]]:
url = "https://open.feishu.cn/open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/records/{record_id}"
access_token = tool_parameters.get("Authorization", "")
if not access_token:
return self.create_text_message("Invalid parameter access_token")
app_token = tool_parameters.get("app_token", "")
if not app_token:
return self.create_text_message("Invalid parameter app_token")
table_id = tool_parameters.get("table_id", "")
if not table_id:
return self.create_text_message("Invalid parameter table_id")
record_id = tool_parameters.get("record_id", "")
if not record_id:
return self.create_text_message("Invalid parameter record_id")
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {access_token}",
}
try:
res = httpx.get(
url.format(app_token=app_token, table_id=table_id, record_id=record_id), headers=headers, timeout=30
)
res_json = res.json()
if res.is_success:
return self.create_text_message(text=json.dumps(res_json))
else:
return self.create_text_message(
f"Failed to read base record, status code: {res.status_code}, response: {res.text}"
)
except Exception as e:
return self.create_text_message("Failed to read base record. {}".format(e))

View File

@ -1,60 +0,0 @@
identity:
name: read_base_record
author: Doug Lea
label:
en_US: Read Base Record
zh_Hans: 根据 record_id 的值检索多维表格数据表的记录
description:
human:
en_US: Read base record
zh_Hans: |
根据 record_id 的值检索多维表格数据表的记录
llm: Retrieve records from a multidimensional table based on the value of record_id
parameters:
- name: Authorization
type: string
required: true
label:
en_US: token
zh_Hans: 凭证
human_description:
en_US: API access token parameter, tenant_access_token or user_access_token
zh_Hans: API 的访问凭证参数tenant_access_token 或 user_access_token
llm_description: API access token parameter, tenant_access_token or user_access_token
form: llm
- name: app_token
type: string
required: true
label:
en_US: app_token
zh_Hans: 多维表格
human_description:
en_US: bitable app token
zh_Hans: 多维表格的唯一标识符 app_token
llm_description: bitable app token
form: llm
- name: table_id
type: string
required: true
label:
en_US: table_id
zh_Hans: 多维表格的数据表
human_description:
en_US: bitable table id
zh_Hans: 多维表格数据表的唯一标识符 table_id
llm_description: bitable table id
form: llm
- name: record_id
type: string
required: true
label:
en_US: record_id
zh_Hans: 单条记录的 id
human_description:
en_US: The id of a single record
zh_Hans: 单条记录的 id
llm_description: The id of a single record
form: llm

View File

@ -0,0 +1,21 @@
from typing import Any
from core.tools.entities.tool_entities import ToolInvokeMessage
from core.tools.tool.builtin_tool import BuiltinTool
from core.tools.utils.feishu_api_utils import FeishuRequest
class ReadRecordsTool(BuiltinTool):
def _invoke(self, user_id: str, tool_parameters: dict[str, Any]) -> ToolInvokeMessage:
app_id = self.runtime.credentials.get("app_id")
app_secret = self.runtime.credentials.get("app_secret")
client = FeishuRequest(app_id, app_secret)
app_token = tool_parameters.get("app_token")
table_id = tool_parameters.get("table_id")
table_name = tool_parameters.get("table_name")
record_ids = tool_parameters.get("record_ids")
user_id_type = tool_parameters.get("user_id_type", "open_id")
res = client.read_records(app_token, table_id, table_name, record_ids, user_id_type)
return self.create_json_message(res)

View File

@ -0,0 +1,86 @@
identity:
name: read_records
author: Doug Lea
label:
en_US: Read Records
zh_Hans: 批量获取记录
description:
human:
en_US: Batch Retrieve Records from Multidimensional Table
zh_Hans: 批量获取多维表格数据表中的记录信息
llm: A tool for batch retrieving records from a multidimensional table, supporting up to 100 records per call. (批量获取多维表格数据表中的记录信息,单次调用最多支持查询 100 条记录)
parameters:
- name: app_token
type: string
required: true
label:
en_US: app_token
zh_Hans: app_token
human_description:
en_US: Unique identifier for the multidimensional table, supports inputting document URL.
zh_Hans: 多维表格的唯一标识符,支持输入文档 URL。
llm_description: 多维表格的唯一标识符,支持输入文档 URL。
form: llm
- name: table_id
type: string
required: false
label:
en_US: table_id
zh_Hans: table_id
human_description:
en_US: Unique identifier for the multidimensional table data, either table_id or table_name must be provided, cannot be empty simultaneously.
zh_Hans: 多维表格数据表的唯一标识符table_id 和 table_name 至少需要提供一个,不能同时为空。
llm_description: 多维表格数据表的唯一标识符table_id 和 table_name 至少需要提供一个,不能同时为空。
form: llm
- name: table_name
type: string
required: false
label:
en_US: table_name
zh_Hans: table_name
human_description:
en_US: Name of the multidimensional table data, either table_name or table_id must be provided, cannot be empty simultaneously.
zh_Hans: 多维表格数据表的名称table_name 和 table_id 至少需要提供一个,不能同时为空。
llm_description: 多维表格数据表的名称table_name 和 table_id 至少需要提供一个,不能同时为空。
form: llm
- name: record_ids
type: string
required: true
label:
en_US: record_ids
zh_Hans: 记录 ID 列表
human_description:
en_US: List of record IDs, which can be obtained by calling the "Query Records API".
zh_Hans: 记录 ID 列表,可以通过调用"查询记录接口"获取。
llm_description: 记录 ID 列表,可以通过调用"查询记录接口"获取。
form: llm
- name: user_id_type
type: select
required: false
options:
- value: open_id
label:
en_US: open_id
zh_Hans: open_id
- value: union_id
label:
en_US: union_id
zh_Hans: union_id
- value: user_id
label:
en_US: user_id
zh_Hans: user_id
default: "open_id"
label:
en_US: user_id_type
zh_Hans: 用户 ID 类型
human_description:
en_US: User ID type, optional values are open_id, union_id, user_id, with a default value of open_id.
zh_Hans: 用户 ID 类型,可选值有 open_id、union_id、user_id默认值为 open_id。
llm_description: 用户 ID 类型,可选值有 open_id、union_id、user_id默认值为 open_id。
form: form

View File

@ -0,0 +1,39 @@
from typing import Any
from core.tools.entities.tool_entities import ToolInvokeMessage
from core.tools.tool.builtin_tool import BuiltinTool
from core.tools.utils.feishu_api_utils import FeishuRequest
class SearchRecordsTool(BuiltinTool):
def _invoke(self, user_id: str, tool_parameters: dict[str, Any]) -> ToolInvokeMessage:
app_id = self.runtime.credentials.get("app_id")
app_secret = self.runtime.credentials.get("app_secret")
client = FeishuRequest(app_id, app_secret)
app_token = tool_parameters.get("app_token")
table_id = tool_parameters.get("table_id")
table_name = tool_parameters.get("table_name")
view_id = tool_parameters.get("view_id")
field_names = tool_parameters.get("field_names")
sort = tool_parameters.get("sort")
filters = tool_parameters.get("filter")
page_token = tool_parameters.get("page_token")
automatic_fields = tool_parameters.get("automatic_fields", False)
user_id_type = tool_parameters.get("user_id_type", "open_id")
page_size = tool_parameters.get("page_size", 20)
res = client.search_record(
app_token,
table_id,
table_name,
view_id,
field_names,
sort,
filters,
page_token,
automatic_fields,
user_id_type,
page_size,
)
return self.create_json_message(res)

View File

@ -0,0 +1,163 @@
identity:
name: search_records
author: Doug Lea
label:
en_US: Search Records
zh_Hans: 查询记录
description:
human:
en_US: Query records in a multidimensional table, up to 500 rows per query.
zh_Hans: 查询多维表格数据表中的记录,单次最多查询 500 行记录。
llm: A tool for querying records in a multidimensional table, up to 500 rows per query. (查询多维表格数据表中的记录,单次最多查询 500 行记录)
parameters:
- name: app_token
type: string
required: true
label:
en_US: app_token
zh_Hans: app_token
human_description:
en_US: Unique identifier for the multidimensional table, supports inputting document URL.
zh_Hans: 多维表格的唯一标识符,支持输入文档 URL。
llm_description: 多维表格的唯一标识符,支持输入文档 URL。
form: llm
- name: table_id
type: string
required: false
label:
en_US: table_id
zh_Hans: table_id
human_description:
en_US: Unique identifier for the multidimensional table data, either table_id or table_name must be provided, cannot be empty simultaneously.
zh_Hans: 多维表格数据表的唯一标识符table_id 和 table_name 至少需要提供一个,不能同时为空。
llm_description: 多维表格数据表的唯一标识符table_id 和 table_name 至少需要提供一个,不能同时为空。
form: llm
- name: table_name
type: string
required: false
label:
en_US: table_name
zh_Hans: table_name
human_description:
en_US: Name of the multidimensional table data, either table_name or table_id must be provided, cannot be empty simultaneously.
zh_Hans: 多维表格数据表的名称table_name 和 table_id 至少需要提供一个,不能同时为空。
llm_description: 多维表格数据表的名称table_name 和 table_id 至少需要提供一个,不能同时为空。
form: llm
- name: view_id
type: string
required: false
label:
en_US: view_id
zh_Hans: 视图唯一标识
human_description:
en_US: |
Unique identifier for a view in a multidimensional table. It can be found in the URL's query parameter with the key 'view'. For example: https://svi136aogf123.feishu.cn/base/KWC8bYsYXahYqGsTtqectNn9n3e?table=tblE8a2fmBIEflaE&view=vewlkAVpRx.
zh_Hans: 多维表格中视图的唯一标识,可在多维表格的 URL 地址栏中找到query 参数中 key 为 view 的部分。例如https://svi136aogf123.feishu.cn/base/KWC8bYsYXahYqGsTtqectNn9n3e?table=tblE8a2fmBIEflaE&view=vewlkAVpRx。
llm_description: 多维表格中视图的唯一标识,可在多维表格的 URL 地址栏中找到query 参数中 key 为 view 的部分。例如https://svi136aogf123.feishu.cn/base/KWC8bYsYXahYqGsTtqectNn9n3e?table=tblE8a2fmBIEflaE&view=vewlkAVpRx。
form: llm
- name: field_names
type: string
required: false
label:
en_US: field_names
zh_Hans: 字段名称
human_description:
en_US: |
Field names to specify which fields to include in the returned records. Example value: ["Field1", "Field2"].
zh_Hans: 字段名称,用于指定本次查询返回记录中包含的字段。示例值:["字段1","字段2"]。
llm_description: 字段名称,用于指定本次查询返回记录中包含的字段。示例值:["字段1","字段2"]。
form: llm
- name: sort
type: string
required: false
label:
en_US: sort
zh_Hans: 排序条件
human_description:
en_US: |
Sorting conditions, for example: [{"field_name":"Multiline Text","desc":true}].
zh_Hans: 排序条件,例如:[{"field_name":"多行文本","desc":true}]。
llm_description: 排序条件,例如:[{"field_name":"多行文本","desc":true}]。
form: llm
- name: filter
type: string
required: false
label:
en_US: filter
zh_Hans: 筛选条件
human_description:
en_US: Object containing filter information. For details on how to fill in the filter, refer to the record filter parameter guide (https://open.larkoffice.com/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-table-record/record-filter-guide).
zh_Hans: 包含条件筛选信息的对象。了解如何填写 filter参考记录筛选参数填写指南(https://open.larkoffice.com/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-table-record/record-filter-guide)。
llm_description: 包含条件筛选信息的对象。了解如何填写 filter参考记录筛选参数填写指南(https://open.larkoffice.com/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-table-record/record-filter-guide)。
form: llm
- name: automatic_fields
type: boolean
required: false
label:
en_US: automatic_fields
zh_Hans: automatic_fields
human_description:
en_US: Whether to return automatically calculated fields. Default is false, meaning they are not returned.
zh_Hans: 是否返回自动计算的字段。默认为 false表示不返回。
llm_description: 是否返回自动计算的字段。默认为 false表示不返回。
form: form
- name: user_id_type
type: select
required: false
options:
- value: open_id
label:
en_US: open_id
zh_Hans: open_id
- value: union_id
label:
en_US: union_id
zh_Hans: union_id
- value: user_id
label:
en_US: user_id
zh_Hans: user_id
default: "open_id"
label:
en_US: user_id_type
zh_Hans: 用户 ID 类型
human_description:
en_US: User ID type, optional values are open_id, union_id, user_id, with a default value of open_id.
zh_Hans: 用户 ID 类型,可选值有 open_id、union_id、user_id默认值为 open_id。
llm_description: 用户 ID 类型,可选值有 open_id、union_id、user_id默认值为 open_id。
form: form
- name: page_size
type: number
required: false
default: 20
label:
en_US: page_size
zh_Hans: 分页大小
human_description:
en_US: |
Page size, default value: 20, maximum value: 500.
zh_Hans: 分页大小默认值20最大值500。
llm_description: 分页大小默认值20最大值500。
form: llm
- name: page_token
type: string
required: false
label:
en_US: page_token
zh_Hans: 分页标记
human_description:
en_US: |
Page token, leave empty for the first request to start from the beginning; a new page_token will be returned if there are more items in the paginated query results, which can be used for the next traversal. Example value: "tblsRc9GRRXKqhvW".
zh_Hans: 分页标记,第一次请求不填,表示从头开始遍历;分页查询结果还有更多项时会同时返回新的 page_token下次遍历可采用该 page_token 获取查询结果。示例值:"tblsRc9GRRXKqhvW"。
llm_description: 分页标记,第一次请求不填,表示从头开始遍历;分页查询结果还有更多项时会同时返回新的 page_token下次遍历可采用该 page_token 获取查询结果。示例值:"tblsRc9GRRXKqhvW"。
form: llm

View File

@ -1,60 +0,0 @@
import json
from typing import Any, Union
import httpx
from core.tools.entities.tool_entities import ToolInvokeMessage
from core.tools.tool.builtin_tool import BuiltinTool
class UpdateBaseRecordTool(BuiltinTool):
def _invoke(
self, user_id: str, tool_parameters: dict[str, Any]
) -> Union[ToolInvokeMessage, list[ToolInvokeMessage]]:
url = "https://open.feishu.cn/open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/records/{record_id}"
access_token = tool_parameters.get("Authorization", "")
if not access_token:
return self.create_text_message("Invalid parameter access_token")
app_token = tool_parameters.get("app_token", "")
if not app_token:
return self.create_text_message("Invalid parameter app_token")
table_id = tool_parameters.get("table_id", "")
if not table_id:
return self.create_text_message("Invalid parameter table_id")
record_id = tool_parameters.get("record_id", "")
if not record_id:
return self.create_text_message("Invalid parameter record_id")
fields = tool_parameters.get("fields", "")
if not fields:
return self.create_text_message("Invalid parameter fields")
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {access_token}",
}
params = {}
payload = {"fields": json.loads(fields)}
try:
res = httpx.put(
url.format(app_token=app_token, table_id=table_id, record_id=record_id),
headers=headers,
params=params,
json=payload,
timeout=30,
)
res_json = res.json()
if res.is_success:
return self.create_text_message(text=json.dumps(res_json))
else:
return self.create_text_message(
f"Failed to update base record, status code: {res.status_code}, response: {res.text}"
)
except Exception as e:
return self.create_text_message("Failed to update base record. {}".format(e))

View File

@ -1,78 +0,0 @@
identity:
name: update_base_record
author: Doug Lea
label:
en_US: Update Base Record
zh_Hans: 更新多维表格数据表中的一条记录
description:
human:
en_US: Update base record
zh_Hans: |
更新多维表格数据表中的一条记录详细请参考https://open.larkoffice.com/document/server-docs/docs/bitable-v1/app-table-record/update
llm: Update a record in a multidimensional table data table
parameters:
- name: Authorization
type: string
required: true
label:
en_US: token
zh_Hans: 凭证
human_description:
en_US: API access token parameter, tenant_access_token or user_access_token
zh_Hans: API 的访问凭证参数tenant_access_token 或 user_access_token
llm_description: API access token parameter, tenant_access_token or user_access_token
form: llm
- name: app_token
type: string
required: true
label:
en_US: app_token
zh_Hans: 多维表格
human_description:
en_US: bitable app token
zh_Hans: 多维表格的唯一标识符 app_token
llm_description: bitable app token
form: llm
- name: table_id
type: string
required: true
label:
en_US: table_id
zh_Hans: 多维表格的数据表
human_description:
en_US: bitable table id
zh_Hans: 多维表格数据表的唯一标识符 table_id
llm_description: bitable table id
form: llm
- name: record_id
type: string
required: true
label:
en_US: record_id
zh_Hans: 单条记录的 id
human_description:
en_US: The id of a single record
zh_Hans: 单条记录的 id
llm_description: The id of a single record
form: llm
- name: fields
type: string
required: true
label:
en_US: fields
zh_Hans: 数据表的列字段内容
human_description:
en_US: The fields of a multidimensional table data table, that is, the columns of the data table.
zh_Hans: |
要更新一行多维表格记录,字段结构拼接如下:{"多行文本":"多行文本内容","单选":"选项1","多选":["选项1","选项2"],"复选框":true,"人员":[{"id":"ou_2910013f1e6456f16a0ce75ede950a0a"}],"群组":[{"id":"oc_cd07f55f14d6f4a4f1b51504e7e97f48"}],"电话号码":"13026162666"}
当前接口支持的字段类型为:多行文本、单选、条码、多选、日期、人员、附件、复选框、超链接、数字、单向关联、双向关联、电话号码、地理位置。
不同类型字段的数据结构请参考数据结构概述https://open.larkoffice.com/document/server-docs/docs/bitable-v1/bitable-structure
llm_description: |
要更新一行多维表格记录,字段结构拼接如下:{"多行文本":"多行文本内容","单选":"选项1","多选":["选项1","选项2"],"复选框":true,"人员":[{"id":"ou_2910013f1e6456f16a0ce75ede950a0a"}],"群组":[{"id":"oc_cd07f55f14d6f4a4f1b51504e7e97f48"}],"电话号码":"13026162666"}
当前接口支持的字段类型为:多行文本、单选、条码、多选、日期、人员、附件、复选框、超链接、数字、单向关联、双向关联、电话号码、地理位置。
不同类型字段的数据结构请参考数据结构概述https://open.larkoffice.com/document/server-docs/docs/bitable-v1/bitable-structure
form: llm

View File

@ -0,0 +1,21 @@
from typing import Any
from core.tools.entities.tool_entities import ToolInvokeMessage
from core.tools.tool.builtin_tool import BuiltinTool
from core.tools.utils.feishu_api_utils import FeishuRequest
class UpdateRecordsTool(BuiltinTool):
def _invoke(self, user_id: str, tool_parameters: dict[str, Any]) -> ToolInvokeMessage:
app_id = self.runtime.credentials.get("app_id")
app_secret = self.runtime.credentials.get("app_secret")
client = FeishuRequest(app_id, app_secret)
app_token = tool_parameters.get("app_token")
table_id = tool_parameters.get("table_id")
table_name = tool_parameters.get("table_name")
records = tool_parameters.get("records")
user_id_type = tool_parameters.get("user_id_type", "open_id")
res = client.update_records(app_token, table_id, table_name, records, user_id_type)
return self.create_json_message(res)

View File

@ -0,0 +1,91 @@
identity:
name: update_records
author: Doug Lea
label:
en_US: Update Records
zh_Hans: 更新多条记录
description:
human:
en_US: Update Multiple Records in Multidimensional Table
zh_Hans: 更新多维表格数据表中的多条记录
llm: A tool for updating multiple records in a multidimensional table. (更新多维表格数据表中的多条记录)
parameters:
- name: app_token
type: string
required: true
label:
en_US: app_token
zh_Hans: app_token
human_description:
en_US: Unique identifier for the multidimensional table, supports inputting document URL.
zh_Hans: 多维表格的唯一标识符,支持输入文档 URL。
llm_description: 多维表格的唯一标识符,支持输入文档 URL。
form: llm
- name: table_id
type: string
required: false
label:
en_US: table_id
zh_Hans: table_id
human_description:
en_US: Unique identifier for the multidimensional table data, either table_id or table_name must be provided, cannot be empty simultaneously.
zh_Hans: 多维表格数据表的唯一标识符table_id 和 table_name 至少需要提供一个,不能同时为空。
llm_description: 多维表格数据表的唯一标识符table_id 和 table_name 至少需要提供一个,不能同时为空。
form: llm
- name: table_name
type: string
required: false
label:
en_US: table_name
zh_Hans: table_name
human_description:
en_US: Name of the multidimensional table data, either table_name or table_id must be provided, cannot be empty simultaneously.
zh_Hans: 多维表格数据表的名称table_name 和 table_id 至少需要提供一个,不能同时为空。
llm_description: 多维表格数据表的名称table_name 和 table_id 至少需要提供一个,不能同时为空。
form: llm
- name: records
type: string
required: true
label:
en_US: records
zh_Hans: 记录列表
human_description:
en_US: |
List of records to be updated in this request. Example value: [{"fields":{"multi-line-text":"text content","single_select":"option 1","date":1674206443000},"record_id":"recupK4f4RM5RX"}].
For supported field types, refer to the integration guide (https://open.larkoffice.com/document/server-docs/docs/bitable-v1/notification). For data structures of different field types, refer to the data structure overview (https://open.larkoffice.com/document/server-docs/docs/bitable-v1/bitable-structure).
zh_Hans: |
本次请求将要更新的记录列表,示例值:[{"fields":{"多行文本":"文本内容","单选":"选项 1","日期":1674206443000},"record_id":"recupK4f4RM5RX"}]。
当前接口支持的字段类型请参考接入指南(https://open.larkoffice.com/document/server-docs/docs/bitable-v1/notification),不同类型字段的数据结构请参考数据结构概述(https://open.larkoffice.com/document/server-docs/docs/bitable-v1/bitable-structure)。
llm_description: |
本次请求将要更新的记录列表,示例值:[{"fields":{"多行文本":"文本内容","单选":"选项 1","日期":1674206443000},"record_id":"recupK4f4RM5RX"}]。
当前接口支持的字段类型请参考接入指南(https://open.larkoffice.com/document/server-docs/docs/bitable-v1/notification),不同类型字段的数据结构请参考数据结构概述(https://open.larkoffice.com/document/server-docs/docs/bitable-v1/bitable-structure)。
form: llm
- name: user_id_type
type: select
required: false
options:
- value: open_id
label:
en_US: open_id
zh_Hans: open_id
- value: union_id
label:
en_US: union_id
zh_Hans: union_id
- value: user_id
label:
en_US: user_id
zh_Hans: user_id
default: "open_id"
label:
en_US: user_id_type
zh_Hans: 用户 ID 类型
human_description:
en_US: User ID type, optional values are open_id, union_id, user_id, with a default value of open_id.
zh_Hans: 用户 ID 类型,可选值有 open_id、union_id、user_id默认值为 open_id。
llm_description: 用户 ID 类型,可选值有 open_id、union_id、user_id默认值为 open_id。
form: form

View File

@ -0,0 +1,24 @@
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="100" height="100" rx="20" fill="#4A90E2" />
<path
d="M50 25C40.6 25 33 32.6 33 42V58C33 67.4 40.6 75 50 75C59.4 75 67 67.4 67 58V42C67 32.6 59.4 25 50 25ZM61 58C61 64.1 56.1 69 50 69C43.9 69 39 64.1 39 58V42C39 35.9 43.9 31 50 31C56.1 31 61 35.9 61 42V58Z"
fill="white" />
<path d="M50 37C47.2 37 45 39.2 45 42V58C45 60.8 47.2 63 50 63C52.8 63 55 60.8 55 58V42C55 39.2 52.8 37 50 37Z"
fill="white" />
<path
d="M73 49H69V58C69 68.5 60.5 77 50 77C39.5 77 31 68.5 31 58V49H27V58C27 70.7 37.3 81 50 81C62.7 81 73 70.7 73 58V49Z"
fill="white" />
<path d="M50 85C51.1 85 52 84.1 52 83V81H48V83C48 84.1 48.9 85 50 85Z" fill="white" />
<path
d="M35 45C36.1046 45 37 44.1046 37 43C37 41.8954 36.1046 41 35 41C33.8954 41 33 41.8954 33 43C33 44.1046 33.8954 45 35 45Z"
fill="white" />
<path
d="M35 55C36.1046 55 37 54.1046 37 53C37 51.8954 36.1046 51 35 51C33.8954 51 33 51.8954 33 53C33 54.1046 33.8954 55 35 55Z"
fill="white" />
<path
d="M65 45C66.1046 45 67 44.1046 67 43C67 41.8954 66.1046 41 65 41C63.8954 41 63 41.8954 63 43C63 44.1046 63.8954 45 65 45Z"
fill="white" />
<path
d="M65 55C66.1046 55 67 54.1046 67 53C67 51.8954 66.1046 51 65 51C63.8954 51 63 51.8954 63 53C63 54.1046 63.8954 55 65 55Z"
fill="white" />
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,33 @@
from typing import Any
import openai
from core.tools.errors import ToolProviderCredentialValidationError
from core.tools.provider.builtin_tool_provider import BuiltinToolProviderController
class PodcastGeneratorProvider(BuiltinToolProviderController):
def _validate_credentials(self, credentials: dict[str, Any]) -> None:
tts_service = credentials.get("tts_service")
api_key = credentials.get("api_key")
if not tts_service:
raise ToolProviderCredentialValidationError("TTS service is not specified")
if not api_key:
raise ToolProviderCredentialValidationError("API key is missing")
if tts_service == "openai":
self._validate_openai_credentials(api_key)
else:
raise ToolProviderCredentialValidationError(f"Unsupported TTS service: {tts_service}")
def _validate_openai_credentials(self, api_key: str) -> None:
client = openai.OpenAI(api_key=api_key)
try:
# We're using a simple API call to validate the credentials
client.models.list()
except openai.AuthenticationError:
raise ToolProviderCredentialValidationError("Invalid OpenAI API key")
except Exception as e:
raise ToolProviderCredentialValidationError(f"Error validating OpenAI API key: {str(e)}")

View File

@ -0,0 +1,34 @@
identity:
author: Dify
name: podcast_generator
label:
en_US: Podcast Generator
zh_Hans: 播客生成器
description:
en_US: Generate podcast audio using Text-to-Speech services
zh_Hans: 使用文字转语音服务生成播客音频
icon: icon.svg
credentials_for_provider:
tts_service:
type: select
required: true
label:
en_US: TTS Service
zh_Hans: TTS 服务
placeholder:
en_US: Select a TTS service
zh_Hans: 选择一个 TTS 服务
options:
- label:
en_US: OpenAI TTS
zh_Hans: OpenAI TTS
value: openai
api_key:
type: secret-input
required: true
label:
en_US: API Key
zh_Hans: API 密钥
placeholder:
en_US: Enter your TTS service API key
zh_Hans: 输入您的 TTS 服务 API 密钥

View File

@ -0,0 +1,100 @@
import concurrent.futures
import io
import random
from typing import Any, Literal, Optional, Union
import openai
from pydub import AudioSegment
from core.tools.entities.tool_entities import ToolInvokeMessage
from core.tools.errors import ToolParameterValidationError, ToolProviderCredentialValidationError
from core.tools.tool.builtin_tool import BuiltinTool
class PodcastAudioGeneratorTool(BuiltinTool):
@staticmethod
def _generate_silence(duration: float):
# Generate silent WAV data using pydub
silence = AudioSegment.silent(duration=int(duration * 1000)) # pydub uses milliseconds
return silence
@staticmethod
def _generate_audio_segment(
client: openai.OpenAI,
line: str,
voice: Literal["alloy", "echo", "fable", "onyx", "nova", "shimmer"],
index: int,
) -> tuple[int, Union[AudioSegment, str], Optional[AudioSegment]]:
try:
response = client.audio.speech.create(model="tts-1", voice=voice, input=line.strip(), response_format="wav")
audio = AudioSegment.from_wav(io.BytesIO(response.content))
silence_duration = random.uniform(0.1, 1.5)
silence = PodcastAudioGeneratorTool._generate_silence(silence_duration)
return index, audio, silence
except Exception as e:
return index, f"Error generating audio: {str(e)}", None
def _invoke(
self, user_id: str, tool_parameters: dict[str, Any]
) -> Union[ToolInvokeMessage, list[ToolInvokeMessage]]:
# Extract parameters
script = tool_parameters.get("script", "")
host1_voice = tool_parameters.get("host1_voice")
host2_voice = tool_parameters.get("host2_voice")
# Split the script into lines
script_lines = [line for line in script.split("\n") if line.strip()]
# Ensure voices are provided
if not host1_voice or not host2_voice:
raise ToolParameterValidationError("Host voices are required")
# Get OpenAI API key from credentials
if not self.runtime or not self.runtime.credentials:
raise ToolProviderCredentialValidationError("Tool runtime or credentials are missing")
api_key = self.runtime.credentials.get("api_key")
if not api_key:
raise ToolProviderCredentialValidationError("OpenAI API key is missing")
# Initialize OpenAI client
client = openai.OpenAI(api_key=api_key)
# Create a thread pool
max_workers = 5
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
futures = []
for i, line in enumerate(script_lines):
voice = host1_voice if i % 2 == 0 else host2_voice
future = executor.submit(self._generate_audio_segment, client, line, voice, i)
futures.append(future)
# Collect results
audio_segments: list[Any] = [None] * len(script_lines)
for future in concurrent.futures.as_completed(futures):
index, audio, silence = future.result()
if isinstance(audio, str): # Error occurred
return self.create_text_message(audio)
audio_segments[index] = (audio, silence)
# Combine audio segments in the correct order
combined_audio = AudioSegment.empty()
for i, (audio, silence) in enumerate(audio_segments):
if audio:
combined_audio += audio
if i < len(audio_segments) - 1 and silence:
combined_audio += silence
# Export the combined audio to a WAV file in memory
buffer = io.BytesIO()
combined_audio.export(buffer, format="wav")
wav_bytes = buffer.getvalue()
# Create a blob message with the combined audio
return [
self.create_text_message("Audio generated successfully"),
self.create_blob_message(
blob=wav_bytes,
meta={"mime_type": "audio/x-wav"},
save_as=self.VariableKey.AUDIO,
),
]

View File

@ -0,0 +1,95 @@
identity:
name: podcast_audio_generator
author: Dify
label:
en_US: Podcast Audio Generator
zh_Hans: 播客音频生成器
description:
human:
en_US: Generate a podcast audio file from a script with two alternating voices using OpenAI's TTS service.
zh_Hans: 使用 OpenAI 的 TTS 服务,从包含两个交替声音的脚本生成播客音频文件。
llm: This tool converts a prepared podcast script into an audio file using OpenAI's Text-to-Speech service, with two specified voices for alternating hosts.
parameters:
- name: script
type: string
required: true
label:
en_US: Podcast Script
zh_Hans: 播客脚本
human_description:
en_US: A string containing alternating lines for two hosts, separated by newline characters.
zh_Hans: 包含两位主持人交替台词的字符串,每行用换行符分隔。
llm_description: A string representing the script, with alternating lines for two hosts separated by newline characters.
form: llm
- name: host1_voice
type: select
required: true
label:
en_US: Host 1 Voice
zh_Hans: 主持人1 音色
human_description:
en_US: The voice for the first host.
zh_Hans: 第一位主持人的音色。
llm_description: The voice identifier for the first host's voice.
options:
- label:
en_US: Alloy
zh_Hans: Alloy
value: alloy
- label:
en_US: Echo
zh_Hans: Echo
value: echo
- label:
en_US: Fable
zh_Hans: Fable
value: fable
- label:
en_US: Onyx
zh_Hans: Onyx
value: onyx
- label:
en_US: Nova
zh_Hans: Nova
value: nova
- label:
en_US: Shimmer
zh_Hans: Shimmer
value: shimmer
form: form
- name: host2_voice
type: select
required: true
label:
en_US: Host 2 Voice
zh_Hans: 主持人2 音色
human_description:
en_US: The voice for the second host.
zh_Hans: 第二位主持人的音色。
llm_description: The voice identifier for the second host's voice.
options:
- label:
en_US: Alloy
zh_Hans: Alloy
value: alloy
- label:
en_US: Echo
zh_Hans: Echo
value: echo
- label:
en_US: Fable
zh_Hans: Fable
value: fable
- label:
en_US: Onyx
zh_Hans: Onyx
value: onyx
- label:
en_US: Nova
zh_Hans: Nova
value: nova
- label:
en_US: Shimmer
zh_Hans: Shimmer
value: shimmer
form: form

View File

@ -1,71 +0,0 @@
from typing import Any
from core.tools.entities.tool_entities import ToolParameter
class ToolParameterConverter:
@staticmethod
def get_parameter_type(parameter_type: str | ToolParameter.ToolParameterType) -> str:
match parameter_type:
case (
ToolParameter.ToolParameterType.STRING
| ToolParameter.ToolParameterType.SECRET_INPUT
| ToolParameter.ToolParameterType.SELECT
):
return "string"
case ToolParameter.ToolParameterType.BOOLEAN:
return "boolean"
case ToolParameter.ToolParameterType.NUMBER:
return "number"
case _:
raise ValueError(f"Unsupported parameter type {parameter_type}")
@staticmethod
def cast_parameter_by_type(value: Any, parameter_type: str) -> Any:
# convert tool parameter config to correct type
try:
match parameter_type:
case (
ToolParameter.ToolParameterType.STRING
| ToolParameter.ToolParameterType.SECRET_INPUT
| ToolParameter.ToolParameterType.SELECT
):
if value is None:
return ""
else:
return value if isinstance(value, str) else str(value)
case ToolParameter.ToolParameterType.BOOLEAN:
if value is None:
return False
elif isinstance(value, str):
# Allowed YAML boolean value strings: https://yaml.org/type/bool.html
# and also '0' for False and '1' for True
match value.lower():
case "true" | "yes" | "y" | "1":
return True
case "false" | "no" | "n" | "0":
return False
case _:
return bool(value)
else:
return value if isinstance(value, bool) else bool(value)
case ToolParameter.ToolParameterType.NUMBER:
if isinstance(value, int) | isinstance(value, float):
return value
elif isinstance(value, str) and value != "":
if "." in value:
return float(value)
else:
return int(value)
case ToolParameter.ToolParameterType.FILE:
return value
case _:
return str(value)
except Exception:
raise ValueError(f"The tool parameter value {value} is not in correct type of {parameter_type}.")

View File

@ -1,7 +1,12 @@
from .segment_group import SegmentGroup
from .segments import (
ArrayAnySegment,
ArrayFileSegment,
ArrayNumberSegment,
ArrayObjectSegment,
ArraySegment,
ArrayStringSegment,
FileSegment,
FloatSegment,
IntegerSegment,
NoneSegment,
@ -15,6 +20,7 @@ from .variables import (
ArrayNumberVariable,
ArrayObjectVariable,
ArrayStringVariable,
FileVariable,
FloatVariable,
IntegerVariable,
NoneVariable,
@ -46,4 +52,10 @@ __all__ = [
"ArrayNumberVariable",
"ArrayObjectVariable",
"ArraySegment",
"ArrayFileSegment",
"ArrayNumberSegment",
"ArrayObjectSegment",
"ArrayStringSegment",
"FileSegment",
"FileVariable",
]

View File

@ -5,6 +5,8 @@ from typing import Any
from pydantic import BaseModel, ConfigDict, field_validator
from core.file import File
from .types import SegmentType
@ -39,6 +41,9 @@ class Segment(BaseModel):
@property
def size(self) -> int:
"""
Return the size of the value in bytes.
"""
return sys.getsizeof(self.value)
def to_object(self) -> Any:
@ -51,15 +56,15 @@ class NoneSegment(Segment):
@property
def text(self) -> str:
return "null"
return ""
@property
def log(self) -> str:
return "null"
return ""
@property
def markdown(self) -> str:
return "null"
return ""
class StringSegment(Segment):
@ -99,13 +104,27 @@ class ArraySegment(Segment):
def markdown(self) -> str:
items = []
for item in self.value:
if hasattr(item, "to_markdown"):
items.append(item.to_markdown())
else:
items.append(str(item))
items.append(str(item))
return "\n".join(items)
class FileSegment(Segment):
value_type: SegmentType = SegmentType.FILE
value: File
@property
def markdown(self) -> str:
return self.value.markdown
@property
def log(self) -> str:
return str(self.value)
@property
def text(self) -> str:
return str(self.value)
class ArrayAnySegment(ArraySegment):
value_type: SegmentType = SegmentType.ARRAY_ANY
value: Sequence[Any]
@ -124,3 +143,15 @@ class ArrayNumberSegment(ArraySegment):
class ArrayObjectSegment(ArraySegment):
value_type: SegmentType = SegmentType.ARRAY_OBJECT
value: Sequence[Mapping[str, Any]]
class ArrayFileSegment(ArraySegment):
value_type: SegmentType = SegmentType.ARRAY_FILE
value: Sequence[File]
@property
def markdown(self) -> str:
items = []
for item in self.value:
items.append(item.markdown)
return "\n".join(items)

View File

@ -11,5 +11,7 @@ class SegmentType(str, Enum):
ARRAY_NUMBER = "array[number]"
ARRAY_OBJECT = "array[object]"
OBJECT = "object"
FILE = "file"
ARRAY_FILE = "array[file]"
GROUP = "group"

View File

@ -7,6 +7,7 @@ from .segments import (
ArrayNumberSegment,
ArrayObjectSegment,
ArrayStringSegment,
FileSegment,
FloatSegment,
IntegerSegment,
NoneSegment,
@ -73,3 +74,7 @@ class SecretVariable(StringVariable):
class NoneVariable(NoneSegment, Variable):
value_type: SegmentType = SegmentType.NONE
value: None = None
class FileVariable(FileSegment, Variable):
pass

View File

@ -1,7 +1,6 @@
from typing import Optional
from core.model_runtime.utils.encoders import jsonable_encoder
from core.workflow.callbacks.base_workflow_callback import WorkflowCallback
from core.workflow.graph_engine.entities.event import (
GraphEngineEvent,
GraphRunFailedEvent,
@ -20,6 +19,8 @@ from core.workflow.graph_engine.entities.event import (
ParallelBranchRunSucceededEvent,
)
from .base_workflow_callback import WorkflowCallback
_TEXT_COLOR_MAPPING = {
"blue": "36;1",
"yellow": "33;1",

View File

@ -0,0 +1,3 @@
SYSTEM_VARIABLE_NODE_ID = "sys"
ENVIRONMENT_VARIABLE_NODE_ID = "env"
CONVERSATION_VARIABLE_NODE_ID = "conversation"

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