agentle 0.9.35__py3-none-any.whl → 0.9.37__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 +11 -9
- 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 +177 -203
- agentle/generations/providers/openrouter/_adapters/agentle_message_to_openrouter_message_adapter.py +127 -7
- agentle/generations/providers/openrouter/openrouter_generation_provider.py +17 -8
- {agentle-0.9.35.dist-info → agentle-0.9.37.dist-info}/METADATA +2 -1
- {agentle-0.9.35.dist-info → agentle-0.9.37.dist-info}/RECORD +14 -14
- {agentle-0.9.35.dist-info → agentle-0.9.37.dist-info}/WHEEL +0 -0
- {agentle-0.9.35.dist-info → agentle-0.9.37.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]
|
|
@@ -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
|
-
"""
|
|
2033
|
-
|
|
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
|
|
2051
|
-
|
|
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
|
|
2072
|
+
def _create_whatsapp_renderer(self) -> mistune.HTMLRenderer:
|
|
2073
|
+
"""Create a mistune renderer for WhatsApp formatting."""
|
|
2069
2074
|
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
processed_lines.append(line)
|
|
2073
|
-
i += 1
|
|
2074
|
-
continue
|
|
2075
|
+
class WhatsAppRenderer(mistune.HTMLRenderer):
|
|
2076
|
+
"""Custom renderer for WhatsApp markdown format."""
|
|
2075
2077
|
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
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
|
|
2140
|
+
def text(self, text: str) -> str:
|
|
2141
|
+
"""Render plain text."""
|
|
2142
|
+
return text
|
|
2163
2143
|
|
|
2164
|
-
|
|
2165
|
-
"""Check if a line is a table row."""
|
|
2166
|
-
stripped = line.strip()
|
|
2167
|
-
return stripped.startswith("|") and stripped.endswith("|")
|
|
2144
|
+
return WhatsAppRenderer()
|
|
2168
2145
|
|
|
2169
|
-
def
|
|
2170
|
-
"""Convert
|
|
2171
|
-
|
|
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
|
-
|
|
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,
|
|
@@ -3639,6 +3593,25 @@ class WhatsAppBot[T_Schema: WhatsAppResponseBase = WhatsAppResponseBase](BaseMod
|
|
|
3639
3593
|
elif msg_content.audioMessage:
|
|
3640
3594
|
logger.debug("[PARSE_EVOLUTION] Found audio message")
|
|
3641
3595
|
audio_msg = msg_content.audioMessage
|
|
3596
|
+
|
|
3597
|
+
# CRITICAL FIX: Check if audio comes with base64 data instead of URL
|
|
3598
|
+
# This happens when WhatsApp sends audio directly in the webhook
|
|
3599
|
+
audio_url = audio_msg.url if audio_msg else ""
|
|
3600
|
+
audio_base64 = msg_content.base64 if msg_content.base64 else None
|
|
3601
|
+
|
|
3602
|
+
if not audio_url and audio_base64:
|
|
3603
|
+
logger.info(
|
|
3604
|
+
"[PARSE_EVOLUTION] 🎵 Audio message has base64 data but no URL - using base64"
|
|
3605
|
+
)
|
|
3606
|
+
elif audio_url:
|
|
3607
|
+
logger.debug(
|
|
3608
|
+
f"[PARSE_EVOLUTION] Audio message has URL: {audio_url[:50]}..."
|
|
3609
|
+
)
|
|
3610
|
+
else:
|
|
3611
|
+
logger.warning(
|
|
3612
|
+
"[PARSE_EVOLUTION] ⚠️ Audio message has neither URL nor base64 data"
|
|
3613
|
+
)
|
|
3614
|
+
|
|
3642
3615
|
return WhatsAppAudioMessage(
|
|
3643
3616
|
id=message_id,
|
|
3644
3617
|
from_number=from_number,
|
|
@@ -3647,10 +3620,11 @@ class WhatsAppBot[T_Schema: WhatsAppResponseBase = WhatsAppResponseBase](BaseMod
|
|
|
3647
3620
|
timestamp=datetime.fromtimestamp(
|
|
3648
3621
|
(data.messageTimestamp or 0) / 1000
|
|
3649
3622
|
),
|
|
3650
|
-
media_url=
|
|
3623
|
+
media_url=audio_url or "",
|
|
3651
3624
|
media_mime_type=audio_msg.mimetype
|
|
3652
3625
|
if audio_msg and audio_msg.mimetype
|
|
3653
3626
|
else "audio/ogg",
|
|
3627
|
+
base64_data=audio_base64, # Store base64 data if available
|
|
3654
3628
|
)
|
|
3655
3629
|
elif msg_content.videoMessage:
|
|
3656
3630
|
logger.debug("[PARSE_EVOLUTION] Found video message")
|
agentle/generations/providers/openrouter/_adapters/agentle_message_to_openrouter_message_adapter.py
CHANGED
|
@@ -29,14 +29,17 @@ from agentle.generations.providers.openrouter._types import (
|
|
|
29
29
|
OpenRouterMessage,
|
|
30
30
|
OpenRouterSystemMessage,
|
|
31
31
|
OpenRouterToolCall,
|
|
32
|
+
OpenRouterToolMessage,
|
|
32
33
|
OpenRouterUserMessage,
|
|
33
34
|
)
|
|
35
|
+
from agentle.generations.tools.tool_execution_result import ToolExecutionResult
|
|
36
|
+
import json
|
|
34
37
|
|
|
35
38
|
|
|
36
39
|
class AgentleMessageToOpenRouterMessageAdapter(
|
|
37
40
|
Adapter[
|
|
38
41
|
AssistantMessage | DeveloperMessage | UserMessage,
|
|
39
|
-
OpenRouterMessage,
|
|
42
|
+
OpenRouterMessage | list[OpenRouterMessage],
|
|
40
43
|
]
|
|
41
44
|
):
|
|
42
45
|
"""
|
|
@@ -44,15 +47,18 @@ class AgentleMessageToOpenRouterMessageAdapter(
|
|
|
44
47
|
|
|
45
48
|
Handles conversion of:
|
|
46
49
|
- DeveloperMessage -> OpenRouterSystemMessage
|
|
47
|
-
- UserMessage -> OpenRouterUserMessage
|
|
50
|
+
- UserMessage -> OpenRouterUserMessage (or OpenRouterToolMessage if contains tool results)
|
|
48
51
|
- AssistantMessage -> OpenRouterAssistantMessage (with tool calls)
|
|
52
|
+
|
|
53
|
+
Note: When a message contains ToolExecutionResult parts, they are extracted
|
|
54
|
+
and returned as separate OpenRouterToolMessage objects.
|
|
49
55
|
"""
|
|
50
56
|
|
|
51
57
|
@override
|
|
52
58
|
def adapt(
|
|
53
59
|
self,
|
|
54
60
|
_f: AssistantMessage | DeveloperMessage | UserMessage,
|
|
55
|
-
) -> OpenRouterMessage:
|
|
61
|
+
) -> OpenRouterMessage | list[OpenRouterMessage]:
|
|
56
62
|
"""
|
|
57
63
|
Convert an Agentle message to OpenRouter format.
|
|
58
64
|
|
|
@@ -60,7 +66,9 @@ class AgentleMessageToOpenRouterMessageAdapter(
|
|
|
60
66
|
_f: The Agentle message to convert.
|
|
61
67
|
|
|
62
68
|
Returns:
|
|
63
|
-
The corresponding OpenRouter message.
|
|
69
|
+
The corresponding OpenRouter message(s). Returns a list when the message
|
|
70
|
+
contains ToolExecutionResult parts that need to be split into separate
|
|
71
|
+
tool messages.
|
|
64
72
|
"""
|
|
65
73
|
message = _f
|
|
66
74
|
part_adapter = AgentlePartToOpenRouterPartAdapter()
|
|
@@ -76,12 +84,28 @@ class AgentleMessageToOpenRouterMessageAdapter(
|
|
|
76
84
|
)
|
|
77
85
|
|
|
78
86
|
case UserMessage():
|
|
87
|
+
# Check if this message contains tool execution results
|
|
88
|
+
tool_results = [
|
|
89
|
+
p for p in message.parts if isinstance(p, ToolExecutionResult)
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
if tool_results:
|
|
93
|
+
# Convert each tool result to a separate tool message
|
|
94
|
+
return [
|
|
95
|
+
OpenRouterToolMessage(
|
|
96
|
+
role="tool",
|
|
97
|
+
tool_call_id=result.suggestion.id,
|
|
98
|
+
content=self._serialize_tool_result(result.result),
|
|
99
|
+
)
|
|
100
|
+
for result in tool_results
|
|
101
|
+
]
|
|
102
|
+
|
|
79
103
|
# User messages can have multimodal content
|
|
80
|
-
# Filter out non-content parts (like tool execution suggestions)
|
|
104
|
+
# Filter out non-content parts (like tool execution suggestions and results)
|
|
81
105
|
content_parts = [
|
|
82
106
|
p
|
|
83
107
|
for p in message.parts
|
|
84
|
-
if not isinstance(p, ToolExecutionSuggestion)
|
|
108
|
+
if not isinstance(p, (ToolExecutionSuggestion, ToolExecutionResult))
|
|
85
109
|
]
|
|
86
110
|
|
|
87
111
|
# If only text parts, concatenate into a string
|
|
@@ -102,6 +126,69 @@ class AgentleMessageToOpenRouterMessageAdapter(
|
|
|
102
126
|
)
|
|
103
127
|
|
|
104
128
|
case AssistantMessage():
|
|
129
|
+
# Check if this message contains tool execution results
|
|
130
|
+
tool_results = [
|
|
131
|
+
p for p in message.parts if isinstance(p, ToolExecutionResult)
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
if tool_results:
|
|
135
|
+
# If assistant message has tool results, we need to split it
|
|
136
|
+
# First, create the assistant message with tool calls (if any)
|
|
137
|
+
messages: list[OpenRouterMessage] = []
|
|
138
|
+
|
|
139
|
+
# Separate text content from tool calls
|
|
140
|
+
text_parts = [p for p in message.parts if isinstance(p, TextPart)]
|
|
141
|
+
tool_suggestions = [
|
|
142
|
+
p
|
|
143
|
+
for p in message.parts
|
|
144
|
+
if isinstance(p, ToolExecutionSuggestion)
|
|
145
|
+
]
|
|
146
|
+
|
|
147
|
+
# Only create assistant message if there's content or tool calls
|
|
148
|
+
if text_parts or tool_suggestions:
|
|
149
|
+
content = (
|
|
150
|
+
"".join(str(p) for p in text_parts) if text_parts else None
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
tool_calls: list[OpenRouterToolCall] = [
|
|
154
|
+
OpenRouterToolCall(
|
|
155
|
+
id=suggestion.id,
|
|
156
|
+
type="function",
|
|
157
|
+
function={
|
|
158
|
+
"name": suggestion.tool_name,
|
|
159
|
+
"arguments": self._serialize_tool_arguments(
|
|
160
|
+
suggestion.args
|
|
161
|
+
),
|
|
162
|
+
},
|
|
163
|
+
)
|
|
164
|
+
for suggestion in tool_suggestions
|
|
165
|
+
]
|
|
166
|
+
|
|
167
|
+
assistant_msg = OpenRouterAssistantMessage(
|
|
168
|
+
role="assistant",
|
|
169
|
+
content=content,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
if tool_calls:
|
|
173
|
+
assistant_msg["tool_calls"] = tool_calls
|
|
174
|
+
|
|
175
|
+
if hasattr(message, "reasoning") and message.reasoning:
|
|
176
|
+
assistant_msg["reasoning"] = message.reasoning
|
|
177
|
+
|
|
178
|
+
messages.append(assistant_msg)
|
|
179
|
+
|
|
180
|
+
# Add tool result messages
|
|
181
|
+
for result in tool_results:
|
|
182
|
+
messages.append(
|
|
183
|
+
OpenRouterToolMessage(
|
|
184
|
+
role="tool",
|
|
185
|
+
tool_call_id=result.suggestion.id,
|
|
186
|
+
content=self._serialize_tool_result(result.result),
|
|
187
|
+
)
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
return messages
|
|
191
|
+
|
|
105
192
|
# Separate text content from tool calls
|
|
106
193
|
text_parts = [p for p in message.parts if isinstance(p, TextPart)]
|
|
107
194
|
tool_suggestions = [
|
|
@@ -118,7 +205,9 @@ class AgentleMessageToOpenRouterMessageAdapter(
|
|
|
118
205
|
type="function",
|
|
119
206
|
function={
|
|
120
207
|
"name": suggestion.tool_name,
|
|
121
|
-
"arguments":
|
|
208
|
+
"arguments": self._serialize_tool_arguments(
|
|
209
|
+
suggestion.args
|
|
210
|
+
),
|
|
122
211
|
},
|
|
123
212
|
)
|
|
124
213
|
for suggestion in tool_suggestions
|
|
@@ -137,3 +226,34 @@ class AgentleMessageToOpenRouterMessageAdapter(
|
|
|
137
226
|
result["reasoning"] = message.reasoning
|
|
138
227
|
|
|
139
228
|
return result
|
|
229
|
+
|
|
230
|
+
def _serialize_tool_arguments(self, args: object) -> str:
|
|
231
|
+
"""
|
|
232
|
+
Serialize tool arguments to JSON string.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
args: The arguments to serialize.
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
JSON string representation of the arguments.
|
|
239
|
+
"""
|
|
240
|
+
if isinstance(args, str):
|
|
241
|
+
return args
|
|
242
|
+
return json.dumps(args)
|
|
243
|
+
|
|
244
|
+
def _serialize_tool_result(self, result: object) -> str:
|
|
245
|
+
"""
|
|
246
|
+
Serialize tool execution result to string.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
result: The result to serialize.
|
|
250
|
+
|
|
251
|
+
returns:
|
|
252
|
+
String representation of the result.
|
|
253
|
+
"""
|
|
254
|
+
if isinstance(result, str):
|
|
255
|
+
return result
|
|
256
|
+
try:
|
|
257
|
+
return json.dumps(result)
|
|
258
|
+
except (TypeError, ValueError):
|
|
259
|
+
return str(result)
|
|
@@ -71,6 +71,7 @@ from agentle.generations.providers.openrouter._types import (
|
|
|
71
71
|
OpenRouterFileParserPlugin,
|
|
72
72
|
OpenRouterModelsResponse,
|
|
73
73
|
OpenRouterModel,
|
|
74
|
+
OpenRouterMessage,
|
|
74
75
|
)
|
|
75
76
|
from agentle.generations.providers.openrouter.error_handler import (
|
|
76
77
|
parse_and_raise_openrouter_error,
|
|
@@ -1247,10 +1248,14 @@ class OpenRouterGenerationProvider(GenerationProvider):
|
|
|
1247
1248
|
),
|
|
1248
1249
|
)
|
|
1249
1250
|
|
|
1250
|
-
# Convert messages
|
|
1251
|
-
openrouter_messages = [
|
|
1252
|
-
|
|
1253
|
-
|
|
1251
|
+
# Convert messages - adapter may return single message or list of messages
|
|
1252
|
+
openrouter_messages: list[OpenRouterMessage] = []
|
|
1253
|
+
for message in messages_list:
|
|
1254
|
+
adapted = self.message_adapter.adapt(message)
|
|
1255
|
+
if isinstance(adapted, list):
|
|
1256
|
+
openrouter_messages.extend(adapted)
|
|
1257
|
+
else:
|
|
1258
|
+
openrouter_messages.append(adapted)
|
|
1254
1259
|
|
|
1255
1260
|
# Convert tools if provided
|
|
1256
1261
|
openrouter_tools = (
|
|
@@ -1404,10 +1409,14 @@ class OpenRouterGenerationProvider(GenerationProvider):
|
|
|
1404
1409
|
"""
|
|
1405
1410
|
_generation_config = self._normalize_generation_config(generation_config)
|
|
1406
1411
|
|
|
1407
|
-
# Convert messages
|
|
1408
|
-
openrouter_messages = [
|
|
1409
|
-
|
|
1410
|
-
|
|
1412
|
+
# Convert messages - adapter may return single message or list of messages
|
|
1413
|
+
openrouter_messages: list[OpenRouterMessage] = []
|
|
1414
|
+
for message in messages:
|
|
1415
|
+
adapted = self.message_adapter.adapt(message)
|
|
1416
|
+
if isinstance(adapted, list):
|
|
1417
|
+
openrouter_messages.extend(adapted)
|
|
1418
|
+
else:
|
|
1419
|
+
openrouter_messages.append(adapted)
|
|
1411
1420
|
|
|
1412
1421
|
# Convert tools if provided
|
|
1413
1422
|
openrouter_tools = (
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentle
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.37
|
|
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=_SL-fsEmfPBQKRSV8e3aH1d9bl5Lj84pEYnLmNne06E,165252
|
|
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=f7wOgKIaMdq8cLe0HVJS0T9NK9eNqe-gUCBMP_eQfyg,4142
|
|
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
|
|
@@ -355,9 +355,9 @@ agentle/generations/providers/openrouter/__init__.py,sha256=47DEQpj8HBSa-_TImW-5
|
|
|
355
355
|
agentle/generations/providers/openrouter/_types.py,sha256=VSAf1auxopTqhYwLHPWl_Q0-okWUfLKJa0JmWlAFuGg,10557
|
|
356
356
|
agentle/generations/providers/openrouter/error_handler.py,sha256=4qm8v_cjdrB-59UXyCJLnIkOxzIfmsltZY8Q137-8Qg,8075
|
|
357
357
|
agentle/generations/providers/openrouter/exceptions.py,sha256=o3_-tuyhewc0v5L2cAXH0f9ixHyyDgZbHq0KX5cUyPE,23179
|
|
358
|
-
agentle/generations/providers/openrouter/openrouter_generation_provider.py,sha256=
|
|
358
|
+
agentle/generations/providers/openrouter/openrouter_generation_provider.py,sha256=XhXU_Ix10jmWl2SzRUHwsVSPRIWq6zHnia7IMaY6Yy4,67129
|
|
359
359
|
agentle/generations/providers/openrouter/_adapters/__init__.py,sha256=orgZeEBqH4X_cpyOMiClvfZHY5cLwLNqhAYLqNjGIH4,1826
|
|
360
|
-
agentle/generations/providers/openrouter/_adapters/agentle_message_to_openrouter_message_adapter.py,sha256=
|
|
360
|
+
agentle/generations/providers/openrouter/_adapters/agentle_message_to_openrouter_message_adapter.py,sha256=IvaovVP0ChYEreysiDyqF8SXdXxnD_Y9MjA2raEoncw,9790
|
|
361
361
|
agentle/generations/providers/openrouter/_adapters/agentle_part_to_openrouter_part_adapter.py,sha256=eIfwQQ9BokHy3FQ90GJ4i_J3L46XCiSBd1RWnzu-gAo,5967
|
|
362
362
|
agentle/generations/providers/openrouter/_adapters/agentle_tool_to_openrouter_tool_adapter.py,sha256=41i3B6awaTNZsdQ46Oi1e10cuNhQqWL-KBJ5V_sHkiI,9547
|
|
363
363
|
agentle/generations/providers/openrouter/_adapters/openrouter_message_to_generated_assistant_message_adapter.py,sha256=uB4TYc0fuwsoYdRnEnLhbwQISyaZ2Z2RWkPFGQXUc80,5295
|
|
@@ -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.37.dist-info/METADATA,sha256=ExGCJbs2vKKmRLF8LSq-9K5Xty2ggfXYhqnUJxNollM,86879
|
|
1022
|
+
agentle-0.9.37.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
1023
|
+
agentle-0.9.37.dist-info/licenses/LICENSE,sha256=T90S9vqRS6qP-voULxAcvwEs558wRRo6dHuZrjgcOUI,1085
|
|
1024
|
+
agentle-0.9.37.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|