Skip to content

Commit 3980484

Browse files
committed
feat(whatsapp): Add structured response base model for WhatsApp bot
- Create WhatsAppResponseBase model to standardize bot response structure - Enhance WhatsAppBot to support generic structured output schemas - Update _send_response method to handle structured response parsing - Add type hints and documentation for new response handling mechanism - Improve logging for structured response processing - Enable more flexible and type-safe bot response generation This change introduces a base model for WhatsApp bot responses that ensures a consistent response structure while allowing for extensible, type-safe output schemas.
1 parent 4342719 commit 3980484

File tree

2 files changed

+84
-12
lines changed

2 files changed

+84
-12
lines changed
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Em: agentle/agents/whatsapp/models/whatsapp_response_base.py
2+
3+
from rsb.models.base_model import BaseModel
4+
from rsb.models.field import Field
5+
6+
7+
class WhatsAppResponseBase(BaseModel):
8+
"""
9+
Base class for WhatsApp bot structured responses.
10+
11+
This class ensures that all structured outputs from the WhatsApp bot
12+
contain a 'response' field with the text to be sent to the user.
13+
14+
Developers can extend this class to add additional structured data
15+
that they want to extract from the conversation.
16+
17+
Example:
18+
```python
19+
class CustomerServiceResponse(WhatsAppResponseBase):
20+
response: str # Inherited - text to send to user
21+
sentiment: Literal["happy", "neutral", "frustrated", "angry"]
22+
urgency: int = Field(ge=1, le=5, description="Urgency level 1-5")
23+
requires_human: bool = False
24+
suggested_actions: list[str] = Field(default_factory=list)
25+
```
26+
"""
27+
28+
response: str = Field(
29+
...,
30+
description="The text response that will be sent to the WhatsApp user. This field is required.",
31+
)

agentle/agents/whatsapp/whatsapp_bot.py

Lines changed: 53 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
from agentle.agents.whatsapp.models.whatsapp_image_message import WhatsAppImageMessage
3434
from agentle.agents.whatsapp.models.whatsapp_media_message import WhatsAppMediaMessage
3535
from agentle.agents.whatsapp.models.whatsapp_message import WhatsAppMessage
36+
from agentle.agents.whatsapp.models.whatsapp_response_base import WhatsAppResponseBase
3637
from agentle.agents.whatsapp.models.whatsapp_session import WhatsAppSession
3738
from agentle.agents.whatsapp.models.whatsapp_text_message import WhatsAppTextMessage
3839
from agentle.agents.whatsapp.models.whatsapp_video_message import WhatsAppVideoMessage
@@ -128,14 +129,42 @@ class CallbackWithContext:
128129
context: dict[str, Any] = field(default_factory=dict)
129130

130131

131-
class WhatsAppBot(BaseModel):
132+
class WhatsAppBot[T_Schema: WhatsAppResponseBase = WhatsAppResponseBase](BaseModel):
132133
"""
133134
WhatsApp bot that wraps an Agentle agent with enhanced message batching and spam protection.
134135
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+
```
136165
"""
137166

138-
agent: Agent[Any]
167+
agent: Agent[T_Schema]
139168
provider: WhatsAppProvider
140169
tts_provider: TtsProvider | None = Field(default=None)
141170
file_storage_manager: FileStorageManager | None = Field(default=None)
@@ -1302,7 +1331,7 @@ async def _batch_processor(
13021331

13031332
async def _process_message_batch(
13041333
self, phone_number: PhoneNumber, session: WhatsAppSession, processing_token: str
1305-
) -> GeneratedAssistantMessage[Any] | None:
1334+
) -> GeneratedAssistantMessage[T_Schema] | None:
13061335
"""Process a batch of messages for a user with enhanced timeout protection.
13071336
13081337
This method processes multiple messages that were received in quick succession
@@ -1504,7 +1533,7 @@ async def _process_single_message(
15041533
message: WhatsAppMessage,
15051534
session: WhatsAppSession,
15061535
chat_id: ChatId | None = None,
1507-
) -> GeneratedAssistantMessage[Any]:
1536+
) -> GeneratedAssistantMessage[T_Schema]:
15081537
"""Process a single message immediately with quote message support."""
15091538
logger.info(
15101539
"[SINGLE_MESSAGE] ═══════════ SINGLE MESSAGE PROCESSING START ═══════════"
@@ -2207,7 +2236,7 @@ def _format_blockquote(self, line: str) -> str:
22072236
async def _send_response(
22082237
self,
22092238
to: PhoneNumber,
2210-
response: GeneratedAssistantMessage[Any] | str,
2239+
response: GeneratedAssistantMessage[T_Schema] | str,
22112240
reply_to: str | None = None,
22122241
) -> None:
22132242
"""Send response message(s) to user with enhanced error handling and retry logic.
@@ -2255,12 +2284,24 @@ async def _send_response(
22552284
... reply_to="msg_123"
22562285
... )
22572286
"""
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
22642305

22652306
# Apply WhatsApp-specific markdown formatting
22662307
response_text = self._format_whatsapp_markdown(response_text)

0 commit comments

Comments
 (0)