symbolicai 0.20.2__py3-none-any.whl → 1.0.0__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.
Files changed (123) hide show
  1. symai/__init__.py +96 -64
  2. symai/backend/base.py +93 -80
  3. symai/backend/engines/drawing/engine_bfl.py +12 -11
  4. symai/backend/engines/drawing/engine_gpt_image.py +108 -87
  5. symai/backend/engines/embedding/engine_llama_cpp.py +25 -28
  6. symai/backend/engines/embedding/engine_openai.py +3 -5
  7. symai/backend/engines/execute/engine_python.py +6 -5
  8. symai/backend/engines/files/engine_io.py +74 -67
  9. symai/backend/engines/imagecaptioning/engine_blip2.py +3 -3
  10. symai/backend/engines/imagecaptioning/engine_llavacpp_client.py +54 -38
  11. symai/backend/engines/index/engine_pinecone.py +23 -24
  12. symai/backend/engines/index/engine_vectordb.py +16 -14
  13. symai/backend/engines/lean/engine_lean4.py +38 -34
  14. symai/backend/engines/neurosymbolic/__init__.py +41 -13
  15. symai/backend/engines/neurosymbolic/engine_anthropic_claudeX_chat.py +262 -182
  16. symai/backend/engines/neurosymbolic/engine_anthropic_claudeX_reasoning.py +263 -191
  17. symai/backend/engines/neurosymbolic/engine_deepseekX_reasoning.py +53 -49
  18. symai/backend/engines/neurosymbolic/engine_google_geminiX_reasoning.py +212 -211
  19. symai/backend/engines/neurosymbolic/engine_groq.py +87 -63
  20. symai/backend/engines/neurosymbolic/engine_huggingface.py +21 -24
  21. symai/backend/engines/neurosymbolic/engine_llama_cpp.py +117 -48
  22. symai/backend/engines/neurosymbolic/engine_openai_gptX_chat.py +256 -229
  23. symai/backend/engines/neurosymbolic/engine_openai_gptX_reasoning.py +270 -150
  24. symai/backend/engines/ocr/engine_apilayer.py +6 -8
  25. symai/backend/engines/output/engine_stdout.py +1 -4
  26. symai/backend/engines/search/engine_openai.py +7 -7
  27. symai/backend/engines/search/engine_perplexity.py +5 -5
  28. symai/backend/engines/search/engine_serpapi.py +12 -14
  29. symai/backend/engines/speech_to_text/engine_local_whisper.py +20 -27
  30. symai/backend/engines/symbolic/engine_wolframalpha.py +3 -3
  31. symai/backend/engines/text_to_speech/engine_openai.py +5 -7
  32. symai/backend/engines/text_vision/engine_clip.py +7 -11
  33. symai/backend/engines/userinput/engine_console.py +3 -3
  34. symai/backend/engines/webscraping/engine_requests.py +81 -48
  35. symai/backend/mixin/__init__.py +13 -0
  36. symai/backend/mixin/anthropic.py +4 -2
  37. symai/backend/mixin/deepseek.py +2 -0
  38. symai/backend/mixin/google.py +2 -0
  39. symai/backend/mixin/openai.py +11 -3
  40. symai/backend/settings.py +83 -16
  41. symai/chat.py +101 -78
  42. symai/collect/__init__.py +7 -1
  43. symai/collect/dynamic.py +77 -69
  44. symai/collect/pipeline.py +35 -27
  45. symai/collect/stats.py +75 -63
  46. symai/components.py +198 -169
  47. symai/constraints.py +15 -12
  48. symai/core.py +698 -359
  49. symai/core_ext.py +32 -34
  50. symai/endpoints/api.py +80 -73
  51. symai/extended/.DS_Store +0 -0
  52. symai/extended/__init__.py +46 -12
  53. symai/extended/api_builder.py +11 -8
  54. symai/extended/arxiv_pdf_parser.py +13 -12
  55. symai/extended/bibtex_parser.py +2 -3
  56. symai/extended/conversation.py +101 -90
  57. symai/extended/document.py +17 -10
  58. symai/extended/file_merger.py +18 -13
  59. symai/extended/graph.py +18 -13
  60. symai/extended/html_style_template.py +2 -4
  61. symai/extended/interfaces/blip_2.py +1 -2
  62. symai/extended/interfaces/clip.py +1 -2
  63. symai/extended/interfaces/console.py +7 -1
  64. symai/extended/interfaces/dall_e.py +1 -1
  65. symai/extended/interfaces/flux.py +1 -1
  66. symai/extended/interfaces/gpt_image.py +1 -1
  67. symai/extended/interfaces/input.py +1 -1
  68. symai/extended/interfaces/llava.py +0 -1
  69. symai/extended/interfaces/naive_vectordb.py +7 -8
  70. symai/extended/interfaces/naive_webscraping.py +1 -1
  71. symai/extended/interfaces/ocr.py +1 -1
  72. symai/extended/interfaces/pinecone.py +6 -5
  73. symai/extended/interfaces/serpapi.py +1 -1
  74. symai/extended/interfaces/terminal.py +2 -3
  75. symai/extended/interfaces/tts.py +1 -1
  76. symai/extended/interfaces/whisper.py +1 -1
  77. symai/extended/interfaces/wolframalpha.py +1 -1
  78. symai/extended/metrics/__init__.py +11 -1
  79. symai/extended/metrics/similarity.py +11 -13
  80. symai/extended/os_command.py +17 -16
  81. symai/extended/packages/__init__.py +29 -3
  82. symai/extended/packages/symdev.py +19 -16
  83. symai/extended/packages/sympkg.py +12 -9
  84. symai/extended/packages/symrun.py +21 -19
  85. symai/extended/repo_cloner.py +11 -10
  86. symai/extended/seo_query_optimizer.py +1 -2
  87. symai/extended/solver.py +20 -23
  88. symai/extended/summarizer.py +4 -3
  89. symai/extended/taypan_interpreter.py +10 -12
  90. symai/extended/vectordb.py +99 -82
  91. symai/formatter/__init__.py +9 -1
  92. symai/formatter/formatter.py +12 -16
  93. symai/formatter/regex.py +62 -63
  94. symai/functional.py +176 -122
  95. symai/imports.py +136 -127
  96. symai/interfaces.py +56 -27
  97. symai/memory.py +14 -13
  98. symai/misc/console.py +49 -39
  99. symai/misc/loader.py +5 -3
  100. symai/models/__init__.py +17 -1
  101. symai/models/base.py +269 -181
  102. symai/models/errors.py +0 -1
  103. symai/ops/__init__.py +32 -22
  104. symai/ops/measures.py +11 -15
  105. symai/ops/primitives.py +348 -228
  106. symai/post_processors.py +32 -28
  107. symai/pre_processors.py +39 -41
  108. symai/processor.py +6 -4
  109. symai/prompts.py +59 -45
  110. symai/server/huggingface_server.py +23 -20
  111. symai/server/llama_cpp_server.py +7 -5
  112. symai/shell.py +3 -4
  113. symai/shellsv.py +499 -375
  114. symai/strategy.py +517 -287
  115. symai/symbol.py +111 -116
  116. symai/utils.py +42 -36
  117. {symbolicai-0.20.2.dist-info → symbolicai-1.0.0.dist-info}/METADATA +4 -2
  118. symbolicai-1.0.0.dist-info/RECORD +163 -0
  119. symbolicai-0.20.2.dist-info/RECORD +0 -162
  120. {symbolicai-0.20.2.dist-info → symbolicai-1.0.0.dist-info}/WHEEL +0 -0
  121. {symbolicai-0.20.2.dist-info → symbolicai-1.0.0.dist-info}/entry_points.txt +0 -0
  122. {symbolicai-0.20.2.dist-info → symbolicai-1.0.0.dist-info}/licenses/LICENSE +0 -0
  123. {symbolicai-0.20.2.dist-info → symbolicai-1.0.0.dist-info}/top_level.txt +0 -0
