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,22 @@ 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
- ToolUseBlock)
8
+ from anthropic.types import (
9
+ InputJSONDelta,
10
+ Message,
11
+ RawContentBlockDeltaEvent,
12
+ RawContentBlockStartEvent,
13
+ RawContentBlockStopEvent,
14
+ TextBlock,
15
+ TextDelta,
16
+ ToolUseBlock,
17
+ )
14
18
 
15
19
  from ....components import SelfPrompt
16
- from ....misc.console import ConsoleStyle
17
- from ....symbol import Symbol
18
- from ....utils import CustomUserWarning, encode_media_frames
20
+ from ....utils import UserMessage, encode_media_frames
19
21
  from ...base import Engine
20
22
  from ...mixin.anthropic import AnthropicMixin
21
23
  from ...settings import SYMAI_CONFIG
@@ -34,7 +36,7 @@ class TokenizerWrapper:
34
36
  return self.compute_tokens_func([{"role": "user", "content": text}])
35
37
 
36
38
  class ClaudeXChatEngine(Engine, AnthropicMixin):
37
- def __init__(self, api_key: Optional[str] = None, model: Optional[str] = None):
39
+ def __init__(self, api_key: str | None = None, model: str | None = None):
38
40
  super().__init__()
39
41
  self.config = deepcopy(SYMAI_CONFIG)
40
42
  # In case we use EngineRepository.register to inject the api_key and model => dynamically change the engine at runtime
@@ -69,54 +71,76 @@ class ClaudeXChatEngine(Engine, AnthropicMixin):
69
71
  self.model = kwargs['NEUROSYMBOLIC_ENGINE_MODEL']
70
72
 
71
73
  def compute_required_tokens(self, messages) -> int:
74
+ claude_messages, system_content = self._build_claude_messages(messages)
75
+
76
+ if not claude_messages:
77
+ return 0
78
+
79
+ return self._count_claude_tokens(claude_messages, system_content)
80
+
81
+ def _build_claude_messages(self, messages):
72
82
  claude_messages = []
73
83
  system_content = None
74
84
 
85
+ for role, content_str in self._message_parts(messages):
86
+ if role == 'system':
87
+ system_content = content_str
88
+ continue
89
+
90
+ if role in ['user', 'assistant']:
91
+ message_content = self._build_message_content(content_str)
92
+ if message_content:
93
+ claude_messages.append(self._create_claude_message(role, message_content))
94
+
95
+ return claude_messages, system_content
96
+
97
+ def _message_parts(self, messages):
75
98
  for msg in messages:
76
- if not isinstance(msg, list):
77
- msg = [msg]
78
- for part in msg:
79
- if isinstance(part, str):
80
- role = 'user'
81
- content_str = part
82
- elif isinstance(part, dict):
83
- role = part.get('role')
84
- content_str = str(part.get('content', ''))
85
- else:
86
- CustomUserWarning(f"Unsupported message part type: {type(part)}", raise_with=ValueError)
87
-
88
- if role == 'system':
89
- system_content = content_str
90
- continue
91
-
92
- if role in ['user', 'assistant']:
93
- message_content = []
94
-
95
- image_content = self._handle_image_content(content_str)
96
- message_content.extend(image_content)
97
-
98
- text_content = self._remove_vision_pattern(content_str)
99
- if text_content:
100
- message_content.append({
101
- "type": "text",
102
- "text": text_content
103
- })
104
-
105
- if message_content:
106
- if len(message_content) == 1 and message_content[0].get('type') == 'text':
107
- claude_messages.append({
108
- 'role': role,
109
- 'content': message_content[0]['text']
110
- })
111
- else:
112
- claude_messages.append({
113
- 'role': role,
114
- 'content': message_content
115
- })
99
+ msg_parts = msg if isinstance(msg, list) else [msg]
100
+ for part in msg_parts:
101
+ yield self._extract_message_details(part)
116
102
 
117
- if not claude_messages:
118
- return 0
103
+ def _extract_message_details(self, part):
104
+ if isinstance(part, str):
105
+ return 'user', part
106
+
107
+ if isinstance(part, dict):
108
+ role = part.get('role')
109
+ content_str = str(part.get('content', ''))
110
+ return role, content_str
111
+
112
+ msg = f"Unsupported message part type: {type(part)}"
113
+ UserMessage(msg, raise_with=ValueError)
114
+ raise ValueError(msg)
115
+
116
+ def _build_message_content(self, content_str: str) -> list:
117
+ message_content = []
119
118
 
