merge main
This commit is contained in:
parent
ae00211691
commit
bdb990eb90
20
api/configs/middleware/vdb/upstash_config.py
Normal file
20
api/configs/middleware/vdb/upstash_config.py
Normal 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,
|
||||
)
|
@ -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)
|
@ -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
|
1
api/core/file/constants.py
Normal file
1
api/core/file/constants.py
Normal file
@ -0,0 +1 @@
|
||||
FILE_MODEL_IDENTITY = "__dify__file__"
|
55
api/core/file/enums.py
Normal file
55
api/core/file/enums.py
Normal 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"
|
156
api/core/file/file_manager.py
Normal file
156
api/core/file/file_manager.py
Normal 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}")
|
@ -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''
|
||||
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
|
32
api/core/file/file_repository.py
Normal file
32
api/core/file/file_repository.py
Normal 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
48
api/core/file/helpers.py
Normal 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
|
@ -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
140
api/core/file/models.py
Normal 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''
|
||||
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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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"
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
0
api/core/rag/datasource/vdb/upstash/__init__.py
Normal file
0
api/core/rag/datasource/vdb/upstash/__init__.py
Normal file
129
api/core/rag/datasource/vdb/upstash/upstash_vector.py
Normal file
129
api/core/rag/datasource/vdb/upstash/upstash_vector.py
Normal 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,
|
||||
),
|
||||
)
|
@ -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
|
32
api/core/tools/provider/builtin/aliyuque/_assets/icon.svg
Normal file
32
api/core/tools/provider/builtin/aliyuque/_assets/icon.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 7.1 KiB |
19
api/core/tools/provider/builtin/aliyuque/aliyuque.py
Normal file
19
api/core/tools/provider/builtin/aliyuque/aliyuque.py
Normal 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))
|
29
api/core/tools/provider/builtin/aliyuque/aliyuque.yaml
Normal file
29
api/core/tools/provider/builtin/aliyuque/aliyuque.yaml
Normal 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
|
50
api/core/tools/provider/builtin/aliyuque/tools/base.py
Normal file
50
api/core/tools/provider/builtin/aliyuque/tools/base.py
Normal 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
|
@ -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"))
|
@ -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.
|
@ -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}")
|
||||
)
|
@ -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.
|
@ -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")
|
||||
)
|
@ -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.
|
@ -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"))
|
@ -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.
|
@ -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))
|
@ -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.
|
@ -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}")
|
||||
)
|
@ -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.
|
@ -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"))
|
@ -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).
|
@ -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}")
|
||||
)
|
@ -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.
|
BIN
api/core/tools/provider/builtin/feishu_base/_assets/icon.png
Normal file
BIN
api/core/tools/provider/builtin/feishu_base/_assets/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.1 KiB |
@ -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 |
@ -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))
|
@ -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
|
@ -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)
|
@ -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
|
@ -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))
|
@ -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
|
@ -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)
|
@ -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
|
@ -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))
|
@ -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
|
@ -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))
|
@ -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
|
@ -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)
|
@ -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
|
@ -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)
|
@ -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
|
@ -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))
|
@ -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
|
@ -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))
|
@ -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
|
@ -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))
|
@ -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
|
@ -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)
|
@ -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
|
@ -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))
|
@ -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
|
@ -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)
|
@ -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
|
@ -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)
|
@ -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
|
@ -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))
|
@ -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
|
@ -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)
|
@ -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
|
@ -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 |
@ -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)}")
|
@ -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 密钥
|
@ -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,
|
||||
),
|
||||
]
|
@ -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
|
@ -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}.")
|
@ -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",
|
||||
]
|
@ -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)
|
@ -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"
|
@ -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
|
@ -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",
|
3
api/core/workflow/constants.py
Normal file
3
api/core/workflow/constants.py
Normal 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
Loading…
Reference in New Issue
Block a user