@@ -2,20 +2,24 @@ import json
2
2
  import logging
3
3
  import re
4
4
  from copy import copy, deepcopy
5
- from typing import List, Optional
6
5
 
7
6
  import anthropic
8
7
  from anthropic._types import NOT_GIVEN
9
- from anthropic.types import (InputJSONDelta, Message,
10
- RawContentBlockDeltaEvent,
11
- RawContentBlockStartEvent,
12
- RawContentBlockStopEvent, TextBlock, TextDelta,
13
- ThinkingBlock, ThinkingDelta, ToolUseBlock)
8
+ from anthropic.types import (
9
+ InputJSONDelta,
10
+ Message,
11
+ RawContentBlockDeltaEvent,
12
+ RawContentBlockStartEvent,
13
+ RawContentBlockStopEvent,
14
+ TextBlock,
15
+ TextDelta,
16
+ ThinkingBlock,
17
+ ThinkingDelta,
18
+ ToolUseBlock,
19
+ )
14
20
 
15
21
  from ....components import SelfPrompt
16
- from ....misc.console import ConsoleStyle
17
- from ....symbol import Symbol
18
- from ....utils import CustomUserWarning, encode_media_frames
22
+ from ....utils import UserMessage, encode_media_frames
19
23
  from ...base import Engine
