mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-02-12 15:55:03 -05:00
Compare commits
17 Commits
chore/comb
...
swiftyos/e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b5f2c6f11 | ||
|
|
8aba4a5d48 | ||
|
|
6ba9fd9cb4 | ||
|
|
b16c2eed52 | ||
|
|
5af718c9f5 | ||
|
|
a588cf1dc5 | ||
|
|
98a1adc397 | ||
|
|
19728ebc05 | ||
|
|
3e117aac5d | ||
|
|
8dabe6c70d | ||
|
|
a04919beca | ||
|
|
c6b22842a4 | ||
|
|
614f751a90 | ||
|
|
c458bec9c7 | ||
|
|
040bde3f49 | ||
|
|
71028d57d7 | ||
|
|
dbf014f936 |
@@ -173,6 +173,9 @@ EXA_API_KEY=
|
|||||||
# E2B
|
# E2B
|
||||||
E2B_API_KEY=
|
E2B_API_KEY=
|
||||||
|
|
||||||
|
# Example API Key
|
||||||
|
EXAMPLE_API_KEY=
|
||||||
|
|
||||||
# Mem0
|
# Mem0
|
||||||
MEM0_API_KEY=
|
MEM0_API_KEY=
|
||||||
|
|
||||||
|
|||||||
491
autogpt_platform/backend/backend/blocks/elevenlabs/_api.py
Normal file
491
autogpt_platform/backend/backend/blocks/elevenlabs/_api.py
Normal file
@@ -0,0 +1,491 @@
|
|||||||
|
"""
|
||||||
|
API module for ElevenLabs API integration.
|
||||||
|
|
||||||
|
This module provides a client for the ElevenLabs API, which offers text-to-speech
|
||||||
|
and speech-to-speech capabilities.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
from io import BytesIO
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from backend.data.model import APIKeyCredentials
|
||||||
|
from backend.util.request import Requests
|
||||||
|
|
||||||
|
|
||||||
|
class ElevenLabsException(Exception):
|
||||||
|
"""Exception raised for ElevenLabs API errors."""
|
||||||
|
|
||||||
|
def __init__(self, message: str, status_code: int):
|
||||||
|
super().__init__(message)
|
||||||
|
self.status_code = status_code
|
||||||
|
|
||||||
|
|
||||||
|
class VoiceSettings(BaseModel):
|
||||||
|
"""Model for voice settings."""
|
||||||
|
|
||||||
|
stability: float
|
||||||
|
similarity_boost: float
|
||||||
|
|
||||||
|
|
||||||
|
class ElevenLabsClient:
|
||||||
|
"""Client for the ElevenLabs API."""
|
||||||
|
|
||||||
|
API_BASE_URL = "https://api.elevenlabs.io/v1"
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
credentials: Optional[APIKeyCredentials] = None,
|
||||||
|
custom_requests: Optional[Requests] = None,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Initialize the ElevenLabs API client.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
credentials: API key credentials for ElevenLabs.
|
||||||
|
custom_requests: Custom request handler (for testing).
|
||||||
|
"""
|
||||||
|
if custom_requests:
|
||||||
|
self._requests = custom_requests
|
||||||
|
else:
|
||||||
|
headers: Dict[str, str] = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
if credentials:
|
||||||
|
headers["xi-api-key"] = credentials.api_key.get_secret_value()
|
||||||
|
|
||||||
|
self._requests = Requests(
|
||||||
|
extra_headers=headers,
|
||||||
|
raise_for_status=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _handle_response(self, response, raw_binary=False) -> Any:
|
||||||
|
"""
|
||||||
|
Handle API response and check for errors.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
response: Response object from the request.
|
||||||
|
raw_binary: If True, return the raw binary content instead of parsing as JSON.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Parsed response data.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ElevenLabsException: If the API request fails.
|
||||||
|
"""
|
||||||
|
if not response.ok:
|
||||||
|
error_message = "Unknown error"
|
||||||
|
try:
|
||||||
|
error_data = response.json()
|
||||||
|
if isinstance(error_data, dict):
|
||||||
|
error_message = error_data.get(
|
||||||
|
"detail", error_data.get("message", "Unknown error")
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
error_message = response.text or f"Error: HTTP {response.status_code}"
|
||||||
|
|
||||||
|
raise ElevenLabsException(
|
||||||
|
f"ElevenLabs API error ({response.status_code}): {error_message}",
|
||||||
|
response.status_code,
|
||||||
|
)
|
||||||
|
|
||||||
|
if raw_binary:
|
||||||
|
return response.content
|
||||||
|
|
||||||
|
try:
|
||||||
|
return response.json()
|
||||||
|
except Exception:
|
||||||
|
return response.content
|
||||||
|
|
||||||
|
# === Voice Management Endpoints ===
|
||||||
|
|
||||||
|
def get_models(self) -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Retrieve a list of all available TTS and STS models.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of model objects.
|
||||||
|
"""
|
||||||
|
response = self._requests.get(f"{self.API_BASE_URL}/models")
|
||||||
|
return self._handle_response(response)
|
||||||
|
|
||||||
|
def get_voices(self) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Retrieve a list of all voices available to the user.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary containing the list of voices.
|
||||||
|
"""
|
||||||
|
response = self._requests.get(f"{self.API_BASE_URL}/voices")
|
||||||
|
return self._handle_response(response)
|
||||||
|
|
||||||
|
def get_voice(self, voice_id: str) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Retrieve metadata about a specific voice.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
voice_id: ID of the voice to retrieve.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Voice metadata.
|
||||||
|
"""
|
||||||
|
response = self._requests.get(f"{self.API_BASE_URL}/voices/{voice_id}")
|
||||||
|
return self._handle_response(response)
|
||||||
|
|
||||||
|
def add_voice(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
files: List[BytesIO],
|
||||||
|
description: Optional[str] = None,
|
||||||
|
labels: Optional[Dict[str, str]] = None,
|
||||||
|
) -> Dict[str, str]:
|
||||||
|
"""
|
||||||
|
Add a new voice by uploading audio samples.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: Name for the new voice.
|
||||||
|
files: List of audio file objects.
|
||||||
|
description: Optional description for the voice.
|
||||||
|
labels: Optional labels as key-value pairs.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with the new voice_id.
|
||||||
|
"""
|
||||||
|
data = {"name": name}
|
||||||
|
if description:
|
||||||
|
data["description"] = description
|
||||||
|
if labels:
|
||||||
|
data["labels"] = json.dumps(labels)
|
||||||
|
|
||||||
|
files_data = []
|
||||||
|
for i, file_obj in enumerate(files):
|
||||||
|
files_data.append(("files", (f"sample_{i}.mp3", file_obj, "audio/mpeg")))
|
||||||
|
|
||||||
|
# Custom request for multipart/form-data
|
||||||
|
response = self._requests.post(
|
||||||
|
f"{self.API_BASE_URL}/voices/add",
|
||||||
|
headers={"xi-api-key": self._requests.extra_headers.get("xi-api-key", "") if self._requests.extra_headers else ""},
|
||||||
|
data=data,
|
||||||
|
files=files_data,
|
||||||
|
)
|
||||||
|
return self._handle_response(response)
|
||||||
|
|
||||||
|
def edit_voice(
|
||||||
|
self,
|
||||||
|
voice_id: str,
|
||||||
|
name: Optional[str] = None,
|
||||||
|
description: Optional[str] = None,
|
||||||
|
labels: Optional[Dict[str, str]] = None,
|
||||||
|
files: Optional[List[BytesIO]] = None,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Edit an existing voice.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
voice_id: ID of the voice to edit.
|
||||||
|
name: Optional new name for the voice.
|
||||||
|
description: Optional new description.
|
||||||
|
labels: Optional new labels.
|
||||||
|
files: Optional new audio samples.
|
||||||
|
"""
|
||||||
|
data = {}
|
||||||
|
if name:
|
||||||
|
data["name"] = name
|
||||||
|
if description:
|
||||||
|
data["description"] = description
|
||||||
|
if labels:
|
||||||
|
data["labels"] = json.dumps(labels)
|
||||||
|
|
||||||
|
files_data = []
|
||||||
|
if files:
|
||||||
|
for i, file_obj in enumerate(files):
|
||||||
|
files_data.append(
|
||||||
|
("files", (f"sample_{i}.mp3", file_obj, "audio/mpeg"))
|
||||||
|
)
|
||||||
|
|
||||||
|
# Custom request for multipart/form-data
|
||||||
|
response = self._requests.post(
|
||||||
|
f"{self.API_BASE_URL}/voices/{voice_id}/edit",
|
||||||
|
headers={"xi-api-key": self._requests.extra_headers.get("xi-api-key", "") if self._requests.extra_headers else ""},
|
||||||
|
data=data,
|
||||||
|
files=files_data,
|
||||||
|
)
|
||||||
|
self._handle_response(response)
|
||||||
|
|
||||||
|
def delete_voice(self, voice_id: str) -> None:
|
||||||
|
"""
|
||||||
|
Delete a voice.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
voice_id: ID of the voice to delete.
|
||||||
|
"""
|
||||||
|
response = self._requests.delete(f"{self.API_BASE_URL}/voices/{voice_id}")
|
||||||
|
self._handle_response(response)
|
||||||
|
|
||||||
|
def get_voice_settings(self, voice_id: str) -> Dict[str, float]:
|
||||||
|
"""
|
||||||
|
Retrieve a voice's settings.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
voice_id: ID of the voice.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Voice settings (stability and similarity_boost).
|
||||||
|
"""
|
||||||
|
response = self._requests.get(f"{self.API_BASE_URL}/voices/{voice_id}/settings")
|
||||||
|
return self._handle_response(response)
|
||||||
|
|
||||||
|
def edit_voice_settings(
|
||||||
|
self, voice_id: str, stability: float, similarity_boost: float
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Edit a voice's settings.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
voice_id: ID of the voice.
|
||||||
|
stability: Stability setting (0.0 to 1.0).
|
||||||
|
similarity_boost: Similarity boost setting (0.0 to 1.0).
|
||||||
|
"""
|
||||||
|
data = {"stability": stability, "similarity_boost": similarity_boost}
|
||||||
|
response = self._requests.post(
|
||||||
|
f"{self.API_BASE_URL}/voices/{voice_id}/settings/edit",
|
||||||
|
json=data,
|
||||||
|
)
|
||||||
|
self._handle_response(response)
|
||||||
|
|
||||||
|
# === Text-to-Speech Endpoints ===
|
||||||
|
|
||||||
|
def text_to_speech(
|
||||||
|
self,
|
||||||
|
voice_id: str,
|
||||||
|
text: str,
|
||||||
|
model_id: Optional[str] = None,
|
||||||
|
voice_settings: Optional[VoiceSettings] = None,
|
||||||
|
) -> bytes:
|
||||||
|
"""
|
||||||
|
Convert text to speech using a specific voice.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
voice_id: ID of the voice to use.
|
||||||
|
text: Text to convert to speech.
|
||||||
|
model_id: Optional model ID to use (e.g., "eleven_multilingual_v2").
|
||||||
|
voice_settings: Optional voice settings to override defaults.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Audio data as bytes.
|
||||||
|
"""
|
||||||
|
data = {"text": text }
|
||||||
|
if model_id:
|
||||||
|
data["model_id"] = model_id
|
||||||
|
if voice_settings:
|
||||||
|
data["voice_settings"] = voice_settings.model_dump()
|
||||||
|
|
||||||
|
response = self._requests.post(
|
||||||
|
f"{self.API_BASE_URL}/text-to-speech/{voice_id}",
|
||||||
|
json=data,
|
||||||
|
)
|
||||||
|
return self._handle_response(response, raw_binary=True)
|
||||||
|
|
||||||
|
def text_to_speech_stream(
|
||||||
|
self,
|
||||||
|
voice_id: str,
|
||||||
|
text: str,
|
||||||
|
model_id: Optional[str] = None,
|
||||||
|
voice_settings: Optional[VoiceSettings] = None,
|
||||||
|
) -> bytes:
|
||||||
|
"""
|
||||||
|
Stream text-to-speech in real-time.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
voice_id: ID of the voice to use.
|
||||||
|
text: Text to convert to speech.
|
||||||
|
model_id: Optional model ID to use.
|
||||||
|
voice_settings: Optional voice settings.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Complete audio data as bytes after stream finishes.
|
||||||
|
"""
|
||||||
|
data = {"text": text}
|
||||||
|
if model_id:
|
||||||
|
data["model_id"] = model_id
|
||||||
|
if voice_settings:
|
||||||
|
data["voice_settings"] = json.dumps(voice_settings.dict())
|
||||||
|
|
||||||
|
response = self._requests.post(
|
||||||
|
f"{self.API_BASE_URL}/text-to-speech/{voice_id}/stream",
|
||||||
|
json=data,
|
||||||
|
)
|
||||||
|
return self._handle_response(response, raw_binary=True)
|
||||||
|
|
||||||
|
# === Speech-to-Speech Endpoints ===
|
||||||
|
|
||||||
|
def speech_to_speech(
|
||||||
|
self,
|
||||||
|
voice_id: str,
|
||||||
|
audio: BytesIO,
|
||||||
|
voice_settings: Optional[VoiceSettings] = None,
|
||||||
|
) -> bytes:
|
||||||
|
"""
|
||||||
|
Transform audio from one voice to another.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
voice_id: ID of the target voice.
|
||||||
|
audio: Input audio file.
|
||||||
|
voice_settings: Optional voice settings.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Transformed audio data as bytes.
|
||||||
|
"""
|
||||||
|
data = {}
|
||||||
|
if voice_settings:
|
||||||
|
data["voice_settings"] = json.dumps(voice_settings.dict())
|
||||||
|
|
||||||
|
files = [("audio", ("input.mp3", audio, "audio/mpeg"))]
|
||||||
|
|
||||||
|
# Custom request for multipart/form-data
|
||||||
|
response = self._requests.post(
|
||||||
|
f"{self.API_BASE_URL}/speech-to-speech/{voice_id}",
|
||||||
|
headers={"xi-api-key": self._requests.extra_headers.get("xi-api-key", "") if self._requests.extra_headers else ""},
|
||||||
|
data=data,
|
||||||
|
files=files,
|
||||||
|
)
|
||||||
|
return self._handle_response(response, raw_binary=True)
|
||||||
|
|
||||||
|
def speech_to_speech_stream(
|
||||||
|
self,
|
||||||
|
voice_id: str,
|
||||||
|
audio: BytesIO,
|
||||||
|
voice_settings: Optional[VoiceSettings] = None,
|
||||||
|
) -> bytes:
|
||||||
|
"""
|
||||||
|
Stream speech-to-speech transformation in real-time.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
voice_id: ID of the target voice.
|
||||||
|
audio: Input audio file.
|
||||||
|
voice_settings: Optional voice settings.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Complete audio data as bytes after stream finishes.
|
||||||
|
"""
|
||||||
|
data = {}
|
||||||
|
if voice_settings:
|
||||||
|
data["voice_settings"] = json.dumps(voice_settings.dict())
|
||||||
|
|
||||||
|
files = [("audio", ("input.mp3", audio, "audio/mpeg"))]
|
||||||
|
|
||||||
|
# Custom request for multipart/form-data
|
||||||
|
response = self._requests.post(
|
||||||
|
f"{self.API_BASE_URL}/speech-to-speech/{voice_id}/stream",
|
||||||
|
headers={"xi-api-key": self._requests.extra_headers.get("xi-api-key", "") if self._requests.extra_headers else ""},
|
||||||
|
data=data,
|
||||||
|
files=files,
|
||||||
|
)
|
||||||
|
return self._handle_response(response, raw_binary=True)
|
||||||
|
|
||||||
|
# === History Endpoints ===
|
||||||
|
|
||||||
|
def get_history(
|
||||||
|
self,
|
||||||
|
page_size: Optional[int] = None,
|
||||||
|
start_after_history_item_id: Optional[str] = None,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Retrieve a list of generation history items.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
page_size: Optional number of items per page.
|
||||||
|
start_after_history_item_id: Optional pagination marker.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
History data.
|
||||||
|
"""
|
||||||
|
params = {}
|
||||||
|
if page_size:
|
||||||
|
params["page_size"] = page_size
|
||||||
|
if start_after_history_item_id:
|
||||||
|
params["start_after_history_item_id"] = start_after_history_item_id
|
||||||
|
|
||||||
|
response = self._requests.get(f"{self.API_BASE_URL}/history", params=params)
|
||||||
|
return self._handle_response(response)
|
||||||
|
|
||||||
|
def get_history_item(self, history_item_id: str) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Retrieve metadata for a specific history item.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
history_item_id: ID of the history item.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
History item metadata.
|
||||||
|
"""
|
||||||
|
response = self._requests.get(f"{self.API_BASE_URL}/history/{history_item_id}")
|
||||||
|
return self._handle_response(response)
|
||||||
|
|
||||||
|
def get_history_audio(self, history_item_id: str) -> bytes:
|
||||||
|
"""
|
||||||
|
Download audio from a history item.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
history_item_id: ID of the history item.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Audio data as bytes.
|
||||||
|
"""
|
||||||
|
response = self._requests.get(
|
||||||
|
f"{self.API_BASE_URL}/history/{history_item_id}/audio"
|
||||||
|
)
|
||||||
|
return self._handle_response(response, raw_binary=True)
|
||||||
|
|
||||||
|
def delete_history_item(self, history_item_id: str) -> None:
|
||||||
|
"""
|
||||||
|
Delete a history item.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
history_item_id: ID of the history item to delete.
|
||||||
|
"""
|
||||||
|
response = self._requests.delete(
|
||||||
|
f"{self.API_BASE_URL}/history/{history_item_id}"
|
||||||
|
)
|
||||||
|
self._handle_response(response)
|
||||||
|
|
||||||
|
def download_history_items(self, history_item_ids: List[str]) -> bytes:
|
||||||
|
"""
|
||||||
|
Download multiple history items as a ZIP file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
history_item_ids: List of history item IDs to download.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ZIP file data as bytes.
|
||||||
|
"""
|
||||||
|
data = {"history_item_ids": history_item_ids}
|
||||||
|
response = self._requests.post(
|
||||||
|
f"{self.API_BASE_URL}/history/download",
|
||||||
|
json=data,
|
||||||
|
)
|
||||||
|
return self._handle_response(response, raw_binary=True)
|
||||||
|
|
||||||
|
# === User Endpoints ===
|
||||||
|
|
||||||
|
def get_user_info(self) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Retrieve user account information.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
User information.
|
||||||
|
"""
|
||||||
|
response = self._requests.get(f"{self.API_BASE_URL}/user")
|
||||||
|
return self._handle_response(response)
|
||||||
|
|
||||||
|
def get_subscription_info(self) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Retrieve subscription details.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Subscription information.
|
||||||
|
"""
|
||||||
|
response = self._requests.get(f"{self.API_BASE_URL}/user/subscription")
|
||||||
|
return self._handle_response(response)
|
||||||
36
autogpt_platform/backend/backend/blocks/elevenlabs/_auth.py
Normal file
36
autogpt_platform/backend/backend/blocks/elevenlabs/_auth.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
"""
|
||||||
|
Authentication module for ElevenLabs API integration.
|
||||||
|
|
||||||
|
This module provides credential types and test credentials for the ElevenLabs API integration.
|
||||||
|
It defines the structure for API key credentials used to authenticate with the ElevenLabs API
|
||||||
|
and provides mock credentials for testing purposes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
from pydantic import SecretStr
|
||||||
|
|
||||||
|
from backend.data.model import APIKeyCredentials, CredentialsMetaInput
|
||||||
|
from backend.integrations.providers import ProviderName
|
||||||
|
|
||||||
|
# Define the type of credentials input expected for ElevenLabs API
|
||||||
|
ElevenLabsCredentialsInput = CredentialsMetaInput[
|
||||||
|
Literal[ProviderName.ELEVENLABS], Literal["api_key"]
|
||||||
|
]
|
||||||
|
|
||||||
|
# Mock credentials for testing ElevenLabs API integration
|
||||||
|
TEST_CREDENTIALS = APIKeyCredentials(
|
||||||
|
id="f8274359-45c8-48b7-b54d-a1c8c09ac2e8",
|
||||||
|
provider="elevenlabs",
|
||||||
|
api_key=SecretStr("mock-elevenlabs-api-key"),
|
||||||
|
title="Mock ElevenLabs API key",
|
||||||
|
expires_at=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Dictionary representation of test credentials for input fields
|
||||||
|
TEST_CREDENTIALS_INPUT = {
|
||||||
|
"provider": TEST_CREDENTIALS.provider,
|
||||||
|
"id": TEST_CREDENTIALS.id,
|
||||||
|
"type": TEST_CREDENTIALS.type,
|
||||||
|
"title": TEST_CREDENTIALS.title,
|
||||||
|
}
|
||||||
@@ -0,0 +1,495 @@
|
|||||||
|
"""
|
||||||
|
ElevenLabs integration for text-to-speech capabilities.
|
||||||
|
|
||||||
|
This module provides blocks for interacting with the ElevenLabs API,
|
||||||
|
which offers high-quality text-to-speech and speech-to-speech conversion.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import logging
|
||||||
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
|
||||||
|
from backend.data.model import APIKeyCredentials, CredentialsField, SchemaField
|
||||||
|
|
||||||
|
from ._api import ElevenLabsClient, ElevenLabsException, VoiceSettings
|
||||||
|
from ._auth import TEST_CREDENTIALS, TEST_CREDENTIALS_INPUT, ElevenLabsCredentialsInput
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Voice(BaseModel):
|
||||||
|
"""Model representing an ElevenLabs voice."""
|
||||||
|
|
||||||
|
voice_id: str
|
||||||
|
name: str
|
||||||
|
category: Optional[str] = None
|
||||||
|
description: Optional[str] = None
|
||||||
|
preview_url: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class TextToSpeechBlock(Block):
|
||||||
|
"""Block for converting text to speech using ElevenLabs API."""
|
||||||
|
|
||||||
|
class Input(BlockSchema):
|
||||||
|
text: str = SchemaField(
|
||||||
|
description="The text to convert to speech.",
|
||||||
|
placeholder="Enter your text here...",
|
||||||
|
)
|
||||||
|
voice_id: str = SchemaField(
|
||||||
|
description="The ID of the voice to use.",
|
||||||
|
placeholder="21m00Tcm4TlvDq8ikWAM",
|
||||||
|
)
|
||||||
|
model_id: Optional[str] = SchemaField(
|
||||||
|
description="The ID of the model to use (e.g., eleven_multilingual_v2).",
|
||||||
|
placeholder="eleven_multilingual_v2",
|
||||||
|
default=None,
|
||||||
|
advanced=True,
|
||||||
|
)
|
||||||
|
stability: float = SchemaField(
|
||||||
|
description="Voice stability (0.0 to 1.0). Higher values make voice more consistent.",
|
||||||
|
placeholder="0.75",
|
||||||
|
default=0.75,
|
||||||
|
advanced=True,
|
||||||
|
)
|
||||||
|
similarity_boost: float = SchemaField(
|
||||||
|
description="Similarity boost (0.0 to 1.0). Higher values make voice sound more like the original.",
|
||||||
|
placeholder="0.85",
|
||||||
|
default=0.85,
|
||||||
|
advanced=True,
|
||||||
|
)
|
||||||
|
stream: bool = SchemaField(
|
||||||
|
description="Whether to stream the audio response.",
|
||||||
|
default=False,
|
||||||
|
advanced=True,
|
||||||
|
)
|
||||||
|
credentials: ElevenLabsCredentialsInput = CredentialsField(
|
||||||
|
description="ElevenLabs API credentials."
|
||||||
|
)
|
||||||
|
|
||||||
|
class Output(BlockSchema):
|
||||||
|
audio_data: str = SchemaField(
|
||||||
|
description="The generated audio data in Base64 format."
|
||||||
|
)
|
||||||
|
content_type: str = SchemaField(
|
||||||
|
description="The MIME type of the audio (e.g., audio/mpeg)."
|
||||||
|
)
|
||||||
|
text: str = SchemaField(description="The text that was converted to speech.")
|
||||||
|
voice_id: str = SchemaField(description="The ID of the voice used.")
|
||||||
|
error: str = SchemaField(
|
||||||
|
description="Error message if the text-to-speech conversion failed."
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(
|
||||||
|
id="d923f6a8-beb2-4a57-90e2-b9c2f7e30f91",
|
||||||
|
description="Convert text to speech using ElevenLabs' high-quality voices.",
|
||||||
|
categories={BlockCategory.AI, BlockCategory.MULTIMEDIA},
|
||||||
|
input_schema=TextToSpeechBlock.Input,
|
||||||
|
output_schema=TextToSpeechBlock.Output,
|
||||||
|
test_input={
|
||||||
|
"text": "Hello, this is a test of the ElevenLabs text-to-speech API.",
|
||||||
|
"voice_id": "21m00Tcm4TlvDq8ikWAM",
|
||||||
|
"model_id": "eleven_multilingual_v2",
|
||||||
|
"stability": 0.75,
|
||||||
|
"similarity_boost": 0.85,
|
||||||
|
"stream": False,
|
||||||
|
"credentials": TEST_CREDENTIALS_INPUT,
|
||||||
|
},
|
||||||
|
test_output=[
|
||||||
|
("audio_data", "base64_encoded_audio_data"),
|
||||||
|
("content_type", "audio/mpeg"),
|
||||||
|
("text", "Hello, this is a test of the ElevenLabs text-to-speech API."),
|
||||||
|
("voice_id", "21m00Tcm4TlvDq8ikWAM"),
|
||||||
|
],
|
||||||
|
test_mock={
|
||||||
|
"generate_speech": lambda *args, **kwargs: (
|
||||||
|
base64.b64encode(b"mock_audio_data").decode("utf-8"),
|
||||||
|
"audio/mpeg",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
test_credentials=TEST_CREDENTIALS,
|
||||||
|
)
|
||||||
|
|
||||||
|
def generate_speech(
|
||||||
|
self,
|
||||||
|
client: ElevenLabsClient,
|
||||||
|
text: str,
|
||||||
|
voice_id: str,
|
||||||
|
model_id: Optional[str] = None,
|
||||||
|
voice_settings: Optional[VoiceSettings] = None,
|
||||||
|
stream: bool = False,
|
||||||
|
) -> tuple[str, str]:
|
||||||
|
"""
|
||||||
|
Generate speech from text using ElevenLabs API.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
client: Initialized ElevenLabsClient.
|
||||||
|
text: Text to convert to speech.
|
||||||
|
voice_id: ID of the voice to use.
|
||||||
|
model_id: Optional model ID.
|
||||||
|
voice_settings: Optional voice settings.
|
||||||
|
stream: Whether to use streaming endpoint.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (base64_encoded_audio_data, content_type).
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if stream:
|
||||||
|
audio_data = client.text_to_speech_stream(
|
||||||
|
voice_id=voice_id,
|
||||||
|
text=text,
|
||||||
|
model_id=model_id,
|
||||||
|
voice_settings=voice_settings,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
audio_data = client.text_to_speech(
|
||||||
|
voice_id=voice_id,
|
||||||
|
text=text,
|
||||||
|
model_id=model_id,
|
||||||
|
voice_settings=voice_settings,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Encode the binary audio data to base64 for transmission
|
||||||
|
base64_audio = base64.b64encode(audio_data).decode("utf-8")
|
||||||
|
return base64_audio, "audio/mpeg"
|
||||||
|
except ElevenLabsException as e:
|
||||||
|
logger.error(f"ElevenLabs API error: {str(e)}")
|
||||||
|
raise e
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Unexpected error in speech generation: {str(e)}")
|
||||||
|
raise ElevenLabsException(f"Failed to generate speech: {str(e)}", 500)
|
||||||
|
|
||||||
|
def run(
|
||||||
|
self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs
|
||||||
|
) -> BlockOutput:
|
||||||
|
"""
|
||||||
|
Run the text-to-speech conversion.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
input_data: Input data containing text and voice settings.
|
||||||
|
credentials: ElevenLabs API credentials.
|
||||||
|
|
||||||
|
Yields:
|
||||||
|
Audio data and metadata.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
client = ElevenLabsClient(credentials=credentials)
|
||||||
|
|
||||||
|
# Create voice settings if provided
|
||||||
|
voice_settings = None
|
||||||
|
if hasattr(input_data, "stability") and hasattr(
|
||||||
|
input_data, "similarity_boost"
|
||||||
|
):
|
||||||
|
voice_settings = VoiceSettings(
|
||||||
|
stability=input_data.stability,
|
||||||
|
similarity_boost=input_data.similarity_boost,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Generate speech
|
||||||
|
audio_data, content_type = self.generate_speech(
|
||||||
|
client=client,
|
||||||
|
text=input_data.text,
|
||||||
|
voice_id=input_data.voice_id,
|
||||||
|
model_id=input_data.model_id,
|
||||||
|
voice_settings=voice_settings,
|
||||||
|
stream=input_data.stream,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Yield results
|
||||||
|
yield "audio_data", audio_data
|
||||||
|
yield "content_type", content_type
|
||||||
|
yield "text", input_data.text
|
||||||
|
yield "voice_id", input_data.voice_id
|
||||||
|
|
||||||
|
except ElevenLabsException as e:
|
||||||
|
yield "error", f"ElevenLabs API error: {str(e)}"
|
||||||
|
except Exception as e:
|
||||||
|
yield "error", f"Unexpected error: {str(e)}"
|
||||||
|
|
||||||
|
|
||||||
|
class ListVoicesBlock(Block):
|
||||||
|
"""Block for listing available voices from ElevenLabs API."""
|
||||||
|
|
||||||
|
class Input(BlockSchema):
|
||||||
|
credentials: ElevenLabsCredentialsInput = CredentialsField(
|
||||||
|
description="ElevenLabs API credentials."
|
||||||
|
)
|
||||||
|
|
||||||
|
class Output(BlockSchema):
|
||||||
|
voices: List[Voice] = SchemaField(description="List of available voices.")
|
||||||
|
voice_ids: List[str] = SchemaField(description="List of voice IDs only.")
|
||||||
|
error: str = SchemaField(
|
||||||
|
description="Error message if the operation failed."
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(
|
||||||
|
id="4eaa8b1e-c0bc-45d2-a566-5fd4a5ce738d",
|
||||||
|
description="List all available voices from your ElevenLabs account.",
|
||||||
|
categories={BlockCategory.AI, BlockCategory.MULTIMEDIA},
|
||||||
|
input_schema=ListVoicesBlock.Input,
|
||||||
|
output_schema=ListVoicesBlock.Output,
|
||||||
|
test_input={
|
||||||
|
"credentials": TEST_CREDENTIALS_INPUT,
|
||||||
|
},
|
||||||
|
test_output=[
|
||||||
|
(
|
||||||
|
"voices",
|
||||||
|
[
|
||||||
|
Voice(
|
||||||
|
voice_id="21m00Tcm4TlvDq8ikWAM",
|
||||||
|
name="Rachel",
|
||||||
|
category="premade",
|
||||||
|
),
|
||||||
|
Voice(
|
||||||
|
voice_id="AZnzlk1XvdvUeBnXmlld",
|
||||||
|
name="Domi",
|
||||||
|
category="premade",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
("voice_ids", ["21m00Tcm4TlvDq8ikWAM", "AZnzlk1XvdvUeBnXmlld"]),
|
||||||
|
],
|
||||||
|
test_mock={
|
||||||
|
"get_voices_list": lambda *args, **kwargs: [
|
||||||
|
Voice(
|
||||||
|
voice_id="21m00Tcm4TlvDq8ikWAM",
|
||||||
|
name="Rachel",
|
||||||
|
category="premade",
|
||||||
|
),
|
||||||
|
Voice(
|
||||||
|
voice_id="AZnzlk1XvdvUeBnXmlld",
|
||||||
|
name="Domi",
|
||||||
|
category="premade",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
},
|
||||||
|
test_credentials=TEST_CREDENTIALS,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_voices_list(self, client: ElevenLabsClient) -> List[Voice]:
|
||||||
|
"""
|
||||||
|
Get list of voices from ElevenLabs.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
client: Initialized ElevenLabsClient.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of Voice objects.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
response = client.get_voices()
|
||||||
|
voices = []
|
||||||
|
for voice_data in response.get("voices", []):
|
||||||
|
voice = Voice(
|
||||||
|
voice_id=voice_data.get("voice_id"),
|
||||||
|
name=voice_data.get("name"),
|
||||||
|
category=voice_data.get("category"),
|
||||||
|
description=voice_data.get("description"),
|
||||||
|
preview_url=voice_data.get("preview_url"),
|
||||||
|
)
|
||||||
|
voices.append(voice)
|
||||||
|
return voices
|
||||||
|
except ElevenLabsException as e:
|
||||||
|
logger.error(f"ElevenLabs API error when listing voices: {str(e)}")
|
||||||
|
raise e
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Unexpected error when listing voices: {str(e)}")
|
||||||
|
raise ElevenLabsException(f"Failed to list voices: {str(e)}", 500)
|
||||||
|
|
||||||
|
def run(
|
||||||
|
self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs
|
||||||
|
) -> BlockOutput:
|
||||||
|
"""
|
||||||
|
Run the list voices operation.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
input_data: Input data (mainly credentials).
|
||||||
|
credentials: ElevenLabs API credentials.
|
||||||
|
|
||||||
|
Yields:
|
||||||
|
List of voices and voice IDs.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
client = ElevenLabsClient(credentials=credentials)
|
||||||
|
voices = self.get_voices_list(client)
|
||||||
|
|
||||||
|
yield "voices", voices
|
||||||
|
yield "voice_ids", [voice.voice_id for voice in voices]
|
||||||
|
|
||||||
|
except ElevenLabsException as e:
|
||||||
|
yield "error", f"ElevenLabs API error: {str(e)}"
|
||||||
|
except Exception as e:
|
||||||
|
yield "error", f"Unexpected error: {str(e)}"
|
||||||
|
|
||||||
|
|
||||||
|
class VoiceSettingsBlock(Block):
|
||||||
|
"""Block for managing voice settings in ElevenLabs."""
|
||||||
|
|
||||||
|
class Input(BlockSchema):
|
||||||
|
voice_id: str = SchemaField(
|
||||||
|
description="The ID of the voice to manage.",
|
||||||
|
placeholder="21m00Tcm4TlvDq8ikWAM",
|
||||||
|
)
|
||||||
|
action: str = SchemaField(
|
||||||
|
description="Action to perform on the voice settings.",
|
||||||
|
placeholder="get",
|
||||||
|
options=["get", "update"],
|
||||||
|
)
|
||||||
|
stability: Optional[float] = SchemaField(
|
||||||
|
description="Voice stability (0.0 to 1.0) for update action.",
|
||||||
|
placeholder="0.75",
|
||||||
|
default=None,
|
||||||
|
advanced=True,
|
||||||
|
)
|
||||||
|
similarity_boost: Optional[float] = SchemaField(
|
||||||
|
description="Similarity boost (0.0 to 1.0) for update action.",
|
||||||
|
placeholder="0.85",
|
||||||
|
default=None,
|
||||||
|
advanced=True,
|
||||||
|
)
|
||||||
|
credentials: ElevenLabsCredentialsInput = CredentialsField(
|
||||||
|
description="ElevenLabs API credentials."
|
||||||
|
)
|
||||||
|
|
||||||
|
class Output(BlockSchema):
|
||||||
|
stability: Optional[float] = SchemaField(
|
||||||
|
description="Current voice stability setting."
|
||||||
|
)
|
||||||
|
similarity_boost: Optional[float] = SchemaField(
|
||||||
|
description="Current voice similarity boost setting."
|
||||||
|
)
|
||||||
|
success: bool = SchemaField(description="Whether the operation was successful.")
|
||||||
|
message: str = SchemaField(description="Operation result message.")
|
||||||
|
error: str = SchemaField(
|
||||||
|
description="Error message if the operation failed."
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(
|
||||||
|
id="5f3c4b87-1a9d-47bc-9fb3-d7c4e63a10e9",
|
||||||
|
description="Get or update voice settings in ElevenLabs.",
|
||||||
|
categories={BlockCategory.AI, BlockCategory.MULTIMEDIA},
|
||||||
|
input_schema=VoiceSettingsBlock.Input,
|
||||||
|
output_schema=VoiceSettingsBlock.Output,
|
||||||
|
test_input={
|
||||||
|
"voice_id": "21m00Tcm4TlvDq8ikWAM",
|
||||||
|
"action": "get",
|
||||||
|
"credentials": TEST_CREDENTIALS_INPUT,
|
||||||
|
},
|
||||||
|
test_output=[
|
||||||
|
("stability", 0.75),
|
||||||
|
("similarity_boost", 0.85),
|
||||||
|
("success", True),
|
||||||
|
("message", "Voice settings retrieved successfully."),
|
||||||
|
],
|
||||||
|
test_mock={
|
||||||
|
"get_voice_settings": lambda *args, **kwargs: {
|
||||||
|
"stability": 0.75,
|
||||||
|
"similarity_boost": 0.85,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
test_credentials=TEST_CREDENTIALS,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_voice_settings(
|
||||||
|
self, client: ElevenLabsClient, voice_id: str
|
||||||
|
) -> Dict[str, float]:
|
||||||
|
"""
|
||||||
|
Get current settings for a voice.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
client: Initialized ElevenLabsClient.
|
||||||
|
voice_id: ID of the voice.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with stability and similarity_boost values.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return client.get_voice_settings(voice_id)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error getting voice settings: {str(e)}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def update_voice_settings(
|
||||||
|
self,
|
||||||
|
client: ElevenLabsClient,
|
||||||
|
voice_id: str,
|
||||||
|
stability: float,
|
||||||
|
similarity_boost: float,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Update settings for a voice.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
client: Initialized ElevenLabsClient.
|
||||||
|
voice_id: ID of the voice.
|
||||||
|
stability: Stability setting (0.0 to 1.0).
|
||||||
|
similarity_boost: Similarity boost setting (0.0 to 1.0).
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
client.edit_voice_settings(voice_id, stability, similarity_boost)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error updating voice settings: {str(e)}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def run(
|
||||||
|
self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs
|
||||||
|
) -> BlockOutput:
|
||||||
|
"""
|
||||||
|
Run the voice settings operation.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
input_data: Input data with voice ID and action.
|
||||||
|
credentials: ElevenLabs API credentials.
|
||||||
|
|
||||||
|
Yields:
|
||||||
|
Current settings or operation result.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
client = ElevenLabsClient(credentials=credentials)
|
||||||
|
|
||||||
|
if input_data.action == "get":
|
||||||
|
settings = self.get_voice_settings(client, input_data.voice_id)
|
||||||
|
yield "stability", settings.get("stability")
|
||||||
|
yield "similarity_boost", settings.get("similarity_boost")
|
||||||
|
yield "success", True
|
||||||
|
yield "message", "Voice settings retrieved successfully."
|
||||||
|
|
||||||
|
elif input_data.action == "update":
|
||||||
|
if input_data.stability is None or input_data.similarity_boost is None:
|
||||||
|
yield "error", "Both stability and similarity_boost must be provided for update action."
|
||||||
|
yield "success", False
|
||||||
|
yield "message", "Update failed: missing parameters."
|
||||||
|
return
|
||||||
|
|
||||||
|
self.update_voice_settings(
|
||||||
|
client,
|
||||||
|
input_data.voice_id,
|
||||||
|
input_data.stability,
|
||||||
|
input_data.similarity_boost,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get updated settings
|
||||||
|
updated_settings = self.get_voice_settings(client, input_data.voice_id)
|
||||||
|
yield "stability", updated_settings.get("stability")
|
||||||
|
yield "similarity_boost", updated_settings.get("similarity_boost")
|
||||||
|
yield "success", True
|
||||||
|
yield "message", "Voice settings updated successfully."
|
||||||
|
|
||||||
|
else:
|
||||||
|
yield "error", f"Unknown action: {input_data.action}"
|
||||||
|
yield "success", False
|
||||||
|
yield "message", f"Failed: {input_data.action} is not a valid action."
|
||||||
|
|
||||||
|
except ElevenLabsException as e:
|
||||||
|
yield "error", f"ElevenLabs API error: {str(e)}"
|
||||||
|
yield "success", False
|
||||||
|
yield "message", f"Operation failed: {str(e)}"
|
||||||
|
except Exception as e:
|
||||||
|
yield "error", f"Unexpected error: {str(e)}"
|
||||||
|
yield "success", False
|
||||||
|
yield "message", f"Operation failed with unexpected error: {str(e)}"
|
||||||
137
autogpt_platform/backend/backend/blocks/example/_api.py
Normal file
137
autogpt_platform/backend/backend/blocks/example/_api.py
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
"""
|
||||||
|
API module for Example API integration.
|
||||||
|
|
||||||
|
This module provides a example of how to create a client for an API.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# We also have a Json Wrapper library available in backend.util.json
|
||||||
|
from json import JSONDecodeError
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from backend.data.model import APIKeyCredentials
|
||||||
|
|
||||||
|
# This is a wrapper around the requests library that is used to make API requests.
|
||||||
|
from backend.util.request import Requests
|
||||||
|
|
||||||
|
|
||||||
|
class ExampleAPIException(Exception):
|
||||||
|
def __init__(self, message: str, status_code: int):
|
||||||
|
super().__init__(message)
|
||||||
|
self.status_code = status_code
|
||||||
|
|
||||||
|
|
||||||
|
class CreateResourceResponse(BaseModel):
|
||||||
|
message: str
|
||||||
|
is_funny: bool
|
||||||
|
|
||||||
|
|
||||||
|
class GetResourceResponse(BaseModel):
|
||||||
|
message: str
|
||||||
|
is_funny: bool
|
||||||
|
|
||||||
|
|
||||||
|
class ExampleClient:
|
||||||
|
"""Client for the Example API"""
|
||||||
|
|
||||||
|
API_BASE_URL = "https://api.example.com/v1"
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
credentials: Optional[APIKeyCredentials] = None,
|
||||||
|
custom_requests: Optional[Requests] = None,
|
||||||
|
):
|
||||||
|
if custom_requests:
|
||||||
|
self._requests = custom_requests
|
||||||
|
else:
|
||||||
|
headers: dict[str, str] = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
if credentials:
|
||||||
|
headers["Authorization"] = credentials.auth_header()
|
||||||
|
|
||||||
|
self._requests = Requests(
|
||||||
|
extra_headers=headers,
|
||||||
|
raise_for_status=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _handle_response(response) -> Any:
|
||||||
|
"""
|
||||||
|
Handles API response and checks for errors.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
response: The response object from the request.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The parsed JSON response data.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ExampleAPIException: If the API request fails.
|
||||||
|
"""
|
||||||
|
if not response.ok:
|
||||||
|
try:
|
||||||
|
error_data = response.json()
|
||||||
|
error_message = error_data.get("error", {}).get("message", "")
|
||||||
|
except JSONDecodeError:
|
||||||
|
error_message = response.text
|
||||||
|
|
||||||
|
raise ExampleAPIException(
|
||||||
|
f"Example API request failed ({response.status_code}): {error_message}",
|
||||||
|
response.status_code,
|
||||||
|
)
|
||||||
|
|
||||||
|
response_data = response.json()
|
||||||
|
if "errors" in response_data:
|
||||||
|
# This is an example error and needs to be
|
||||||
|
# replaced with how the real API returns errors
|
||||||
|
error_messages = [
|
||||||
|
error.get("message", "") for error in response_data["errors"]
|
||||||
|
]
|
||||||
|
raise ExampleAPIException(
|
||||||
|
f"Example API returned errors: {', '.join(error_messages)}",
|
||||||
|
response.status_code,
|
||||||
|
)
|
||||||
|
|
||||||
|
return response_data
|
||||||
|
|
||||||
|
def get_resource(self, resource_id: str) -> GetResourceResponse:
|
||||||
|
"""
|
||||||
|
Fetches a resource from the Example API.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
resource_id: The ID of the resource to fetch.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The resource data as a GetResourceResponse object.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ExampleAPIException: If the API request fails.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
response = self._requests.get(
|
||||||
|
f"{self.API_BASE_URL}/resources/{resource_id}"
|
||||||
|
)
|
||||||
|
return GetResourceResponse(**self._handle_response(response))
|
||||||
|
except Exception as e:
|
||||||
|
raise ExampleAPIException(f"Failed to get resource: {str(e)}", 500)
|
||||||
|
|
||||||
|
def create_resource(self, data: dict) -> CreateResourceResponse:
|
||||||
|
"""
|
||||||
|
Creates a new resource via the Example API.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data: The resource data to create.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The created resource data as a CreateResourceResponse object.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ExampleAPIException: If the API request fails.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
response = self._requests.post(f"{self.API_BASE_URL}/resources", json=data)
|
||||||
|
return CreateResourceResponse(**self._handle_response(response))
|
||||||
|
except Exception as e:
|
||||||
|
raise ExampleAPIException(f"Failed to create resource: {str(e)}", 500)
|
||||||
37
autogpt_platform/backend/backend/blocks/example/_auth.py
Normal file
37
autogpt_platform/backend/backend/blocks/example/_auth.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
"""
|
||||||
|
Authentication module for Example API integration.
|
||||||
|
|
||||||
|
This module provides credential types and test credentials for the Example API integration.
|
||||||
|
It defines the structure for API key credentials used to authenticate with the Example API
|
||||||
|
and provides mock credentials for testing purposes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
from pydantic import SecretStr
|
||||||
|
|
||||||
|
from backend.data.model import APIKeyCredentials, CredentialsMetaInput
|
||||||
|
from backend.integrations.providers import ProviderName
|
||||||
|
|
||||||
|
# Define the type of credentials input expected for Example API
|
||||||
|
ExampleCredentialsInput = CredentialsMetaInput[
|
||||||
|
Literal[ProviderName.EXAMPLE_PROVIDER], Literal["api_key"]
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# Mock credentials for testing Example API integration
|
||||||
|
TEST_CREDENTIALS = APIKeyCredentials(
|
||||||
|
id="9191c4f0-498f-4235-a79c-59c0e37454d4",
|
||||||
|
provider="example-provider",
|
||||||
|
api_key=SecretStr("mock-example-api-key"),
|
||||||
|
title="Mock Example API key",
|
||||||
|
expires_at=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Dictionary representation of test credentials for input fields
|
||||||
|
TEST_CREDENTIALS_INPUT = {
|
||||||
|
"provider": TEST_CREDENTIALS.provider,
|
||||||
|
"id": TEST_CREDENTIALS.id,
|
||||||
|
"type": TEST_CREDENTIALS.type,
|
||||||
|
"title": TEST_CREDENTIALS.title,
|
||||||
|
}
|
||||||
154
autogpt_platform/backend/backend/blocks/example/example.py
Normal file
154
autogpt_platform/backend/backend/blocks/example/example.py
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
|
||||||
|
from backend.data.model import APIKeyCredentials, CredentialsField, SchemaField
|
||||||
|
|
||||||
|
from ._api import ExampleClient
|
||||||
|
from ._auth import TEST_CREDENTIALS, TEST_CREDENTIALS_INPUT, ExampleCredentialsInput
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class GreetingMessage(BaseModel):
|
||||||
|
message: str
|
||||||
|
is_funny: bool
|
||||||
|
|
||||||
|
|
||||||
|
class ExampleBlock(Block):
|
||||||
|
|
||||||
|
class Input(BlockSchema):
|
||||||
|
name: str = SchemaField(
|
||||||
|
description="The name of the example block", placeholder="Enter a name"
|
||||||
|
)
|
||||||
|
greetings: list[str] = SchemaField(
|
||||||
|
description="The greetings to display", default=["Hello", "Hi", "Hey"]
|
||||||
|
)
|
||||||
|
is_funny: bool = SchemaField(
|
||||||
|
description="Whether the block is funny",
|
||||||
|
placeholder="True",
|
||||||
|
default=True,
|
||||||
|
# Advanced fields are moved to the "Advanced" dropdown in the UI
|
||||||
|
advanced=True,
|
||||||
|
)
|
||||||
|
greeting_context: str = SchemaField(
|
||||||
|
description="The context of the greeting",
|
||||||
|
placeholder="Enter a context",
|
||||||
|
default="The user is looking for an inspirational greeting",
|
||||||
|
# Hidden fields are not shown in the UI at all
|
||||||
|
hidden=True,
|
||||||
|
)
|
||||||
|
# Only if the block needs credentials
|
||||||
|
credentials: ExampleCredentialsInput = CredentialsField(
|
||||||
|
description="The credentials for the example block"
|
||||||
|
)
|
||||||
|
|
||||||
|
class Output(BlockSchema):
|
||||||
|
response: GreetingMessage = SchemaField(
|
||||||
|
description="The response object generated by the example block."
|
||||||
|
)
|
||||||
|
all_responses: list[GreetingMessage] = SchemaField(
|
||||||
|
description="All the responses from the example block."
|
||||||
|
)
|
||||||
|
greeting_count: int = SchemaField(
|
||||||
|
description="The number of greetings in the input."
|
||||||
|
)
|
||||||
|
error: str = SchemaField(description="The error from the example block")
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(
|
||||||
|
# The unique identifier for the block, this value will be persisted in the DB.
|
||||||
|
# It should be unique and constant across the application run.
|
||||||
|
# Use the UUID format for the ID.
|
||||||
|
id="380694d5-3b2e-4130-bced-b43752b70de9",
|
||||||
|
# The description of the block, explaining what the block does.
|
||||||
|
description="The example block",
|
||||||
|
# The set of categories that the block belongs to.
|
||||||
|
# Each category is an instance of BlockCategory Enum.
|
||||||
|
categories={BlockCategory.BASIC},
|
||||||
|
# The schema, defined as a Pydantic model, for the input data.
|
||||||
|
input_schema=ExampleBlock.Input,
|
||||||
|
# The schema, defined as a Pydantic model, for the output data.
|
||||||
|
output_schema=ExampleBlock.Output,
|
||||||
|
# The list or single sample input data for the block, for testing.
|
||||||
|
# This is an instance of the Input schema with sample values.
|
||||||
|
test_input={
|
||||||
|
"name": "Craig",
|
||||||
|
"greetings": ["Hello", "Hi", "Hey"],
|
||||||
|
"is_funny": True,
|
||||||
|
"credentials": TEST_CREDENTIALS_INPUT,
|
||||||
|
},
|
||||||
|
# The list or single expected output if the test_input is run.
|
||||||
|
# Each output is a tuple of (output_name, output_data).
|
||||||
|
test_output=[
|
||||||
|
("response", GreetingMessage(message="Hello, world!", is_funny=True)),
|
||||||
|
(
|
||||||
|
"response",
|
||||||
|
GreetingMessage(message="Hello, world!", is_funny=True),
|
||||||
|
), # We mock the function
|
||||||
|
(
|
||||||
|
"response",
|
||||||
|
GreetingMessage(message="Hello, world!", is_funny=True),
|
||||||
|
), # We mock the function
|
||||||
|
(
|
||||||
|
"all_responses",
|
||||||
|
[
|
||||||
|
GreetingMessage(message="Hello, world!", is_funny=True),
|
||||||
|
GreetingMessage(message="Hello, world!", is_funny=True),
|
||||||
|
GreetingMessage(message="Hello, world!", is_funny=True),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
("greeting_count", 3),
|
||||||
|
],
|
||||||
|
# Function names on the block implementation to mock on test run.
|
||||||
|
# Each mock is a dictionary with function names as keys and mock implementations as values.
|
||||||
|
test_mock={
|
||||||
|
"my_function_that_can_be_mocked": lambda *args, **kwargs: GreetingMessage(
|
||||||
|
message="Hello, world!", is_funny=True
|
||||||
|
)
|
||||||
|
},
|
||||||
|
# The credentials required for testing the block.
|
||||||
|
# This is an instance of APIKeyCredentials with sample values.
|
||||||
|
test_credentials=TEST_CREDENTIALS,
|
||||||
|
)
|
||||||
|
|
||||||
|
def my_function_that_can_be_mocked(
|
||||||
|
self, name: str, credentials: APIKeyCredentials
|
||||||
|
) -> GreetingMessage:
|
||||||
|
logger.info("my_function_that_can_be_mocked called with input: %s", name)
|
||||||
|
|
||||||
|
# Use the ExampleClient from _api.py to make an API call
|
||||||
|
client = ExampleClient(credentials=credentials)
|
||||||
|
|
||||||
|
# Create a sample resource using the client
|
||||||
|
resource_data = {"name": name, "type": "greeting"}
|
||||||
|
# If your API response object matches the return type of the function,
|
||||||
|
# there is no need to convert the object. In this case we have a different
|
||||||
|
# object type for the response and the return type of the function.
|
||||||
|
return GreetingMessage(**client.create_resource(resource_data).model_dump())
|
||||||
|
|
||||||
|
def run(
|
||||||
|
self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs
|
||||||
|
) -> BlockOutput:
|
||||||
|
"""
|
||||||
|
The run function implements the block's core logic. It processes the input_data
|
||||||
|
and yields the block's output.
|
||||||
|
|
||||||
|
In addition to credentials, the following parameters can be specified:
|
||||||
|
graph_id: The ID of the graph containing this block.
|
||||||
|
node_id: The ID of this block's node in the graph.
|
||||||
|
graph_exec_id: The ID of the current graph execution.
|
||||||
|
node_exec_id: The ID of the current node execution.
|
||||||
|
user_id: The ID of the user executing the block.
|
||||||
|
"""
|
||||||
|
rtn_all_responses: list[GreetingMessage] = []
|
||||||
|
# Here we deomonstrate best practice for blocks that need to yield multiple items.
|
||||||
|
# We yield each item from the list to allow for operations on each element.
|
||||||
|
# We also yield the complete list for situations when the full list is needed.
|
||||||
|
for greeting in input_data.greetings:
|
||||||
|
message = self.my_function_that_can_be_mocked(greeting, credentials)
|
||||||
|
rtn_all_responses.append(message)
|
||||||
|
yield "response", message
|
||||||
|
yield "all_responses", rtn_all_responses
|
||||||
|
yield "greeting_count", len(input_data.greetings)
|
||||||
65
autogpt_platform/backend/backend/blocks/example/triggers.py
Normal file
65
autogpt_platform/backend/backend/blocks/example/triggers.py
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
from backend.data.block import (
|
||||||
|
Block,
|
||||||
|
BlockCategory,
|
||||||
|
BlockManualWebhookConfig,
|
||||||
|
BlockOutput,
|
||||||
|
BlockSchema,
|
||||||
|
)
|
||||||
|
from backend.data.model import SchemaField
|
||||||
|
from backend.integrations.webhooks.example import ExampleWebhookEventType
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ExampleTriggerBlock(Block):
|
||||||
|
"""
|
||||||
|
A trigger block that is activated by an external webhook event.
|
||||||
|
|
||||||
|
Unlike standard blocks that are manually executed, trigger blocks are automatically
|
||||||
|
activated when a webhook event is received from the specified provider.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Input(BlockSchema):
|
||||||
|
# The payload field is hidden because it's automatically populated by the webhook
|
||||||
|
# system rather than being manually entered by the user
|
||||||
|
payload: dict = SchemaField(hidden=True)
|
||||||
|
|
||||||
|
class Output(BlockSchema):
|
||||||
|
event_data: dict = SchemaField(
|
||||||
|
description="The contents of the example webhook event."
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(
|
||||||
|
id="7c5933ce-d60c-42dd-9c4e-db82496474a3",
|
||||||
|
description="This block will output the contents of an example webhook event.",
|
||||||
|
categories={BlockCategory.BASIC},
|
||||||
|
input_schema=ExampleTriggerBlock.Input,
|
||||||
|
output_schema=ExampleTriggerBlock.Output,
|
||||||
|
# The webhook_config is a key difference from standard blocks
|
||||||
|
# It defines which external service can trigger this block and what type of events it responds to
|
||||||
|
webhook_config=BlockManualWebhookConfig(
|
||||||
|
provider="example_provider", # The external service that will send webhook events
|
||||||
|
webhook_type=ExampleWebhookEventType.EXAMPLE_EVENT, # The specific event type this block responds to
|
||||||
|
),
|
||||||
|
# Test input for trigger blocks should mimic the payload structure that would be received from the webhook
|
||||||
|
test_input=[
|
||||||
|
{
|
||||||
|
"payload": {
|
||||||
|
"event_type": "example",
|
||||||
|
"data": "Sample webhook data",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
test_output=[
|
||||||
|
("event_data", {"event_type": "example", "data": "Sample webhook data"})
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
def run(self, input_data: Input, **kwargs) -> BlockOutput:
|
||||||
|
# For trigger blocks, the run method is called automatically when a webhook event is received
|
||||||
|
# The payload from the webhook is passed in as input_data.payload
|
||||||
|
logger.info("Example trigger block run with payload: %s", input_data.payload)
|
||||||
|
yield "event_data", input_data.payload
|
||||||
@@ -2,6 +2,7 @@ from typing import Type
|
|||||||
|
|
||||||
from backend.blocks.ai_music_generator import AIMusicGeneratorBlock
|
from backend.blocks.ai_music_generator import AIMusicGeneratorBlock
|
||||||
from backend.blocks.ai_shortform_video_block import AIShortformVideoCreatorBlock
|
from backend.blocks.ai_shortform_video_block import AIShortformVideoCreatorBlock
|
||||||
|
from backend.blocks.example.example import ExampleBlock
|
||||||
from backend.blocks.ideogram import IdeogramModelBlock
|
from backend.blocks.ideogram import IdeogramModelBlock
|
||||||
from backend.blocks.jina.embeddings import JinaEmbeddingBlock
|
from backend.blocks.jina.embeddings import JinaEmbeddingBlock
|
||||||
from backend.blocks.jina.search import ExtractWebsiteContentBlock, SearchTheWebBlock
|
from backend.blocks.jina.search import ExtractWebsiteContentBlock, SearchTheWebBlock
|
||||||
@@ -23,6 +24,7 @@ from backend.data.cost import BlockCost, BlockCostType
|
|||||||
from backend.integrations.credentials_store import (
|
from backend.integrations.credentials_store import (
|
||||||
anthropic_credentials,
|
anthropic_credentials,
|
||||||
did_credentials,
|
did_credentials,
|
||||||
|
example_credentials,
|
||||||
groq_credentials,
|
groq_credentials,
|
||||||
ideogram_credentials,
|
ideogram_credentials,
|
||||||
jina_credentials,
|
jina_credentials,
|
||||||
@@ -267,4 +269,16 @@ BLOCK_COSTS: dict[Type[Block], list[BlockCost]] = {
|
|||||||
)
|
)
|
||||||
],
|
],
|
||||||
SmartDecisionMakerBlock: LLM_COST,
|
SmartDecisionMakerBlock: LLM_COST,
|
||||||
|
ExampleBlock: [
|
||||||
|
BlockCost(
|
||||||
|
cost_amount=1,
|
||||||
|
cost_filter={
|
||||||
|
"credentials": {
|
||||||
|
"id": example_credentials.id,
|
||||||
|
"provider": example_credentials.provider,
|
||||||
|
"type": example_credentials.type,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -169,7 +169,24 @@ zerobounce_credentials = APIKeyCredentials(
|
|||||||
expires_at=None,
|
expires_at=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
example_credentials = APIKeyCredentials(
|
||||||
|
id="a2b7f68f-aa6a-4995-99ec-b45b40d33498",
|
||||||
|
provider="example-provider",
|
||||||
|
api_key=SecretStr(settings.secrets.example_api_key),
|
||||||
|
title="Use Credits for Example",
|
||||||
|
expires_at=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
elevenlabs_credentials = APIKeyCredentials(
|
||||||
|
id="a2b7f68f-aa6a-4995-99ec-b45b40d33498",
|
||||||
|
provider="elevenlabs",
|
||||||
|
api_key=SecretStr(settings.secrets.elevenlabs_api_key),
|
||||||
|
title="Use Credits for ElevenLabs",
|
||||||
|
expires_at=None,
|
||||||
|
)
|
||||||
|
|
||||||
DEFAULT_CREDENTIALS = [
|
DEFAULT_CREDENTIALS = [
|
||||||
|
example_credentials,
|
||||||
ollama_credentials,
|
ollama_credentials,
|
||||||
revid_credentials,
|
revid_credentials,
|
||||||
ideogram_credentials,
|
ideogram_credentials,
|
||||||
@@ -225,6 +242,8 @@ class IntegrationCredentialsStore:
|
|||||||
all_credentials.append(ollama_credentials)
|
all_credentials.append(ollama_credentials)
|
||||||
|
|
||||||
# These will only be added if the API key is set
|
# These will only be added if the API key is set
|
||||||
|
if settings.secrets.example_api_key:
|
||||||
|
all_credentials.append(example_credentials)
|
||||||
if settings.secrets.revid_api_key:
|
if settings.secrets.revid_api_key:
|
||||||
all_credentials.append(revid_credentials)
|
all_credentials.append(revid_credentials)
|
||||||
if settings.secrets.ideogram_api_key:
|
if settings.secrets.ideogram_api_key:
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ class ProviderName(str, Enum):
|
|||||||
D_ID = "d_id"
|
D_ID = "d_id"
|
||||||
E2B = "e2b"
|
E2B = "e2b"
|
||||||
EXA = "exa"
|
EXA = "exa"
|
||||||
|
EXAMPLE_PROVIDER = "example-provider"
|
||||||
FAL = "fal"
|
FAL = "fal"
|
||||||
GITHUB = "github"
|
GITHUB = "github"
|
||||||
GOOGLE = "google"
|
GOOGLE = "google"
|
||||||
@@ -39,4 +40,5 @@ class ProviderName(str, Enum):
|
|||||||
TODOIST = "todoist"
|
TODOIST = "todoist"
|
||||||
UNREAL_SPEECH = "unreal_speech"
|
UNREAL_SPEECH = "unreal_speech"
|
||||||
ZEROBOUNCE = "zerobounce"
|
ZEROBOUNCE = "zerobounce"
|
||||||
|
ELEVENLABS = "elevenlabs"
|
||||||
# --8<-- [end:ProviderName]
|
# --8<-- [end:ProviderName]
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from .compass import CompassWebhookManager
|
from .compass import CompassWebhookManager
|
||||||
|
from .example import ExampleWebhookManager
|
||||||
from .github import GithubWebhooksManager
|
from .github import GithubWebhooksManager
|
||||||
from .slant3d import Slant3DWebhooksManager
|
from .slant3d import Slant3DWebhooksManager
|
||||||
|
|
||||||
@@ -15,6 +16,7 @@ WEBHOOK_MANAGERS_BY_NAME: dict["ProviderName", type["BaseWebhooksManager"]] = {
|
|||||||
CompassWebhookManager,
|
CompassWebhookManager,
|
||||||
GithubWebhooksManager,
|
GithubWebhooksManager,
|
||||||
Slant3DWebhooksManager,
|
Slant3DWebhooksManager,
|
||||||
|
ExampleWebhookManager,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
# --8<-- [end:WEBHOOK_MANAGERS_BY_NAME]
|
# --8<-- [end:WEBHOOK_MANAGERS_BY_NAME]
|
||||||
|
|||||||
@@ -0,0 +1,147 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from fastapi import Request
|
||||||
|
from strenum import StrEnum
|
||||||
|
|
||||||
|
from backend.data import integrations
|
||||||
|
from backend.data.model import APIKeyCredentials, Credentials
|
||||||
|
from backend.integrations.providers import ProviderName
|
||||||
|
|
||||||
|
from ._manual_base import ManualWebhookManagerBase
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ExampleWebhookEventType(StrEnum):
|
||||||
|
EXAMPLE_EVENT = "example_event"
|
||||||
|
ANOTHER_EXAMPLE_EVENT = "another_example_event"
|
||||||
|
|
||||||
|
|
||||||
|
# ExampleWebhookManager is a class that manages webhooks for a hypothetical provider.
|
||||||
|
# It extends ManualWebhookManagerBase, which provides base functionality for manual webhook management.
|
||||||
|
class ExampleWebhookManager(ManualWebhookManagerBase):
|
||||||
|
# Define the provider name for this webhook manager.
|
||||||
|
PROVIDER_NAME = ProviderName.EXAMPLE_PROVIDER
|
||||||
|
# Define the types of webhooks this manager can handle.
|
||||||
|
WebhookEventType = ExampleWebhookEventType
|
||||||
|
|
||||||
|
BASE_URL = "https://api.example.com"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def validate_payload(
|
||||||
|
cls, webhook: integrations.Webhook, request: Request
|
||||||
|
) -> tuple[dict, str]:
|
||||||
|
"""
|
||||||
|
Validate the incoming webhook payload.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
webhook (integrations.Webhook): The webhook object.
|
||||||
|
request (Request): The incoming request object.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple: A tuple containing the payload as a dictionary and the event type as a string.
|
||||||
|
"""
|
||||||
|
# Extract the JSON payload from the request.
|
||||||
|
payload = await request.json()
|
||||||
|
# Set the event type based on the webhook type in the payload.
|
||||||
|
event_type = payload.get("webhook_type", ExampleWebhookEventType.EXAMPLE_EVENT)
|
||||||
|
|
||||||
|
# For the payload its better to return a pydantic model
|
||||||
|
# rather than a weakly typed dict here
|
||||||
|
return payload, event_type
|
||||||
|
|
||||||
|
async def _register_webhook(
|
||||||
|
self,
|
||||||
|
credentials: Credentials,
|
||||||
|
webhook_type: str,
|
||||||
|
resource: str,
|
||||||
|
events: list[str],
|
||||||
|
ingress_url: str,
|
||||||
|
secret: str,
|
||||||
|
) -> tuple[str, dict]:
|
||||||
|
"""
|
||||||
|
Register a new webhook with the provider.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
credentials (Credentials): The credentials required for authentication.
|
||||||
|
webhook_type (str): The type of webhook to register.
|
||||||
|
resource (str): The resource associated with the webhook.
|
||||||
|
events (list[str]): The list of events to subscribe to.
|
||||||
|
ingress_url (str): The URL where the webhook will send data.
|
||||||
|
secret (str): A secret for securing the webhook.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple: A tuple containing the provider's webhook ID, if any, and the webhook configuration as a dictionary.
|
||||||
|
"""
|
||||||
|
# Ensure the credentials are of the correct type.
|
||||||
|
if not isinstance(credentials, APIKeyCredentials):
|
||||||
|
raise ValueError("API key is required to register a webhook")
|
||||||
|
|
||||||
|
# Prepare the headers for the request, including the API key.
|
||||||
|
headers = {
|
||||||
|
"api-key": credentials.api_key.get_secret_value(),
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Prepare the payload for the request. Note that the events list is not used.
|
||||||
|
# This is just a fake example
|
||||||
|
payload = {"endPoint": ingress_url}
|
||||||
|
|
||||||
|
# Send a POST request to register the webhook.
|
||||||
|
response = requests.post(
|
||||||
|
f"{self.BASE_URL}/example/webhookSubscribe", headers=headers, json=payload
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if the response indicates a failure.
|
||||||
|
if not response.ok:
|
||||||
|
error = response.json().get("error", "Unknown error")
|
||||||
|
raise RuntimeError(f"Failed to register webhook: {error}")
|
||||||
|
|
||||||
|
# Prepare the webhook configuration to return.
|
||||||
|
webhook_config = {
|
||||||
|
"endpoint": ingress_url,
|
||||||
|
"provider": self.PROVIDER_NAME,
|
||||||
|
"events": ["example_event"],
|
||||||
|
"type": webhook_type,
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", webhook_config
|
||||||
|
|
||||||
|
async def _deregister_webhook(
|
||||||
|
self, webhook: integrations.Webhook, credentials: Credentials
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Deregister a webhook with the provider.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
webhook (integrations.Webhook): The webhook object to deregister.
|
||||||
|
credentials (Credentials): The credentials associated with the webhook.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If the webhook doesn't belong to the credentials or if deregistration fails.
|
||||||
|
"""
|
||||||
|
if webhook.credentials_id != credentials.id:
|
||||||
|
raise ValueError(
|
||||||
|
f"Webhook #{webhook.id} does not belong to credentials {credentials.id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not isinstance(credentials, APIKeyCredentials):
|
||||||
|
raise ValueError("API key is required to deregister a webhook")
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"api-key": credentials.api_key.get_secret_value(),
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Construct the delete URL based on the webhook information
|
||||||
|
delete_url = f"{self.BASE_URL}/example/webhooks/{webhook.provider_webhook_id}"
|
||||||
|
|
||||||
|
response = requests.delete(delete_url, headers=headers)
|
||||||
|
|
||||||
|
if response.status_code not in [204, 404]:
|
||||||
|
# 204 means successful deletion, 404 means the webhook was already deleted
|
||||||
|
error = response.json().get("error", "Unknown error")
|
||||||
|
raise ValueError(f"Failed to delete webhook: {error}")
|
||||||
|
|
||||||
|
# If we reach here, the webhook was successfully deleted or didn't exist
|
||||||
@@ -404,8 +404,11 @@ class Secrets(UpdateTrackingModel["Secrets"], BaseSettings):
|
|||||||
smartlead_api_key: str = Field(default="", description="SmartLead API Key")
|
smartlead_api_key: str = Field(default="", description="SmartLead API Key")
|
||||||
zerobounce_api_key: str = Field(default="", description="ZeroBounce API Key")
|
zerobounce_api_key: str = Field(default="", description="ZeroBounce API Key")
|
||||||
|
|
||||||
|
example_api_key: str = Field(default="", description="Example API Key")
|
||||||
# Add more secret fields as needed
|
# Add more secret fields as needed
|
||||||
|
|
||||||
|
elevenlabs_api_key: str = Field(default="", description="ElevenLabs API Key")
|
||||||
|
|
||||||
model_config = SettingsConfigDict(
|
model_config = SettingsConfigDict(
|
||||||
env_file=".env",
|
env_file=".env",
|
||||||
env_file_encoding="utf-8",
|
env_file_encoding="utf-8",
|
||||||
|
|||||||
@@ -90,6 +90,8 @@ export const providerIcons: Record<
|
|||||||
smartlead: fallbackIcon,
|
smartlead: fallbackIcon,
|
||||||
todoist: fallbackIcon,
|
todoist: fallbackIcon,
|
||||||
zerobounce: fallbackIcon,
|
zerobounce: fallbackIcon,
|
||||||
|
example: fallbackIcon,
|
||||||
|
elevenlabs: fallbackIcon,
|
||||||
};
|
};
|
||||||
// --8<-- [end:ProviderIconsEmbed]
|
// --8<-- [end:ProviderIconsEmbed]
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ const providerDisplayNames: Record<CredentialsProviderName, string> = {
|
|||||||
todoist: "Todoist",
|
todoist: "Todoist",
|
||||||
unreal_speech: "Unreal Speech",
|
unreal_speech: "Unreal Speech",
|
||||||
zerobounce: "ZeroBounce",
|
zerobounce: "ZeroBounce",
|
||||||
|
example: "Example",
|
||||||
|
elevenlabs: "ElevenLabs",
|
||||||
} as const;
|
} as const;
|
||||||
// --8<-- [end:CredentialsProviderNames]
|
// --8<-- [end:CredentialsProviderNames]
|
||||||
|
|
||||||
|
|||||||
@@ -148,6 +148,8 @@ export const PROVIDER_NAMES = {
|
|||||||
UNREAL_SPEECH: "unreal_speech",
|
UNREAL_SPEECH: "unreal_speech",
|
||||||
TODOIST: "todoist",
|
TODOIST: "todoist",
|
||||||
ZEROBOUNCE: "zerobounce",
|
ZEROBOUNCE: "zerobounce",
|
||||||
|
EXAMPLE: "example",
|
||||||
|
ELEVENLABS: "elevenlabs",
|
||||||
} as const;
|
} as const;
|
||||||
// --8<-- [end:BlockIOCredentialsSubSchema]
|
// --8<-- [end:BlockIOCredentialsSubSchema]
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user