agentle 0.9.4__py3-none-any.whl → 0.9.28__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agentle/agents/agent.py +175 -10
- agentle/agents/agent_run_output.py +8 -1
- agentle/agents/apis/__init__.py +79 -6
- agentle/agents/apis/api.py +342 -73
- agentle/agents/apis/api_key_authentication.py +43 -0
- agentle/agents/apis/api_key_location.py +11 -0
- agentle/agents/apis/api_metrics.py +16 -0
- agentle/agents/apis/auth_type.py +17 -0
- agentle/agents/apis/authentication.py +32 -0
- agentle/agents/apis/authentication_base.py +42 -0
- agentle/agents/apis/authentication_config.py +117 -0
- agentle/agents/apis/basic_authentication.py +34 -0
- agentle/agents/apis/bearer_authentication.py +52 -0
- agentle/agents/apis/cache_strategy.py +12 -0
- agentle/agents/apis/circuit_breaker.py +69 -0
- agentle/agents/apis/circuit_breaker_error.py +7 -0
- agentle/agents/apis/circuit_breaker_state.py +11 -0
- agentle/agents/apis/endpoint.py +413 -254
- agentle/agents/apis/file_upload.py +23 -0
- agentle/agents/apis/hmac_authentication.py +56 -0
- agentle/agents/apis/no_authentication.py +27 -0
- agentle/agents/apis/oauth2_authentication.py +111 -0
- agentle/agents/apis/oauth2_grant_type.py +12 -0
- agentle/agents/apis/object_schema.py +86 -1
- agentle/agents/apis/params/__init__.py +10 -1
- agentle/agents/apis/params/boolean_param.py +44 -0
- agentle/agents/apis/params/number_param.py +56 -0
- agentle/agents/apis/rate_limit_error.py +7 -0
- agentle/agents/apis/rate_limiter.py +57 -0
- agentle/agents/apis/request_config.py +126 -4
- agentle/agents/apis/request_hook.py +16 -0
- agentle/agents/apis/response_cache.py +49 -0
- agentle/agents/apis/retry_strategy.py +12 -0
- agentle/agents/whatsapp/human_delay_calculator.py +462 -0
- agentle/agents/whatsapp/models/audio_message.py +6 -4
- agentle/agents/whatsapp/models/key.py +2 -2
- agentle/agents/whatsapp/models/whatsapp_bot_config.py +375 -21
- agentle/agents/whatsapp/models/whatsapp_response_base.py +31 -0
- agentle/agents/whatsapp/models/whatsapp_webhook_payload.py +5 -1
- agentle/agents/whatsapp/providers/base/whatsapp_provider.py +51 -0
- agentle/agents/whatsapp/providers/evolution/evolution_api_provider.py +237 -10
- agentle/agents/whatsapp/providers/meta/meta_whatsapp_provider.py +126 -0
- agentle/agents/whatsapp/v2/batch_processor_manager.py +4 -0
- agentle/agents/whatsapp/v2/bot_config.py +188 -0
- agentle/agents/whatsapp/v2/message_limit.py +9 -0
- agentle/agents/whatsapp/v2/payload.py +0 -0
- agentle/agents/whatsapp/v2/whatsapp_bot.py +13 -0
- agentle/agents/whatsapp/v2/whatsapp_cloud_api_provider.py +0 -0
- agentle/agents/whatsapp/v2/whatsapp_provider.py +0 -0
- agentle/agents/whatsapp/whatsapp_bot.py +827 -45
- agentle/generations/providers/google/adapters/generate_generate_content_response_to_generation_adapter.py +13 -10
- agentle/generations/providers/google/google_generation_provider.py +35 -5
- agentle/generations/providers/openrouter/_adapters/openrouter_message_to_generated_assistant_message_adapter.py +35 -1
- agentle/mcp/servers/stdio_mcp_server.py +23 -4
- agentle/parsing/parsers/docx.py +8 -0
- agentle/parsing/parsers/file_parser.py +4 -0
- agentle/parsing/parsers/pdf.py +7 -1
- agentle/storage/__init__.py +11 -0
- agentle/storage/file_storage_manager.py +44 -0
- agentle/storage/local_file_storage_manager.py +122 -0
- agentle/storage/s3_file_storage_manager.py +124 -0
- agentle/tts/audio_format.py +6 -0
- agentle/tts/elevenlabs_tts_provider.py +108 -0
- agentle/tts/output_format_type.py +26 -0
- agentle/tts/speech_config.py +14 -0
- agentle/tts/speech_result.py +15 -0
- agentle/tts/tts_provider.py +16 -0
- agentle/tts/voice_settings.py +30 -0
- agentle/utils/parse_streaming_json.py +39 -13
- agentle/voice_cloning/__init__.py +0 -0
- agentle/voice_cloning/voice_cloner.py +0 -0
- agentle/web/extractor.py +282 -148
- {agentle-0.9.4.dist-info → agentle-0.9.28.dist-info}/METADATA +1 -1
- {agentle-0.9.4.dist-info → agentle-0.9.28.dist-info}/RECORD +78 -39
- agentle/tts/real_time/definitions/audio_data.py +0 -20
- agentle/tts/real_time/definitions/speech_config.py +0 -27
- agentle/tts/real_time/definitions/speech_result.py +0 -14
- agentle/tts/real_time/definitions/tts_stream_chunk.py +0 -15
- agentle/tts/real_time/definitions/voice_gender.py +0 -9
- agentle/tts/real_time/definitions/voice_info.py +0 -18
- agentle/tts/real_time/real_time_speech_to_text_provider.py +0 -66
- /agentle/{tts/real_time → agents/whatsapp/v2}/__init__.py +0 -0
- /agentle/{tts/real_time/definitions/__init__.py → agents/whatsapp/v2/in_memory_batch_processor_manager.py} +0 -0
- {agentle-0.9.4.dist-info → agentle-0.9.28.dist-info}/WHEEL +0 -0
- {agentle-0.9.4.dist-info → agentle-0.9.28.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import os
|
|
5
|
+
from collections.abc import AsyncIterator
|
|
6
|
+
from typing import TYPE_CHECKING, override
|
|
7
|
+
|
|
8
|
+
from agentle.tts.audio_format import AudioFormat
|
|
9
|
+
from agentle.tts.output_format_type import OutputFormatType
|
|
10
|
+
from agentle.tts.speech_config import SpeechConfig
|
|
11
|
+
from agentle.tts.speech_result import SpeechResult
|
|
12
|
+
from agentle.tts.tts_provider import TtsProvider
|
|
13
|
+
from agentle.tts.voice_settings import VoiceSettings
|
|
14
|
+
from agentle.utils.needs import check_modules
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from elevenlabs import AsyncElevenLabs
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ElevenLabsTtsProvider(TtsProvider):
|
|
21
|
+
_client: AsyncElevenLabs
|
|
22
|
+
|
|
23
|
+
def __init__(self, api_key: str | None = None) -> None:
|
|
24
|
+
super().__init__()
|
|
25
|
+
check_modules("elevenlabs")
|
|
26
|
+
from elevenlabs import AsyncElevenLabs
|
|
27
|
+
|
|
28
|
+
self._client = AsyncElevenLabs(
|
|
29
|
+
api_key=api_key or os.getenv("ELEVENLABS_API_KEY")
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
@override
|
|
33
|
+
async def synthesize_async(self, text: str, config: SpeechConfig) -> SpeechResult:
|
|
34
|
+
from elevenlabs import AsyncElevenLabs
|
|
35
|
+
from elevenlabs.types.voice_settings import (
|
|
36
|
+
VoiceSettings as ElevenLabsVoiceSettings,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
elevenlabs = AsyncElevenLabs()
|
|
40
|
+
audio_stream: AsyncIterator[bytes] = elevenlabs.text_to_speech.convert(
|
|
41
|
+
text=text,
|
|
42
|
+
voice_id=config.voice_id,
|
|
43
|
+
model_id=config.model_id,
|
|
44
|
+
output_format=config.output_format,
|
|
45
|
+
voice_settings=ElevenLabsVoiceSettings(
|
|
46
|
+
stability=config.voice_settings.stability,
|
|
47
|
+
use_speaker_boost=config.voice_settings.use_speaker_boost,
|
|
48
|
+
similarity_boost=config.voice_settings.similarity_boost,
|
|
49
|
+
style=config.voice_settings.style,
|
|
50
|
+
speed=config.voice_settings.speed,
|
|
51
|
+
)
|
|
52
|
+
if config.voice_settings
|
|
53
|
+
else None,
|
|
54
|
+
language_code=config.language_code,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Collect all chunks into bytes
|
|
58
|
+
chunks: list[bytes] = []
|
|
59
|
+
async for chunk in audio_stream:
|
|
60
|
+
chunks.append(chunk)
|
|
61
|
+
audio_bytes = b"".join(chunks)
|
|
62
|
+
|
|
63
|
+
audio_base64 = base64.b64encode(audio_bytes).decode("utf-8")
|
|
64
|
+
|
|
65
|
+
return SpeechResult(
|
|
66
|
+
audio=audio_base64,
|
|
67
|
+
mime_type=self._get_mime_type(config.output_format),
|
|
68
|
+
format=config.output_format,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
def _get_mime_type(self, output_format: OutputFormatType) -> AudioFormat:
|
|
72
|
+
"""Convert ElevenLabs output format to MIME type."""
|
|
73
|
+
if output_format.startswith("mp3_"):
|
|
74
|
+
return "audio/mpeg"
|
|
75
|
+
elif output_format.startswith("pcm_"):
|
|
76
|
+
return "audio/wav" # or "audio/pcm" depending on your use case
|
|
77
|
+
elif output_format.startswith("ulaw_"):
|
|
78
|
+
return "audio/basic"
|
|
79
|
+
elif output_format.startswith("alaw_"):
|
|
80
|
+
return "audio/basic"
|
|
81
|
+
elif output_format.startswith("opus_"):
|
|
82
|
+
return "audio/opus"
|
|
83
|
+
else:
|
|
84
|
+
return "application/octet-stream" # fallback
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
if __name__ == "__main__":
|
|
88
|
+
from dotenv import load_dotenv
|
|
89
|
+
|
|
90
|
+
load_dotenv(override=True)
|
|
91
|
+
tts_provider = ElevenLabsTtsProvider()
|
|
92
|
+
audio = tts_provider.synthesize(
|
|
93
|
+
"Oi, eu sou a Júlia. Assistente pessoal da Dany Braga do estúdio de fotografia. Em que posso ajudar você hoje?",
|
|
94
|
+
config=SpeechConfig(
|
|
95
|
+
voice_id="lWq4KDY8znfkV0DrK8Vb",
|
|
96
|
+
model_id="eleven_v3",
|
|
97
|
+
language_code="pt",
|
|
98
|
+
voice_settings=VoiceSettings(
|
|
99
|
+
stability=0.0,
|
|
100
|
+
use_speaker_boost=None,
|
|
101
|
+
similarity_boost=None,
|
|
102
|
+
style=None,
|
|
103
|
+
speed=None,
|
|
104
|
+
),
|
|
105
|
+
),
|
|
106
|
+
)
|
|
107
|
+
with open("audio.mp3", "wb") as file:
|
|
108
|
+
file.write(base64.b64decode(audio.audio))
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from typing import Literal
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
OutputFormatType = Literal[
|
|
5
|
+
"mp3_22050_32",
|
|
6
|
+
"mp3_24000_48",
|
|
7
|
+
"mp3_44100_32",
|
|
8
|
+
"mp3_44100_64",
|
|
9
|
+
"mp3_44100_96",
|
|
10
|
+
"mp3_44100_128",
|
|
11
|
+
"mp3_44100_192",
|
|
12
|
+
"pcm_8000",
|
|
13
|
+
"pcm_16000",
|
|
14
|
+
"pcm_22050",
|
|
15
|
+
"pcm_24000",
|
|
16
|
+
"pcm_32000",
|
|
17
|
+
"pcm_44100",
|
|
18
|
+
"pcm_48000",
|
|
19
|
+
"ulaw_8000",
|
|
20
|
+
"alaw_8000",
|
|
21
|
+
"opus_48000_32",
|
|
22
|
+
"opus_48000_64",
|
|
23
|
+
"opus_48000_96",
|
|
24
|
+
"opus_48000_128",
|
|
25
|
+
"opus_48000_192",
|
|
26
|
+
]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from rsb.models import BaseModel, Field
|
|
4
|
+
|
|
5
|
+
from agentle.tts.output_format_type import OutputFormatType
|
|
6
|
+
from agentle.tts.voice_settings import VoiceSettings
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SpeechConfig(BaseModel):
|
|
10
|
+
voice_id: str
|
|
11
|
+
model_id: Optional[str] = Field(default=None)
|
|
12
|
+
output_format: OutputFormatType = Field(default="mp3_22050_32")
|
|
13
|
+
language_code: Optional[str] = Field(default=None)
|
|
14
|
+
voice_settings: Optional[VoiceSettings] = Field(default=None)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from rsb.models import BaseModel, Field
|
|
2
|
+
|
|
3
|
+
from agentle.tts.audio_format import AudioFormat
|
|
4
|
+
from agentle.tts.output_format_type import OutputFormatType
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class SpeechResult(BaseModel):
|
|
8
|
+
audio: str = Field(...)
|
|
9
|
+
"""The speech in base-64 format"""
|
|
10
|
+
|
|
11
|
+
mime_type: AudioFormat = Field(...)
|
|
12
|
+
"""`audio/mpeg`, `audio/wav`, `audio/opus`"""
|
|
13
|
+
|
|
14
|
+
format: OutputFormatType = Field(...)
|
|
15
|
+
"""The original format string like "mp3_44100_128"""
|
agentle/tts/tts_provider.py
CHANGED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
|
|
3
|
+
from rsb.coroutines.run_sync import run_sync
|
|
4
|
+
|
|
5
|
+
from agentle.tts.speech_config import SpeechConfig
|
|
6
|
+
from agentle.tts.speech_result import SpeechResult
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TtsProvider(abc.ABC):
|
|
10
|
+
def synthesize(self, text: str, config: SpeechConfig) -> SpeechResult:
|
|
11
|
+
return run_sync(self.synthesize_async, text=text, config=config)
|
|
12
|
+
|
|
13
|
+
@abc.abstractmethod
|
|
14
|
+
async def synthesize_async(
|
|
15
|
+
self, text: str, config: SpeechConfig
|
|
16
|
+
) -> SpeechResult: ...
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from rsb.models import BaseModel, Field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class VoiceSettings(BaseModel):
|
|
7
|
+
stability: Optional[float] = Field(default=None)
|
|
8
|
+
"""
|
|
9
|
+
Determines how stable the voice is and the randomness between each generation. Lower values introduce broader emotional range for the voice. Higher values can result in a monotonous voice with limited emotion.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
use_speaker_boost: Optional[bool] = Field(default=None)
|
|
13
|
+
"""
|
|
14
|
+
This setting boosts the similarity to the original speaker. Using this setting requires a slightly higher computational load, which in turn increases latency.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
similarity_boost: Optional[float] = Field(default=None)
|
|
18
|
+
"""
|
|
19
|
+
Determines how closely the AI should adhere to the original voice when attempting to replicate it.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
style: Optional[float] = Field(default=None)
|
|
23
|
+
"""
|
|
24
|
+
Determines the style exaggeration of the voice. This setting attempts to amplify the style of the original speaker. It does consume additional computational resources and might increase latency if set to anything other than 0.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
speed: Optional[float] = Field(default=None)
|
|
28
|
+
"""
|
|
29
|
+
Adjusts the speed of the voice. A value of 1.0 is the default speed, while values less than 1.0 slow down the speech, and values greater than 1.0 speed it up.
|
|
30
|
+
"""
|
|
@@ -21,8 +21,6 @@ def parse_streaming_json[T: BaseModel](potential_json: str | None, model: type[T
|
|
|
21
21
|
if potential_json is None:
|
|
22
22
|
return model()
|
|
23
23
|
|
|
24
|
-
# print(f"parsing: {potential_json}")
|
|
25
|
-
|
|
26
24
|
def find_json_boundaries(text: str) -> tuple[int, int]:
|
|
27
25
|
"""Find the start and potential end of JSON in the text."""
|
|
28
26
|
|
|
@@ -95,17 +93,32 @@ def parse_streaming_json[T: BaseModel](potential_json: str | None, model: type[T
|
|
|
95
93
|
# Remove any leading/trailing whitespace
|
|
96
94
|
json_str = json_str.strip()
|
|
97
95
|
|
|
98
|
-
# Fix missing closing quotes on string values (at the end)
|
|
99
|
-
# Look for patterns like: "key": "value without closing quote
|
|
100
|
-
json_str = re.sub(r':\s*"([^"]*?)(?:\s*[,}]|$)', r': "\1"', json_str)
|
|
101
|
-
|
|
102
|
-
# Fix missing closing quotes for keys
|
|
103
|
-
# Look for patterns like: "key without quotes:
|
|
104
|
-
json_str = re.sub(r'"([^"]*?)(?=\s*:)', r'"\1"', json_str)
|
|
105
|
-
|
|
106
96
|
# Remove trailing commas before closing braces/brackets
|
|
107
97
|
json_str = re.sub(r",\s*([}\]])", r"\1", json_str)
|
|
108
98
|
|
|
99
|
+
# For streaming JSON, we need to handle incomplete strings carefully
|
|
100
|
+
# Check if we have an unclosed string at the end
|
|
101
|
+
in_string = False
|
|
102
|
+
escape_next = False
|
|
103
|
+
last_quote_pos = -1
|
|
104
|
+
|
|
105
|
+
for i, char in enumerate(json_str):
|
|
106
|
+
if escape_next:
|
|
107
|
+
escape_next = False
|
|
108
|
+
continue
|
|
109
|
+
if char == '\\':
|
|
110
|
+
escape_next = True
|
|
111
|
+
continue
|
|
112
|
+
if char == '"':
|
|
113
|
+
in_string = not in_string
|
|
114
|
+
if in_string:
|
|
115
|
+
last_quote_pos = i
|
|
116
|
+
|
|
117
|
+
# If we're in a string at the end (incomplete), close it properly
|
|
118
|
+
if in_string and last_quote_pos != -1:
|
|
119
|
+
# Add closing quote for the incomplete string
|
|
120
|
+
json_str += '"'
|
|
121
|
+
|
|
109
122
|
# Ensure the JSON has proper closing braces if it appears incomplete
|
|
110
123
|
open_braces = json_str.count("{") - json_str.count("}")
|
|
111
124
|
open_brackets = json_str.count("[") - json_str.count("]")
|
|
@@ -124,12 +137,25 @@ def parse_streaming_json[T: BaseModel](potential_json: str | None, model: type[T
|
|
|
124
137
|
data = {}
|
|
125
138
|
|
|
126
139
|
# Extract string key-value pairs with quoted keys
|
|
127
|
-
#
|
|
128
|
-
|
|
129
|
-
|
|
140
|
+
# IMPROVED: Handle long strings that may contain newlines, special chars, etc.
|
|
141
|
+
# Pattern: "key": "value..." - capture everything until the next unescaped quote or EOF
|
|
142
|
+
string_pattern = r'["\']([\w]+)["\']:\s*["\']([^"\']*?)(?:["\']|$)'
|
|
143
|
+
string_matches = re.findall(string_pattern, json_str, re.DOTALL)
|
|
144
|
+
|
|
145
|
+
# Also try to capture very long strings that span multiple lines
|
|
146
|
+
# This catches incomplete strings during streaming
|
|
147
|
+
long_string_pattern = r'["\']([\w_]+)["\']:\s*["\'](.+?)(?:["\'],?\s*["}]|$)'
|
|
148
|
+
long_matches = re.findall(long_string_pattern, json_str, re.DOTALL)
|
|
130
149
|
|
|
131
150
|
for key, value in string_matches:
|
|
132
151
|
data[key] = value
|
|
152
|
+
|
|
153
|
+
# Prefer long_matches for fields that might be truncated in string_matches
|
|
154
|
+
for key, value in long_matches:
|
|
155
|
+
# Only override if the long match has more content
|
|
156
|
+
existing = data.get(key, "")
|
|
157
|
+
if key not in data or (isinstance(existing, str) and len(value) > len(existing)):
|
|
158
|
+
data[key] = value
|
|
133
159
|
|
|
134
160
|
# Extract string key-value pairs with unquoted keys
|
|
135
161
|
# Pattern: key: "value" (no quotes around key)
|
|
File without changes
|
|
File without changes
|