agentle 0.9.24__py3-none-any.whl → 0.9.26__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/apis/api.py +25 -7
- agentle/agents/apis/endpoint.py +14 -8
- agentle/agents/whatsapp/human_delay_calculator.py +462 -0
- agentle/agents/whatsapp/models/audio_message.py +6 -4
- agentle/agents/whatsapp/models/whatsapp_bot_config.py +352 -21
- agentle/agents/whatsapp/v2/__init__.py +0 -0
- agentle/agents/whatsapp/v2/batch_processor_manager.py +4 -0
- agentle/agents/whatsapp/v2/bot_config.py +188 -0
- agentle/agents/whatsapp/v2/in_memory_batch_processor_manager.py +0 -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 +559 -12
- agentle/web/extractor.py +282 -165
- {agentle-0.9.24.dist-info → agentle-0.9.26.dist-info}/METADATA +1 -1
- {agentle-0.9.24.dist-info → agentle-0.9.26.dist-info}/RECORD +20 -10
- {agentle-0.9.24.dist-info → agentle-0.9.26.dist-info}/WHEEL +0 -0
- {agentle-0.9.24.dist-info → agentle-0.9.26.dist-info}/licenses/LICENSE +0 -0
agentle/agents/apis/api.py
CHANGED
|
@@ -14,6 +14,7 @@ Provides advanced features for managing collections of related endpoints with:
|
|
|
14
14
|
from __future__ import annotations
|
|
15
15
|
|
|
16
16
|
import logging
|
|
17
|
+
import re
|
|
17
18
|
from collections.abc import (
|
|
18
19
|
Coroutine,
|
|
19
20
|
Mapping,
|
|
@@ -513,13 +514,30 @@ class API(BaseModel):
|
|
|
513
514
|
continue
|
|
514
515
|
|
|
515
516
|
# Create endpoint
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
517
|
+
# Generate a valid function name from the path
|
|
518
|
+
if operation_id:
|
|
519
|
+
endpoint_name = operation_id
|
|
520
|
+
else:
|
|
521
|
+
# Clean the path to create a valid function name
|
|
522
|
+
# Remove leading/trailing slashes and replace special chars
|
|
523
|
+
clean_path = (
|
|
524
|
+
path.strip("/")
|
|
525
|
+
.replace("/", "_")
|
|
526
|
+
.replace("{", "")
|
|
527
|
+
.replace("}", "")
|
|
528
|
+
.replace("-", "_")
|
|
529
|
+
)
|
|
530
|
+
# Remove any consecutive underscores
|
|
531
|
+
clean_path = re.sub(r"_+", "_", clean_path)
|
|
532
|
+
# Ensure it doesn't start with a number
|
|
533
|
+
if clean_path and clean_path[0].isdigit():
|
|
534
|
+
clean_path = f"n{clean_path}"
|
|
535
|
+
# If empty after cleaning, use a generic name
|
|
536
|
+
if not clean_path:
|
|
537
|
+
clean_path = "root"
|
|
538
|
+
endpoint_name = f"{method.lower()}_{clean_path}"
|
|
539
|
+
|
|
540
|
+
endpoint_name = cast(str, endpoint_name)
|
|
523
541
|
|
|
524
542
|
endpoint_description: str = cast(
|
|
525
543
|
str,
|
agentle/agents/apis/endpoint.py
CHANGED
|
@@ -359,7 +359,7 @@ class Endpoint(BaseModel):
|
|
|
359
359
|
await self._auth_handler.refresh_if_needed()
|
|
360
360
|
await self._auth_handler.apply_auth(None, url, headers, query_params) # type: ignore
|
|
361
361
|
|
|
362
|
-
# Prepare connector
|
|
362
|
+
# Prepare connector kwargs (will be used to create fresh connector for each attempt)
|
|
363
363
|
connector_kwargs: dict[str, Any] = {
|
|
364
364
|
"limit": 10,
|
|
365
365
|
"limit_per_host": 5,
|
|
@@ -369,8 +369,6 @@ class Endpoint(BaseModel):
|
|
|
369
369
|
if not self.request_config.verify_ssl:
|
|
370
370
|
connector_kwargs["ssl"] = False
|
|
371
371
|
|
|
372
|
-
connector = aiohttp.TCPConnector(**connector_kwargs)
|
|
373
|
-
|
|
374
372
|
# Prepare timeout
|
|
375
373
|
timeout = aiohttp.ClientTimeout(
|
|
376
374
|
total=self.request_config.timeout,
|
|
@@ -381,9 +379,11 @@ class Endpoint(BaseModel):
|
|
|
381
379
|
# Define the request function for circuit breaker
|
|
382
380
|
async def make_single_request() -> Any:
|
|
383
381
|
"""Make a single request attempt."""
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
382
|
+
# Create a fresh connector for each request attempt to avoid "Session is closed" errors on retries
|
|
383
|
+
connector = aiohttp.TCPConnector(**connector_kwargs)
|
|
384
|
+
session = None
|
|
385
|
+
try:
|
|
386
|
+
session = aiohttp.ClientSession(connector=connector, timeout=timeout)
|
|
387
387
|
# Prepare request kwargs
|
|
388
388
|
request_kwargs: dict[str, Any] = {
|
|
389
389
|
"headers": headers,
|
|
@@ -486,6 +486,12 @@ class Endpoint(BaseModel):
|
|
|
486
486
|
await self._response_cache.set(url, kwargs, result)
|
|
487
487
|
|
|
488
488
|
return result
|
|
489
|
+
finally:
|
|
490
|
+
# Always close the session to prevent "Session is closed" errors on retries
|
|
491
|
+
if session is not None:
|
|
492
|
+
await session.close()
|
|
493
|
+
# Give the connector time to close properly
|
|
494
|
+
await asyncio.sleep(0.01)
|
|
489
495
|
|
|
490
496
|
# Execute with retries
|
|
491
497
|
last_exception = None
|
|
@@ -569,11 +575,11 @@ class Endpoint(BaseModel):
|
|
|
569
575
|
|
|
570
576
|
if hasattr(param, "enum") and param.enum:
|
|
571
577
|
param_info["enum"] = list(param.enum)
|
|
572
|
-
|
|
578
|
+
|
|
573
579
|
# Add constraints for number/primitive types
|
|
574
580
|
if hasattr(param, "parameter_schema") and param.parameter_schema:
|
|
575
581
|
from agentle.agents.apis.primitive_schema import PrimitiveSchema
|
|
576
|
-
|
|
582
|
+
|
|
577
583
|
schema = param.parameter_schema
|
|
578
584
|
# Only PrimitiveSchema has minimum, maximum, format
|
|
579
585
|
if isinstance(schema, PrimitiveSchema):
|
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
"""Human-like delay calculator for WhatsApp bot message processing.
|
|
2
|
+
|
|
3
|
+
This module provides realistic delay calculations that simulate human behavior patterns
|
|
4
|
+
for reading messages, typing responses, and sending messages. The delays help prevent
|
|
5
|
+
platform detection and account restrictions while maintaining natural interaction timing.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
import random
|
|
12
|
+
from typing import TYPE_CHECKING
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from agentle.agents.whatsapp.models.whatsapp_bot_config import WhatsAppBotConfig
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class HumanDelayCalculator:
|
|
21
|
+
"""Calculate human-like delays for WhatsApp message processing.
|
|
22
|
+
|
|
23
|
+
This calculator simulates realistic human behavior patterns by computing delays
|
|
24
|
+
based on content length and configured parameters. It supports three types of delays:
|
|
25
|
+
|
|
26
|
+
1. Read delays: Time to read and comprehend incoming messages
|
|
27
|
+
2. Typing delays: Time to compose and type responses
|
|
28
|
+
3. Send delays: Brief final review time before sending
|
|
29
|
+
|
|
30
|
+
The calculator applies jitter (random variation) to prevent detectable patterns
|
|
31
|
+
and clamps all delays to configured minimum and maximum bounds.
|
|
32
|
+
|
|
33
|
+
Attributes:
|
|
34
|
+
config: WhatsApp bot configuration containing delay bounds and behavior settings
|
|
35
|
+
reading_speed_wpm: Reading speed in words per minute (default: 200)
|
|
36
|
+
typing_speed_wpm: Typing speed in words per minute (default: 40)
|
|
37
|
+
jitter_factor: Random variation factor for delays (default: 0.20 for ±20%)
|
|
38
|
+
|
|
39
|
+
Example:
|
|
40
|
+
>>> config = WhatsAppBotConfig.production()
|
|
41
|
+
>>> calculator = HumanDelayCalculator(config)
|
|
42
|
+
>>> delay = calculator.calculate_read_delay("Hello, how are you?")
|
|
43
|
+
>>> print(f"Read delay: {delay:.2f} seconds")
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
# Constants for human behavior simulation
|
|
47
|
+
READING_SPEED_WPM = 200 # Average reading speed in words per minute
|
|
48
|
+
TYPING_SPEED_WPM = 40 # Average typing speed in words per minute
|
|
49
|
+
JITTER_FACTOR = 0.20 # ±20% random variation
|
|
50
|
+
|
|
51
|
+
def __init__(self, config: WhatsAppBotConfig) -> None:
|
|
52
|
+
"""Initialize the delay calculator with configuration.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
config: WhatsApp bot configuration containing delay bounds and settings
|
|
56
|
+
"""
|
|
57
|
+
self.config = config
|
|
58
|
+
self.reading_speed_wpm = self.READING_SPEED_WPM
|
|
59
|
+
self.typing_speed_wpm = self.TYPING_SPEED_WPM
|
|
60
|
+
self.jitter_factor = self.JITTER_FACTOR
|
|
61
|
+
|
|
62
|
+
# Log initialization with configuration details
|
|
63
|
+
logger.info(
|
|
64
|
+
f"[DELAY_CALC_INIT] HumanDelayCalculator initialized with parameters: "
|
|
65
|
+
+ f"reading_speed={self.reading_speed_wpm}wpm, "
|
|
66
|
+
+ f"typing_speed={self.typing_speed_wpm}wpm, "
|
|
67
|
+
+ f"jitter_factor={self.jitter_factor:.2f}, "
|
|
68
|
+
+ f"jitter_enabled={config.enable_delay_jitter}"
|
|
69
|
+
)
|
|
70
|
+
logger.debug(
|
|
71
|
+
f"[DELAY_CALC_INIT] Read delay bounds: "
|
|
72
|
+
+ f"[{config.min_read_delay_seconds:.2f}s - {config.max_read_delay_seconds:.2f}s]"
|
|
73
|
+
)
|
|
74
|
+
logger.debug(
|
|
75
|
+
f"[DELAY_CALC_INIT] Typing delay bounds: "
|
|
76
|
+
+ f"[{config.min_typing_delay_seconds:.2f}s - {config.max_typing_delay_seconds:.2f}s]"
|
|
77
|
+
)
|
|
78
|
+
logger.debug(
|
|
79
|
+
f"[DELAY_CALC_INIT] Send delay bounds: "
|
|
80
|
+
+ f"[{config.min_send_delay_seconds:.2f}s - {config.max_send_delay_seconds:.2f}s]"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
def calculate_read_delay(self, message_text: str) -> float:
|
|
84
|
+
"""Calculate delay for reading a message.
|
|
85
|
+
|
|
86
|
+
This method simulates the time a human would take to read and comprehend
|
|
87
|
+
a message. The calculation includes:
|
|
88
|
+
- Base reading time based on character count and reading speed
|
|
89
|
+
- Context switching time (1.5-3.5 seconds)
|
|
90
|
+
- Comprehension time (0.5-2.0 seconds)
|
|
91
|
+
- Random jitter (±20% variation)
|
|
92
|
+
- Clamping to configured min/max bounds
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
message_text: The message content to read
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Delay in seconds (float), clamped to configured bounds
|
|
99
|
+
|
|
100
|
+
Example:
|
|
101
|
+
>>> calculator = HumanDelayCalculator(config)
|
|
102
|
+
>>> delay = calculator.calculate_read_delay("Hello, how are you today?")
|
|
103
|
+
>>> print(f"Read delay: {delay:.2f}s")
|
|
104
|
+
"""
|
|
105
|
+
# Calculate base delay from character count
|
|
106
|
+
char_count = len(message_text)
|
|
107
|
+
word_count = self._estimate_word_count(char_count)
|
|
108
|
+
|
|
109
|
+
logger.debug(
|
|
110
|
+
f"[DELAY_CALC] 📖 Calculating read delay: chars={char_count}, words={word_count:.1f}"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Base reading time: words / (words per minute / 60 seconds)
|
|
114
|
+
words_per_second = self.reading_speed_wpm / 60.0
|
|
115
|
+
base_delay = word_count / words_per_second
|
|
116
|
+
|
|
117
|
+
# Add context switching time (random 1.5-3.5 seconds)
|
|
118
|
+
context_switch_time = random.uniform(1.5, 3.5)
|
|
119
|
+
|
|
120
|
+
# Add comprehension time (random 0.5-2.0 seconds)
|
|
121
|
+
comprehension_time = random.uniform(0.5, 2.0)
|
|
122
|
+
|
|
123
|
+
# Combine all components
|
|
124
|
+
total_delay = base_delay + context_switch_time + comprehension_time
|
|
125
|
+
|
|
126
|
+
logger.debug(
|
|
127
|
+
f"[DELAY_CALC] 📖 Read delay components: base={base_delay:.2f}s, "
|
|
128
|
+
+ f"context_switch={context_switch_time:.2f}s, comprehension={comprehension_time:.2f}s, "
|
|
129
|
+
+ f"total_before_jitter={total_delay:.2f}s"
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# Apply jitter (±20% random variation)
|
|
133
|
+
delay_before_jitter = total_delay
|
|
134
|
+
total_delay = self._apply_jitter(total_delay)
|
|
135
|
+
|
|
136
|
+
if self.config.enable_delay_jitter:
|
|
137
|
+
logger.debug(
|
|
138
|
+
f"[DELAY_CALC] 📖 Applied jitter: before={delay_before_jitter:.2f}s, "
|
|
139
|
+
+ f"after={total_delay:.2f}s"
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
# Clamp to configured bounds
|
|
143
|
+
delay_before_clamp = total_delay
|
|
144
|
+
final_delay = self._clamp_delay(
|
|
145
|
+
total_delay,
|
|
146
|
+
self.config.min_read_delay_seconds,
|
|
147
|
+
self.config.max_read_delay_seconds,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
if delay_before_clamp != final_delay:
|
|
151
|
+
logger.debug(
|
|
152
|
+
f"[DELAY_CALC] 📖 Clamped delay: before={delay_before_clamp:.2f}s, "
|
|
153
|
+
+ f"after={final_delay:.2f}s, "
|
|
154
|
+
+ f"bounds=[{self.config.min_read_delay_seconds:.2f}s-{self.config.max_read_delay_seconds:.2f}s]"
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
logger.info(
|
|
158
|
+
f"[DELAY_CALC] 📖 Read delay calculated: {final_delay:.2f}s "
|
|
159
|
+
+ f"for {char_count} chars ({word_count:.1f} words)"
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
return final_delay
|
|
163
|
+
|
|
164
|
+
def calculate_typing_delay(self, response_text: str) -> float:
|
|
165
|
+
"""Calculate delay for typing a response.
|
|
166
|
+
|
|
167
|
+
This method simulates the time a human would take to compose and type
|
|
168
|
+
a response. The calculation includes:
|
|
169
|
+
- Base typing time based on character count and typing speed
|
|
170
|
+
- Composition planning time (2-5 seconds)
|
|
171
|
+
- Multitasking overhead multiplier (1.2-1.5x)
|
|
172
|
+
- Random jitter (±20% variation)
|
|
173
|
+
- Clamping to configured min/max bounds
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
response_text: The response content to type
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
Delay in seconds (float), clamped to configured bounds
|
|
180
|
+
|
|
181
|
+
Example:
|
|
182
|
+
>>> calculator = HumanDelayCalculator(config)
|
|
183
|
+
>>> delay = calculator.calculate_typing_delay("I can help you with that!")
|
|
184
|
+
>>> print(f"Typing delay: {delay:.2f}s")
|
|
185
|
+
"""
|
|
186
|
+
# Calculate base delay from character count
|
|
187
|
+
char_count = len(response_text)
|
|
188
|
+
word_count = self._estimate_word_count(char_count)
|
|
189
|
+
|
|
190
|
+
logger.debug(
|
|
191
|
+
f"[DELAY_CALC] ⌨️ Calculating typing delay: chars={char_count}, words={word_count:.1f}"
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
# Base typing time: words / (words per minute / 60 seconds)
|
|
195
|
+
words_per_second = self.typing_speed_wpm / 60.0
|
|
196
|
+
base_delay = word_count / words_per_second
|
|
197
|
+
|
|
198
|
+
# Add composition planning time (random 2-5 seconds)
|
|
199
|
+
planning_time = random.uniform(2.0, 5.0)
|
|
200
|
+
|
|
201
|
+
# Combine base delay and planning time
|
|
202
|
+
total_delay = base_delay + planning_time
|
|
203
|
+
|
|
204
|
+
# Apply multitasking overhead multiplier (random 1.2-1.5x)
|
|
205
|
+
multitasking_multiplier = random.uniform(1.2, 1.5)
|
|
206
|
+
delay_before_multitasking = total_delay
|
|
207
|
+
total_delay *= multitasking_multiplier
|
|
208
|
+
|
|
209
|
+
logger.debug(
|
|
210
|
+
f"[DELAY_CALC] ⌨️ Typing delay components: base={base_delay:.2f}s, "
|
|
211
|
+
+ f"planning={planning_time:.2f}s, before_multitasking={delay_before_multitasking:.2f}s, "
|
|
212
|
+
+ f"multiplier={multitasking_multiplier:.2f}x, after_multitasking={total_delay:.2f}s"
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
# Apply jitter (±20% random variation)
|
|
216
|
+
delay_before_jitter = total_delay
|
|
217
|
+
total_delay = self._apply_jitter(total_delay)
|
|
218
|
+
|
|
219
|
+
if self.config.enable_delay_jitter:
|
|
220
|
+
logger.debug(
|
|
221
|
+
f"[DELAY_CALC] ⌨️ Applied jitter: before={delay_before_jitter:.2f}s, "
|
|
222
|
+
+ f"after={total_delay:.2f}s"
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
# Clamp to configured bounds
|
|
226
|
+
delay_before_clamp = total_delay
|
|
227
|
+
final_delay = self._clamp_delay(
|
|
228
|
+
total_delay,
|
|
229
|
+
self.config.min_typing_delay_seconds,
|
|
230
|
+
self.config.max_typing_delay_seconds,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
if delay_before_clamp != final_delay:
|
|
234
|
+
logger.debug(
|
|
235
|
+
f"[DELAY_CALC] ⌨️ Clamped delay: before={delay_before_clamp:.2f}s, "
|
|
236
|
+
+ f"after={final_delay:.2f}s, "
|
|
237
|
+
+ f"bounds=[{self.config.min_typing_delay_seconds:.2f}s-{self.config.max_typing_delay_seconds:.2f}s]"
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
logger.info(
|
|
241
|
+
f"[DELAY_CALC] ⌨️ Typing delay calculated: {final_delay:.2f}s "
|
|
242
|
+
+ f"for {char_count} chars ({word_count:.1f} words)"
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
return final_delay
|
|
246
|
+
|
|
247
|
+
def calculate_send_delay(self) -> float:
|
|
248
|
+
"""Calculate brief delay before sending message.
|
|
249
|
+
|
|
250
|
+
This method simulates the final review time before a human sends a message.
|
|
251
|
+
The calculation includes:
|
|
252
|
+
- Random delay within configured send delay bounds
|
|
253
|
+
- Optional jitter if enabled in configuration
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
Delay in seconds (float), within configured bounds
|
|
257
|
+
|
|
258
|
+
Example:
|
|
259
|
+
>>> calculator = HumanDelayCalculator(config)
|
|
260
|
+
>>> delay = calculator.calculate_send_delay()
|
|
261
|
+
>>> print(f"Send delay: {delay:.2f}s")
|
|
262
|
+
"""
|
|
263
|
+
logger.debug(
|
|
264
|
+
f"[DELAY_CALC] 📤 Calculating send delay within bounds: "
|
|
265
|
+
+ f"[{self.config.min_send_delay_seconds:.2f}s-{self.config.max_send_delay_seconds:.2f}s]"
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
# Generate random delay within configured bounds
|
|
269
|
+
delay = random.uniform(
|
|
270
|
+
self.config.min_send_delay_seconds, self.config.max_send_delay_seconds
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
delay_before_jitter = delay
|
|
274
|
+
|
|
275
|
+
# Apply jitter if enabled in configuration
|
|
276
|
+
if self.config.enable_delay_jitter:
|
|
277
|
+
delay = self._apply_jitter(delay)
|
|
278
|
+
logger.debug(
|
|
279
|
+
f"[DELAY_CALC] 📤 Applied jitter: before={delay_before_jitter:.2f}s, "
|
|
280
|
+
+ f"after={delay:.2f}s"
|
|
281
|
+
)
|
|
282
|
+
# Re-clamp after jitter to ensure we stay within bounds
|
|
283
|
+
delay_before_reclamp = delay
|
|
284
|
+
delay = self._clamp_delay(
|
|
285
|
+
delay,
|
|
286
|
+
self.config.min_send_delay_seconds,
|
|
287
|
+
self.config.max_send_delay_seconds,
|
|
288
|
+
)
|
|
289
|
+
if delay_before_reclamp != delay:
|
|
290
|
+
logger.debug(
|
|
291
|
+
f"[DELAY_CALC] 📤 Re-clamped after jitter: before={delay_before_reclamp:.2f}s, "
|
|
292
|
+
+ f"after={delay:.2f}s"
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
logger.info(f"[DELAY_CALC] 📤 Send delay calculated: {delay:.2f}s")
|
|
296
|
+
|
|
297
|
+
return delay
|
|
298
|
+
|
|
299
|
+
def calculate_batch_read_delay(self, messages: list[str]) -> float:
|
|
300
|
+
"""Calculate delay for reading a batch of messages.
|
|
301
|
+
|
|
302
|
+
This method simulates the time a human would take to read multiple messages
|
|
303
|
+
in sequence. The calculation includes:
|
|
304
|
+
- Individual read delays for each message (without context switching)
|
|
305
|
+
- 0.5 second pause between each message
|
|
306
|
+
- Compression factor (0.7x by default) to simulate faster batch reading
|
|
307
|
+
- Clamping to reasonable bounds (2-20 seconds suggested)
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
messages: List of message texts in the batch
|
|
311
|
+
|
|
312
|
+
Returns:
|
|
313
|
+
Delay in seconds (float), clamped to reasonable bounds
|
|
314
|
+
|
|
315
|
+
Example:
|
|
316
|
+
>>> calculator = HumanDelayCalculator(config)
|
|
317
|
+
>>> messages = ["Hello", "How are you?", "I need help"]
|
|
318
|
+
>>> delay = calculator.calculate_batch_read_delay(messages)
|
|
319
|
+
>>> print(f"Batch read delay: {delay:.2f}s for {len(messages)} messages")
|
|
320
|
+
"""
|
|
321
|
+
if not messages:
|
|
322
|
+
logger.debug("[DELAY_CALC] 📚 Batch read delay: 0.0s (empty batch)")
|
|
323
|
+
return 0.0
|
|
324
|
+
|
|
325
|
+
total_chars = sum(len(msg) for msg in messages)
|
|
326
|
+
total_words = sum(self._estimate_word_count(len(msg)) for msg in messages)
|
|
327
|
+
|
|
328
|
+
logger.debug(
|
|
329
|
+
f"[DELAY_CALC] 📚 Calculating batch read delay: messages={len(messages)}, "
|
|
330
|
+
+ f"total_chars={total_chars}, total_words={total_words:.1f}"
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
total_delay = 0.0
|
|
334
|
+
|
|
335
|
+
# Calculate individual read delays for each message
|
|
336
|
+
for i, message_text in enumerate(messages):
|
|
337
|
+
char_count = len(message_text)
|
|
338
|
+
word_count = self._estimate_word_count(char_count)
|
|
339
|
+
|
|
340
|
+
# Base reading time (without context switching or comprehension time)
|
|
341
|
+
words_per_second = self.reading_speed_wpm / 60.0
|
|
342
|
+
base_delay = word_count / words_per_second
|
|
343
|
+
|
|
344
|
+
total_delay += base_delay
|
|
345
|
+
|
|
346
|
+
logger.debug(
|
|
347
|
+
f"[DELAY_CALC] 📚 Message {i + 1}/{len(messages)}: chars={char_count}, "
|
|
348
|
+
+ f"words={word_count:.1f}, delay={base_delay:.2f}s"
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
# Add 0.5 second pause between each message
|
|
352
|
+
if len(messages) > 1:
|
|
353
|
+
pause_time = (len(messages) - 1) * 0.5
|
|
354
|
+
total_delay += pause_time
|
|
355
|
+
logger.debug(
|
|
356
|
+
f"[DELAY_CALC] 📚 Added pause time: {pause_time:.2f}s "
|
|
357
|
+
+ f"({len(messages) - 1} pauses × 0.5s)"
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
delay_before_compression = total_delay
|
|
361
|
+
|
|
362
|
+
# Apply compression factor (0.7x by default) to simulate faster batch reading
|
|
363
|
+
compression_factor = self.config.batch_read_compression_factor
|
|
364
|
+
total_delay *= compression_factor
|
|
365
|
+
|
|
366
|
+
logger.debug(
|
|
367
|
+
f"[DELAY_CALC] 📚 Applied compression: before={delay_before_compression:.2f}s, "
|
|
368
|
+
+ f"factor={compression_factor:.2f}x, after={total_delay:.2f}s"
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
# Clamp to reasonable bounds (2-20 seconds suggested for baseline)
|
|
372
|
+
# Use configured read delay bounds as a guide
|
|
373
|
+
min_batch_delay = max(2.0, self.config.min_read_delay_seconds)
|
|
374
|
+
max_batch_delay = min(20.0, self.config.max_read_delay_seconds * 1.5)
|
|
375
|
+
|
|
376
|
+
delay_before_clamp = total_delay
|
|
377
|
+
final_delay = self._clamp_delay(total_delay, min_batch_delay, max_batch_delay)
|
|
378
|
+
|
|
379
|
+
if delay_before_clamp != final_delay:
|
|
380
|
+
logger.debug(
|
|
381
|
+
f"[DELAY_CALC] 📚 Clamped delay: before={delay_before_clamp:.2f}s, "
|
|
382
|
+
+ f"after={final_delay:.2f}s, bounds=[{min_batch_delay:.2f}s-{max_batch_delay:.2f}s]"
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
logger.info(
|
|
386
|
+
f"[DELAY_CALC] 📚 Batch read delay calculated: {final_delay:.2f}s "
|
|
387
|
+
+ f"for {len(messages)} messages ({total_chars} chars, {total_words:.1f} words)"
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
return final_delay
|
|
391
|
+
|
|
392
|
+
def _apply_jitter(self, delay: float) -> float:
|
|
393
|
+
"""Apply random variation to a delay value.
|
|
394
|
+
|
|
395
|
+
This method adds random jitter to prevent detectable patterns in delay timing.
|
|
396
|
+
The jitter is applied as a multiplier within the range defined by jitter_factor.
|
|
397
|
+
|
|
398
|
+
Args:
|
|
399
|
+
delay: The base delay value in seconds
|
|
400
|
+
|
|
401
|
+
Returns:
|
|
402
|
+
Delay with jitter applied (float)
|
|
403
|
+
|
|
404
|
+
Example:
|
|
405
|
+
>>> # With jitter_factor = 0.20 (±20%)
|
|
406
|
+
>>> calculator = HumanDelayCalculator(config)
|
|
407
|
+
>>> base_delay = 10.0
|
|
408
|
+
>>> jittered = calculator._apply_jitter(base_delay)
|
|
409
|
+
>>> # Result will be between 8.0 and 12.0 seconds
|
|
410
|
+
"""
|
|
411
|
+
if not self.config.enable_delay_jitter:
|
|
412
|
+
return delay
|
|
413
|
+
|
|
414
|
+
# Generate random factor between (1 - jitter_factor) and (1 + jitter_factor)
|
|
415
|
+
# Example: With 20% jitter, factor ranges from 0.8 to 1.2
|
|
416
|
+
min_factor = 1.0 - self.jitter_factor
|
|
417
|
+
max_factor = 1.0 + self.jitter_factor
|
|
418
|
+
jitter_multiplier = random.uniform(min_factor, max_factor)
|
|
419
|
+
|
|
420
|
+
return delay * jitter_multiplier
|
|
421
|
+
|
|
422
|
+
def _clamp_delay(self, delay: float, min_delay: float, max_delay: float) -> float:
|
|
423
|
+
"""Clamp a delay value to minimum and maximum bounds.
|
|
424
|
+
|
|
425
|
+
This method ensures that calculated delays stay within configured limits,
|
|
426
|
+
preventing unrealistically short or long delays.
|
|
427
|
+
|
|
428
|
+
Args:
|
|
429
|
+
delay: The delay value to clamp
|
|
430
|
+
min_delay: Minimum allowed delay in seconds
|
|
431
|
+
max_delay: Maximum allowed delay in seconds
|
|
432
|
+
|
|
433
|
+
Returns:
|
|
434
|
+
Clamped delay value (float)
|
|
435
|
+
|
|
436
|
+
Example:
|
|
437
|
+
>>> calculator = HumanDelayCalculator(config)
|
|
438
|
+
>>> clamped = calculator._clamp_delay(100.0, 2.0, 15.0)
|
|
439
|
+
>>> print(clamped) # Output: 15.0
|
|
440
|
+
"""
|
|
441
|
+
return max(min_delay, min(delay, max_delay))
|
|
442
|
+
|
|
443
|
+
def _estimate_word_count(self, char_count: int) -> float:
|
|
444
|
+
"""Estimate word count from character count.
|
|
445
|
+
|
|
446
|
+
This method uses an average word length of 5 characters to estimate
|
|
447
|
+
the number of words in a text based on its character count.
|
|
448
|
+
|
|
449
|
+
Args:
|
|
450
|
+
char_count: Number of characters in the text
|
|
451
|
+
|
|
452
|
+
Returns:
|
|
453
|
+
Estimated word count (float)
|
|
454
|
+
|
|
455
|
+
Example:
|
|
456
|
+
>>> calculator = HumanDelayCalculator(config)
|
|
457
|
+
>>> words = calculator._estimate_word_count(100)
|
|
458
|
+
>>> print(words) # Output: 20.0
|
|
459
|
+
"""
|
|
460
|
+
# Assume average word length of 5 characters
|
|
461
|
+
avg_word_length = 5.0
|
|
462
|
+
return char_count / avg_word_length
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import Any
|
|
1
|
+
from typing import Any, cast
|
|
2
2
|
|
|
3
3
|
from pydantic import field_validator
|
|
4
4
|
from rsb.models.base_model import BaseModel
|
|
@@ -42,15 +42,17 @@ class AudioMessage(BaseModel):
|
|
|
42
42
|
mode="before",
|
|
43
43
|
)
|
|
44
44
|
@classmethod
|
|
45
|
-
def convert_long_to_str(cls, v:
|
|
45
|
+
def convert_long_to_str(cls, v: float | None) -> str | None:
|
|
46
46
|
"""Converte objetos Long do protobuf para string."""
|
|
47
47
|
if v is None:
|
|
48
48
|
return None
|
|
49
|
+
|
|
49
50
|
if isinstance(v, dict) and "low" in v:
|
|
50
|
-
low = v.get("low", 0)
|
|
51
|
-
high = v.get("high", 0)
|
|
51
|
+
low: int = cast(int, v.get("low", 0))
|
|
52
|
+
high: int = cast(int, v.get("high", 0))
|
|
52
53
|
value = (high << 32) | low
|
|
53
54
|
return str(value)
|
|
55
|
+
|
|
54
56
|
return str(v)
|
|
55
57
|
|
|
56
58
|
@field_validator(
|