20
24
  from ...mixin.anthropic import AnthropicMixin
21
25
  from ...settings import SYMAI_CONFIG
@@ -35,7 +39,7 @@ class TokenizerWrapper:
35
39
  return self.compute_tokens_func([{"role": "user", "content": text}])
36
40
 
37
41
  class ClaudeXReasoningEngine(Engine, AnthropicMixin):
38
- def __init__(self, api_key: Optional[str] = None, model: Optional[str] = None):
42
+ def __init__(self, api_key: str | None = None, model: str | None = None):
39
43
  super().__init__()
40
44
  self.config = deepcopy(SYMAI_CONFIG)
41
45
  # In case we use EngineRepository.register to inject the api_key and model => dynamically change the engine at runtime
@@ -70,50 +74,7 @@ class ClaudeXReasoningEngine(Engine, AnthropicMixin):
70
74
  self.model = kwargs['NEUROSYMBOLIC_ENGINE_MODEL']
71
75
 
72
76
  def compute_required_tokens(self, messages) -> int:
73
- claude_messages = []
74
- system_content = None
75
-
76
- for msg in messages:
77
- if not isinstance(msg, list):
78
- msg = [msg]
79
- for part in msg:
80
- if isinstance(part, str):
81
- role = 'user'
82
- content_str = part
83
- elif isinstance(part, dict):
84
- role = part.get('role')
85
- content_str = str(part.get('content', ''))
86
- else:
87
- CustomUserWarning(f"Unsupported message part type: {type(part)}", raise_with=ValueError)
88
-
89
- if role == 'system':
90
- system_content = content_str
91
- continue
92
-
93
- if role in ['user', 'assistant']:
94
- message_content = []
95
-
96
- image_content = self._handle_image_content(content_str)
97
- message_content.extend(image_content)
98
-
99
- text_content = self._remove_vision_pattern(content_str)
100
- if text_content:
101
- message_content.append({
102
- "type": "text",
103
- "text": text_content
104
- })
105
-
106
- if message_content:
107
- if len(message_content) == 1 and message_content[0].get('type') == 'text':
108
- claude_messages.append({
109
- 'role': role,
110
- 'content': message_content[0]['text']
111
- })
112
- else:
113
- claude_messages.append({
114
- 'role': role,
115
- 'content': message_content
116
- })
77
+ claude_messages, system_content = self._normalize_messages_for_claude(messages)
117
78
 
118
79
  if not claude_messages:
119
80
  return 0
@@ -128,11 +89,65 @@ class ClaudeXReasoningEngine(Engine, AnthropicMixin):
128
89
  count_response = self.client.messages.count_tokens(**count_params)
129
90
  return count_response.input_tokens
130
91
  except Exception as e:
131
- logging.error(f"Claude count_tokens failed: {e}")
132
- CustomUserWarning(f"Error counting tokens for Claude: {str(e)}", raise_with=RuntimeError)
92
+ UserMessage(f"Claude count_tokens failed: {e}")
93
+ UserMessage(f"Error counting tokens for Claude: {e!s}", raise_with=RuntimeError)
94
+
95
+ def _normalize_messages_for_claude(self, messages):
96
+ claude_messages = []
97
+ system_content = None
98
+
99
+ for msg in messages:
100
+ msg_parts = msg if isinstance(msg, list) else [msg]
101
+ for part in msg_parts:
102
+ role, content_str = self._extract_role_and_content(part)
103
+ if role == 'system':
104
+ system_content = content_str
105
+ continue
106
+
107
+ if role in ['user', 'assistant']:
108
+ message_payload = self._build_message_payload(role, content_str)
109
+ if message_payload:
110
+ claude_messages.append(message_payload)
111
+
112
+ return claude_messages, system_content
133
113
 
