diff --git a/api/core/tools/provider/builtin/slidespeak/_assets/icon.png b/api/core/tools/provider/builtin/slidespeak/_assets/icon.png new file mode 100644 index 0000000000..4cac578330 Binary files /dev/null and b/api/core/tools/provider/builtin/slidespeak/_assets/icon.png differ diff --git a/api/core/tools/provider/builtin/slidespeak/slidespeak.py b/api/core/tools/provider/builtin/slidespeak/slidespeak.py new file mode 100644 index 0000000000..14c7c4880e --- /dev/null +++ b/api/core/tools/provider/builtin/slidespeak/slidespeak.py @@ -0,0 +1,28 @@ +from typing import Any + +import requests +from yarl import URL + +from core.tools.errors import ToolProviderCredentialValidationError +from core.tools.provider.builtin_tool_provider import BuiltinToolProviderController + + +class SlideSpeakProvider(BuiltinToolProviderController): + def _validate_credentials(self, credentials: dict[str, Any]) -> None: + api_key = credentials.get("slidespeak_api_key") + base_url = credentials.get("base_url") + + if not api_key: + raise ToolProviderCredentialValidationError("API key is missing") + + if base_url: + base_url = str(URL(base_url) / "v1") + + headers = {"Content-Type": "application/json", "X-API-Key": api_key} + + test_task_id = "xxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + url = f"{base_url or 'https://api.slidespeak.co/api/v1'}/task_status/{test_task_id}" + + response = requests.get(url, headers=headers) + if response.status_code != 200: + raise ToolProviderCredentialValidationError("Invalid SlidePeak API key") diff --git a/api/core/tools/provider/builtin/slidespeak/slidespeak.yaml b/api/core/tools/provider/builtin/slidespeak/slidespeak.yaml new file mode 100644 index 0000000000..9f6927f1bd --- /dev/null +++ b/api/core/tools/provider/builtin/slidespeak/slidespeak.yaml @@ -0,0 +1,22 @@ +identity: + author: Kalo Chin + name: slidespeak + label: + en_US: SlideSpeak + zh_Hans: SlideSpeak + description: + en_US: Generate presentation slides using SlideSpeak API + zh_Hans: 使用 SlideSpeak API 生成演示幻灯片 + icon: icon.png + +credentials_for_provider: + slidespeak_api_key: + type: secret-input + required: true + label: + en_US: API Key + zh_Hans: API 密钥 + placeholder: + en_US: Enter your SlideSpeak API key + zh_Hans: 输入您的 SlideSpeak API 密钥 + url: https://app.slidespeak.co/settings/developer diff --git a/api/core/tools/provider/builtin/slidespeak/tools/slides_generator.py b/api/core/tools/provider/builtin/slidespeak/tools/slides_generator.py new file mode 100644 index 0000000000..74742bf4b7 --- /dev/null +++ b/api/core/tools/provider/builtin/slidespeak/tools/slides_generator.py @@ -0,0 +1,163 @@ +import asyncio +from dataclasses import asdict, dataclass +from enum import Enum +from typing import Any, Optional, Union + +import aiohttp +from pydantic import ConfigDict + +from core.tools.entities.tool_entities import ToolInvokeMessage +from core.tools.errors import ToolProviderCredentialValidationError +from core.tools.tool.builtin_tool import BuiltinTool + + +class SlidesGeneratorTool(BuiltinTool): + """ + Tool for generating presentations using the SlideSpeak API. + """ + + model_config = ConfigDict(arbitrary_types_allowed=True) + + headers: Optional[dict[str, str]] = None + base_url: Optional[str] = None + timeout: Optional[aiohttp.ClientTimeout] = None + poll_interval: Optional[int] = None + + class TaskState(Enum): + FAILURE = "FAILURE" + REVOKED = "REVOKED" + SUCCESS = "SUCCESS" + PENDING = "PENDING" + RECEIVED = "RECEIVED" + STARTED = "STARTED" + + @dataclass + class PresentationRequest: + plain_text: str + length: Optional[int] = None + theme: Optional[str] = None + + async def _generate_presentation( + self, + session: aiohttp.ClientSession, + request: PresentationRequest, + ) -> dict[str, Any]: + """Generate a new presentation asynchronously""" + async with session.post( + f"{self.base_url}/presentation/generate", + headers=self.headers, + json=asdict(request), + timeout=self.timeout, + ) as response: + response.raise_for_status() + return await response.json() + + async def _get_task_status( + self, + session: aiohttp.ClientSession, + task_id: str, + ) -> dict[str, Any]: + """Get the status of a task asynchronously""" + async with session.get( + f"{self.base_url}/task_status/{task_id}", + headers=self.headers, + timeout=self.timeout, + ) as response: + response.raise_for_status() + return await response.json() + + async def _wait_for_completion( + self, + session: aiohttp.ClientSession, + task_id: str, + ) -> str: + """Wait for task completion and return download URL""" + while True: + status = await self._get_task_status(session, task_id) + task_status = self.TaskState(status["task_status"]) + if task_status == self.TaskState.SUCCESS: + return status["task_result"]["url"] + if task_status in [self.TaskState.FAILURE, self.TaskState.REVOKED]: + raise Exception(f"Task failed with status: {task_status.value}") + await asyncio.sleep(self.poll_interval) + + async def _generate_slides( + self, + plain_text: str, + length: Optional[int], + theme: Optional[str], + ) -> str: + """Generate slides and return the download URL""" + async with aiohttp.ClientSession() as session: + request = self.PresentationRequest( + plain_text=plain_text, + length=length, + theme=theme, + ) + result = await self._generate_presentation(session, request) + task_id = result["task_id"] + download_url = await self._wait_for_completion(session, task_id) + return download_url + + async def _fetch_presentation( + self, + session: aiohttp.ClientSession, + download_url: str, + ) -> bytes: + """Fetch the presentation file from the download URL""" + async with session.get(download_url, timeout=self.timeout) as response: + response.raise_for_status() + return await response.read() + + def _invoke( + self, + user_id: str, + tool_parameters: dict[str, Any], + ) -> Union[ToolInvokeMessage, list[ToolInvokeMessage]]: + """Synchronous invoke method that runs asynchronous code""" + + async def async_invoke(): + # Extract parameters + plain_text = tool_parameters.get("plain_text", "") + length = tool_parameters.get("length") + theme = tool_parameters.get("theme") + + # Ensure runtime and credentials + if not self.runtime or not self.runtime.credentials: + raise ToolProviderCredentialValidationError("Tool runtime or credentials are missing") + + # Get API key from credentials + api_key = self.runtime.credentials.get("slidespeak_api_key") + if not api_key: + raise ToolProviderCredentialValidationError("SlideSpeak API key is missing") + + # Set configuration + self.headers = { + "Content-Type": "application/json", + "X-API-Key": api_key, + } + self.base_url = "https://api.slidespeak.co/api/v1" + self.timeout = aiohttp.ClientTimeout(total=30) + self.poll_interval = 2 + + # Run the asynchronous slide generation + try: + download_url = await self._generate_slides(plain_text, length, theme) + + # Fetch the presentation file + async with aiohttp.ClientSession() as session: + presentation_bytes = await self._fetch_presentation(session, download_url) + + return [ + self.create_text_message("Presentation generated successfully"), + self.create_blob_message( + blob=presentation_bytes, + meta={"mime_type": "application/vnd.openxmlformats-officedocument.presentationml.presentation"}, + ), + ] + except Exception as e: + return [self.create_text_message(f"An error occurred: {str(e)}")] + + # Run the asynchronous code synchronously + result = asyncio.run(async_invoke()) + return result diff --git a/api/core/tools/provider/builtin/slidespeak/tools/slides_generator.yaml b/api/core/tools/provider/builtin/slidespeak/tools/slides_generator.yaml new file mode 100644 index 0000000000..f881dadb20 --- /dev/null +++ b/api/core/tools/provider/builtin/slidespeak/tools/slides_generator.yaml @@ -0,0 +1,102 @@ +identity: + name: slide_generator + author: Kalo Chin + label: + en_US: Slides Generator + zh_Hans: 幻灯片生成器 +description: + human: + en_US: Generate presentation slides from text using SlideSpeak API. + zh_Hans: 使用 SlideSpeak API 从文本生成演示幻灯片。 + llm: This tool converts text input into a presentation using the SlideSpeak API service, with options for slide length and theme. +parameters: + - name: plain_text + type: string + required: true + label: + en_US: Topic or Content + zh_Hans: 主题或内容 + human_description: + en_US: The topic or content to be converted into presentation slides. + zh_Hans: 需要转换为幻灯片的内容或主题。 + llm_description: A string containing the topic or content to be transformed into presentation slides. + form: llm + - name: length + type: number + required: false + label: + en_US: Number of Slides + zh_Hans: 幻灯片数量 + human_description: + en_US: The desired number of slides in the presentation (optional). + zh_Hans: 演示文稿中所需的幻灯片数量(可选)。 + llm_description: Optional parameter specifying the number of slides to generate. + form: form + - name: theme + type: select + required: false + label: + en_US: Presentation Theme + zh_Hans: 演示主题 + human_description: + en_US: The visual theme for the presentation (optional). + zh_Hans: 演示文稿的视觉主题(可选)。 + llm_description: Optional parameter specifying the presentation theme. + options: + - label: + en_US: Adam + zh_Hans: Adam + value: adam + - label: + en_US: Aurora + zh_Hans: Aurora + value: aurora + - label: + en_US: Bruno + zh_Hans: Bruno + value: bruno + - label: + en_US: Clyde + zh_Hans: Clyde + value: clyde + - label: + en_US: Daniel + zh_Hans: Daniel + value: daniel + - label: + en_US: Default + zh_Hans: Default + value: default + - label: + en_US: Eddy + zh_Hans: Eddy + value: eddy + - label: + en_US: Felix + zh_Hans: Felix + value: felix + - label: + en_US: Gradient + zh_Hans: Gradient + value: gradient + - label: + en_US: Iris + zh_Hans: Iris + value: iris + - label: + en_US: Lavender + zh_Hans: Lavender + value: lavender + - label: + en_US: Monolith + zh_Hans: Monolith + value: monolith + - label: + en_US: Nebula + zh_Hans: Nebula + value: nebula + - label: + en_US: Nexus + zh_Hans: Nexus + value: nexus + form: form