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
|
@@ -8,7 +8,7 @@ from __future__ import annotations
|
|
|
8
8
|
import logging
|
|
9
9
|
import re
|
|
10
10
|
import time
|
|
11
|
-
from collections.abc import Mapping, MutableMapping
|
|
11
|
+
from collections.abc import Mapping, MutableMapping, Sequence
|
|
12
12
|
from datetime import datetime
|
|
13
13
|
from typing import Any, override
|
|
14
14
|
from urllib.parse import urljoin
|
|
@@ -307,7 +307,7 @@ class EvolutionAPIProvider(WhatsAppProvider):
|
|
|
307
307
|
method: str,
|
|
308
308
|
url: str,
|
|
309
309
|
data: Mapping[str, Any] | None = None,
|
|
310
|
-
expected_status: int = 200,
|
|
310
|
+
expected_status: int | Sequence[int] = 200,
|
|
311
311
|
) -> Mapping[str, Any]:
|
|
312
312
|
"""
|
|
313
313
|
Make HTTP request with comprehensive resilience mechanisms.
|
|
@@ -416,7 +416,7 @@ class EvolutionAPIProvider(WhatsAppProvider):
|
|
|
416
416
|
method: str,
|
|
417
417
|
url: str,
|
|
418
418
|
data: Mapping[str, Any] | None = None,
|
|
419
|
-
expected_status: int = 200,
|
|
419
|
+
expected_status: int | Sequence[int] = 200,
|
|
420
420
|
) -> Mapping[str, Any]:
|
|
421
421
|
"""
|
|
422
422
|
Make HTTP request with proper error handling and metrics.
|
|
@@ -531,7 +531,7 @@ class EvolutionAPIProvider(WhatsAppProvider):
|
|
|
531
531
|
async def _handle_response(
|
|
532
532
|
self,
|
|
533
533
|
response: aiohttp.ClientResponse,
|
|
534
|
-
expected_status: int,
|
|
534
|
+
expected_status: int | Sequence[int],
|
|
535
535
|
request_url: str,
|
|
536
536
|
request_data: Mapping[str, Any] | None,
|
|
537
537
|
start_time: float,
|
|
@@ -571,7 +571,10 @@ class EvolutionAPIProvider(WhatsAppProvider):
|
|
|
571
571
|
},
|
|
572
572
|
)
|
|
573
573
|
|
|
574
|
-
if
|
|
574
|
+
if isinstance(expected_status, int):
|
|
575
|
+
expected_status = [expected_status]
|
|
576
|
+
|
|
577
|
+
if response.status in expected_status:
|
|
575
578
|
try:
|
|
576
579
|
response_data = await response.json()
|
|
577
580
|
logger.debug(f"Response data received: {response_data}")
|
|
@@ -771,7 +774,7 @@ class EvolutionAPIProvider(WhatsAppProvider):
|
|
|
771
774
|
|
|
772
775
|
url = self._build_url(f"sendText/{self.config.instance_name}")
|
|
773
776
|
response_data = await self._make_request_with_resilience(
|
|
774
|
-
"POST", url, payload, expected_status=201
|
|
777
|
+
"POST", url, payload, expected_status=[200, 201]
|
|
775
778
|
)
|
|
776
779
|
|
|
777
780
|
message_id = response_data["key"]["id"]
|
|
@@ -885,7 +888,7 @@ class EvolutionAPIProvider(WhatsAppProvider):
|
|
|
885
888
|
|
|
886
889
|
url = self._build_url(f"{endpoint}/{self.config.instance_name}")
|
|
887
890
|
response_data = await self._make_request_with_resilience(
|
|
888
|
-
"POST", url, payload, expected_status=201
|
|
891
|
+
"POST", url, payload, expected_status=[200, 201]
|
|
889
892
|
)
|
|
890
893
|
|
|
891
894
|
message_id = response_data["key"]["id"]
|
|
@@ -947,6 +950,168 @@ class EvolutionAPIProvider(WhatsAppProvider):
|
|
|
947
950
|
|
|
948
951
|
# [Continue with remaining methods using enhanced patterns...]
|
|
949
952
|
|
|
953
|
+
async def send_audio_message(
|
|
954
|
+
self,
|
|
955
|
+
to: str,
|
|
956
|
+
audio_base64: str,
|
|
957
|
+
quoted_message_id: str | None = None,
|
|
958
|
+
) -> WhatsAppMediaMessage:
|
|
959
|
+
"""Send an audio message via Evolution API with enhanced error handling."""
|
|
960
|
+
logger.info(f"Sending audio message to {to}")
|
|
961
|
+
if quoted_message_id:
|
|
962
|
+
logger.debug(f"Audio message is quoting message ID: {quoted_message_id}")
|
|
963
|
+
|
|
964
|
+
try:
|
|
965
|
+
# CRITICAL FIX: Check if there's a stored remoteJid for this contact
|
|
966
|
+
session = await self.get_session(to)
|
|
967
|
+
remote_jid = session.context_data.get("remote_jid") if session else None
|
|
968
|
+
|
|
969
|
+
if remote_jid:
|
|
970
|
+
logger.info(
|
|
971
|
+
f"🔑 Using stored remoteJid for audio to {to}: {remote_jid}"
|
|
972
|
+
)
|
|
973
|
+
normalized_to = remote_jid
|
|
974
|
+
else:
|
|
975
|
+
normalized_to = self._normalize_phone(to)
|
|
976
|
+
logger.debug(f"Normalized phone number: {to} -> {normalized_to}")
|
|
977
|
+
|
|
978
|
+
payload: MutableMapping[str, Any] = {
|
|
979
|
+
"number": normalized_to,
|
|
980
|
+
"audio": audio_base64,
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
if quoted_message_id:
|
|
984
|
+
payload["quoted"] = {"key": {"id": quoted_message_id}}
|
|
985
|
+
|
|
986
|
+
url = self._build_url(f"sendWhatsAppAudio/{self.config.instance_name}")
|
|
987
|
+
response_data = await self._make_request_with_resilience(
|
|
988
|
+
"POST", url, payload, expected_status=[200, 201]
|
|
989
|
+
)
|
|
990
|
+
|
|
991
|
+
message_id = response_data["key"]["id"]
|
|
992
|
+
from_jid = response_data["key"]["remoteJid"]
|
|
993
|
+
|
|
994
|
+
message = WhatsAppAudioMessage(
|
|
995
|
+
id=message_id,
|
|
996
|
+
from_number=from_jid,
|
|
997
|
+
to_number=to,
|
|
998
|
+
timestamp=datetime.now(),
|
|
999
|
+
status=WhatsAppMessageStatus.SENT,
|
|
1000
|
+
media_url="", # Base64 audio doesn't have a URL
|
|
1001
|
+
media_mime_type="audio/ogg",
|
|
1002
|
+
quoted_message_id=quoted_message_id,
|
|
1003
|
+
is_voice_note=True,
|
|
1004
|
+
)
|
|
1005
|
+
|
|
1006
|
+
logger.info(
|
|
1007
|
+
f"Audio message sent successfully to {to}: {message_id}",
|
|
1008
|
+
extra={
|
|
1009
|
+
"message_id": message_id,
|
|
1010
|
+
"to_number": to,
|
|
1011
|
+
"normalized_to": normalized_to,
|
|
1012
|
+
"from_jid": from_jid,
|
|
1013
|
+
"has_quote": quoted_message_id is not None,
|
|
1014
|
+
},
|
|
1015
|
+
)
|
|
1016
|
+
return message
|
|
1017
|
+
|
|
1018
|
+
except EvolutionAPIError:
|
|
1019
|
+
logger.error(f"Evolution API error while sending audio message to {to}")
|
|
1020
|
+
raise
|
|
1021
|
+
except Exception as e:
|
|
1022
|
+
logger.error(
|
|
1023
|
+
f"Failed to send audio message to {to}: {type(e).__name__}: {e}",
|
|
1024
|
+
extra={
|
|
1025
|
+
"to_number": to,
|
|
1026
|
+
"error_type": type(e).__name__,
|
|
1027
|
+
"has_quote": quoted_message_id is not None,
|
|
1028
|
+
},
|
|
1029
|
+
)
|
|
1030
|
+
raise EvolutionAPIError(f"Failed to send audio message: {e}")
|
|
1031
|
+
|
|
1032
|
+
async def send_audio_message_by_url(
|
|
1033
|
+
self,
|
|
1034
|
+
to: str,
|
|
1035
|
+
audio_url: str,
|
|
1036
|
+
quoted_message_id: str | None = None,
|
|
1037
|
+
) -> WhatsAppMediaMessage:
|
|
1038
|
+
"""Send an audio message via URL using Evolution API."""
|
|
1039
|
+
logger.info(f"Sending audio message via URL to {to}: {audio_url}")
|
|
1040
|
+
if quoted_message_id:
|
|
1041
|
+
logger.debug(f"Audio message is quoting message ID: {quoted_message_id}")
|
|
1042
|
+
|
|
1043
|
+
try:
|
|
1044
|
+
# CRITICAL FIX: Check if there's a stored remoteJid for this contact
|
|
1045
|
+
session = await self.get_session(to)
|
|
1046
|
+
remote_jid = session.context_data.get("remote_jid") if session else None
|
|
1047
|
+
|
|
1048
|
+
if remote_jid:
|
|
1049
|
+
logger.info(
|
|
1050
|
+
f"🔑 Using stored remoteJid for audio URL to {to}: {remote_jid}"
|
|
1051
|
+
)
|
|
1052
|
+
normalized_to = remote_jid
|
|
1053
|
+
else:
|
|
1054
|
+
normalized_to = self._normalize_phone(to)
|
|
1055
|
+
logger.debug(f"Normalized phone number: {to} -> {normalized_to}")
|
|
1056
|
+
|
|
1057
|
+
payload: MutableMapping[str, Any] = {
|
|
1058
|
+
"number": normalized_to,
|
|
1059
|
+
"audioUrl": audio_url, # Use URL instead of base64
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
if quoted_message_id:
|
|
1063
|
+
payload["quoted"] = {"key": {"id": quoted_message_id}}
|
|
1064
|
+
|
|
1065
|
+
url = self._build_url(f"sendWhatsAppAudio/{self.config.instance_name}")
|
|
1066
|
+
response_data = await self._make_request_with_resilience(
|
|
1067
|
+
"POST", url, payload, expected_status=[200, 201]
|
|
1068
|
+
)
|
|
1069
|
+
|
|
1070
|
+
message_id = response_data["key"]["id"]
|
|
1071
|
+
from_jid = response_data["key"]["remoteJid"]
|
|
1072
|
+
|
|
1073
|
+
message = WhatsAppAudioMessage(
|
|
1074
|
+
id=message_id,
|
|
1075
|
+
from_number=from_jid,
|
|
1076
|
+
to_number=to,
|
|
1077
|
+
timestamp=datetime.now(),
|
|
1078
|
+
status=WhatsAppMessageStatus.SENT,
|
|
1079
|
+
media_url=audio_url, # Store the URL
|
|
1080
|
+
media_mime_type="audio/ogg",
|
|
1081
|
+
quoted_message_id=quoted_message_id,
|
|
1082
|
+
is_voice_note=True,
|
|
1083
|
+
)
|
|
1084
|
+
|
|
1085
|
+
logger.info(
|
|
1086
|
+
f"Audio message sent successfully via URL to {to}: {message_id}",
|
|
1087
|
+
extra={
|
|
1088
|
+
"message_id": message_id,
|
|
1089
|
+
"to_number": to,
|
|
1090
|
+
"normalized_to": normalized_to,
|
|
1091
|
+
"from_jid": from_jid,
|
|
1092
|
+
"audio_url": audio_url,
|
|
1093
|
+
"has_quote": quoted_message_id is not None,
|
|
1094
|
+
},
|
|
1095
|
+
)
|
|
1096
|
+
return message
|
|
1097
|
+
|
|
1098
|
+
except EvolutionAPIError:
|
|
1099
|
+
logger.error(
|
|
1100
|
+
f"Evolution API error while sending audio message via URL to {to}"
|
|
1101
|
+
)
|
|
1102
|
+
raise
|
|
1103
|
+
except Exception as e:
|
|
1104
|
+
logger.error(
|
|
1105
|
+
f"Failed to send audio message via URL to {to}: {type(e).__name__}: {e}",
|
|
1106
|
+
extra={
|
|
1107
|
+
"to_number": to,
|
|
1108
|
+
"audio_url": audio_url,
|
|
1109
|
+
"error_type": type(e).__name__,
|
|
1110
|
+
"has_quote": quoted_message_id is not None,
|
|
1111
|
+
},
|
|
1112
|
+
)
|
|
1113
|
+
raise EvolutionAPIError(f"Failed to send audio message via URL: {e}")
|
|
1114
|
+
|
|
950
1115
|
async def send_typing_indicator(self, to: str, duration: int = 3) -> None:
|
|
951
1116
|
"""Send typing indicator via Evolution API."""
|
|
952
1117
|
logger.debug(f"Sending typing indicator to {to} for {duration}s")
|
|
@@ -981,7 +1146,7 @@ class EvolutionAPIProvider(WhatsAppProvider):
|
|
|
981
1146
|
use_message_prefix=False,
|
|
982
1147
|
)
|
|
983
1148
|
await self._make_request_with_resilience(
|
|
984
|
-
"POST", url, payload, expected_status=201
|
|
1149
|
+
"POST", url, payload, expected_status=[200, 201]
|
|
985
1150
|
)
|
|
986
1151
|
|
|
987
1152
|
logger.debug(
|
|
@@ -1009,6 +1174,68 @@ class EvolutionAPIProvider(WhatsAppProvider):
|
|
|
1009
1174
|
},
|
|
1010
1175
|
)
|
|
1011
1176
|
|
|
1177
|
+
async def send_recording_indicator(self, to: str, duration: int = 3) -> None:
|
|
1178
|
+
"""Send recording indicator via Evolution API."""
|
|
1179
|
+
logger.debug(f"Sending recording indicator to {to} for {duration}s")
|
|
1180
|
+
|
|
1181
|
+
try:
|
|
1182
|
+
# CRITICAL FIX: Check if there's a stored remoteJid for this contact
|
|
1183
|
+
# This is essential for @lid numbers (Brazilian WhatsApp contacts)
|
|
1184
|
+
session = await self.get_session(to)
|
|
1185
|
+
remote_jid = session.context_data.get("remote_jid") if session else None
|
|
1186
|
+
|
|
1187
|
+
if remote_jid:
|
|
1188
|
+
logger.debug(
|
|
1189
|
+
f"🔑 Using stored remoteJid for recording indicator to {to}: {remote_jid}"
|
|
1190
|
+
)
|
|
1191
|
+
normalized_to = remote_jid
|
|
1192
|
+
else:
|
|
1193
|
+
normalized_to = self._normalize_phone(to)
|
|
1194
|
+
|
|
1195
|
+
payload = {
|
|
1196
|
+
"number": normalized_to,
|
|
1197
|
+
"presence": "recording",
|
|
1198
|
+
"delay": duration * 1000,
|
|
1199
|
+
"options": {
|
|
1200
|
+
"delay": duration * 1000,
|
|
1201
|
+
"presence": "recording",
|
|
1202
|
+
"number": normalized_to,
|
|
1203
|
+
}, # Evolution API expects milliseconds
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
url = self._build_url(
|
|
1207
|
+
f"chat/sendPresence/{self.config.instance_name}",
|
|
1208
|
+
use_message_prefix=False,
|
|
1209
|
+
)
|
|
1210
|
+
await self._make_request_with_resilience(
|
|
1211
|
+
"POST", url, payload, expected_status=[200, 201]
|
|
1212
|
+
)
|
|
1213
|
+
|
|
1214
|
+
logger.debug(
|
|
1215
|
+
f"Recording indicator sent successfully to {to} for {duration}s",
|
|
1216
|
+
extra={
|
|
1217
|
+
"to_number": to,
|
|
1218
|
+
"normalized_to": normalized_to,
|
|
1219
|
+
"duration_seconds": duration,
|
|
1220
|
+
},
|
|
1221
|
+
)
|
|
1222
|
+
|
|
1223
|
+
except EvolutionAPIError as e:
|
|
1224
|
+
# Recording indicator failures are non-critical
|
|
1225
|
+
logger.warning(
|
|
1226
|
+
f"Failed to send recording indicator to {to}: {e}",
|
|
1227
|
+
extra={"to_number": to, "duration_seconds": duration, "error": str(e)},
|
|
1228
|
+
)
|
|
1229
|
+
except Exception as e:
|
|
1230
|
+
logger.warning(
|
|
1231
|
+
f"Failed to send recording indicator to {to}: {type(e).__name__}: {e}",
|
|
1232
|
+
extra={
|
|
1233
|
+
"to_number": to,
|
|
1234
|
+
"duration_seconds": duration,
|
|
1235
|
+
"error_type": type(e).__name__,
|
|
1236
|
+
},
|
|
1237
|
+
)
|
|
1238
|
+
|
|
1012
1239
|
async def mark_message_as_read(self, message_id: str) -> None:
|
|
1013
1240
|
"""Mark a message as read via Evolution API."""
|
|
1014
1241
|
logger.debug(f"Marking message as read: {message_id}")
|
|
@@ -1032,7 +1259,7 @@ class EvolutionAPIProvider(WhatsAppProvider):
|
|
|
1032
1259
|
use_message_prefix=False,
|
|
1033
1260
|
)
|
|
1034
1261
|
await self._make_request_with_resilience(
|
|
1035
|
-
"POST", url, payload, expected_status=201
|
|
1262
|
+
"POST", url, payload, expected_status=[200, 201]
|
|
1036
1263
|
)
|
|
1037
1264
|
|
|
1038
1265
|
logger.debug(
|
|
@@ -1282,7 +1509,7 @@ class EvolutionAPIProvider(WhatsAppProvider):
|
|
|
1282
1509
|
payload = {"message": {"key": {"id": media_id}}}
|
|
1283
1510
|
|
|
1284
1511
|
response_data = await self._make_request_with_resilience(
|
|
1285
|
-
"POST", url, payload, expected_status=201
|
|
1512
|
+
"POST", url, payload, expected_status=[200, 201]
|
|
1286
1513
|
)
|
|
1287
1514
|
|
|
1288
1515
|
if "base64" not in response_data:
|
|
@@ -837,3 +837,129 @@ class MetaWhatsAppProvider(WhatsAppProvider):
|
|
|
837
837
|
base_stats["session_stats"] = session_stats
|
|
838
838
|
|
|
839
839
|
return base_stats
|
|
840
|
+
|
|
841
|
+
async def send_audio_message(
|
|
842
|
+
self,
|
|
843
|
+
to: str,
|
|
844
|
+
audio_base64: str,
|
|
845
|
+
quoted_message_id: str | None = None,
|
|
846
|
+
) -> WhatsAppMediaMessage:
|
|
847
|
+
"""Send an audio message via Meta WhatsApp Business API."""
|
|
848
|
+
logger.info(f"Sending audio message to {to}")
|
|
849
|
+
|
|
850
|
+
try:
|
|
851
|
+
# Upload audio to Meta first
|
|
852
|
+
media_id = await self._upload_audio_base64(audio_base64)
|
|
853
|
+
|
|
854
|
+
# Send audio message
|
|
855
|
+
payload = {
|
|
856
|
+
"messaging_product": "whatsapp",
|
|
857
|
+
"to": self._normalize_phone(to),
|
|
858
|
+
"type": "audio",
|
|
859
|
+
"audio": {"id": media_id},
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
if quoted_message_id:
|
|
863
|
+
payload["context"] = {"message_id": quoted_message_id}
|
|
864
|
+
|
|
865
|
+
url = self._build_url(f"{self.config.phone_number_id}/messages")
|
|
866
|
+
response_data = await self._make_request("POST", url, payload)
|
|
867
|
+
|
|
868
|
+
message_id = response_data["messages"][0]["id"]
|
|
869
|
+
|
|
870
|
+
return WhatsAppAudioMessage(
|
|
871
|
+
id=message_id,
|
|
872
|
+
from_number=self.config.phone_number_id,
|
|
873
|
+
to_number=to,
|
|
874
|
+
timestamp=datetime.now(),
|
|
875
|
+
status=WhatsAppMessageStatus.SENT,
|
|
876
|
+
media_url=media_id,
|
|
877
|
+
media_mime_type="audio/ogg",
|
|
878
|
+
quoted_message_id=quoted_message_id,
|
|
879
|
+
is_voice_note=True,
|
|
880
|
+
)
|
|
881
|
+
|
|
882
|
+
except Exception as e:
|
|
883
|
+
logger.error(f"Failed to send audio message: {e}")
|
|
884
|
+
raise MetaWhatsAppError(f"Failed to send audio message: {e}")
|
|
885
|
+
|
|
886
|
+
async def send_audio_message_by_url(
|
|
887
|
+
self,
|
|
888
|
+
to: str,
|
|
889
|
+
audio_url: str,
|
|
890
|
+
quoted_message_id: str | None = None,
|
|
891
|
+
) -> WhatsAppMediaMessage:
|
|
892
|
+
"""Send an audio message via URL using Meta WhatsApp Business API."""
|
|
893
|
+
logger.info(f"Sending audio message via URL to {to}: {audio_url}")
|
|
894
|
+
|
|
895
|
+
try:
|
|
896
|
+
# Upload audio from URL to Meta
|
|
897
|
+
media_id = await self._upload_media(audio_url, "audio")
|
|
898
|
+
|
|
899
|
+
# Send audio message
|
|
900
|
+
payload = {
|
|
901
|
+
"messaging_product": "whatsapp",
|
|
902
|
+
"to": self._normalize_phone(to),
|
|
903
|
+
"type": "audio",
|
|
904
|
+
"audio": {"id": media_id},
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
if quoted_message_id:
|
|
908
|
+
payload["context"] = {"message_id": quoted_message_id}
|
|
909
|
+
|
|
910
|
+
url = self._build_url(f"{self.config.phone_number_id}/messages")
|
|
911
|
+
response_data = await self._make_request("POST", url, payload)
|
|
912
|
+
|
|
913
|
+
message_id = response_data["messages"][0]["id"]
|
|
914
|
+
|
|
915
|
+
return WhatsAppAudioMessage(
|
|
916
|
+
id=message_id,
|
|
917
|
+
from_number=self.config.phone_number_id,
|
|
918
|
+
to_number=to,
|
|
919
|
+
timestamp=datetime.now(),
|
|
920
|
+
status=WhatsAppMessageStatus.SENT,
|
|
921
|
+
media_url=audio_url,
|
|
922
|
+
media_mime_type="audio/ogg",
|
|
923
|
+
quoted_message_id=quoted_message_id,
|
|
924
|
+
is_voice_note=True,
|
|
925
|
+
)
|
|
926
|
+
|
|
927
|
+
except Exception as e:
|
|
928
|
+
logger.error(f"Failed to send audio message via URL: {e}")
|
|
929
|
+
raise MetaWhatsAppError(f"Failed to send audio message via URL: {e}")
|
|
930
|
+
|
|
931
|
+
async def _upload_audio_base64(self, audio_base64: str) -> str:
|
|
932
|
+
"""Upload base64 audio to Meta and return media ID."""
|
|
933
|
+
try:
|
|
934
|
+
import base64
|
|
935
|
+
|
|
936
|
+
# Decode base64 to bytes
|
|
937
|
+
audio_data = base64.b64decode(audio_base64)
|
|
938
|
+
|
|
939
|
+
# Upload to Meta
|
|
940
|
+
upload_url = self._build_url(f"{self.config.phone_number_id}/media")
|
|
941
|
+
|
|
942
|
+
form_data = aiohttp.FormData()
|
|
943
|
+
form_data.add_field("messaging_product", "whatsapp")
|
|
944
|
+
form_data.add_field("type", "audio")
|
|
945
|
+
form_data.add_field(
|
|
946
|
+
"file",
|
|
947
|
+
audio_data,
|
|
948
|
+
filename="audio.ogg",
|
|
949
|
+
content_type="audio/ogg",
|
|
950
|
+
)
|
|
951
|
+
|
|
952
|
+
# Create a separate session for file upload
|
|
953
|
+
headers = {"Authorization": f"Bearer {self.config.access_token}"}
|
|
954
|
+
timeout = aiohttp.ClientTimeout(total=self.config.timeout)
|
|
955
|
+
|
|
956
|
+
async with aiohttp.ClientSession(
|
|
957
|
+
headers=headers, timeout=timeout
|
|
958
|
+
) as upload_session:
|
|
959
|
+
async with upload_session.post(upload_url, data=form_data) as response:
|
|
960
|
+
response_data = await self._handle_response(response, 200)
|
|
961
|
+
return response_data["id"]
|
|
962
|
+
|
|
963
|
+
except Exception as e:
|
|
964
|
+
logger.error(f"Failed to upload audio base64: {e}")
|
|
965
|
+
raise MetaWhatsAppError(f"Failed to upload audio base64: {e}")
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from rsb.models.base_model import BaseModel
|
|
4
|
+
from rsb.models.field import Field
|
|
5
|
+
|
|
6
|
+
from agentle.agents.whatsapp.v2.message_limit import MessageLimit
|
|
7
|
+
from agentle.tts.speech_config import SpeechConfig
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BotConfig(BaseModel):
|
|
11
|
+
"""Configuration for WhatsApp bot behavior with simplified constructors and better organization.
|
|
12
|
+
|
|
13
|
+
This configuration class provides comprehensive control over WhatsApp bot behavior including:
|
|
14
|
+
- Core bot behavior (typing indicators, message reading, quoting)
|
|
15
|
+
- Message batching for handling rapid message sequences
|
|
16
|
+
- Spam protection and rate limiting
|
|
17
|
+
- Human-like delays to simulate realistic human behavior patterns
|
|
18
|
+
- Text-to-speech integration
|
|
19
|
+
- Error handling and retry logic
|
|
20
|
+
- Debug and monitoring settings
|
|
21
|
+
|
|
22
|
+
Human-Like Delays Feature:
|
|
23
|
+
The human-like delays feature simulates realistic human behavior patterns by introducing
|
|
24
|
+
configurable delays at three critical points in message processing:
|
|
25
|
+
|
|
26
|
+
1. Read Delay: Time between receiving a message and marking it as read
|
|
27
|
+
- Simulates the time a human takes to read and comprehend a message
|
|
28
|
+
- Calculated based on message length using realistic reading speeds
|
|
29
|
+
|
|
30
|
+
2. Typing Delay: Time between generating a response and sending it
|
|
31
|
+
- Simulates the time a human takes to compose and type a response
|
|
32
|
+
- Calculated based on response length using realistic typing speeds
|
|
33
|
+
|
|
34
|
+
3. Send Delay: Brief final delay before message transmission
|
|
35
|
+
- Simulates the final review time before a human sends a message
|
|
36
|
+
- Random delay within configured bounds
|
|
37
|
+
|
|
38
|
+
These delays help prevent platform detection and account restrictions while
|
|
39
|
+
maintaining natural interaction timing. All delays support jitter (random variation)
|
|
40
|
+
to prevent detectable patterns.
|
|
41
|
+
|
|
42
|
+
Configuration Presets:
|
|
43
|
+
Use the class methods to create pre-configured instances optimized for specific use cases:
|
|
44
|
+
- development(): Fast iteration with delays disabled
|
|
45
|
+
- production(): Balanced configuration with delays enabled
|
|
46
|
+
- high_volume(): Optimized for throughput with balanced delays
|
|
47
|
+
- customer_service(): Professional timing with thoughtful delays
|
|
48
|
+
- minimal(): Bare minimum configuration with delays disabled
|
|
49
|
+
|
|
50
|
+
Examples:
|
|
51
|
+
>>> # Create a production configuration with default delay settings
|
|
52
|
+
>>> config = BotConfig.production()
|
|
53
|
+
|
|
54
|
+
>>> # Create a custom configuration with specific delay bounds
|
|
55
|
+
>>> config = BotConfig(
|
|
56
|
+
... enable_human_delays=True,
|
|
57
|
+
... min_read_delay_seconds=3.0,
|
|
58
|
+
... max_read_delay_seconds=20.0,
|
|
59
|
+
... min_typing_delay_seconds=5.0,
|
|
60
|
+
... max_typing_delay_seconds=60.0
|
|
61
|
+
... )
|
|
62
|
+
|
|
63
|
+
>>> # Override delay settings on an existing configuration
|
|
64
|
+
>>> prod_config = BotConfig.production()
|
|
65
|
+
>>> custom_config = prod_config.with_overrides(
|
|
66
|
+
... min_read_delay_seconds=5.0,
|
|
67
|
+
... max_typing_delay_seconds=90.0
|
|
68
|
+
... )
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
quote_messages: bool = Field(
|
|
72
|
+
default=False, description="Whether to quote user messages in replies"
|
|
73
|
+
)
|
|
74
|
+
session_timeout_minutes: int = Field(
|
|
75
|
+
default=30, description="Minutes of inactivity before session reset"
|
|
76
|
+
)
|
|
77
|
+
max_message_length: MessageLimit = Field(
|
|
78
|
+
default=MessageLimit.NEWLY_CREATED,
|
|
79
|
+
description="Maximum message length (WhatsApp limit)",
|
|
80
|
+
)
|
|
81
|
+
max_split_messages: int = Field(
|
|
82
|
+
default=5,
|
|
83
|
+
description="Maximum number of split messages to send (remaining will be grouped)",
|
|
84
|
+
)
|
|
85
|
+
error_message: str = Field(
|
|
86
|
+
default="Sorry, I encountered an error processing your message. Please try again.",
|
|
87
|
+
description="Default error message",
|
|
88
|
+
)
|
|
89
|
+
welcome_message: str | None = Field(
|
|
90
|
+
default=None, description="Message to send on first interaction"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# === Message Batching (Simplified) ===
|
|
94
|
+
enable_message_batching: bool = Field(
|
|
95
|
+
default=True, description="Enable message batching to prevent spam"
|
|
96
|
+
)
|
|
97
|
+
batch_delay_seconds: float = Field(
|
|
98
|
+
default=15.0,
|
|
99
|
+
description="Time to wait for additional messages before processing batch",
|
|
100
|
+
)
|
|
101
|
+
max_batch_size: int = Field(
|
|
102
|
+
default=10, description="Maximum number of messages to batch together"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# === Spam Protection ===
|
|
106
|
+
spam_protection_enabled: bool = Field(
|
|
107
|
+
default=True, description="Enable spam protection mechanisms"
|
|
108
|
+
)
|
|
109
|
+
min_message_interval_seconds: float = Field(
|
|
110
|
+
default=1,
|
|
111
|
+
description="Minimum interval between processing messages from same user",
|
|
112
|
+
)
|
|
113
|
+
max_messages_per_minute: int = Field(
|
|
114
|
+
default=20,
|
|
115
|
+
description="Maximum messages per minute per user before rate limiting",
|
|
116
|
+
)
|
|
117
|
+
rate_limit_cooldown_seconds: int = Field(
|
|
118
|
+
default=60, description="Cooldown period after rate limit is triggered"
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# === Text-to-Speech (TTS) ===
|
|
122
|
+
speech_play_chance: float = Field(
|
|
123
|
+
default=0.0,
|
|
124
|
+
ge=0.0,
|
|
125
|
+
le=1.0,
|
|
126
|
+
description="Probability (0.0-1.0) of sending audio response instead of text",
|
|
127
|
+
)
|
|
128
|
+
speech_config: SpeechConfig | None = Field(
|
|
129
|
+
default=None,
|
|
130
|
+
description="Optional SpeechConfig for TTS provider customization",
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# === Error Handling ===
|
|
134
|
+
retry_failed_messages: bool = Field(
|
|
135
|
+
default=True, description="Retry processing failed messages"
|
|
136
|
+
)
|
|
137
|
+
max_retry_attempts: int = Field(
|
|
138
|
+
default=3, description="Maximum number of retry attempts for failed messages"
|
|
139
|
+
)
|
|
140
|
+
retry_delay_seconds: float = Field(
|
|
141
|
+
default=1.0, description="Delay between retry attempts"
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
# === Human-Like Delays ===
|
|
145
|
+
enable_human_delays: bool = Field(
|
|
146
|
+
default=False,
|
|
147
|
+
description="Enable human-like delays for message processing to simulate realistic human behavior patterns",
|
|
148
|
+
)
|
|
149
|
+
min_read_delay_seconds: float = Field(
|
|
150
|
+
default=2.0,
|
|
151
|
+
ge=0.0,
|
|
152
|
+
description="Minimum delay before marking message as read (seconds). Simulates time to read incoming messages.",
|
|
153
|
+
)
|
|
154
|
+
max_read_delay_seconds: float = Field(
|
|
155
|
+
default=15.0,
|
|
156
|
+
ge=0.0,
|
|
157
|
+
description="Maximum delay before marking message as read (seconds). Prevents excessively long read delays.",
|
|
158
|
+
)
|
|
159
|
+
min_typing_delay_seconds: float = Field(
|
|
160
|
+
default=3.0,
|
|
161
|
+
ge=0.0,
|
|
162
|
+
description="Minimum delay before sending response (seconds). Simulates time to compose a response.",
|
|
163
|
+
)
|
|
164
|
+
max_typing_delay_seconds: float = Field(
|
|
165
|
+
default=45.0,
|
|
166
|
+
ge=0.0,
|
|
167
|
+
description="Maximum delay before sending response (seconds). Prevents excessively long typing delays.",
|
|
168
|
+
)
|
|
169
|
+
min_send_delay_seconds: float = Field(
|
|
170
|
+
default=0.5,
|
|
171
|
+
ge=0.0,
|
|
172
|
+
description="Minimum delay before message transmission (seconds). Simulates final message review time.",
|
|
173
|
+
)
|
|
174
|
+
max_send_delay_seconds: float = Field(
|
|
175
|
+
default=4.0,
|
|
176
|
+
ge=0.0,
|
|
177
|
+
description="Maximum delay before message transmission (seconds). Prevents excessively long send delays.",
|
|
178
|
+
)
|
|
179
|
+
enable_delay_jitter: bool = Field(
|
|
180
|
+
default=True,
|
|
181
|
+
description="Enable random variation (±20%) in delay calculations to prevent detectable patterns and simulate natural human behavior variability",
|
|
182
|
+
)
|
|
183
|
+
batch_read_compression_factor: float = Field(
|
|
184
|
+
default=0.7,
|
|
185
|
+
ge=0.1,
|
|
186
|
+
le=1.0,
|
|
187
|
+
description="Compression factor (0.1-1.0) applied to batch read delays. Lower values simulate faster batch reading (e.g., 0.7 = 30% faster than reading individually)",
|
|
188
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from pydantic import ConfigDict
|
|
5
|
+
from rsb.models.base_model import BaseModel
|
|
6
|
+
|
|
7
|
+
from agentle.agents.whatsapp.v2.batch_processor_manager import BatchProcessorManager
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class WhatsAppBot(BaseModel):
|
|
11
|
+
webhook_handlers: list[Callable[..., Any]]
|
|
12
|
+
batch_processor_manager: BatchProcessorManager
|
|
13
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
File without changes
|
|
File without changes
|