134
- def compute_remaining_tokens(self, prompts: list) -> int:
135
- CustomUserWarning('Method not implemented.', raise_with=NotImplementedError)
114
+ def _extract_role_and_content(self, part):
115
+ if isinstance(part, str):
116
+ return 'user', part
117
+ if isinstance(part, dict):
118
+ return part.get('role'), str(part.get('content', ''))
119
+ UserMessage(f"Unsupported message part type: {type(part)}", raise_with=ValueError)
120
+ return None, ''
121
+
122
+ def _build_message_payload(self, role, content_str):
123
+ message_content = []
124
+
125
+ image_content = self._handle_image_content(content_str)
126
+ message_content.extend(image_content)
127
+
128
+ text_content = self._remove_vision_pattern(content_str)
129
+ if text_content:
130
+ message_content.append({
131
+ "type": "text",
132
+ "text": text_content
133
+ })
134
+
135
+ if not message_content:
136
+ return None
137
+
138
+ if len(message_content) == 1 and message_content[0].get('type') == 'text':
139
+ return {
140
+ 'role': role,
141
+ 'content': message_content[0]['text']
142
+ }
143
+
144
+ return {
145
+ 'role': role,
146
+ 'content': message_content
147
+ }
148
+
149
+ def compute_remaining_tokens(self, _prompts: list) -> int:
150
+ UserMessage('Method not implemented.', raise_with=NotImplementedError)
136
151
 
137
152
  def _handle_image_content(self, content: str) -> list:
138
153
  """Handle image content by processing vision patterns and returning image file data."""
@@ -158,7 +173,7 @@ class ClaudeXReasoningEngine(Engine, AnthropicMixin):
158
173
  elif len(buffer) == 1:
159
174
  image_files.append({'data': buffer[0], 'media_type': f'image/{ext}', 'type': 'base64'})
160
175
  else:
161
- CustomUserWarning(f'No frames found for image!')
176
+ UserMessage('No frames found for image!')
162
177
  return image_files
163
178
 
164
179
  def _remove_vision_pattern(self, text: str) -> str:
@@ -181,21 +196,21 @@ class ClaudeXReasoningEngine(Engine, AnthropicMixin):
181
196
  except Exception as e:
182
197
  if anthropic.api_key is None or anthropic.api_key == '':
183
198
  msg = 'Anthropic API key is not set. Please set it in the config file or pass it as an argument to the command method.'
184
- logging.error(msg)
199
+ UserMessage(msg)
185
200
  if self.config['NEUROSYMBOLIC_ENGINE_API_KEY'] is None or self.config['NEUROSYMBOLIC_ENGINE_API_KEY'] == '':
186
- CustomUserWarning(msg, raise_with=ValueError)
201
+ UserMessage(msg, raise_with=ValueError)
187
202
  anthropic.api_key = self.config['NEUROSYMBOLIC_ENGINE_API_KEY']
188
203
 
189
204
  callback = self.client.messages.create
190
- kwargs['model'] = kwargs['model'] if 'model' in kwargs else self.model
205
+ kwargs['model'] = kwargs.get('model', self.model)
191
206
 
192
207
  if except_remedy is not None:
193
208
  res = except_remedy(self, e, callback, argument)
194
209
  else:
195
- CustomUserWarning(f'Error during generation. Caused by: {e}', raise_with=ValueError)
210
+ UserMessage(f'Error during generation. Caused by: {e}', raise_with=ValueError)
196
211
 
197
212
  if payload['stream']:
198
- res = [_ for _ in res] # Unpack the iterator to a list
213
+ res = list(res) # Unpack the iterator to a list
199
214
  metadata = {'raw_output': res}
200
215
  response_data = self._collect_response(res)
201
216
 
@@ -214,11 +229,13 @@ class ClaudeXReasoningEngine(Engine, AnthropicMixin):
214
229
 
215
230
  def _prepare_raw_input(self, argument):
216
231
  if not argument.prop.processed_input:
217
- raise ValueError('Need to provide a prompt instruction to the engine if `raw_input` is enabled!')
232
+ msg = 'Need to provide a prompt instruction to the engine if `raw_input` is enabled!'
233
+ UserMessage(msg)
234
+ raise ValueError(msg)
218
235
  system = NOT_GIVEN
