浏览代码

feat: slidespeak slides generation (#10955)

Kalo Chin 5 月之前
父节点
当前提交
817b85001f

二进制
api/core/tools/provider/builtin/slidespeak/_assets/icon.png


+ 28 - 0
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")

+ 22 - 0
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

+ 163 - 0
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

+ 102 - 0
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