agentle 0.9.34__py3-none-any.whl → 0.9.36__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.
@@ -2679,11 +2679,25 @@ class WhatsAppBot[T_Schema: WhatsAppResponseBase = WhatsAppResponseBase](BaseMod
2679
2679
  return "mp3" # default
2680
2680
 
2681
2681
  def _split_message_by_line_breaks(self, text: str) -> Sequence[str]:
2682
- """Split message by line breaks first, then by length if needed with enhanced validation."""
2682
+ """Split message by line breaks first, then by length if needed with enhanced validation.
2683
+
2684
+ CRITICAL: This method must preserve line breaks within messages for proper WhatsApp formatting.
2685
+ """
2683
2686
  if not text or not text.strip():
2684
2687
  return ["[Mensagem vazia]"] # Portuguese: "Empty message"
2685
2688
 
2686
2689
  try:
2690
+ # Check if entire text fits in one message - if so, return it as-is
2691
+ if len(text) <= self.config.max_message_length:
2692
+ logger.debug(
2693
+ f"[SPLIT_MESSAGE] Message fits in single message ({len(text)} chars), returning as-is"
2694
+ )
2695
+ return [text]
2696
+
2697
+ logger.info(
2698
+ f"[SPLIT_MESSAGE] Message too long ({len(text)} chars), splitting into multiple messages"
2699
+ )
2700
+
2687
2701
  # First split by double line breaks (paragraphs)
2688
2702
  paragraphs = text.split("\n\n")
2689
2703
  messages: MutableSequence[str] = []
@@ -2692,38 +2706,54 @@ class WhatsAppBot[T_Schema: WhatsAppResponseBase = WhatsAppResponseBase](BaseMod
2692
2706
  if not paragraph.strip():
2693
2707
  continue
2694
2708
 
2709
+ # If paragraph fits, keep it intact with all line breaks
2710
+ if len(paragraph) <= self.config.max_message_length:
2711
+ messages.append(paragraph)
2712
+ continue
2713
+
2714
+ # Paragraph is too long - need to split it
2695
2715
  # Check if paragraph is a list (has list markers)
2696
2716
  lines = paragraph.split("\n")
2697
2717
  is_list_paragraph = self._is_list_content(lines)
2698
2718
 
2699
2719
  if is_list_paragraph:
2700
- # Group list items together instead of splitting each line
2701
- # IMPORTANT: Keep line breaks intact for list formatting
2720
+ # Group list items together, preserving line breaks
2702
2721
  grouped_list = self._group_list_items(lines)
2703
2722
  messages.extend(grouped_list)
2704
2723
  else:
2705
- # For non-list paragraphs, keep the whole paragraph together if possible
2706
- paragraph_text = paragraph.strip()
2707
- if not paragraph_text:
2708
- continue
2709
-
2710
- # Check if paragraph fits within message length limits
2711
- if len(paragraph_text) <= self.config.max_message_length:
2712
- messages.append(paragraph_text)
2713
- else:
2714
- # Split by individual lines only if paragraph is too long
2715
- for line in lines:
2716
- line = line.strip()
2717
- if not line:
2718
- continue
2724
+ # For non-list paragraphs, try to keep lines together
2725
+ current_chunk = ""
2726
+ for line in lines:
2727
+ if not line.strip():
2728
+ # Preserve empty lines for spacing
2729
+ if current_chunk:
2730
+ current_chunk += "\n"
2731
+ continue
2732
+
2733
+ # Try adding this line to current chunk
2734
+ test_chunk = (
2735
+ current_chunk + ("\n" if current_chunk else "") + line
2736
+ )
2719
2737
 
2720
- # Check if this line fits within message length limits
2721
- if len(line) <= self.config.max_message_length:
2722
- messages.append(line)
2723
- else:
2724
- # Split long lines by length
2738
+ if len(test_chunk) <= self.config.max_message_length:
2739
+ current_chunk = test_chunk
2740
+ else:
2741
+ # Current chunk is full, save it
2742
+ if current_chunk:
2743
+ messages.append(current_chunk)
2744
+
2745
+ # Check if single line is too long
2746
+ if len(line) > self.config.max_message_length:
2747
+ # Split long line by length
2725
2748
  split_lines = self._split_long_line(line)
2726
2749
  messages.extend(split_lines)
2750
+ current_chunk = ""
2751
+ else:
2752
+ current_chunk = line
2753
+
2754
+ # Add remaining chunk
2755
+ if current_chunk:
2756
+ messages.append(current_chunk)
2727
2757
 
2728
2758
  # Filter out empty messages and validate
2729
2759
  final_messages = []
@@ -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": str(suggestion.args), # Should be JSON string
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
- self.message_adapter.adapt(message) for message in messages_list
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
- self.message_adapter.adapt(message) for message in messages
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.34
3
+ Version: 0.9.36
4
4
  Summary: ...
5
5
  Author-email: Arthur Brenno <64020210+arthurbrenno@users.noreply.github.com>
6
6
  License-File: LICENSE
@@ -137,7 +137,7 @@ 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=RH4WDpaqD4yTazKkjyYYPlhb-3BA-sX8h4pWZWHWznU,163775
140
+ agentle/agents/whatsapp/whatsapp_bot.py,sha256=tF35s2c4G9Fo0bLVmePYXWSnSNEgg-Rpi9V0MrrRCCA,164948
141
141
  agentle/agents/whatsapp/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
142
142
  agentle/agents/whatsapp/models/audio_message.py,sha256=kUqG1HdNW6DCYD-CqscJ6WHlAyv9ufmTSKMdjio9XWk,2705
143
143
  agentle/agents/whatsapp/models/context_info.py,sha256=sk80KuNE36S6VRnLh7n6UXmzZCXIB4E4lNxnRyVizg8,563
@@ -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=c2fQMdm-xFzIR2Z3ClAXiu6BzKfrnQQTpzX2jJQSNlI,66596
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=xqLumYTs__1UHD1_WwvHsHhzpEUKOAwMAj4pvo4BWMU,4957
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.34.dist-info/METADATA,sha256=RGHCMaogcrdNvsf-g95zBnU_RdGssPcmfltQ4440Q9I,86849
1022
- agentle-0.9.34.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
1023
- agentle-0.9.34.dist-info/licenses/LICENSE,sha256=T90S9vqRS6qP-voULxAcvwEs558wRRo6dHuZrjgcOUI,1085
1024
- agentle-0.9.34.dist-info/RECORD,,
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,,