219
236
  prompt = copy(argument.prop.processed_input)
220
- if type(prompt) != list:
221
- if type(prompt) != dict:
237
+ if not isinstance(prompt, list):
238
+ if not isinstance(prompt, dict):
222
239
  prompt = {'role': 'user', 'content': str(prompt)}
223
240
  prompt = [prompt]
224
241
  if len(prompt) > 1:
@@ -238,19 +255,35 @@ class ClaudeXReasoningEngine(Engine, AnthropicMixin):
238
255
  return
239
256
 
240
257
  _non_verbose_output = """<META_INSTRUCTION/>\nYou do not output anything else, like verbose preambles or post explanation, such as "Sure, let me...", "Hope that was helpful...", "Yes, I can help you with that...", etc. Consider well formatted output, e.g. for sentences use punctuation, spaces etc. or for code use indentation, etc. Never add meta instructions information to your output!\n\n"""
241
- user: str = ""
242
- system: str = ""
258
+ image_files = self._handle_image_content(str(argument.prop.processed_input))
259
+ system = self._build_system_prompt(argument, _non_verbose_output, image_files)
260
+ user_text = self._build_user_text(argument, image_files)
261
+
262
+ if not user_text:
263
+ # Anthropic doesn't allow empty user prompts; force it
264
+ user_text = "N/A"
265
+
266
+ system, user_prompt = self._apply_self_prompt_if_needed(
267
+ argument,
268
+ system,
269
+ user_text,
270
+ image_files
271
+ )
272
+
273
+ argument.prop.prepared_input = (system, [user_prompt])
274
+
275
+ def _build_system_prompt(self, argument, non_verbose_output, image_files):
276
+ system = ""
243
277
 
244
278
  if argument.prop.suppress_verbose_output:
245
- system += _non_verbose_output
246
- system = f'{system}\n' if system and len(system) > 0 else ''
279
+ system = f"{non_verbose_output}\n"
247
280
 
248
281
  if argument.prop.response_format:
249
- _rsp_fmt = argument.prop.response_format
250
- if not (_rsp_fmt.get('type') is not None):
251
- CustomUserWarning('Response format type is required! Expected format `{"type": "json_object"}` or other supported types. Refer to Anthropic documentation for details.', raise_with=AssertionError)
252
- system += _non_verbose_output
253
- system += f'<RESPONSE_FORMAT/>\n{_rsp_fmt["type"]}\n\n'
282
+ response_format = argument.prop.response_format
283
+ if not (response_format.get('type') is not None):
284
+ UserMessage('Response format type is required! Expected format `{"type": "json_object"}` or other supported types. Refer to Anthropic documentation for details.', raise_with=AssertionError)
285
+ system += non_verbose_output
286
+ system += f"<RESPONSE_FORMAT/>\n{response_format['type']}\n\n"
254
287
 
255
288
  ref = argument.prop.instance
256
289
  static_ctxt, dyn_ctxt = ref.global_context
@@ -261,66 +294,66 @@ class ClaudeXReasoningEngine(Engine, AnthropicMixin):
261
294
  system += f"<DYNAMIC_CONTEXT/>\n{dyn_ctxt}\n\n"
262
295
 
263
296
  payload = argument.prop.payload
264
- if argument.prop.payload:
265
- system += f"<ADDITIONAL_CONTEXT/>\n{str(payload)}\n\n"
297
+ if payload:
298
+ system += f"<ADDITIONAL_CONTEXT/>\n{payload!s}\n\n"
266
299
 
267
- examples: List[str] = argument.prop.examples
300
+ examples: list[str] = argument.prop.examples
268
301
  if examples and len(examples) > 0:
269
- system += f"<EXAMPLES/>\n{str(examples)}\n\n"
270
-
271
- image_files = self._handle_image_content(str(argument.prop.processed_input))
302
+ system += f"<EXAMPLES/>\n{examples!s}\n\n"
272
303
 
273
304
  if argument.prop.prompt is not None and len(argument.prop.prompt) > 0:
274
- val = str(argument.prop.prompt)
305
+ value = str(argument.prop.prompt)
275
306
  if len(image_files) > 0:
