From 817b85001fe440ebeb38dd6cbf38a32750757375 Mon Sep 17 00:00:00 2001 From: Kalo Chin <91766386+fdb02983rhy@users.noreply.github.com> Date: Fri, 22 Nov 2024 11:30:21 +0900 Subject: [PATCH] feat: slidespeak slides generation (#10955) --- .../builtin/slidespeak/_assets/icon.png | Bin 0 -> 512 bytes .../provider/builtin/slidespeak/slidespeak.py | 28 +++ .../builtin/slidespeak/slidespeak.yaml | 22 +++ .../slidespeak/tools/slides_generator.py | 163 ++++++++++++++++++ .../slidespeak/tools/slides_generator.yaml | 102 +++++++++++ 5 files changed, 315 insertions(+) create mode 100644 api/core/tools/provider/builtin/slidespeak/_assets/icon.png create mode 100644 api/core/tools/provider/builtin/slidespeak/slidespeak.py create mode 100644 api/core/tools/provider/builtin/slidespeak/slidespeak.yaml create mode 100644 api/core/tools/provider/builtin/slidespeak/tools/slides_generator.py create mode 100644 api/core/tools/provider/builtin/slidespeak/tools/slides_generator.yaml 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 0000000000000000000000000000000000000000..4cac578330b15602fe79b7151956de358e706f3e GIT binary patch literal 512 zcmV+b0{{JqP)FT~RK?>|lGz@{QkDuYe8SHV)2JpxsLYa9uEOH~`)Z)@DB{9<2iRCUcDe5};p3 z=8wSQi(wOJZY)GcWBAty{9I^ZvS4(q{dBu^85HF;EhD4Fmst~djJ+&r#`Yet^Z=Eg}9tFPbw2FLOA(;sCP@;#%!;sVgI?ioa?zho6zYBDY*NL+P=*v9wW@8Y* zx2J+er8tFUgt|iO7Vn9(D?!ri`J)QSTmqhiD&P}wtx^G@sOkw&3)BL&KrK)U)B=?X z7!RwRfJuVwv=X*`2;2$4(`gBzai*Q+-_Y?%j8}j;HXA#U;DPNXuq>D{>;YRBB7Fhw zo^B%Y-plz=Ai?y}-_P>g#RQ<;OCN#m>!jJ?=x2DHZnthq1s0PW=v0BcX!p_%Fc0j5 z-VN3+j<}kwN}9f!6@Zo!m>O{wFiXJ6a#{lVU&SwMN~}O;%g`AB0000 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