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,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 (
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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 ....
|
|
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:
|
|
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
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
118
|
-
|
|
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
|
-
|
|
131
|
-
|
|
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,
|
|
134
|
-
|
|
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
|
-
|
|
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
|
-
|
|
207
|
+
UserMessage(msg)
|
|
184
208
|
if self.config['NEUROSYMBOLIC_ENGINE_API_KEY'] is None or self.config['NEUROSYMBOLIC_ENGINE_API_KEY'] == '':
|
|
185
|
-
|
|
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
|
|
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
|
-
|
|
218
|
+
UserMessage(f'Error during generation. Caused by: {e}', raise_with=ValueError)
|
|
195
219
|
|
|
196
220
|
if payload['stream']:
|
|
197
|
-
res =
|
|
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
|
-
|
|
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
|
|
217
|
-
if
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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 +=
|
|
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
|
-
|
|
246
|
-
assert
|
|
247
|
-
system +=
|
|
248
|
-
system += f'<RESPONSE_FORMAT/>\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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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(
|
|
256
|
-
system += f"<DYNAMIC_CONTEXT/>\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{
|
|
305
|
+
system += f"<ADDITIONAL_CONTEXT/>\n{payload!s}\n\n"
|
|
261
306
|
|
|
262
|
-
examples:
|
|
307
|
+
examples: list[str] = argument.prop.examples
|
|
263
308
|
if examples and len(examples) > 0:
|
|
264
|
-
system += f"<EXAMPLES/>\n{
|
|
309
|
+
system += f"<EXAMPLES/>\n{examples!s}\n\n"
|
|
265
310
|
|
|
266
|
-
|
|
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
|
-
|
|
270
|
-
if
|
|
271
|
-
|
|
272
|
-
system += f"<INSTRUCTION/>\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
|
-
|
|
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
|
-
|
|
333
|
+
user_text = f"{suffix}"
|
|
334
|
+
if not user_text:
|
|
335
|
+
user_text = "N/A"
|
|
279
336
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
-
|
|
285
|
-
|
|
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
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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
|
-
|
|
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
|
+
}
|