276
- val = self._remove_vision_pattern(val)
277
- system += f"<INSTRUCTION/>\n{val}\n\n"
307
+ value = self._remove_vision_pattern(value)
308
+ system += f"<INSTRUCTION/>\n{value}\n\n"
309
+
310
+ return self._append_template_suffix(system, argument.prop.template_suffix)
278
311
 
279
- suffix: str = str(argument.prop.processed_input)
312
+ def _build_user_text(self, argument, image_files):
313
+ suffix = str(argument.prop.processed_input)
280
314
  if len(image_files) > 0:
281
315
  suffix = self._remove_vision_pattern(suffix)
316
+ return suffix
282
317
 
283
- user += f"{suffix}"
318
+ def _append_template_suffix(self, system, template_suffix):
319
+ if template_suffix:
320
+ return system + (
321
+ f' You will only generate content for the placeholder `{template_suffix!s}` '
322
+ 'following the instructions and the provided context information.\n\n'
323
+ )
324
+ return system
284
325
 
285
- if not len(user):
286
- # Anthropic doesn't allow empty user prompts; force it
287
- user = "N/A"
326
+ def _apply_self_prompt_if_needed(self, argument, system, user_text, image_files):
327
+ if not self._is_self_prompt_enabled(argument):
328
+ return system, self._format_user_prompt(user_text, image_files)
288
329
 
289
- if argument.prop.template_suffix:
290
- system += f' You will only generate content for the placeholder `{str(argument.prop.template_suffix)}` following the instructions and the provided context information.\n\n'
330
+ self_prompter = SelfPrompt()
331
+ response = self_prompter(
332
+ {'user': user_text, 'system': system},
333
+ max_tokens=argument.kwargs.get('max_tokens', self.max_response_tokens),
334
+ thinking=argument.kwargs.get('thinking', NOT_GIVEN),
335
+ )
336
+ if response is None:
337
+ UserMessage("Self-prompting failed to return a response.", raise_with=ValueError)
291
338
 
292
- if len(image_files) > 0:
293
- images = [{ 'type': 'image', "source": im } for im in image_files]
294
- user_prompt = { "role": "user", "content": [
295
- *images,
296
- { 'type': 'text', 'text': user }
297
- ]}
298
- else:
299
- user_prompt = { "role": "user", "content": user }
300
-
301
- # First check if the `Symbol` instance has the flag set, otherwise check if it was passed as an argument to a method
302
- if argument.prop.instance._kwargs.get('self_prompt', False) or argument.prop.self_prompt:
303
- self_prompter = SelfPrompt()
304
-
305
- res = self_prompter(
306
- {'user': user, 'system': system},
307
- max_tokens=argument.kwargs.get('max_tokens', self.max_response_tokens),
308
- thinking=argument.kwargs.get('thinking', NOT_GIVEN),
309
- )
310
- if res is None:
311
- CustomUserWarning("Self-prompting failed to return a response.", raise_with=ValueError)
339
+ updated_prompt = self._format_user_prompt(response['user'], image_files)
340
+ return response['system'], updated_prompt
312
341
 
313
- if len(image_files) > 0:
314
- user_prompt = { "role": "user", "content": [
342
+ def _is_self_prompt_enabled(self, argument):
343
+ return argument.prop.instance._kwargs.get('self_prompt', False) or argument.prop.self_prompt
344
+
345
+ def _format_user_prompt(self, user_text, image_files):
346
+ if len(image_files) > 0:
347
+ images = [{'type': 'image', 'source': im} for im in image_files]
348
+ return {
349
+ 'role': 'user',
350
+ 'content': [
315
351
  *images,
316
- { 'type': 'text', 'text': res['user'] }
317
- ]}
318
- else:
319
- user_prompt = { "role": "user", "content": res['user'] }
352
+ {'type': 'text', 'text': user_text}
353
+ ]
354
+ }
320
355
 
321
- system = res['system']
322
-
323
- argument.prop.prepared_input = (system, [user_prompt])
356
+ return {'role': 'user', 'content': user_text}
324
357
 
325
358
  def _prepare_request_payload(self, argument):
326
359
  kwargs = argument.kwargs
@@ -342,7 +375,7 @@ class ClaudeXReasoningEngine(Engine, AnthropicMixin):
342
375
  metadata_anthropic = kwargs.get('metadata', NOT_GIVEN)
343
376
  max_tokens = kwargs.get('max_tokens', self.max_response_tokens)
