agentle 0.9.36__py3-none-any.whl → 0.9.38__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.
@@ -6,7 +6,6 @@ from collections.abc import Callable
6
6
  from typing import Any
7
7
 
8
8
  from rsb.models.base_model import BaseModel
9
- from rsb.models.field import Field
10
9
 
11
10
 
12
11
  class RequestHook(BaseModel):
@@ -21,9 +21,10 @@ class AudioMessage(BaseModel):
21
21
  mediaKeyTimestamp: Timestamp da chave de mídia
22
22
  streamingSidecar: Dados para streaming do áudio (opcional)
23
23
  waveform: Forma de onda do áudio em base64 (opcional)
24
+ base64_data: Dados do áudio em base64 (quando não há URL disponível)
24
25
  """
25
26
 
26
- url: str
27
+ url: str | None = Field(default=None)
27
28
  mimetype: str | None = Field(default=None)
28
29
  fileSha256: str | dict[str, Any] | None = Field(default=None)
29
30
  fileLength: str | dict[str, Any] | None = Field(default=None)
@@ -35,6 +36,7 @@ class AudioMessage(BaseModel):
35
36
  mediaKeyTimestamp: str | dict[str, Any] | None = Field(default=None)
36
37
  streamingSidecar: str | dict[str, Any] | None = Field(default=None)
37
38
  waveform: str | dict[str, Any] | None = Field(default=None)
39
+ base64_data: str | None = Field(default=None)
38
40
 
39
41
  @field_validator(
40
42
  "fileLength",
@@ -68,6 +70,8 @@ class AudioMessage(BaseModel):
68
70
  """Converte objetos Buffer/Bytes do protobuf para string."""
69
71
  if v is None:
70
72
  return None
71
- if isinstance(v, dict) and all(k.isdigit() for k in v.keys()):
72
- return str(v)
73
- return str(v) if not isinstance(v, str) else v
73
+ if isinstance(v, dict):
74
+ keys = [str(k) for k in v.keys()] # pyright: ignore[reportUnknownArgumentType]
75
+ if all(k.isdigit() for k in keys):
76
+ return str(dict(v)) # pyright: ignore[reportUnknownArgumentType]
77
+ return str(v) if not isinstance(v, str) else v # pyright: ignore[reportUnknownArgumentType]
@@ -46,11 +46,11 @@ class DocumentMessage(BaseModel):
46
46
  if v is None:
47
47
  return None
48
48
  if isinstance(v, dict) and "low" in v:
49
- low = v.get("low", 0)
50
- high = v.get("high", 0)
51
- value = (high << 32) | low
49
+ low: int = int(v.get("low", 0)) # type: ignore[arg-type]
50
+ high: int = int(v.get("high", 0)) # type: ignore[arg-type]
51
+ value: int = (high << 32) | low
52
52
  return str(value)
53
- return str(v)
53
+ return str(v) # type: ignore[return-value]
54
54
 
55
55
  @field_validator(
56
56
  "fileSha256",
@@ -63,6 +63,8 @@ class DocumentMessage(BaseModel):
63
63
  """Converte objetos Buffer/Bytes do protobuf para string."""
64
64
  if v is None:
65
65
  return None
66
- if isinstance(v, dict) and all(k.isdigit() for k in v.keys()):
67
- return str(v)
68
- return str(v) if not isinstance(v, str) else v
66
+ if isinstance(v, dict):
67
+ keys = list(v.keys()) # type: ignore[var-annotated]
68
+ if all(str(k).isdigit() for k in keys): # type: ignore[arg-type]
69
+ return str(v) # type: ignore[return-value]
70
+ return str(v) if not isinstance(v, str) else v # type: ignore[return-value]
@@ -28,7 +28,7 @@ class ImageMessage(BaseModel):
28
28
  midQualityFileSha256: Hash SHA256 da versão de qualidade média (opcional)
29
29
  """
30
30
 
31
- url: str
31
+ url: str | None = Field(default=None)
32
32
  mimetype: str | None = Field(default=None)
33
33
  caption: str | None = Field(default=None)
34
34
  fileSha256: str | dict[str, Any] | None = Field(default=None)
@@ -63,11 +63,11 @@ class ImageMessage(BaseModel):
63
63
  return None
64
64
  if isinstance(v, dict) and "low" in v:
65
65
  # Converte Long para número inteiro
66
- low = v.get("low", 0)
67
- high = v.get("high", 0)
68
- value = (high << 32) | low
66
+ low: int = int(v.get("low", 0)) # type: ignore[arg-type]
67
+ high: int = int(v.get("high", 0)) # type: ignore[arg-type]
68
+ value: int = (high << 32) | low
69
69
  return str(value)
70
- return str(v)
70
+ return str(v) # type: ignore[return-value]
71
71
 
