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.
@@ -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
- endpoint_name: str = cast(
517
- str,
518
- (
519
- operation_id
520
- or f"{method}_{path.replace('/', '_').replace('{', '').replace('}', '')}"
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,
@@ -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
- async with aiohttp.ClientSession(
385
- connector=connector, timeout=timeout
386
- ) as session:
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: Any) -> str | None:
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(