344
377
 
345
- if stop != NOT_GIVEN and type(stop) != list:
378
+ if stop != NOT_GIVEN and not isinstance(stop, list):
346
379
  stop = [stop]
347
380
 
348
381
  #@NOTE: Anthropic fails if stop is not raw string, so cast it to r'…'
@@ -366,77 +399,116 @@ class ClaudeXReasoningEngine(Engine, AnthropicMixin):
366
399
 
367
400
  def _collect_response(self, res):
368
401
  if isinstance(res, list):
369
- thinking_content = ''
370
- text_content = ''
371
- tool_calls_raw = []
372
- active_tool_calls = {}
373
-
374
- for chunk in res:
375
- if isinstance(chunk, RawContentBlockStartEvent):
376
- if isinstance(chunk.content_block, ToolUseBlock):
377
- active_tool_calls[chunk.index] = {
378
- 'id': chunk.content_block.id,
379
- 'name': chunk.content_block.name,
380
- 'input_json_str': ""
381
- }
382
- elif isinstance(chunk, RawContentBlockDeltaEvent):
383
- if isinstance(chunk.delta, ThinkingDelta):
384
- thinking_content += chunk.delta.thinking
385
- elif isinstance(chunk.delta, TextDelta):
386
- text_content += chunk.delta.text
387
- elif isinstance(chunk.delta, InputJSONDelta):
388
- if chunk.index in active_tool_calls:
389
- active_tool_calls[chunk.index]['input_json_str'] += chunk.delta.partial_json
390
- elif isinstance(chunk, RawContentBlockStopEvent):
391
- if chunk.index in active_tool_calls:
392
- tool_call_info = active_tool_calls.pop(chunk.index)
393
- try:
394
- tool_call_info['input'] = json.loads(tool_call_info['input_json_str'])
395
- except json.JSONDecodeError as e:
396
- logging.error(f"Failed to parse JSON for tool call {tool_call_info['name']}: {e}. Raw JSON: '{tool_call_info['input_json_str']}'")
397
- tool_call_info['input'] = {}
398
- tool_calls_raw.append(tool_call_info)
399
-
400
- function_call_data = None
401
- if tool_calls_raw:
402
- if len(tool_calls_raw) > 1:
403
- CustomUserWarning("Multiple tool calls detected in the stream but only the first one will be processed.")
404
- function_call_data = {
405
- 'name': tool_calls_raw[0]['name'],
406
- 'arguments': tool_calls_raw[0]['input']
407
- }
402
+ return self._collect_stream_response(res)
408
403
 
