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
@@ -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 response.status == expected_status:
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,4 @@
1
+ import abc
2
+
3
+
4
+ class BatchProcessorManager(abc.ABC): ...
@@ -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
+ )
@@ -0,0 +1,9 @@
1
+ from enum import IntEnum
2
+
3
+
4
+ class MessageLimit(IntEnum):
5
+ NEWLY_CREATED = 250
6
+ SCALING_PATH = 2000
7
+ AUTOMATIC_SCALLING_1 = 10_000
8
+ AUTOMATIC_SCALLING_2 = 100_000
9
+ UNLIMITED = 1_000_000
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