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.
Files changed (85) hide show
  1. agentle/agents/agent.py +175 -10
  2. agentle/agents/agent_run_output.py +8 -1
  3. agentle/agents/apis/__init__.py +79 -6
  4. agentle/agents/apis/api.py +342 -73
  5. agentle/agents/apis/api_key_authentication.py +43 -0
  6. agentle/agents/apis/api_key_location.py +11 -0
  7. agentle/agents/apis/api_metrics.py +16 -0
  8. agentle/agents/apis/auth_type.py +17 -0
  9. agentle/agents/apis/authentication.py +32 -0
  10. agentle/agents/apis/authentication_base.py +42 -0
  11. agentle/agents/apis/authentication_config.py +117 -0
  12. agentle/agents/apis/basic_authentication.py +34 -0
  13. agentle/agents/apis/bearer_authentication.py +52 -0
  14. agentle/agents/apis/cache_strategy.py +12 -0
  15. agentle/agents/apis/circuit_breaker.py +69 -0
  16. agentle/agents/apis/circuit_breaker_error.py +7 -0
  17. agentle/agents/apis/circuit_breaker_state.py +11 -0
  18. agentle/agents/apis/endpoint.py +413 -254
  19. agentle/agents/apis/file_upload.py +23 -0
  20. agentle/agents/apis/hmac_authentication.py +56 -0
  21. agentle/agents/apis/no_authentication.py +27 -0
  22. agentle/agents/apis/oauth2_authentication.py +111 -0
  23. agentle/agents/apis/oauth2_grant_type.py +12 -0
  24. agentle/agents/apis/object_schema.py +86 -1
  25. agentle/agents/apis/params/__init__.py +10 -1
  26. agentle/agents/apis/params/boolean_param.py +44 -0
  27. agentle/agents/apis/params/number_param.py +56 -0
  28. agentle/agents/apis/rate_limit_error.py +7 -0
  29. agentle/agents/apis/rate_limiter.py +57 -0
  30. agentle/agents/apis/request_config.py +126 -4
  31. agentle/agents/apis/request_hook.py +16 -0
  32. agentle/agents/apis/response_cache.py +49 -0
  33. agentle/agents/apis/retry_strategy.py +12 -0
  34. agentle/agents/whatsapp/human_delay_calculator.py +462 -0
  35. agentle/agents/whatsapp/models/audio_message.py +6 -4
  36. agentle/agents/whatsapp/models/key.py +2 -2
  37. agentle/agents/whatsapp/models/whatsapp_bot_config.py +375 -21
  38. agentle/agents/whatsapp/models/whatsapp_response_base.py +31 -0
  39. agentle/agents/whatsapp/models/whatsapp_webhook_payload.py +5 -1
  40. agentle/agents/whatsapp/providers/base/whatsapp_provider.py +51 -0
  41. agentle/agents/whatsapp/providers/evolution/evolution_api_provider.py +237 -10
  42. agentle/agents/whatsapp/providers/meta/meta_whatsapp_provider.py +126 -0
  43. agentle/agents/whatsapp/v2/batch_processor_manager.py +4 -0
  44. agentle/agents/whatsapp/v2/bot_config.py +188 -0
  45. agentle/agents/whatsapp/v2/message_limit.py +9 -0
  46. agentle/agents/whatsapp/v2/payload.py +0 -0
  47. agentle/agents/whatsapp/v2/whatsapp_bot.py +13 -0
  48. agentle/agents/whatsapp/v2/whatsapp_cloud_api_provider.py +0 -0
  49. agentle/agents/whatsapp/v2/whatsapp_provider.py +0 -0
  50. agentle/agents/whatsapp/whatsapp_bot.py +827 -45
  51. agentle/generations/providers/google/adapters/generate_generate_content_response_to_generation_adapter.py +13 -10
  52. agentle/generations/providers/google/google_generation_provider.py +35 -5
  53. agentle/generations/providers/openrouter/_adapters/openrouter_message_to_generated_assistant_message_adapter.py +35 -1
  54. agentle/mcp/servers/stdio_mcp_server.py +23 -4
  55. agentle/parsing/parsers/docx.py +8 -0
  56. agentle/parsing/parsers/file_parser.py +4 -0
  57. agentle/parsing/parsers/pdf.py +7 -1
  58. agentle/storage/__init__.py +11 -0
  59. agentle/storage/file_storage_manager.py +44 -0
  60. agentle/storage/local_file_storage_manager.py +122 -0
  61. agentle/storage/s3_file_storage_manager.py +124 -0
  62. agentle/tts/audio_format.py +6 -0
  63. agentle/tts/elevenlabs_tts_provider.py +108 -0
  64. agentle/tts/output_format_type.py +26 -0
  65. agentle/tts/speech_config.py +14 -0
  66. agentle/tts/speech_result.py +15 -0
  67. agentle/tts/tts_provider.py +16 -0
  68. agentle/tts/voice_settings.py +30 -0
  69. agentle/utils/parse_streaming_json.py +39 -13
  70. agentle/voice_cloning/__init__.py +0 -0
  71. agentle/voice_cloning/voice_cloner.py +0 -0
  72. agentle/web/extractor.py +282 -148
  73. {agentle-0.9.4.dist-info → agentle-0.9.28.dist-info}/METADATA +1 -1
  74. {agentle-0.9.4.dist-info → agentle-0.9.28.dist-info}/RECORD +78 -39
  75. agentle/tts/real_time/definitions/audio_data.py +0 -20
  76. agentle/tts/real_time/definitions/speech_config.py +0 -27
  77. agentle/tts/real_time/definitions/speech_result.py +0 -14
  78. agentle/tts/real_time/definitions/tts_stream_chunk.py +0 -15
  79. agentle/tts/real_time/definitions/voice_gender.py +0 -9
  80. agentle/tts/real_time/definitions/voice_info.py +0 -18
  81. agentle/tts/real_time/real_time_speech_to_text_provider.py +0 -66
  82. /agentle/{tts/real_time → agents/whatsapp/v2}/__init__.py +0 -0
  83. /agentle/{tts/real_time/definitions/__init__.py → agents/whatsapp/v2/in_memory_batch_processor_manager.py} +0 -0
  84. {agentle-0.9.4.dist-info → agentle-0.9.28.dist-info}/WHEEL +0 -0
  85. {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"""
@@ -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
- # Pattern: "key": "value" or 'key': 'value'
128
- string_pattern = r'["\']([^"\']+)["\']:\s*["\']([^"\']*)["\']?'
129
- string_matches = re.findall(string_pattern, json_str)
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