409
- return {
410
- "thinking": thinking_content,
411
- "text": text_content,
412
- "function_call": function_call_data
404
+ if isinstance(res, Message):
405
+ return self._collect_message_response(res)
406
+
407
+ UserMessage(f"Unexpected response type from Anthropic API: {type(res)}", raise_with=ValueError)
408
+ return {}
409
+
410
+ def _collect_stream_response(self, response_chunks):
411
+ accumulators = {'thinking': '', 'text': ''}
412
+ tool_calls_raw = []
413
+ active_tool_calls = {}
414
+
415
+ for chunk in response_chunks:
416
+ self._process_stream_chunk(chunk, accumulators, active_tool_calls, tool_calls_raw)
417
+
418
+ function_call_data = self._extract_function_call(tool_calls_raw)
419
+ return {
420
+ 'thinking': accumulators['thinking'],
421
+ 'text': accumulators['text'],
422
+ 'function_call': function_call_data
423
+ }
424
+
425
+ def _process_stream_chunk(self, chunk, accumulators, active_tool_calls, tool_calls_raw):
426
+ if isinstance(chunk, RawContentBlockStartEvent):
427
+ self._register_tool_call(chunk, active_tool_calls)
428
+ elif isinstance(chunk, RawContentBlockDeltaEvent):
429
+ self._handle_delta_chunk(chunk, accumulators, active_tool_calls)
430
+ elif isinstance(chunk, RawContentBlockStopEvent):
431
+ self._finalize_tool_call(chunk, active_tool_calls, tool_calls_raw)
432
+
433
+ def _register_tool_call(self, chunk, active_tool_calls):
434
+ if isinstance(chunk.content_block, ToolUseBlock):
435
+ active_tool_calls[chunk.index] = {
436
+ 'id': chunk.content_block.id,
437
+ 'name': chunk.content_block.name,
438
+ 'input_json_str': ""
413
439
  }
414
440
 
415
- # Non-streamed response (res is a Message object)
416
- if isinstance(res, Message):
417
- thinking_content = ''
418
- text_content = ''
419
- function_call_data = None
420
- hit = False
421
-
422
- for content_block in res.content:
423
- if isinstance(content_block, ThinkingBlock):
424
- thinking_content += content_block.thinking
425
- elif isinstance(content_block, TextBlock):
426
- text_content += content_block.text
427
- elif isinstance(content_block, ToolUseBlock):
428
- if hit:
429
- CustomUserWarning("Multiple tool use blocks detected in the response but only the first one will be processed.")
430
- else:
431
- function_call_data = {
432
- 'name': content_block.name,
433
- 'arguments': content_block.input
434
- }
435
- hit = True
441
+ def _handle_delta_chunk(self, chunk, accumulators, active_tool_calls):
442
+ if isinstance(chunk.delta, ThinkingDelta):
443
+ accumulators['thinking'] += chunk.delta.thinking
444
+ elif isinstance(chunk.delta, TextDelta):
445
+ accumulators['text'] += chunk.delta.text
446
+ elif isinstance(chunk.delta, InputJSONDelta) and chunk.index in active_tool_calls:
447
+ active_tool_calls[chunk.index]['input_json_str'] += chunk.delta.partial_json
448
+
449
+ def _finalize_tool_call(self, chunk, active_tool_calls, tool_calls_raw):
450
+ if chunk.index not in active_tool_calls:
451
+ return
452
+
453
+ tool_call_info = active_tool_calls.pop(chunk.index)
454
+ try:
455
+ tool_call_info['input'] = json.loads(tool_call_info['input_json_str'])
456
+ except json.JSONDecodeError as error:
457
+ UserMessage(
458
+ f"Failed to parse JSON for tool call {tool_call_info['name']}: {error}. Raw JSON: '{tool_call_info['input_json_str']}'"
459
+ )
460
+ tool_call_info['input'] = {}
461
+ tool_calls_raw.append(tool_call_info)
462
+
463
+ def _extract_function_call(self, tool_calls_raw):
464
+ if not tool_calls_raw:
465
+ return None
466
+
467
+ if len(tool_calls_raw) > 1:
468
+ UserMessage("Multiple tool calls detected in the stream but only the first one will be processed.")
469
+
470
+ first_call = tool_calls_raw[0]
471
+ return {
472
+ 'name': first_call['name'],
473
+ 'arguments': first_call['input']
474
+ }
475
+
476
+ def _collect_message_response(self, message):
477
+ accumulators = {'thinking': '', 'text': ''}
478
+ function_call_data = None
479
+ tool_call_detected = False
480
+
481
+ for content_block in message.content:
482
+ function_call_data, tool_call_detected = self._process_message_block(
483
+ content_block,
484
+ accumulators,
485
+ function_call_data,
486
+ tool_call_detected
487
+ )
488
+
489
+ return {
490
+ 'thinking': accumulators['thinking'],
491
+ 'text': accumulators['text'],
492
+ 'function_call': function_call_data
493
+ }
494
+
495
+ def _process_message_block(self, content_block, accumulators, function_call_data, tool_call_detected):
496
+ if isinstance(content_block, ThinkingBlock):
497
+ accumulators['thinking'] += content_block.thinking
498
+ return function_call_data, tool_call_detected
499
+
500
+ if isinstance(content_block, TextBlock):
501
+ accumulators['text'] += content_block.text
502
+ return function_call_data, tool_call_detected
503
+
504
+ if isinstance(content_block, ToolUseBlock):
505
+ if tool_call_detected:
506
+ UserMessage("Multiple tool use blocks detected in the response but only the first one will be processed.")
507
+ return function_call_data, tool_call_detected
508
+
436
509
  return {
437
- "thinking": thinking_content,
438
- "text": text_content,
439
- "function_call": function_call_data
440
- }
510
+ 'name': content_block.name,
511
+ 'arguments': content_block.input
512
+ }, True
441
513
 
442
- CustomUserWarning(f"Unexpected response type from Anthropic API: {type(res)}", raise_with=ValueError)
514
+ return function_call_data, tool_call_detected