119
+ image_content = self._handle_image_content(content_str)
120
+ message_content.extend(image_content)
121
+
122
+ text_content = self._remove_vision_pattern(content_str)
123
+ if text_content:
124
+ message_content.append({
125
+ "type": "text",
126
+ "text": text_content
127
+ })
128
+
129
+ return message_content
130
+
131
+ def _create_claude_message(self, role: str, message_content: list) -> dict:
132
+ if len(message_content) == 1 and message_content[0].get('type') == 'text':
133
+ return {
134
+ 'role': role,
135
+ 'content': message_content[0]['text']
136
+ }
137
+
138
+ return {
139
+ 'role': role,
140
+ 'content': message_content
141
+ }
142
+
143
+ def _count_claude_tokens(self, claude_messages: list, system_content: str | None) -> int:
120
144
  try:
121
145
  count_params = {
122
146
  'model': self.model,
@@ -127,11 +151,11 @@ class ClaudeXChatEngine(Engine, AnthropicMixin):
127
151
  count_response = self.client.messages.count_tokens(**count_params)
128
152
  return count_response.input_tokens
129
153
  except Exception as e:
130
- logging.error(f"Claude count_tokens failed: {e}")
131
- CustomUserWarning(f"Error counting tokens for Claude: {str(e)}", raise_with=RuntimeError)
154
+ UserMessage(f"Claude count_tokens failed: {e}")
155
+ UserMessage(f"Error counting tokens for Claude: {e!s}", raise_with=RuntimeError)
132
156
 
133
- def compute_remaining_tokens(self, prompts: list) -> int:
134
- raise NotImplementedError('Method not implemented.')
157
+ def compute_remaining_tokens(self, _prompts: list) -> int:
158
+ UserMessage('Method not implemented.', raise_with=NotImplementedError)
135
159
 
136
160
  def _handle_image_content(self, content: str) -> list:
137
161
  """Handle image content by processing vision patterns and returning image file data."""
@@ -157,7 +181,7 @@ class ClaudeXChatEngine(Engine, AnthropicMixin):
157
181
  elif len(buffer) == 1:
158
182
  image_files.append({'data': buffer[0], 'media_type': f'image/{ext}', 'type': 'base64'})
159
183
  else:
160
- CustomUserWarning(f'No frames found for image!')
184
+ UserMessage('No frames found for image!')
161
185
  return image_files
162
186
 
163
187
  def _remove_vision_pattern(self, text: str) -> str:
@@ -180,21 +204,21 @@ class ClaudeXChatEngine(Engine, AnthropicMixin):
180
204
  except Exception as e:
181
205
  if anthropic.api_key is None or anthropic.api_key == '':
182
206
  msg = 'Anthropic API key is not set. Please set it in the config file or pass it as an argument to the command method.'
183
- logging.error(msg)
207
+ UserMessage(msg)
184
208
  if self.config['NEUROSYMBOLIC_ENGINE_API_KEY'] is None or self.config['NEUROSYMBOLIC_ENGINE_API_KEY'] == '':
185
- CustomUserWarning(msg, raise_with=ValueError)
209
+ UserMessage(msg, raise_with=ValueError)
186
210
  anthropic.api_key = self.config['NEUROSYMBOLIC_ENGINE_API_KEY']
187
211
 
188
212
  callback = self.client.messages.create
189
- kwargs['model'] = kwargs['model'] if 'model' in kwargs else self.model
213
+ kwargs['model'] = kwargs.get('model', self.model)
190
214
 
191
215
  if except_remedy is not None:
192
216
  res = except_remedy(self, e, callback, argument)
193
217
  else:
194
- CustomUserWarning(f'Error during generation. Caused by: {e}', raise_with=ValueError)
218
+ UserMessage(f'Error during generation. Caused by: {e}', raise_with=ValueError)
195
219
 
196
220
  if payload['stream']:
197
- res = [_ for _ in res] # Unpack the iterator to a list
221
+ res = list(res) # Unpack the iterator to a list
198
222
  metadata = {'raw_output': res}
199
223
  response_data = self._collect_response(res)
200
224
 
@@ -210,11 +234,13 @@ class ClaudeXChatEngine(Engine, AnthropicMixin):
210
234
 
211
235
  def _prepare_raw_input(self, argument):
212
236
  if not argument.prop.processed_input:
213
- raise ValueError('Need to provide a prompt instruction to the engine if `raw_input` is enabled!')
237
+ msg = 'Need to provide a prompt instruction to the engine if `raw_input` is enabled!'
238
+ UserMessage(msg)
239
+ raise ValueError(msg)
214
240
  system = NOT_GIVEN
215
241
  prompt = copy(argument.prop.processed_input)
216
- if type(prompt) != list:
217
- if type(prompt) != dict:
242
+ if not isinstance(prompt, list):
243
+ if not isinstance(prompt, dict):
218
244
  prompt = {'role': 'user', 'content': str(prompt)}
219
245
  prompt = [prompt]
220
246
  if len(prompt) > 1:
@@ -233,85 +259,113 @@ class ClaudeXChatEngine(Engine, AnthropicMixin):
233
259
  argument.prop.prepared_input = self._prepare_raw_input(argument)
234
260
  return
235
261
 
236
- _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"""
237
- user: str = ""
238
- system: str = ""
262
+ 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"""
263
+ image_files = self._handle_image_content(str(argument.prop.processed_input))
264
+ has_image = len(image_files) > 0
265
+
266
+ system = self._build_system_prompt(argument, has_image, non_verbose_output)
267
+ user_text, user_prompt, image_blocks = self._build_user_prompt(argument, image_files)
268
+ system, user_prompt = self._apply_self_prompt_if_needed(argument, system, user_text, image_blocks, user_prompt)
269
+
270
+ argument.prop.prepared_input = (system, [user_prompt])
239
271
 
272
+ def _build_system_prompt(self, argument, has_image: bool, non_verbose_output: str) -> str:
273
+ system = self._build_system_prefix(argument, non_verbose_output)
274
+ system = self._append_context_sections(system, argument)
275
+ system = self._append_instruction_section(system, argument, has_image)
276
+ return self._append_template_suffix(system, argument)
277
+
278
+ def _build_system_prefix(self, argument, non_verbose_output: str) -> str:
279
+ system = ""
240
280
  if argument.prop.suppress_verbose_output:
241
- system += _non_verbose_output
281
+ system += non_verbose_output
282
+
242
283
  system = f'{system}\n' if system and len(system) > 0 else ''
243
284
 
244
285
  if argument.prop.response_format:
245
- _rsp_fmt = argument.prop.response_format
246
- assert _rsp_fmt.get('type') is not None, 'Response format type is required! Expected format `{"type": str}`! The str value will be passed to the engine. Refer to the Anthropic documentation for more information: https://docs.anthropic.com/en/docs/test-and-evaluate/strengthen-guardrails/increase-consistency#example-standardizing-customer-feedback'
247
- system += _non_verbose_output
248
- system += f'<RESPONSE_FORMAT/>\n{_rsp_fmt["type"]}\n\n'
286
+ response_format = argument.prop.response_format
287
+ assert response_format.get('type') is not None, 'Response format type is required! Expected format `{"type": str}`! The str value will be passed to the engine. Refer to the Anthropic documentation for more information: https://docs.anthropic.com/en/docs/test-and-evaluate/strengthen-guardrails/increase-consistency#example-standardizing-customer-feedback'
288
+ system += non_verbose_output
289
+ system += f'<RESPONSE_FORMAT/>\n{response_format["type"]}\n\n'
249
290
 
291
+ return system
292
+
293
+ def _append_context_sections(self, system: str, argument) -> str:
250
294
  ref = argument.prop.instance
251
- static_ctxt, dyn_ctxt = ref.global_context
252
- if len(static_ctxt) > 0:
253
- system += f"<STATIC_CONTEXT/>\n{static_ctxt}\n\n"
295
+ static_context, dynamic_context = ref.global_context
296
+
297
+ if len(static_context) > 0:
298
+ system += f"<STATIC_CONTEXT/>\n{static_context}\n\n"
254
299
 
255
- if len(dyn_ctxt) > 0:
256
- system += f"<DYNAMIC_CONTEXT/>\n{dyn_ctxt}\n\n"
300
+ if len(dynamic_context) > 0:
301
+ system += f"<DYNAMIC_CONTEXT/>\n{dynamic_context}\n\n"
257
302
 
258
303
  payload = argument.prop.payload
259
304
  if argument.prop.payload:
260
- system += f"<ADDITIONAL_CONTEXT/>\n{str(payload)}\n\n"
305
+ system += f"<ADDITIONAL_CONTEXT/>\n{payload!s}\n\n"
261
306
 
262
- examples: List[str] = argument.prop.examples
307
+ examples: list[str] = argument.prop.examples
263
308
  if examples and len(examples) > 0:
264
- system += f"<EXAMPLES/>\n{str(examples)}\n\n"
309
+ system += f"<EXAMPLES/>\n{examples!s}\n\n"
265
310
 
266
- image_files = self._handle_image_content(str(argument.prop.processed_input))
311
+ return system
267
312
 
313
+ def _append_instruction_section(self, system: str, argument, has_image: bool) -> str:
268
314
  if argument.prop.prompt is not None and len(argument.prop.prompt) > 0:
269
- val = str(argument.prop.prompt)
270
- if len(image_files) > 0:
271
- val = self._remove_vision_pattern(val)
272
- system += f"<INSTRUCTION/>\n{val}\n\n"
315
+ instruction_value = str(argument.prop.prompt)
316
+ if has_image:
317
+ instruction_value = self._remove_vision_pattern(instruction_value)
318
+ system += f"<INSTRUCTION/>\n{instruction_value}\n\n"
319
+
320
+ return system
321
+
322
+ def _append_template_suffix(self, system: str, argument) -> str:
323
+ if argument.prop.template_suffix:
324
+ system += f' You will only generate content for the placeholder `{argument.prop.template_suffix!s}` following the instructions and the provided context information.\n\n'
273
325
 
274
- suffix: str = str(argument.prop.processed_input)
326
+ return system
327
+
328
+ def _build_user_prompt(self, argument, image_files):
329
+ suffix = str(argument.prop.processed_input)
275
330
  if len(image_files) > 0:
276
331
  suffix = self._remove_vision_pattern(suffix)
277
332
 
278
- user += f"{suffix}"
333
+ user_text = f"{suffix}"
334
+ if not user_text:
335
+ user_text = "N/A"
279
336
 
280
- if not len(user):
281
- # Anthropic doesn't allow empty user prompts; force it
282
- user = "N/A"
337
+ image_blocks = [{'type': 'image', 'source': image_file} for image_file in image_files]
338
+ user_prompt = self._wrap_user_prompt_content(user_text, image_blocks)
339
+ return user_text, user_prompt, image_blocks
283
340
 
284
- if argument.prop.template_suffix:
285
- 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'
341
+ def _wrap_user_prompt_content(self, user_text: str, image_blocks: list[dict]) -> dict:
342
+ if len(image_blocks) > 0:
343
+ return {
344
+ "role": "user",
345
+ "content": [
346
+ *image_blocks,
347
+ {'type': 'text', 'text': user_text}
348
+ ]
349
+ }
286
350
 
287
- if len(image_files) > 0:
288
- images = [{ 'type': 'image', "source": im } for im in image_files]
289
- user_prompt = { "role": "user", "content": [
290
- *images,
291
- { 'type': 'text', 'text': user }
292
- ]}
293
- else:
294
- user_prompt = { "role": "user", "content": user }
295
-
296
- # First check if the `Symbol` instance has the flag set, otherwise check if it was passed as an argument to a method
297
- if argument.prop.instance._kwargs.get('self_prompt', False) or argument.prop.self_prompt:
298
- self_prompter = SelfPrompt()
299
-
300
- res = self_prompter({'user': user, 'system': system})
301
- if res is None:
302
- raise ValueError("Self-prompting failed!")
303
-
304
- if len(image_files) > 0:
305
- user_prompt = { "role": "user", "content": [
306
- *images,
307
- { 'type': 'text', 'text': res['user'] }
308
- ]}
309
- else:
310
- user_prompt = { "role": "user", "content": res['user'] }
351
+ return {
352
+ "role": "user",
353
+ "content": user_text
354
+ }
311
355
 
312
- system = res['system']
356
+ def _apply_self_prompt_if_needed(self, argument, system: str, user_text: str, image_blocks: list[dict], user_prompt: dict):
357
+ if not (argument.prop.instance._kwargs.get('self_prompt', False) or argument.prop.self_prompt):
358
+ return system, user_prompt
313
359
 
314
- argument.prop.prepared_input = (system, [user_prompt])
360
+ self_prompter = SelfPrompt()
361
+ res = self_prompter({'user': user_text, 'system': system})
362
+ if res is None:
363
+ msg = "Self-prompting failed!"
364
+ UserMessage(msg)
365
+ raise ValueError(msg)
366
+
367
+ updated_user_prompt = self._wrap_user_prompt_content(res['user'], image_blocks)
368
+ return res['system'], updated_user_prompt
315
369
 
316
370
  def _prepare_request_payload(self, argument):
317
371
  kwargs = argument.kwargs
@@ -326,7 +380,7 @@ class ClaudeXChatEngine(Engine, AnthropicMixin):
326
380
  tool_choice = kwargs.get('tool_choice', NOT_GIVEN)
327
381
  metadata_anthropic = kwargs.get('metadata', NOT_GIVEN)
328
382
 
329
- if stop != NOT_GIVEN and type(stop) != list:
383
+ if stop != NOT_GIVEN and not isinstance(stop, list):
330
384
  stop = [stop]
331
385
 
332
386
  #@NOTE: Anthropic fails if stop is not raw string, so cast it to r'…'
@@ -349,69 +403,95 @@ class ClaudeXChatEngine(Engine, AnthropicMixin):
349
403
 
350
404
  def _collect_response(self, res):
351
405
  if isinstance(res, list):
352
- text_content = ''
353
- tool_calls_raw = []
354
- active_tool_calls = {}
355
-
356
- for chunk in res:
357
- if isinstance(chunk, RawContentBlockStartEvent):
358
- if isinstance(chunk.content_block, ToolUseBlock):
359
- active_tool_calls[chunk.index] = {
360
- 'id': chunk.content_block.id,
361
- 'name': chunk.content_block.name,
362
- 'input_json_str': ""
363
- }
364
- elif isinstance(chunk, RawContentBlockDeltaEvent):
365
- if isinstance(chunk.delta, TextDelta):
366
- text_content += chunk.delta.text
367
- elif isinstance(chunk.delta, InputJSONDelta):
368
- if chunk.index in active_tool_calls:
369
- active_tool_calls[chunk.index]['input_json_str'] += chunk.delta.partial_json
370
- elif isinstance(chunk, RawContentBlockStopEvent):
371
- if chunk.index in active_tool_calls:
372
- tool_call_info = active_tool_calls.pop(chunk.index)
373
- try:
374
- tool_call_info['input'] = json.loads(tool_call_info['input_json_str'])
375
- except json.JSONDecodeError as e:
376
- logging.error(f"Failed to parse JSON for tool call {tool_call_info['name']}: {e}. Raw JSON: '{tool_call_info['input_json_str']}'")
377
- tool_call_info['input'] = {}
378
- tool_calls_raw.append(tool_call_info)
379
-
380
- function_call_data = None
381
- if tool_calls_raw:
382
- if len(tool_calls_raw) > 1:
383
- CustomUserWarning("Multiple tool calls detected in the stream but only the first one will be processed.")
384
- function_call_data = {
385
- 'name': tool_calls_raw[0]['name'],
386
- 'arguments': tool_calls_raw[0]['input']
387
- }
406
+ return self._collect_streaming_response(res)
388
407
 
389
- return {
390
- "text": text_content,
391
- "function_call": function_call_data
392
- }
393
-
394
- # Non-streamed response (res is a Message object)
395
408
  if isinstance(res, Message):
396
- text_content = ''
397
- function_call_data = None
398
- hit_tool_use = False
399
-
400
- for content_block in res.content:
401
- if isinstance(content_block, TextBlock):
402
- text_content += content_block.text
403
- elif isinstance(content_block, ToolUseBlock):
404
- if hit_tool_use:
405
- CustomUserWarning("Multiple tool use blocks detected in the response but only the first one will be processed.")
406
- else:
407
- function_call_data = {
408
- 'name': content_block.name,
409
- 'arguments': content_block.input
410
- }
411
- hit_tool_use = True
412
- return {
413
- "text": text_content,
414
- "function_call": function_call_data
409
+ return self._collect_message_response(res)
410
+
411
+ UserMessage(f"Unexpected response type from Anthropic API: {type(res)}", raise_with=ValueError)
412
+ return {}
413
+
414
+ def _collect_streaming_response(self, res):
415
+ text_parts = []
416
+ tool_calls_raw = []
417
+ active_tool_calls = {}
418
+
419
+ for chunk in res:
420
+ if isinstance(chunk, RawContentBlockStartEvent):
421
+ self._start_tool_call(chunk, active_tool_calls)
422
+ elif isinstance(chunk, RawContentBlockDeltaEvent):
423
+ self._update_stream_chunk(chunk, text_parts, active_tool_calls)
424
+ elif isinstance(chunk, RawContentBlockStopEvent):
425
+ tool_call = self._finish_tool_call(chunk, active_tool_calls)
426
+ if tool_call is not None:
427
+ tool_calls_raw.append(tool_call)
428
+
429
+ text_content = ''.join(text_parts)
430
+ function_call_data = self._build_function_call_data(tool_calls_raw)
431
+
432
+ return {
433
+ "text": text_content,
434
+ "function_call": function_call_data
435
+ }
436
+
437
+ def _start_tool_call(self, chunk, active_tool_calls: dict):
438
+ if isinstance(chunk.content_block, ToolUseBlock):
439
+ active_tool_calls[chunk.index] = {
440
+ 'id': chunk.content_block.id,
441
+ 'name': chunk.content_block.name,
442
+ 'input_json_str': ""
415
443
  }
416
444
 
417
- CustomUserWarning(f"Unexpected response type from Anthropic API: {type(res)}", raise_with=ValueError)
445
+ def _update_stream_chunk(self, chunk, text_parts: list, active_tool_calls: dict):
446
+ if isinstance(chunk.delta, TextDelta):
447
+ text_parts.append(chunk.delta.text)
448
+ elif isinstance(chunk.delta, InputJSONDelta) and chunk.index in active_tool_calls:
449
+ active_tool_calls[chunk.index]['input_json_str'] += chunk.delta.partial_json
450
+
451
+ def _finish_tool_call(self, chunk, active_tool_calls: dict):
452
+ if chunk.index not in active_tool_calls:
453
+ return None
454
+
455
+ tool_call_info = active_tool_calls.pop(chunk.index)
456
+ try:
457
+ tool_call_info['input'] = json.loads(tool_call_info['input_json_str'])
458
+ except json.JSONDecodeError as e:
459
+ UserMessage(f"Failed to parse JSON for tool call {tool_call_info['name']}: {e}. Raw JSON: '{tool_call_info['input_json_str']}'")
460
+ tool_call_info['input'] = {}
461
+ return tool_call_info
462
+
463
+ def _build_function_call_data(self, tool_calls_raw: list | None) -> dict | None:
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
+ tool_call = tool_calls_raw[0]
471
+ return {
472
+ 'name': tool_call['name'],
473
+ 'arguments': tool_call['input']
474
+ }
475
+
476
+ def _collect_message_response(self, res: Message):
477
+ text_parts = []
478
+ function_call_data = None
479
+ hit_tool_use = False
480
+
481
+ for content_block in res.content:
482
+ if isinstance(content_block, TextBlock):
483
+ text_parts.append(content_block.text)
484
+ elif isinstance(content_block, ToolUseBlock):
485
+ if hit_tool_use:
486
+ UserMessage("Multiple tool use blocks detected in the response but only the first one will be processed.")
487
+ else:
488
+ function_call_data = {
489
+ 'name': content_block.name,
490
+ 'arguments': content_block.input
491
+ }
492
+ hit_tool_use = True
493
+
494
+ return {
495
+ "text": ''.join(text_parts),
496
+ "function_call": function_call_data
497
+ }