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.
- symai/__init__.py +96 -64
- symai/backend/base.py +93 -80
- symai/backend/engines/drawing/engine_bfl.py +12 -11
- symai/backend/engines/drawing/engine_gpt_image.py +108 -87
- symai/backend/engines/embedding/engine_llama_cpp.py +25 -28
- symai/backend/engines/embedding/engine_openai.py +3 -5
- symai/backend/engines/execute/engine_python.py +6 -5
- symai/backend/engines/files/engine_io.py +74 -67
- symai/backend/engines/imagecaptioning/engine_blip2.py +3 -3
- symai/backend/engines/imagecaptioning/engine_llavacpp_client.py +54 -38
- symai/backend/engines/index/engine_pinecone.py +23 -24
- symai/backend/engines/index/engine_vectordb.py +16 -14
- symai/backend/engines/lean/engine_lean4.py +38 -34
- symai/backend/engines/neurosymbolic/__init__.py +41 -13
- symai/backend/engines/neurosymbolic/engine_anthropic_claudeX_chat.py +262 -182
- symai/backend/engines/neurosymbolic/engine_anthropic_claudeX_reasoning.py +263 -191
- symai/backend/engines/neurosymbolic/engine_deepseekX_reasoning.py +53 -49
- symai/backend/engines/neurosymbolic/engine_google_geminiX_reasoning.py +212 -211
- symai/backend/engines/neurosymbolic/engine_groq.py +87 -63
- symai/backend/engines/neurosymbolic/engine_huggingface.py +21 -24
- symai/backend/engines/neurosymbolic/engine_llama_cpp.py +117 -48
- symai/backend/engines/neurosymbolic/engine_openai_gptX_chat.py +256 -229
- symai/backend/engines/neurosymbolic/engine_openai_gptX_reasoning.py +270 -150
- symai/backend/engines/ocr/engine_apilayer.py +6 -8
- symai/backend/engines/output/engine_stdout.py +1 -4
- symai/backend/engines/search/engine_openai.py +7 -7
- symai/backend/engines/search/engine_perplexity.py +5 -5
- symai/backend/engines/search/engine_serpapi.py +12 -14
- symai/backend/engines/speech_to_text/engine_local_whisper.py +20 -27
- symai/backend/engines/symbolic/engine_wolframalpha.py +3 -3
- symai/backend/engines/text_to_speech/engine_openai.py +5 -7
- symai/backend/engines/text_vision/engine_clip.py +7 -11
- symai/backend/engines/userinput/engine_console.py +3 -3
- symai/backend/engines/webscraping/engine_requests.py +81 -48
- symai/backend/mixin/__init__.py +13 -0
- symai/backend/mixin/anthropic.py +4 -2
- symai/backend/mixin/deepseek.py +2 -0
- symai/backend/mixin/google.py +2 -0
- symai/backend/mixin/openai.py +11 -3
- symai/backend/settings.py +83 -16
- symai/chat.py +101 -78
- symai/collect/__init__.py +7 -1
- symai/collect/dynamic.py +77 -69
- symai/collect/pipeline.py +35 -27
- symai/collect/stats.py +75 -63
- symai/components.py +198 -169
- symai/constraints.py +15 -12
- symai/core.py +698 -359
- symai/core_ext.py +32 -34
- symai/endpoints/api.py +80 -73
- symai/extended/.DS_Store +0 -0
- symai/extended/__init__.py +46 -12
- symai/extended/api_builder.py +11 -8
- symai/extended/arxiv_pdf_parser.py +13 -12
- symai/extended/bibtex_parser.py +2 -3
- symai/extended/conversation.py +101 -90
- symai/extended/document.py +17 -10
- symai/extended/file_merger.py +18 -13
- symai/extended/graph.py +18 -13
- symai/extended/html_style_template.py +2 -4
- symai/extended/interfaces/blip_2.py +1 -2
- symai/extended/interfaces/clip.py +1 -2
- symai/extended/interfaces/console.py +7 -1
- symai/extended/interfaces/dall_e.py +1 -1
- symai/extended/interfaces/flux.py +1 -1
- symai/extended/interfaces/gpt_image.py +1 -1
- symai/extended/interfaces/input.py +1 -1
- symai/extended/interfaces/llava.py +0 -1
- symai/extended/interfaces/naive_vectordb.py +7 -8
- symai/extended/interfaces/naive_webscraping.py +1 -1
- symai/extended/interfaces/ocr.py +1 -1
- symai/extended/interfaces/pinecone.py +6 -5
- symai/extended/interfaces/serpapi.py +1 -1
- symai/extended/interfaces/terminal.py +2 -3
- symai/extended/interfaces/tts.py +1 -1
- symai/extended/interfaces/whisper.py +1 -1
- symai/extended/interfaces/wolframalpha.py +1 -1
- symai/extended/metrics/__init__.py +11 -1
- symai/extended/metrics/similarity.py +11 -13
- symai/extended/os_command.py +17 -16
- symai/extended/packages/__init__.py +29 -3
- symai/extended/packages/symdev.py +19 -16
- symai/extended/packages/sympkg.py +12 -9
- symai/extended/packages/symrun.py +21 -19
- symai/extended/repo_cloner.py +11 -10
- symai/extended/seo_query_optimizer.py +1 -2
- symai/extended/solver.py +20 -23
- symai/extended/summarizer.py +4 -3
- symai/extended/taypan_interpreter.py +10 -12
- symai/extended/vectordb.py +99 -82
- symai/formatter/__init__.py +9 -1
- symai/formatter/formatter.py +12 -16
- symai/formatter/regex.py +62 -63
- symai/functional.py +176 -122
- symai/imports.py +136 -127
- symai/interfaces.py +56 -27
- symai/memory.py +14 -13
- symai/misc/console.py +49 -39
- symai/misc/loader.py +5 -3
- symai/models/__init__.py +17 -1
- symai/models/base.py +269 -181
- symai/models/errors.py +0 -1
- symai/ops/__init__.py +32 -22
- symai/ops/measures.py +11 -15
- symai/ops/primitives.py +348 -228
- symai/post_processors.py +32 -28
- symai/pre_processors.py +39 -41
- symai/processor.py +6 -4
- symai/prompts.py +59 -45
- symai/server/huggingface_server.py +23 -20
- symai/server/llama_cpp_server.py +7 -5
- symai/shell.py +3 -4
- symai/shellsv.py +499 -375
- symai/strategy.py +517 -287
- symai/symbol.py +111 -116
- symai/utils.py +42 -36
- {symbolicai-0.20.2.dist-info → symbolicai-1.0.0.dist-info}/METADATA +4 -2
- symbolicai-1.0.0.dist-info/RECORD +163 -0
- symbolicai-0.20.2.dist-info/RECORD +0 -162
- {symbolicai-0.20.2.dist-info → symbolicai-1.0.0.dist-info}/WHEEL +0 -0
- {symbolicai-0.20.2.dist-info → symbolicai-1.0.0.dist-info}/entry_points.txt +0 -0
- {symbolicai-0.20.2.dist-info → symbolicai-1.0.0.dist-info}/licenses/LICENSE +0 -0
- {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 (
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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 ....
|
|
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:
|
|
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
|
-
|
|
132
|
-
|
|
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
|
|
135
|
-
|
|
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
|
-
|
|
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
|
-
|
|
199
|
+
UserMessage(msg)
|
|
185
200
|
if self.config['NEUROSYMBOLIC_ENGINE_API_KEY'] is None or self.config['NEUROSYMBOLIC_ENGINE_API_KEY'] == '':
|
|
186
|
-
|
|
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
|
|
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
|
-
|
|
210
|
+
UserMessage(f'Error during generation. Caused by: {e}', raise_with=ValueError)
|
|
196
211
|
|
|
197
212
|
if payload['stream']:
|
|
198
|
-
res =
|
|
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
|
-
|
|
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
|
|
221
|
-
if
|
|
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
|
-
|
|
242
|
-
system
|
|
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
|
|
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
|
-
|
|
250
|
-
if not (
|
|
251
|
-
|
|
252
|
-
system +=
|
|
253
|
-
system += f
|
|
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
|
|
265
|
-
system += f"<ADDITIONAL_CONTEXT/>\n{
|
|
297
|
+
if payload:
|
|
298
|
+
system += f"<ADDITIONAL_CONTEXT/>\n{payload!s}\n\n"
|
|
266
299
|
|
|
267
|
-
examples:
|
|
300
|
+
examples: list[str] = argument.prop.examples
|
|
268
301
|
if examples and len(examples) > 0:
|
|
269
|
-
system += f"<EXAMPLES/>\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
|
-
|
|
305
|
+
value = str(argument.prop.prompt)
|
|
275
306
|
if len(image_files) > 0:
|
|
276
|
-
|
|
277
|
-
system += f"<INSTRUCTION/>\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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
-
|
|
290
|
-
|
|
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
|
-
|
|
293
|
-
|
|
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
|
-
|
|
314
|
-
|
|
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
|
-
{
|
|
317
|
-
]
|
|
318
|
-
|
|
319
|
-
user_prompt = { "role": "user", "content": res['user'] }
|
|
352
|
+
{'type': 'text', 'text': user_text}
|
|
353
|
+
]
|
|
354
|
+
}
|
|
320
355
|
|
|
321
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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
|
-
|
|
416
|
-
if isinstance(
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
}
|
|
510
|
+
'name': content_block.name,
|
|
511
|
+
'arguments': content_block.input
|
|
512
|
+
}, True
|
|
441
513
|
|
|
442
|
-
|
|
514
|
+
return function_call_data, tool_call_detected
|