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.
- agentle/agents/apis/request_hook.py +0 -1
- agentle/agents/whatsapp/models/audio_message.py +8 -4
- agentle/agents/whatsapp/models/document_message.py +9 -7
- agentle/agents/whatsapp/models/image_message.py +12 -10
- agentle/agents/whatsapp/models/video_message.py +9 -7
- agentle/agents/whatsapp/models/whatsapp_media_message.py +1 -0
- agentle/agents/whatsapp/models/whatsapp_webhook_payload.py +6 -2
- agentle/agents/whatsapp/whatsapp_bot.py +181 -204
- {agentle-0.9.36.dist-info → agentle-0.9.38.dist-info}/METADATA +2 -1
- {agentle-0.9.36.dist-info → agentle-0.9.38.dist-info}/RECORD +12 -12
- {agentle-0.9.36.dist-info → agentle-0.9.38.dist-info}/WHEEL +0 -0
- {agentle-0.9.36.dist-info → agentle-0.9.38.dist-info}/licenses/LICENSE +0 -0
|
@@ -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)
|
|
72
|
-
|
|
73
|
-
|
|
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)
|
|
67
|
-
|
|
68
|
-
|
|
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)
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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)
|
|
83
|
-
|
|
84
|
-
|
|
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]
|
|
@@ -83,8 +83,12 @@ class WhatsAppWebhookPayload(BaseModel):
|
|
|
83
83
|
|
|
84
84
|
key = self.data.key
|
|
85
85
|
if "@lid" in key.remoteJid:
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
1710
|
-
|
|
1711
|
-
)
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
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
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
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
|
|
2032
|
-
"""
|
|
2072
|
+
def _create_whatsapp_renderer(self) -> mistune.HTMLRenderer:
|
|
2073
|
+
"""Create a mistune renderer for WhatsApp formatting."""
|
|
2033
2074
|
|
|
2034
|
-
|
|
2035
|
-
|
|
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
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
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
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
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
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2095
|
+
def strikethrough(self, text: str) -> str:
|
|
2096
|
+
"""Render strikethrough as WhatsApp strikethrough."""
|
|
2097
|
+
return f"~{text}~"
|
|
2117
2098
|
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2099
|
+
def codespan(self, text: str) -> str:
|
|
2100
|
+
"""Render inline code as WhatsApp monospace."""
|
|
2101
|
+
return f"```{text}```"
|
|
2121
2102
|
|
|
2122
|
-
|
|
2123
|
-
|
|
2103
|
+
def block_code(self, code: str, info: str | None = None) -> str:
|
|
2104
|
+
"""Render code blocks."""
|
|
2105
|
+
return f"```{code}```\n"
|
|
2124
2106
|
|
|
2125
|
-
|
|
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
|
-
|
|
2128
|
-
|
|
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
|
-
|
|
2131
|
-
|
|
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
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2120
|
+
def list(self, text: str, ordered: bool, **attrs: Any) -> str:
|
|
2121
|
+
"""Render lists."""
|
|
2122
|
+
return f"\n{text}"
|
|
2136
2123
|
|
|
2137
|
-
|
|
2138
|
-
|
|
2124
|
+
def list_item(self, text: str, **attrs: Any) -> str:
|
|
2125
|
+
"""Render list items."""
|
|
2126
|
+
return f"{text}\n"
|
|
2139
2127
|
|
|
2140
|
-
|
|
2141
|
-
|
|
2128
|
+
def paragraph(self, text: str) -> str:
|
|
2129
|
+
"""Render paragraphs."""
|
|
2130
|
+
return f"{text}\n\n"
|
|
2142
2131
|
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2132
|
+
def thematic_break(self) -> str:
|
|
2133
|
+
"""Render horizontal rules."""
|
|
2134
|
+
return "─" * 30 + "\n"
|
|
2146
2135
|
|
|
2147
|
-
|
|
2136
|
+
def linebreak(self) -> str:
|
|
2137
|
+
"""Render line breaks."""
|
|
2138
|
+
return "\n"
|
|
2148
2139
|
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
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
|
-
|
|
2170
|
-
"""Convert a markdown table to WhatsApp-friendly vertical list format.
|
|
2144
|
+
return WhatsAppRenderer()
|
|
2171
2145
|
|
|
2172
|
-
|
|
2173
|
-
|
|
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
|
-
|
|
2179
|
-
|
|
2180
|
-
for
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
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
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
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
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
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=
|
|
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.
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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
|
|
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
|
|
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=
|
|
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.
|
|
1022
|
-
agentle-0.9.
|
|
1023
|
-
agentle-0.9.
|
|
1024
|
-
agentle-0.9.
|
|
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,,
|
|
File without changes
|
|
File without changes
|