72
72
  @field_validator(
73
73
  "fileSha256",
@@ -88,8 +88,10 @@ class ImageMessage(BaseModel):
88
88
  """
89
89
  if v is None:
90
90
  return None
91
- if isinstance(v, dict) and all(k.isdigit() for k in v.keys()):
92
- # Converte dicionário de bytes para string base64 ou hex
93
- # Por enquanto, apenas retorna uma representação string
94
- return str(v)
95
- return str(v) if not isinstance(v, str) else v
91
+ if isinstance(v, dict):
92
+ keys = list(v.keys()) # type: ignore[var-annotated]
93
+ if all(str(k).isdigit() for k in keys): # type: ignore[arg-type]
94
+ # Converte dicionário de bytes para string base64 ou hex
95
+ # Por enquanto, apenas retorna uma representação string
96
+ return str(v) # type: ignore[return-value]
97
+ return str(v) if not isinstance(v, str) else v # type: ignore[return-value]
@@ -58,11 +58,11 @@ class VideoMessage(BaseModel):
58
58
  if v is None:
59
59
  return None
60
60
  if isinstance(v, dict) and "low" in v:
61
- low = v.get("low", 0)
62
- high = v.get("high", 0)
63
- value = (high << 32) | low
61
+ low: int = int(v.get("low", 0)) # type: ignore[arg-type]
62
+ high: int = int(v.get("high", 0)) # type: ignore[arg-type]
63
+ value: int = (high << 32) | low
64
64
  return str(value)
65
- return str(v)
65
+ return str(v) # type: ignore[return-value]
66
66
 
67
67
  @field_validator(
68
68
  "fileSha256",
@@ -79,6 +79,8 @@ class VideoMessage(BaseModel):
79
79
  """Converte objetos Buffer/Bytes do protobuf para string."""
80
80
  if v is None:
81
81
  return None
82
- if isinstance(v, dict) and all(k.isdigit() for k in v.keys()):
83
- return str(v)
84
- return str(v) if not isinstance(v, str) else v
82
+ if isinstance(v, dict):
83
+ keys = list(v.keys()) # type: ignore[var-annotated]
84
+ if all(str(k).isdigit() for k in keys): # type: ignore[arg-type]
85
+ return str(v) # type: ignore[return-value]
86
+ return str(v) if not isinstance(v, str) else v # type: ignore[return-value]
@@ -9,3 +9,4 @@ class WhatsAppMediaMessage(WhatsAppMessage):
9
9
  media_size: int | None = None
10
10
  caption: str | None = None
11
11
  filename: str | None = None
12
+ base64_data: str | None = None # Para quando a mídia vem em base64 ao invés de URL
@@ -83,8 +83,12 @@ class WhatsAppWebhookPayload(BaseModel):
83
83
 
84
84
  key = self.data.key
85
85
  if "@lid" in key.remoteJid:
86
- self.phone_number_id = key.remoteJidAlt.split("@")[0]
87
- self.data.key.remoteJid = key.remoteJidAlt
86
+ remote_jid_alt = key.remoteJidAlt
87
+ if remote_jid_alt is None:
88
+ raise ValueError("No remotejidalt was provided.")
89
+
90
+ self.phone_number_id = remote_jid_alt.split("@")[0]
91
+ self.data.key.remoteJid = remote_jid_alt
88
92
  return
89
93
 
90
94
  self.phone_number_id = key.remoteJid.split("@")[0]
@@ -15,6 +15,8 @@ from datetime import datetime
15
15
  from typing import TYPE_CHECKING, Any, Optional, cast
16
16
  from dataclasses import dataclass, field
17
17
 
18
+ import mistune
19
+
18
20
  from rsb.coroutines.run_sync import run_sync
19
21
  from rsb.models.base_model import BaseModel
20
22
  from rsb.models.config_dict import ConfigDict
@@ -1652,6 +1654,9 @@ class WhatsAppBot[T_Schema: WhatsAppResponseBase = WhatsAppResponseBase](BaseMod
1652
1654
  "media_mime_type": message.media_mime_type,
1653
1655
  "caption": message.caption,
1654
1656
  "filename": getattr(message, "filename", None),
1657
+ "base64_data": getattr(
1658
+ message, "base64_data", None
1659
+ ), # Include base64 if available
1655
1660
  }
1656
1661
  )
1657
1662
 
@@ -1706,16 +1711,36 @@ class WhatsAppBot[T_Schema: WhatsAppResponseBase = WhatsAppResponseBase](BaseMod
1706
1711
  "WhatsAppVideoMessage",
1707
1712
  ]:
1708
1713
  try:
1709
- logger.debug(
1710
- f"[BATCH_CONVERSION] Downloading media for message {msg_data['id']}"
1711
- )
1712
- media_data = await self.provider.download_media(msg_data["id"])
1713
- parts.append(
1714
- FilePart(data=media_data.data, mime_type=media_data.mime_type)
1715
- )
1716
- logger.debug(
1717
- f"[BATCH_CONVERSION] Successfully downloaded media for {msg_data['id']}"
1718
- )
1714
+ # CRITICAL FIX: Check if media has base64 data already (for audio messages)
1715
+ # This avoids unnecessary download attempts when media is already available
1716
+ base64_data = msg_data.get("base64_data")
1717
+ if base64_data:
1718
+ logger.info(
1719
+ f"[BATCH_CONVERSION] 🎵 Using base64 data directly for message {msg_data['id']} (no download needed)"
1720
+ )
1721
+ import base64
1722
+
1723
+ media_bytes = base64.b64decode(base64_data)
1724
+ mime_type = msg_data.get(
1725
+ "media_mime_type", "application/octet-stream"
1726
+ )
1727
+ parts.append(FilePart(data=media_bytes, mime_type=mime_type))
1728
+ logger.debug(
1729
+ f"[BATCH_CONVERSION] Successfully decoded base64 media for {msg_data['id']} ({len(media_bytes)} bytes)"
1730
+ )
1731
+ else:
1732
+ logger.debug(
1733
+ f"[BATCH_CONVERSION] Downloading media for message {msg_data['id']}"
1734
+ )
1735
+ media_data = await self.provider.download_media(msg_data["id"])
1736
+ parts.append(
1737
+ FilePart(
1738
+ data=media_data.data, mime_type=media_data.mime_type
1739
+ )
1740
+ )
1741
+ logger.debug(
1742
+ f"[BATCH_CONVERSION] Successfully downloaded media for {msg_data['id']}"
1743
+ )
1719
1744
 
1720
1745
  # Add caption if present
1721
1746
  caption = msg_data.get("caption")
@@ -1828,16 +1853,32 @@ class WhatsAppBot[T_Schema: WhatsAppResponseBase = WhatsAppResponseBase](BaseMod
1828
1853
  # Handle media messages
1829
1854
  elif isinstance(message, WhatsAppMediaMessage):
1830
1855
  try:
1831
- logger.debug(
1832
- f"[SINGLE_CONVERSION] Downloading media for message {message.id}"
1833
- )
1834
- media_data = await self.provider.download_media(message.id)
1835
- parts.append(
1836
- FilePart(data=media_data.data, mime_type=media_data.mime_type)
1837
- )
1838
- logger.debug(
1839
- f"[SINGLE_CONVERSION] Successfully downloaded media for {message.id}"
1840
- )
1856
+ # CRITICAL FIX: Check if media has base64 data already (for audio messages)
1857
+ # This avoids unnecessary download attempts when media is already available
1858
+ if message.base64_data:
1859
+ logger.info(
1860
+ f"[SINGLE_CONVERSION] 🎵 Using base64 data directly for message {message.id} (no download needed)"
1861
+ )
1862
+ import base64
1863
+
1864
+ media_bytes = base64.b64decode(message.base64_data)
1865
+ parts.append(
1866
+ FilePart(data=media_bytes, mime_type=message.media_mime_type)
1867
+ )
1868
+ logger.debug(
1869
+ f"[SINGLE_CONVERSION] Successfully decoded base64 media for {message.id} ({len(media_bytes)} bytes)"
1870
+ )
1871
+ else:
1872
+ logger.debug(
1873
+ f"[SINGLE_CONVERSION] Downloading media for message {message.id}"
1874
+ )
1875
+ media_data = await self.provider.download_media(message.id)
1876
+ parts.append(
1877
+ FilePart(data=media_data.data, mime_type=media_data.mime_type)
1878
+ )
1879
+ logger.debug(
1880
+ f"[SINGLE_CONVERSION] Successfully downloaded media for {message.id}"
1881
+ )
1841
1882
 
1842
1883
  # Add caption if present
1843
1884
  if message.caption:
@@ -2028,210 +2069,123 @@ class WhatsAppBot[T_Schema: WhatsAppResponseBase = WhatsAppResponseBase](BaseMod
2028
2069
 
2029
2070
  return text
2030
2071
 
2031
- def _format_whatsapp_markdown(self, text: str) -> str:
2032
- """Convert standard markdown to WhatsApp-compatible formatting.
2072
+ def _create_whatsapp_renderer(self) -> mistune.HTMLRenderer:
2073
+ """Create a mistune renderer for WhatsApp formatting."""
2033
2074
 
2034
- WhatsApp supports:
2035
- - *bold* for bold text
2036
- - _italic_ for italic text
2037
- - ~strikethrough~ for strikethrough text
2038
- - ```code``` for monospace text
2039
- - No support for headers, tables, or complex markdown structures
2040
-
2041
- This method converts:
2042
- - Headers (# ## ###) to bold text with separators
2043
- - Tables to formatted text
2044
- - Markdown lists to plain text lists (preserving line breaks)
2045
- - Links to "text (url)" format
2046
- - Images to descriptive text
2047
- - Blockquotes to indented text
2048
- """
2049
- if not text:
2050
- return text
2075
+ class WhatsAppRenderer(mistune.HTMLRenderer):
2076
+ """Custom renderer for WhatsApp markdown format."""
2051
2077
 
2052
- # Split text into lines for processing
2053
- lines = text.split("\n")
2054
- processed_lines: list[str] = []
2055
- in_code_block = False
2056
- in_table = False
2057
- table_lines: list[str] = []
2058
-
2059
- i = 0
2060
- while i < len(lines):
2061
- line = lines[i]
2062
-
2063
- # Track code blocks (preserve them as-is)
2064
- if line.strip().startswith("```"):
2065
- in_code_block = not in_code_block
2066
- processed_lines.append(line)
2067
- i += 1
2068
- continue
2069
-
2070
- # If we're in a code block, don't process the line
2071
- if in_code_block:
2072
- processed_lines.append(line)
2073
- i += 1
2074
- continue
2075
-
2076
- # Detect table start (header line followed by separator line)
2077
- if i + 1 < len(lines) and self._is_table_separator(lines[i + 1]):
2078
- in_table = True
2079
- table_lines = [line]
2080
- i += 1
2081
- continue
2082
-
2083
- # Continue collecting table rows
2084
- if in_table:
2085
- if self._is_table_row(line):
2086
- table_lines.append(line)
2087
- i += 1
2088
- continue
2078
+ def heading(self, text: str, level: int, **attrs: Any) -> str:
2079
+ """Render headings as bold text with separators."""
2080
+ if level == 1:
2081
+ return f"\n*{text}*\n{'═' * min(len(text), 30)}\n"
2082
+ elif level == 2:
2083
+ return f"\n*{text}*\n{'─' * min(len(text), 30)}\n"
2089
2084
  else:
2090
- # End of table, process it
2091
- processed_lines.extend(self._format_table(table_lines))
2092
- in_table = False
2093
- table_lines = []
2094
- # Don't increment i, process current line
2095
-
2096
- # Process headers
2097
- if line.strip().startswith("#"):
2098
- processed_lines.append(self._format_header(line))
2099
- i += 1
2100
- continue
2085
+ return f"\n*{text}*\n"
2101
2086
 
2102
- # Process blockquotes
2103
- if line.strip().startswith(">"):
2104
- processed_lines.append(self._format_blockquote(line))
2105
- i += 1
2106
- continue
2087
+ def strong(self, text: str) -> str:
2088
+ """Render bold as WhatsApp bold."""
2089
+ return f"*{text}*"
2107
2090
 
2108
- # Process horizontal rules
2109
- if re.match(r"^[\s]*(-{3,}|\*{3,}|_{3,})[\s]*$", line):
2110
- processed_lines.append("" * 30)
2111
- i += 1
2112
- continue
2091
+ def emphasis(self, text: str) -> str:
2092
+ """Render italic as WhatsApp italic."""
2093
+ return f"_{text}_"
2113
2094
 
2114
- # Process regular line (preserve empty lines for spacing)
2115
- processed_lines.append(line)
2116
- i += 1
2095
+ def strikethrough(self, text: str) -> str:
2096
+ """Render strikethrough as WhatsApp strikethrough."""
2097
+ return f"~{text}~"
2117
2098
 
2118
- # If we ended while in a table, process it
2119
- if table_lines:
2120
- processed_lines.extend(self._format_table(table_lines))
2099
+ def codespan(self, text: str) -> str:
2100
+ """Render inline code as WhatsApp monospace."""
2101
+ return f"```{text}```"
2121
2102
 
2122
- # Rejoin lines - CRITICAL: preserve all line breaks
2123
- text = "\n".join(processed_lines)
2103
+ def block_code(self, code: str, info: str | None = None) -> str:
2104
+ """Render code blocks."""
2105
+ return f"```{code}```\n"
2124
2106
 
2125
- # Handle inline markdown elements
2107
+ def link(self, text: str, url: str, title: str | None = None) -> str:
2108
+ """Render links as text (url)."""
2109
+ return f"{text} ({url})"
2126
2110
 
2127
- # Handle images ![alt](url) -> [Image: alt]
2128
- text = re.sub(r"!\[([^\]]*)\]\([^\)]+\)", r"[Imagem: \1]", text)
2111
+ def image(self, text: str, url: str, title: str | None = None) -> str:
2112
+ """Render images as descriptive text."""
2113
+ return f"[Imagem: {text}]" if text else "[Imagem]"
2129
2114
 
2130
- # Handle links [text](url) -> text (url)
2131
- text = re.sub(r"\[([^\]]+)\]\(([^\)]+)\)", r"\1 (\2)", text)
2115
+ def block_quote(self, text: str) -> str:
2116
+ """Render blockquotes with indentation."""
2117
+ lines = text.strip().split("\n")
2118
+ return "\n".join(f" ┃ {line}" for line in lines) + "\n"
2132
2119
 
2133
- # Handle **bold** -> *bold* (WhatsApp format)
2134
- # Use negative lookbehind and lookahead to avoid matching within code blocks
2135
- text = re.sub(r"(?<!`)\*\*([^*`]+)\*\*(?!`)", r"*\1*", text)
2120
+ def list(self, text: str, ordered: bool, **attrs: Any) -> str:
2121
+ """Render lists."""
2122
+ return f"\n{text}"
2136
2123
 
2137
- # Handle __italic__ -> _italic_
2138
- text = re.sub(r"(?<!`)__([^_`]+)__(?!`)", r"_\1_", text)
2124
+ def list_item(self, text: str, **attrs: Any) -> str:
2125
+ """Render list items."""
2126
+ return f"{text}\n"
2139
2127
 
2140
- # Handle ~~strikethrough~~ -> ~strikethrough~
2141
- text = re.sub(r"(?<!`)~~([^~`]+)~~(?!`)", r"~\1~", text)
2128
+ def paragraph(self, text: str) -> str:
2129
+ """Render paragraphs."""
2130
+ return f"{text}\n\n"
2142
2131
 
2143
- # Handle single backtick `code` -> ```code``` (WhatsApp monospace)
2144
- # But don't convert if already part of triple backticks
2145
- text = re.sub(r"(?<!`)(`(?!``)([^`]+)`)(?!`)", r"```\2```", text)
2132
+ def thematic_break(self) -> str:
2133
+ """Render horizontal rules."""
2134
+ return "" * 30 + "\n"
2146
2135
 
2147
- return text
2136
+ def linebreak(self) -> str:
2137
+ """Render line breaks."""
2138
+ return "\n"
2148
2139
 
2149
- def _is_table_separator(self, line: str) -> bool:
2150
- """Check if a line is a markdown table separator (e.g., |---|---|)."""
2151
- stripped = line.strip()
2152
- if not stripped.startswith("|") or not stripped.endswith("|"):
2153
- return False
2154
- # Remove outer pipes and split
2155
- content = stripped[1:-1]
2156
- cells = content.split("|")
2157
- # Check if all cells are just dashes, colons, and spaces
2158
- for cell in cells:
2159
- cell = cell.strip()
2160
- if cell and not re.match(r"^:?-+:?$", cell):
2161
- return False
2162
- return True
2163
-
2164
- def _is_table_row(self, line: str) -> bool:
2165
- """Check if a line is a table row."""
2166
- stripped = line.strip()
2167
- return stripped.startswith("|") and stripped.endswith("|")
2140
+ def text(self, text: str) -> str:
2141
+ """Render plain text."""
2142
+ return text
2168
2143
 
2169
- def _format_table(self, table_lines: list[str]) -> list[str]:
2170
- """Convert a markdown table to WhatsApp-friendly vertical list format.
2144
+ return WhatsAppRenderer()
2171
2145
 
2172
- Uses a consistent vertical format for all tables to ensure readability
2173
- on all screen sizes and prevent any horizontal overflow issues.
2174
- """
2175
- if not table_lines:
2176
- return []
2146
+ def _format_whatsapp_markdown(self, text: str) -> str:
2147
+ """Convert standard markdown to WhatsApp-compatible formatting using mistune.
2177
2148
 
2178
- # Parse table rows
2179
- rows: list[list[str]] = []
2180
- for line in table_lines:
2181
- if self._is_table_separator(line):
2182
- continue # Skip separator line
2183
- # Remove outer pipes and split
2184
- stripped = line.strip()
2185
- if stripped.startswith("|"):
2186
- stripped = stripped[1:]
2187
- if stripped.endswith("|"):
2188
- stripped = stripped[:-1]
2189
- cells = [cell.strip() for cell in stripped.split("|")]
2190
- rows.append(cells)
2191
-
2192
- if not rows:
2193
- return []
2149
+ WhatsApp supports:
2150
+ - *bold* for bold text
2151
+ - _italic_ for italic text
2152
+ - ~strikethrough~ for strikethrough text
2153
+ - ```code``` for monospace text
2154
+ - No support for headers, tables, or complex markdown structures
2194
2155
 
2195
- # Use vertical list format for all tables (mobile-friendly)
2196
- result: list[str] = []
2197
- result.append("") # Empty line before table
2198
-
2199
- headers = rows[0] if rows else []
2200
-
2201
- for row_idx, row in enumerate(rows[1:], start=1):
2202
- result.append(f"*Item {row_idx}:*")
2203
- for header, value in zip(headers, row):
2204
- result.append(f" • {header}: {value}")
2205
- if row_idx < len(rows) - 1: # Add spacing between items
2206
- result.append("")
2207
-
2208
- result.append("") # Empty line after table
2209
- return result
2210
-
2211
- def _format_header(self, line: str) -> str:
2212
- """Convert markdown header to bold text with decorations."""
2213
- match = re.match(r"^(#+)\s+(.+)$", line.strip())
2214
- if not match:
2215
- return line
2216
-
2217
- level = len(match.group(1))
2218
- text = match.group(2).strip()
2219
-
2220
- if level == 1:
2221
- # H1: Bold text with double line separator
2222
- return f"\n*{text.upper()}*\n{'═' * min(len(text), 30)}"
2223
- elif level == 2:
2224
- # H2: Bold text with single line separator
2225
- return f"\n*{text}*\n{'─' * min(len(text), 30)}"
2226
- else:
2227
- # H3+: Just bold text
2228
- return f"\n*{text}*"
2156
+ This method converts:
2157
+ - Headers (# ## ###) to bold text with separators
2158
+ - Tables to formatted text
2159
+ - Markdown lists to plain text lists (preserving line breaks)
2160
+ - Links to "text (url)" format
2161
+ - Images to descriptive text
2162
+ - Blockquotes to indented text
2163
+ """
2164
+ if not text:
2165
+ return text
2229
2166
 
2230
- def _format_blockquote(self, line: str) -> str:
2231
- """Format blockquote by removing > and adding indentation."""
2232
- # Remove leading > and optional space
2233
- text = re.sub(r"^>\s?", "", line.strip())
2234
- return f" {text}"
2167
+ # Use mistune for markdown parsing
2168
+ try:
2169
+ renderer = self._create_whatsapp_renderer()
2170
+ markdown = mistune.create_markdown(
2171
+ renderer=renderer, plugins=["strikethrough", "table"]
2172
+ )
2173
+ result = markdown(text)
2174
+ # Ensure result is a string
2175
+ if isinstance(result, str):
2176
+ # Clean up extra newlines
2177
+ result = re.sub(r"\n{3,}", "\n\n", result)
2178
+ return result.strip()
2179
+ else:
2180
+ logger.warning(
2181
+ f"[MARKDOWN] Mistune returned non-string result: {type(result)}"
2182
+ )
2183
+ return text
2184
+ except Exception as e:
2185
+ logger.warning(
2186
+ f"[MARKDOWN] Mistune conversion failed, returning original text: {e}"
2187
+ )
2188
+ return text
2235
2189
 
2236
2190
  async def _send_response(
2237
2191
  self,
@@ -3596,6 +3550,8 @@ class WhatsAppBot[T_Schema: WhatsAppResponseBase = WhatsAppResponseBase](BaseMod
3596
3550
  elif msg_content.imageMessage:
3597
3551
  logger.debug("[PARSE_EVOLUTION] Found image message")
3598
3552
  image_msg = msg_content.imageMessage
3553
+ audio_base64 = msg_content.base64 if msg_content.base64 else None
3554
+
3599
3555
  return WhatsAppImageMessage(
3600
3556
  id=message_id,
3601
3557
  from_number=from_number,
@@ -3604,13 +3560,14 @@ class WhatsAppBot[T_Schema: WhatsAppResponseBase = WhatsAppResponseBase](BaseMod
3604
3560
  timestamp=datetime.fromtimestamp(
3605
3561
  (data.messageTimestamp or 0) / 1000
3606
3562
  ),
3607
- media_url=image_msg.url if image_msg else "",
3563
+ media_url=image_msg.url if image_msg.url else "",
3608
3564
  media_mime_type=image_msg.mimetype
3609
3565
  if image_msg and image_msg.mimetype
3610
3566
  else "image/jpeg",
3611
3567
  caption=image_msg.caption
3612
3568
  if image_msg and image_msg.caption
3613
3569
  else "",
3570
+ base64_data=audio_base64
3614
3571
  )
3615
3572
 
3616
3573
  # Handle document messages
@@ -3639,6 +3596,25 @@ class WhatsAppBot[T_Schema: WhatsAppResponseBase = WhatsAppResponseBase](BaseMod
3639
3596
  elif msg_content.audioMessage:
3640
3597
  logger.debug("[PARSE_EVOLUTION] Found audio message")
3641
3598
  audio_msg = msg_content.audioMessage
3599
+
3600
+ # CRITICAL FIX: Check if audio comes with base64 data instead of URL
3601
+ # This happens when WhatsApp sends audio directly in the webhook
3602
+ audio_url = audio_msg.url if audio_msg else ""
3603
+ audio_base64 = msg_content.base64 if msg_content.base64 else None
3604
+
3605
+ if not audio_url and audio_base64:
3606
+ logger.info(
3607
+ "[PARSE_EVOLUTION] 🎵 Audio message has base64 data but no URL - using base64"
3608
+ )
3609
+ elif audio_url:
3610
+ logger.debug(
3611
+ f"[PARSE_EVOLUTION] Audio message has URL: {audio_url[:50]}..."
3612
+ )
3613
+ else:
3614
+ logger.warning(
3615
+ "[PARSE_EVOLUTION] ⚠️ Audio message has neither URL nor base64 data"
3616
+ )
3617
+
3642
3618
  return WhatsAppAudioMessage(
3643
3619
  id=message_id,
3644
3620
  from_number=from_number,
@@ -3647,10 +3623,11 @@ class WhatsAppBot[T_Schema: WhatsAppResponseBase = WhatsAppResponseBase](BaseMod
3647
3623
  timestamp=datetime.fromtimestamp(
3648
3624
  (data.messageTimestamp or 0) / 1000
3649
3625
  ),
3650
- media_url=audio_msg.url if audio_msg else "",
3626
+ media_url=audio_url or "",
3651
3627
  media_mime_type=audio_msg.mimetype
3652
3628
  if audio_msg and audio_msg.mimetype
3653
3629
  else "audio/ogg",
3630
+ base64_data=audio_base64, # Store base64 data if available
3654
3631
  )
3655
3632
  elif msg_content.videoMessage:
3656
3633
  logger.debug("[PARSE_EVOLUTION] Found video message")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentle
3
- Version: 0.9.36
3
+ Version: 0.9.38
4
4
  Summary: ...
5
5
  Author-email: Arthur Brenno <64020210+arthurbrenno@users.noreply.github.com>
6
6
  License-File: LICENSE
@@ -16,6 +16,7 @@ Requires-Dist: html-to-markdown>=2.3.4
16
16
  Requires-Dist: langfuse>=3.8.1
17
17
  Requires-Dist: markitdown>=0.1.3
18
18
  Requires-Dist: mcp[cli]>=1.6.0
19
+ Requires-Dist: mistune>=3.1.4
19
20
  Requires-Dist: orjson>=3.11.3
20
21
  Requires-Dist: pydantic>=2.11.3
21
22
  Requires-Dist: pypdf2>=3.0.1
@@ -94,7 +94,7 @@ agentle/agents/apis/primitive_schema.py,sha256=fmDiMrXxtixORKIDvg8dx_h7w1AqPXpFO
94
94
  agentle/agents/apis/rate_limit_error.py,sha256=9ukRInSwf6PPEg5fPLm-eq4xfgYOExraOgGCRKzVbQk,116
95
95
  agentle/agents/apis/rate_limiter.py,sha256=EZ-39YMDNeotygdvxP3B735cJexSk0-20zO0pxaGKiU,1842
96
96
  agentle/agents/apis/request_config.py,sha256=vHKtoW9uxh2lTsuuZ38B2_EMcrLmOtUvVhL8VkOxjZY,4850
97
- agentle/agents/apis/request_hook.py,sha256=C9p6obF0myGkHoIScWuJTMp2RZlg3eEsbn80mDlE_sw,369
97
+ agentle/agents/apis/request_hook.py,sha256=32ePmlS-Gwq3VcgogJHluBuBcunzZSUXv8E2inMnPiA,334
98
98
  agentle/agents/apis/response_cache.py,sha256=l-Ec_YVf1phhaTKWNdAORBDWZS3pXK_BBUHGP-oQkqQ,1617
99
99
  agentle/agents/apis/retry_strategy.py,sha256=_W8ZXXmA0kPFLJ0uwwV9ZycmSnj4dsicisFrbN8FtAU,219
100
100
  agentle/agents/apis/params/__init__.py,sha256=7aXlrfQTXpGT2GxuyugnFsF74l4AszSkFgxb0keyPRE,383
@@ -137,27 +137,27 @@ agentle/agents/ui/__init__.py,sha256=IjHRV0k2DNwvFrEHebmsXiBvmITE8nQUnsR07h9tVkU
137
137
  agentle/agents/ui/streamlit.py,sha256=9afICL0cxtG1o2pWh6vH39-NdKiVfADKiXo405F2aB0,42829
138
138
  agentle/agents/whatsapp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
139
139
  agentle/agents/whatsapp/human_delay_calculator.py,sha256=BGCDeoNTPsMn4d_QYmG0BWGCG8SiUJC6Fk295ulAsAk,18268
140
- agentle/agents/whatsapp/whatsapp_bot.py,sha256=tF35s2c4G9Fo0bLVmePYXWSnSNEgg-Rpi9V0MrrRCCA,164948
140
+ agentle/agents/whatsapp/whatsapp_bot.py,sha256=3ohpjdjeeX0MPxrcllLamT8lfi6y2UjSzFxYTeVLQ-o,165392
141
141
  agentle/agents/whatsapp/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
142
- agentle/agents/whatsapp/models/audio_message.py,sha256=kUqG1HdNW6DCYD-CqscJ6WHlAyv9ufmTSKMdjio9XWk,2705
142
+ agentle/agents/whatsapp/models/audio_message.py,sha256=af2apMWzxKcCtXfQN6U2qfOFoiwRj0nCUrKmrBD0whE,3067
143
143
  agentle/agents/whatsapp/models/context_info.py,sha256=sk80KuNE36S6VRnLh7n6UXmzZCXIB4E4lNxnRyVizg8,563
144
144
  agentle/agents/whatsapp/models/data.py,sha256=fcq4AUKtGP7R_pze5k_GXNjEsH6FC2QCfSSHo1d3jmg,1274
145
145
  agentle/agents/whatsapp/models/device_list_metadata.py,sha256=Nki7esmgi9Zq70L2t4yjJH77clvaljStchVGrXiDlbU,808
146
- agentle/agents/whatsapp/models/document_message.py,sha256=aZaJCp33GwP1fP5j2DVSWBSVQ4udoE0vj21mc3zjbWQ,2502
146
+ agentle/agents/whatsapp/models/document_message.py,sha256=1sEzoAy7e5pnP05O5B0t3J-Q1q29Xk-tUr-JFjtJW60,2777
147
147
  agentle/agents/whatsapp/models/downloaded_media.py,sha256=TAZEX2oyXp4m4Dk8eRcgoaQCjDyUTN3E6BqkA4pIB9k,204
148
- agentle/agents/whatsapp/models/image_message.py,sha256=9inTFNzJq-VJbUaUNzeHfrI7l6Qx7gSM5ydNF0FuRLY,3859
148
+ agentle/agents/whatsapp/models/image_message.py,sha256=Wf2-jyf_edXzQmpZr1I-DU236WZeSSvcpZyZry4WrkA,4171
149
149
  agentle/agents/whatsapp/models/key.py,sha256=1jlaI--WYxtQjt05Azjy0DCrSeHahvcKsr2NGOgxW9I,472
150
150
  agentle/agents/whatsapp/models/message.py,sha256=HqCqra8vOn3fig53SxnxPgtph20tBEjODlXTLiSErps,1446
151
151
  agentle/agents/whatsapp/models/message_context_info.py,sha256=msCSuu8uMN3G9GDaXdP6mefmOxSjeY-7iL-gN6Q9-i8,741
152
152
  agentle/agents/whatsapp/models/quoted_message.py,sha256=QC4sp7eLPE9g9i-_f3avb0sDO7gKpkzZR2qkbxqptts,1073
153
- agentle/agents/whatsapp/models/video_message.py,sha256=-wujSwdYaE3tst7K_rUYCvx6v4lySTW1JZ5burF6slg,3422
153
+ agentle/agents/whatsapp/models/video_message.py,sha256=0s4ak68euff25a_tXvYscKCFn7rQj8Rj6U89rupQnO0,3697
154
154
  agentle/agents/whatsapp/models/whatsapp_audio_message.py,sha256=AAcnjzJC1O5VjyWZaSWpG_tmZFc2-CdcPn9abjyLrpc,378
155
155
  agentle/agents/whatsapp/models/whatsapp_bot_config.py,sha256=hkbOAdSZbSt634mZEARuPQSer7wf3biL7dL9TFW-a7o,37164
156
156
  agentle/agents/whatsapp/models/whatsapp_contact.py,sha256=6iO6xmFs7z9hd1N9kZzGyNHYvCaUoCHn3Yi1DAJN4YU,240
157
157
  agentle/agents/whatsapp/models/whatsapp_document_message.py,sha256=ECM_hXF-3IbC9itbtZI0eA_XRNXFVefw9Mr-Lo_lrH0,323
158
158
  agentle/agents/whatsapp/models/whatsapp_image_message.py,sha256=xOAPRRSgqj9gQ2ZZOGdFWfOgtmNpE1W8mIUAmB5YTpo,314
159
159
  agentle/agents/whatsapp/models/whatsapp_location_message.py,sha256=CJCJR1DHjrN92OloNopvUteUxUxeEpHlWQSJhj6Ive4,407
160
- agentle/agents/whatsapp/models/whatsapp_media_message.py,sha256=-FY15vezGILzGMiUlO5irZl2jg3SZCawsE8PXm_9A0A,296
160
+ agentle/agents/whatsapp/models/whatsapp_media_message.py,sha256=xmYE0SVdL7UIJuYRWJTfCoGAd5-pyXBnueaxFaECuXg,386
161
161
  agentle/agents/whatsapp/models/whatsapp_message.py,sha256=QtGAJKOF1ykZycsNDld25gk-JUeg3uV7hNXx0ZXO0Rg,1217
162
162
  agentle/agents/whatsapp/models/whatsapp_message_status.py,sha256=jDWShdvSve5EhkgtDkh1jZmpRVNoXCokv4M6at1eSIU,214
163
163
  agentle/agents/whatsapp/models/whatsapp_message_type.py,sha256=GctIGOC1Bc_D_L0ehEmEwgxePFx0ioTEUoBlZEdxdG8,279
@@ -165,7 +165,7 @@ agentle/agents/whatsapp/models/whatsapp_response_base.py,sha256=IIDONx9Ipt593tAZ
165
165
  agentle/agents/whatsapp/models/whatsapp_session.py,sha256=9G1HC-A2G9jTdpwYy3w9bnYkOGK2vvA7kdYAf32oWMU,15640
166
166
  agentle/agents/whatsapp/models/whatsapp_text_message.py,sha256=GpSwFrPC4qpQlVCWKKgYjQJKNv0qvwgYfuoD3ttLzdQ,441
167
167
  agentle/agents/whatsapp/models/whatsapp_video_message.py,sha256=-d-4hnkkxyLVNoje3a1pOEAvzWqoCLFcBn70wUpnyXY,346
168
- agentle/agents/whatsapp/models/whatsapp_webhook_payload.py,sha256=xL8p10ICuGjyplJ42K_YLqmMf0EoThI8YPJ8c7jrFyk,4122
168
+ agentle/agents/whatsapp/models/whatsapp_webhook_payload.py,sha256=aY9pnUt4WJdvrRsXZIkmR8hP7oV9gdOJ1wJiYzFhU8w,4270
169
169
  agentle/agents/whatsapp/providers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
170
170
  agentle/agents/whatsapp/providers/base/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
171
171
  agentle/agents/whatsapp/providers/base/whatsapp_provider.py,sha256=Iaywrv0xer4fhZprMttC-NP4-rRYdU_45UzIZQ7dkYA,5349
@@ -1018,7 +1018,7 @@ agentle/web/actions/scroll.py,sha256=WqVVAORNDK3BL1oASZBPmXJYeSVkPgAOmWA8ibYO82I
1018
1018
  agentle/web/actions/viewport.py,sha256=KCwm88Pri19Qc6GLHC69HsRxmdJz1gEEAODfggC_fHo,287
1019
1019
  agentle/web/actions/wait.py,sha256=IKEywjf-KC4ni9Gkkv4wgc7bY-hk7HwD4F-OFWlyf2w,571
1020
1020
  agentle/web/actions/write_text.py,sha256=9mxfHcpKs_L7BsDnJvOYHQwG8M0GWe61SRJAsKk3xQ8,748
1021
- agentle-0.9.36.dist-info/METADATA,sha256=hSUSyQ9vk4kdTs91_EW7Yb1tEi41flD2JGVlqugY4YA,86849
1022
- agentle-0.9.36.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
1023
- agentle-0.9.36.dist-info/licenses/LICENSE,sha256=T90S9vqRS6qP-voULxAcvwEs558wRRo6dHuZrjgcOUI,1085
1024
- agentle-0.9.36.dist-info/RECORD,,
1021
+ agentle-0.9.38.dist-info/METADATA,sha256=NKHAE9t1G-BjMF1bAHOidMghnkTT0kuN42jjlwn4MwM,86879
1022
+ agentle-0.9.38.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
1023
+ agentle-0.9.38.dist-info/licenses/LICENSE,sha256=T90S9vqRS6qP-voULxAcvwEs558wRRo6dHuZrjgcOUI,1085
1024
+ agentle-0.9.38.dist-info/RECORD,,