|
33 | 33 | from agentle.agents.whatsapp.models.whatsapp_image_message import WhatsAppImageMessage |
34 | 34 | from agentle.agents.whatsapp.models.whatsapp_media_message import WhatsAppMediaMessage |
35 | 35 | from agentle.agents.whatsapp.models.whatsapp_message import WhatsAppMessage |
| 36 | +from agentle.agents.whatsapp.models.whatsapp_response_base import WhatsAppResponseBase |
36 | 37 | from agentle.agents.whatsapp.models.whatsapp_session import WhatsAppSession |
37 | 38 | from agentle.agents.whatsapp.models.whatsapp_text_message import WhatsAppTextMessage |
38 | 39 | from agentle.agents.whatsapp.models.whatsapp_video_message import WhatsAppVideoMessage |
@@ -128,14 +129,42 @@ class CallbackWithContext: |
128 | 129 | context: dict[str, Any] = field(default_factory=dict) |
129 | 130 |
|
130 | 131 |
|
131 | | -class WhatsAppBot(BaseModel): |
| 132 | +class WhatsAppBot[T_Schema: WhatsAppResponseBase = WhatsAppResponseBase](BaseModel): |
132 | 133 | """ |
133 | 134 | WhatsApp bot that wraps an Agentle agent with enhanced message batching and spam protection. |
134 | 135 |
|
135 | | - Now uses the Agent's conversation store directly instead of managing contexts separately. |
| 136 | + Now supports structured outputs through generic type parameter T_Schema. |
| 137 | + The schema must extend WhatsAppResponseBase to ensure a 'response' field is always present. |
| 138 | +
|
| 139 | + Examples: |
| 140 | + ```python |
| 141 | + # Basic usage (no structured output) |
| 142 | + agent = Agent(...) |
| 143 | + bot = WhatsAppBot(agent=agent, provider=provider) |
| 144 | +
|
| 145 | + # With structured output |
| 146 | + class MyResponse(WhatsAppResponseBase): |
| 147 | + sentiment: Literal["happy", "sad", "neutral"] |
| 148 | + urgency_level: int |
| 149 | +
|
| 150 | + agent = Agent[MyResponse]( |
| 151 | + response_schema=MyResponse, |
| 152 | + instructions="Extract sentiment and urgency from the conversation..." |
| 153 | + ) |
| 154 | + bot = WhatsAppBot[MyResponse](agent=agent, provider=provider) |
| 155 | +
|
| 156 | + # Access structured data in callbacks |
| 157 | + async def my_callback(phone, chat_id, response, context): |
| 158 | + if response and response.parsed: |
| 159 | + print(f"Sentiment: {response.parsed.sentiment}") |
| 160 | + print(f"Urgency: {response.parsed.urgency_level}") |
| 161 | + # response.parsed.response is automatically sent to WhatsApp |
| 162 | +
|
| 163 | + bot.add_response_callback(my_callback) |
| 164 | + ``` |
136 | 165 | """ |
137 | 166 |
|
138 | | - agent: Agent[Any] |
| 167 | + agent: Agent[T_Schema] |
139 | 168 | provider: WhatsAppProvider |
140 | 169 | tts_provider: TtsProvider | None = Field(default=None) |
141 | 170 | file_storage_manager: FileStorageManager | None = Field(default=None) |
@@ -1302,7 +1331,7 @@ async def _batch_processor( |
1302 | 1331 |
|
1303 | 1332 | async def _process_message_batch( |
1304 | 1333 | self, phone_number: PhoneNumber, session: WhatsAppSession, processing_token: str |
1305 | | - ) -> GeneratedAssistantMessage[Any] | None: |
| 1334 | + ) -> GeneratedAssistantMessage[T_Schema] | None: |
1306 | 1335 | """Process a batch of messages for a user with enhanced timeout protection. |
1307 | 1336 |
|
1308 | 1337 | This method processes multiple messages that were received in quick succession |
@@ -1504,7 +1533,7 @@ async def _process_single_message( |
1504 | 1533 | message: WhatsAppMessage, |
1505 | 1534 | session: WhatsAppSession, |
1506 | 1535 | chat_id: ChatId | None = None, |
1507 | | - ) -> GeneratedAssistantMessage[Any]: |
| 1536 | + ) -> GeneratedAssistantMessage[T_Schema]: |
1508 | 1537 | """Process a single message immediately with quote message support.""" |
1509 | 1538 | logger.info( |
1510 | 1539 | "[SINGLE_MESSAGE] ═══════════ SINGLE MESSAGE PROCESSING START ═══════════" |
@@ -2207,7 +2236,7 @@ def _format_blockquote(self, line: str) -> str: |
2207 | 2236 | async def _send_response( |
2208 | 2237 | self, |
2209 | 2238 | to: PhoneNumber, |
2210 | | - response: GeneratedAssistantMessage[Any] | str, |
| 2239 | + response: GeneratedAssistantMessage[T_Schema] | str, |
2211 | 2240 | reply_to: str | None = None, |
2212 | 2241 | ) -> None: |
2213 | 2242 | """Send response message(s) to user with enhanced error handling and retry logic. |
@@ -2255,12 +2284,24 @@ async def _send_response( |
2255 | 2284 | ... reply_to="msg_123" |
2256 | 2285 | ... ) |
2257 | 2286 | """ |
2258 | | - # Extract text from GeneratedAssistantMessage if needed |
2259 | | - response_text = ( |
2260 | | - response.text |
2261 | | - if isinstance(response, GeneratedAssistantMessage) |
2262 | | - else response |
2263 | | - ) |
| 2287 | + response_text = "" |
| 2288 | + |
| 2289 | + if isinstance(response, GeneratedAssistantMessage): |
| 2290 | + # Check if we have structured output (parsed) |
| 2291 | + if response.parsed: |
| 2292 | + # Use the 'response' field from structured output |
| 2293 | + response_text = response.parsed.response |
| 2294 | + logger.debug( |
| 2295 | + "[SEND_RESPONSE] Using structured output 'response' field " |
| 2296 | + + f"(schema: {type(response.parsed).__name__})" |
| 2297 | + ) |
| 2298 | + else: |
| 2299 | + # Fallback to text field |
| 2300 | + response_text = response.text |
| 2301 | + logger.debug("[SEND_RESPONSE] Using standard text response") |
| 2302 | + else: |
| 2303 | + # Direct string |
| 2304 | + response_text = response |
2264 | 2305 |
|
2265 | 2306 | # Apply WhatsApp-specific markdown formatting |
2266 | 2307 | response_text = self._format_whatsapp_markdown(response_text) |
|
0 commit comments