symbolicai 1.0.0__py3-none-any.whl → 1.1.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 +198 -134
- symai/backend/base.py +51 -51
- symai/backend/engines/drawing/engine_bfl.py +33 -33
- symai/backend/engines/drawing/engine_gpt_image.py +4 -10
- symai/backend/engines/embedding/engine_llama_cpp.py +50 -35
- symai/backend/engines/embedding/engine_openai.py +22 -16
- symai/backend/engines/execute/engine_python.py +16 -16
- symai/backend/engines/files/engine_io.py +51 -49
- symai/backend/engines/imagecaptioning/engine_blip2.py +27 -23
- symai/backend/engines/imagecaptioning/engine_llavacpp_client.py +53 -46
- symai/backend/engines/index/engine_pinecone.py +116 -88
- symai/backend/engines/index/engine_qdrant.py +1011 -0
- symai/backend/engines/index/engine_vectordb.py +78 -52
- symai/backend/engines/lean/engine_lean4.py +65 -25
- symai/backend/engines/neurosymbolic/__init__.py +28 -28
- symai/backend/engines/neurosymbolic/engine_anthropic_claudeX_chat.py +137 -135
- symai/backend/engines/neurosymbolic/engine_anthropic_claudeX_reasoning.py +145 -152
- symai/backend/engines/neurosymbolic/engine_cerebras.py +328 -0
- symai/backend/engines/neurosymbolic/engine_deepseekX_reasoning.py +75 -49
- symai/backend/engines/neurosymbolic/engine_google_geminiX_reasoning.py +199 -155
- symai/backend/engines/neurosymbolic/engine_groq.py +106 -72
- symai/backend/engines/neurosymbolic/engine_huggingface.py +100 -67
- symai/backend/engines/neurosymbolic/engine_llama_cpp.py +121 -93
- symai/backend/engines/neurosymbolic/engine_openai_gptX_chat.py +213 -132
- symai/backend/engines/neurosymbolic/engine_openai_gptX_reasoning.py +180 -137
- symai/backend/engines/ocr/engine_apilayer.py +18 -20
- symai/backend/engines/output/engine_stdout.py +9 -9
- symai/backend/engines/{webscraping → scrape}/engine_requests.py +25 -11
- symai/backend/engines/search/engine_openai.py +95 -83
- symai/backend/engines/search/engine_parallel.py +665 -0
- symai/backend/engines/search/engine_perplexity.py +40 -41
- symai/backend/engines/search/engine_serpapi.py +33 -28
- symai/backend/engines/speech_to_text/engine_local_whisper.py +37 -27
- symai/backend/engines/symbolic/engine_wolframalpha.py +14 -8
- symai/backend/engines/text_to_speech/engine_openai.py +15 -19
- symai/backend/engines/text_vision/engine_clip.py +34 -28
- symai/backend/engines/userinput/engine_console.py +3 -4
- symai/backend/mixin/anthropic.py +48 -40
- symai/backend/mixin/deepseek.py +4 -5
- symai/backend/mixin/google.py +5 -4
- symai/backend/mixin/groq.py +2 -4
- symai/backend/mixin/openai.py +132 -110
- symai/backend/settings.py +14 -14
- symai/chat.py +164 -94
- symai/collect/dynamic.py +13 -11
- symai/collect/pipeline.py +39 -31
- symai/collect/stats.py +109 -69
- symai/components.py +556 -238
- symai/constraints.py +14 -5
- symai/core.py +1495 -1210
- symai/core_ext.py +55 -50
- symai/endpoints/api.py +113 -58
- symai/extended/api_builder.py +22 -17
- symai/extended/arxiv_pdf_parser.py +13 -5
- symai/extended/bibtex_parser.py +8 -4
- symai/extended/conversation.py +88 -69
- symai/extended/document.py +40 -27
- symai/extended/file_merger.py +45 -7
- symai/extended/graph.py +38 -24
- symai/extended/html_style_template.py +17 -11
- symai/extended/interfaces/blip_2.py +1 -1
- symai/extended/interfaces/clip.py +4 -2
- symai/extended/interfaces/console.py +5 -3
- symai/extended/interfaces/dall_e.py +3 -1
- symai/extended/interfaces/file.py +2 -0
- symai/extended/interfaces/flux.py +3 -1
- symai/extended/interfaces/gpt_image.py +15 -6
- symai/extended/interfaces/input.py +2 -1
- symai/extended/interfaces/llava.py +1 -1
- symai/extended/interfaces/{naive_webscraping.py → naive_scrape.py} +3 -2
- symai/extended/interfaces/naive_vectordb.py +2 -2
- symai/extended/interfaces/ocr.py +4 -2
- symai/extended/interfaces/openai_search.py +2 -0
- symai/extended/interfaces/parallel.py +30 -0
- symai/extended/interfaces/perplexity.py +2 -0
- symai/extended/interfaces/pinecone.py +6 -4
- symai/extended/interfaces/python.py +2 -0
- symai/extended/interfaces/serpapi.py +2 -0
- symai/extended/interfaces/terminal.py +0 -1
- symai/extended/interfaces/tts.py +2 -1
- symai/extended/interfaces/whisper.py +2 -1
- symai/extended/interfaces/wolframalpha.py +1 -0
- symai/extended/metrics/__init__.py +1 -1
- symai/extended/metrics/similarity.py +5 -2
- symai/extended/os_command.py +31 -22
- symai/extended/packages/symdev.py +39 -34
- symai/extended/packages/sympkg.py +30 -27
- symai/extended/packages/symrun.py +46 -35
- symai/extended/repo_cloner.py +10 -9
- symai/extended/seo_query_optimizer.py +15 -12
- symai/extended/solver.py +104 -76
- symai/extended/summarizer.py +8 -7
- symai/extended/taypan_interpreter.py +10 -9
- symai/extended/vectordb.py +28 -15
- symai/formatter/formatter.py +39 -31
- symai/formatter/regex.py +46 -44
- symai/functional.py +184 -86
- symai/imports.py +85 -51
- symai/interfaces.py +1 -1
- symai/memory.py +33 -24
- symai/menu/screen.py +28 -19
- symai/misc/console.py +27 -27
- symai/misc/loader.py +4 -3
- symai/models/base.py +147 -76
- symai/models/errors.py +1 -1
- symai/ops/__init__.py +1 -1
- symai/ops/measures.py +17 -14
- symai/ops/primitives.py +933 -635
- symai/post_processors.py +28 -24
- symai/pre_processors.py +58 -52
- symai/processor.py +15 -9
- symai/prompts.py +714 -649
- symai/server/huggingface_server.py +115 -32
- symai/server/llama_cpp_server.py +14 -6
- symai/server/qdrant_server.py +206 -0
- symai/shell.py +98 -39
- symai/shellsv.py +307 -223
- symai/strategy.py +135 -81
- symai/symbol.py +276 -225
- symai/utils.py +62 -46
- {symbolicai-1.0.0.dist-info → symbolicai-1.1.0.dist-info}/METADATA +19 -9
- symbolicai-1.1.0.dist-info/RECORD +168 -0
- symbolicai-1.0.0.dist-info/RECORD +0 -163
- {symbolicai-1.0.0.dist-info → symbolicai-1.1.0.dist-info}/WHEEL +0 -0
- {symbolicai-1.0.0.dist-info → symbolicai-1.1.0.dist-info}/entry_points.txt +0 -0
- {symbolicai-1.0.0.dist-info → symbolicai-1.1.0.dist-info}/licenses/LICENSE +0 -0
- {symbolicai-1.0.0.dist-info → symbolicai-1.1.0.dist-info}/top_level.txt +0 -0
|
@@ -28,6 +28,7 @@ logging.getLogger("urllib").setLevel(logging.ERROR)
|
|
|
28
28
|
logging.getLogger("httpx").setLevel(logging.ERROR)
|
|
29
29
|
logging.getLogger("httpcore").setLevel(logging.ERROR)
|
|
30
30
|
|
|
31
|
+
|
|
31
32
|
class TokenizerWrapper:
|
|
32
33
|
def __init__(self, compute_tokens_func):
|
|
33
34
|
self.compute_tokens_func = compute_tokens_func
|
|
@@ -35,18 +36,19 @@ class TokenizerWrapper:
|
|
|
35
36
|
def encode(self, text: str) -> int:
|
|
36
37
|
return self.compute_tokens_func([{"role": "user", "content": text}])
|
|
37
38
|
|
|
39
|
+
|
|
38
40
|
class ClaudeXChatEngine(Engine, AnthropicMixin):
|
|
39
41
|
def __init__(self, api_key: str | None = None, model: str | None = None):
|
|
40
42
|
super().__init__()
|
|
41
43
|
self.config = deepcopy(SYMAI_CONFIG)
|
|
42
44
|
# In case we use EngineRepository.register to inject the api_key and model => dynamically change the engine at runtime
|
|
43
45
|
if api_key is not None and model is not None:
|
|
44
|
-
self.config[
|
|
45
|
-
self.config[
|
|
46
|
-
if self.id() !=
|
|
47
|
-
return
|
|
48
|
-
anthropic.api_key = self.config[
|
|
49
|
-
self.model = self.config[
|
|
46
|
+
self.config["NEUROSYMBOLIC_ENGINE_API_KEY"] = api_key
|
|
47
|
+
self.config["NEUROSYMBOLIC_ENGINE_MODEL"] = model
|
|
48
|
+
if self.id() != "neurosymbolic":
|
|
49
|
+
return # do not initialize if not neurosymbolic; avoids conflict with llama.cpp check in EngineRepository.register_from_package
|
|
50
|
+
anthropic.api_key = self.config["NEUROSYMBOLIC_ENGINE_API_KEY"]
|
|
51
|
+
self.model = self.config["NEUROSYMBOLIC_ENGINE_MODEL"]
|
|
50
52
|
self.name = self.__class__.__name__
|
|
51
53
|
self.tokenizer = TokenizerWrapper(self.compute_required_tokens)
|
|
52
54
|
self.max_context_tokens = self.api_max_context_tokens()
|
|
@@ -54,21 +56,25 @@ class ClaudeXChatEngine(Engine, AnthropicMixin):
|
|
|
54
56
|
self.client = anthropic.Anthropic(api_key=anthropic.api_key)
|
|
55
57
|
|
|
56
58
|
def id(self) -> str:
|
|
57
|
-
if
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
59
|
+
if (
|
|
60
|
+
self.config.get("NEUROSYMBOLIC_ENGINE_MODEL")
|
|
61
|
+
and self.config.get("NEUROSYMBOLIC_ENGINE_MODEL").startswith("claude")
|
|
62
|
+
and (
|
|
63
|
+
"3-7" not in self.config.get("NEUROSYMBOLIC_ENGINE_MODEL")
|
|
64
|
+
and "4-0" not in self.config.get("NEUROSYMBOLIC_ENGINE_MODEL")
|
|
65
|
+
and "4-1" not in self.config.get("NEUROSYMBOLIC_ENGINE_MODEL")
|
|
66
|
+
and "4-5" not in self.config.get("NEUROSYMBOLIC_ENGINE_MODEL")
|
|
67
|
+
)
|
|
68
|
+
):
|
|
69
|
+
return "neurosymbolic"
|
|
70
|
+
return super().id() # default to unregistered
|
|
65
71
|
|
|
66
72
|
def command(self, *args, **kwargs):
|
|
67
73
|
super().command(*args, **kwargs)
|
|
68
|
-
if
|
|
69
|
-
anthropic.api_key = kwargs[
|
|
70
|
-
if
|
|
71
|
-
self.model = kwargs[
|
|
74
|
+
if "NEUROSYMBOLIC_ENGINE_API_KEY" in kwargs:
|
|
75
|
+
anthropic.api_key = kwargs["NEUROSYMBOLIC_ENGINE_API_KEY"]
|
|
76
|
+
if "NEUROSYMBOLIC_ENGINE_MODEL" in kwargs:
|
|
77
|
+
self.model = kwargs["NEUROSYMBOLIC_ENGINE_MODEL"]
|
|
72
78
|
|
|
73
79
|
def compute_required_tokens(self, messages) -> int:
|
|
74
80
|
claude_messages, system_content = self._build_claude_messages(messages)
|
|
@@ -83,11 +89,11 @@ class ClaudeXChatEngine(Engine, AnthropicMixin):
|
|
|
83
89
|
system_content = None
|
|
84
90
|
|
|
85
91
|
for role, content_str in self._message_parts(messages):
|
|
86
|
-
if role ==
|
|
92
|
+
if role == "system":
|
|
87
93
|
system_content = content_str
|
|
88
94
|
continue
|
|
89
95
|
|
|
90
|
-
if role in [
|
|
96
|
+
if role in ["user", "assistant"]:
|
|
91
97
|
message_content = self._build_message_content(content_str)
|
|
92
98
|
if message_content:
|
|
93
99
|
claude_messages.append(self._create_claude_message(role, message_content))
|
|
@@ -102,11 +108,11 @@ class ClaudeXChatEngine(Engine, AnthropicMixin):
|
|
|
102
108
|
|
|
103
109
|
def _extract_message_details(self, part):
|
|
104
110
|
if isinstance(part, str):
|
|
105
|
-
return
|
|
111
|
+
return "user", part
|
|
106
112
|
|
|
107
113
|
if isinstance(part, dict):
|
|
108
|
-
role = part.get(
|
|
109
|
-
content_str = str(part.get(
|
|
114
|
+
role = part.get("role")
|
|
115
|
+
content_str = str(part.get("content", ""))
|
|
110
116
|
return role, content_str
|
|
111
117
|
|
|
112
118
|
msg = f"Unsupported message part type: {type(part)}"
|
|
@@ -121,33 +127,21 @@ class ClaudeXChatEngine(Engine, AnthropicMixin):
|
|
|
121
127
|
|
|
122
128
|
text_content = self._remove_vision_pattern(content_str)
|
|
123
129
|
if text_content:
|
|
124
|
-
message_content.append({
|
|
125
|
-
"type": "text",
|
|
126
|
-
"text": text_content
|
|
127
|
-
})
|
|
130
|
+
message_content.append({"type": "text", "text": text_content})
|
|
128
131
|
|
|
129
132
|
return message_content
|
|
130
133
|
|
|
131
134
|
def _create_claude_message(self, role: str, message_content: list) -> dict:
|
|
132
|
-
if len(message_content) == 1 and message_content[0].get(
|
|
133
|
-
return {
|
|
134
|
-
'role': role,
|
|
135
|
-
'content': message_content[0]['text']
|
|
136
|
-
}
|
|
135
|
+
if len(message_content) == 1 and message_content[0].get("type") == "text":
|
|
136
|
+
return {"role": role, "content": message_content[0]["text"]}
|
|
137
137
|
|
|
138
|
-
return {
|
|
139
|
-
'role': role,
|
|
140
|
-
'content': message_content
|
|
141
|
-
}
|
|
138
|
+
return {"role": role, "content": message_content}
|
|
142
139
|
|
|
143
140
|
def _count_claude_tokens(self, claude_messages: list, system_content: str | None) -> int:
|
|
144
141
|
try:
|
|
145
|
-
count_params = {
|
|
146
|
-
'model': self.model,
|
|
147
|
-
'messages': claude_messages
|
|
148
|
-
}
|
|
142
|
+
count_params = {"model": self.model, "messages": claude_messages}
|
|
149
143
|
if system_content:
|
|
150
|
-
count_params[
|
|
144
|
+
count_params["system"] = system_content
|
|
151
145
|
count_response = self.client.messages.count_tokens(**count_params)
|
|
152
146
|
return count_response.input_tokens
|
|
153
147
|
except Exception as e:
|
|
@@ -155,16 +149,17 @@ class ClaudeXChatEngine(Engine, AnthropicMixin):
|
|
|
155
149
|
UserMessage(f"Error counting tokens for Claude: {e!s}", raise_with=RuntimeError)
|
|
156
150
|
|
|
157
151
|
def compute_remaining_tokens(self, _prompts: list) -> int:
|
|
158
|
-
UserMessage(
|
|
152
|
+
UserMessage("Method not implemented.", raise_with=NotImplementedError)
|
|
159
153
|
|
|
160
154
|
def _handle_image_content(self, content: str) -> list:
|
|
161
155
|
"""Handle image content by processing vision patterns and returning image file data."""
|
|
156
|
+
|
|
162
157
|
def extract_pattern(text):
|
|
163
|
-
pattern = r
|
|
158
|
+
pattern = r"<<vision:(.*?):>>"
|
|
164
159
|
return re.findall(pattern, text)
|
|
165
160
|
|
|
166
161
|
image_files = []
|
|
167
|
-
if
|
|
162
|
+
if "<<vision:" in content:
|
|
168
163
|
parts = extract_pattern(content)
|
|
169
164
|
for p in parts:
|
|
170
165
|
img_ = p.strip()
|
|
@@ -172,89 +167,94 @@ class ClaudeXChatEngine(Engine, AnthropicMixin):
|
|
|
172
167
|
max_used_frames = 10
|
|
173
168
|
buffer, ext = encode_media_frames(img_)
|
|
174
169
|
if len(buffer) > 1:
|
|
175
|
-
step = len(buffer) // max_frames_spacing
|
|
170
|
+
step = len(buffer) // max_frames_spacing # max frames spacing
|
|
176
171
|
frames = []
|
|
177
172
|
indices = list(range(0, len(buffer), step))[:max_used_frames]
|
|
178
173
|
for i in indices:
|
|
179
|
-
frames.append(
|
|
174
|
+
frames.append(
|
|
175
|
+
{"data": buffer[i], "media_type": f"image/{ext}", "type": "base64"}
|
|
176
|
+
)
|
|
180
177
|
image_files.extend(frames)
|
|
181
178
|
elif len(buffer) == 1:
|
|
182
|
-
image_files.append(
|
|
179
|
+
image_files.append(
|
|
180
|
+
{"data": buffer[0], "media_type": f"image/{ext}", "type": "base64"}
|
|
181
|
+
)
|
|
183
182
|
else:
|
|
184
|
-
UserMessage(
|
|
183
|
+
UserMessage("No frames found for image!")
|
|
185
184
|
return image_files
|
|
186
185
|
|
|
187
186
|
def _remove_vision_pattern(self, text: str) -> str:
|
|
188
187
|
"""Remove vision patterns from text."""
|
|
189
|
-
pattern = r
|
|
190
|
-
return re.sub(pattern,
|
|
188
|
+
pattern = r"<<vision:(.*?):>>"
|
|
189
|
+
return re.sub(pattern, "", text)
|
|
191
190
|
|
|
192
191
|
def forward(self, argument):
|
|
193
192
|
kwargs = argument.kwargs
|
|
194
193
|
system, messages = argument.prop.prepared_input
|
|
195
194
|
payload = self._prepare_request_payload(argument)
|
|
196
|
-
except_remedy = kwargs.get(
|
|
195
|
+
except_remedy = kwargs.get("except_remedy")
|
|
197
196
|
|
|
198
197
|
try:
|
|
199
|
-
res = self.client.messages.create(
|
|
200
|
-
system=system,
|
|
201
|
-
messages=messages,
|
|
202
|
-
**payload
|
|
203
|
-
)
|
|
198
|
+
res = self.client.messages.create(system=system, messages=messages, **payload)
|
|
204
199
|
except Exception as e:
|
|
205
|
-
if anthropic.api_key is None or anthropic.api_key ==
|
|
206
|
-
msg =
|
|
200
|
+
if anthropic.api_key is None or anthropic.api_key == "":
|
|
201
|
+
msg = "Anthropic API key is not set. Please set it in the config file or pass it as an argument to the command method."
|
|
207
202
|
UserMessage(msg)
|
|
208
|
-
if
|
|
203
|
+
if (
|
|
204
|
+
self.config["NEUROSYMBOLIC_ENGINE_API_KEY"] is None
|
|
205
|
+
or self.config["NEUROSYMBOLIC_ENGINE_API_KEY"] == ""
|
|
206
|
+
):
|
|
209
207
|
UserMessage(msg, raise_with=ValueError)
|
|
210
|
-
anthropic.api_key = self.config[
|
|
208
|
+
anthropic.api_key = self.config["NEUROSYMBOLIC_ENGINE_API_KEY"]
|
|
211
209
|
|
|
212
210
|
callback = self.client.messages.create
|
|
213
|
-
kwargs[
|
|
211
|
+
kwargs["model"] = kwargs.get("model", self.model)
|
|
214
212
|
|
|
215
213
|
if except_remedy is not None:
|
|
216
214
|
res = except_remedy(self, e, callback, argument)
|
|
217
215
|
else:
|
|
218
|
-
UserMessage(f
|
|
216
|
+
UserMessage(f"Error during generation. Caused by: {e}", raise_with=ValueError)
|
|
219
217
|
|
|
220
|
-
if payload[
|
|
221
|
-
res = list(res)
|
|
222
|
-
metadata = {
|
|
218
|
+
if payload["stream"]:
|
|
219
|
+
res = list(res) # Unpack the iterator to a list
|
|
220
|
+
metadata = {"raw_output": res}
|
|
223
221
|
response_data = self._collect_response(res)
|
|
224
222
|
|
|
225
|
-
if response_data.get(
|
|
226
|
-
metadata[
|
|
223
|
+
if response_data.get("function_call"):
|
|
224
|
+
metadata["function_call"] = response_data["function_call"]
|
|
227
225
|
|
|
228
|
-
text_output = response_data.get(
|
|
226
|
+
text_output = response_data.get("text", "")
|
|
229
227
|
if argument.prop.response_format:
|
|
230
228
|
# Anthropic returns JSON in markdown format
|
|
231
|
-
text_output = text_output.replace(
|
|
229
|
+
text_output = text_output.replace("```json", "").replace("```", "")
|
|
232
230
|
|
|
233
231
|
return [text_output], metadata
|
|
234
232
|
|
|
235
233
|
def _prepare_raw_input(self, argument):
|
|
236
234
|
if not argument.prop.processed_input:
|
|
237
|
-
msg =
|
|
235
|
+
msg = "Need to provide a prompt instruction to the engine if `raw_input` is enabled!"
|
|
238
236
|
UserMessage(msg)
|
|
239
237
|
raise ValueError(msg)
|
|
240
238
|
system = NOT_GIVEN
|
|
241
239
|
prompt = copy(argument.prop.processed_input)
|
|
242
240
|
if not isinstance(prompt, list):
|
|
243
241
|
if not isinstance(prompt, dict):
|
|
244
|
-
prompt = {
|
|
242
|
+
prompt = {"role": "user", "content": str(prompt)}
|
|
245
243
|
prompt = [prompt]
|
|
246
244
|
if len(prompt) > 1:
|
|
247
245
|
# assert there are not more than 1 system instruction
|
|
248
|
-
assert len([p for p in prompt if p[
|
|
246
|
+
assert len([p for p in prompt if p["role"] == "system"]) <= 1, (
|
|
247
|
+
"Only one system instruction is allowed!"
|
|
248
|
+
)
|
|
249
249
|
for p in prompt:
|
|
250
|
-
if p[
|
|
251
|
-
system = p[
|
|
250
|
+
if p["role"] == "system":
|
|
251
|
+
system = p["content"]
|
|
252
252
|
prompt.remove(p)
|
|
253
253
|
break
|
|
254
254
|
return system, prompt
|
|
255
255
|
|
|
256
256
|
def prepare(self, argument):
|
|
257
|
-
|
|
257
|
+
# @NOTE: OpenAI compatibility at high level
|
|
258
258
|
if argument.prop.raw_input:
|
|
259
259
|
argument.prop.prepared_input = self._prepare_raw_input(argument)
|
|
260
260
|
return
|
|
@@ -265,7 +265,9 @@ class ClaudeXChatEngine(Engine, AnthropicMixin):
|
|
|
265
265
|
|
|
266
266
|
system = self._build_system_prompt(argument, has_image, non_verbose_output)
|
|
267
267
|
user_text, user_prompt, image_blocks = self._build_user_prompt(argument, image_files)
|
|
268
|
-
system, user_prompt = self._apply_self_prompt_if_needed(
|
|
268
|
+
system, user_prompt = self._apply_self_prompt_if_needed(
|
|
269
|
+
argument, system, user_text, image_blocks, user_prompt
|
|
270
|
+
)
|
|
269
271
|
|
|
270
272
|
argument.prop.prepared_input = (system, [user_prompt])
|
|
271
273
|
|
|
@@ -280,13 +282,15 @@ class ClaudeXChatEngine(Engine, AnthropicMixin):
|
|
|
280
282
|
if argument.prop.suppress_verbose_output:
|
|
281
283
|
system += non_verbose_output
|
|
282
284
|
|
|
283
|
-
system = f
|
|
285
|
+
system = f"{system}\n" if system and len(system) > 0 else ""
|
|
284
286
|
|
|
285
287
|
if argument.prop.response_format:
|
|
286
288
|
response_format = argument.prop.response_format
|
|
287
|
-
assert response_format.get(
|
|
289
|
+
assert response_format.get("type") is not None, (
|
|
290
|
+
'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'
|
|
291
|
+
)
|
|
288
292
|
system += non_verbose_output
|
|
289
|
-
system += f
|
|
293
|
+
system += f"<RESPONSE_FORMAT/>\n{response_format['type']}\n\n"
|
|
290
294
|
|
|
291
295
|
return system
|
|
292
296
|
|
|
@@ -321,7 +325,7 @@ class ClaudeXChatEngine(Engine, AnthropicMixin):
|
|
|
321
325
|
|
|
322
326
|
def _append_template_suffix(self, system: str, argument) -> str:
|
|
323
327
|
if argument.prop.template_suffix:
|
|
324
|
-
system += f
|
|
328
|
+
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"
|
|
325
329
|
|
|
326
330
|
return system
|
|
327
331
|
|
|
@@ -334,59 +338,58 @@ class ClaudeXChatEngine(Engine, AnthropicMixin):
|
|
|
334
338
|
if not user_text:
|
|
335
339
|
user_text = "N/A"
|
|
336
340
|
|
|
337
|
-
image_blocks = [{
|
|
341
|
+
image_blocks = [{"type": "image", "source": image_file} for image_file in image_files]
|
|
338
342
|
user_prompt = self._wrap_user_prompt_content(user_text, image_blocks)
|
|
339
343
|
return user_text, user_prompt, image_blocks
|
|
340
344
|
|
|
341
345
|
def _wrap_user_prompt_content(self, user_text: str, image_blocks: list[dict]) -> dict:
|
|
342
346
|
if len(image_blocks) > 0:
|
|
343
|
-
return {
|
|
344
|
-
"role": "user",
|
|
345
|
-
"content": [
|
|
346
|
-
*image_blocks,
|
|
347
|
-
{'type': 'text', 'text': user_text}
|
|
348
|
-
]
|
|
349
|
-
}
|
|
347
|
+
return {"role": "user", "content": [*image_blocks, {"type": "text", "text": user_text}]}
|
|
350
348
|
|
|
351
|
-
return {
|
|
352
|
-
"role": "user",
|
|
353
|
-
"content": user_text
|
|
354
|
-
}
|
|
349
|
+
return {"role": "user", "content": user_text}
|
|
355
350
|
|
|
356
|
-
def _apply_self_prompt_if_needed(
|
|
357
|
-
|
|
351
|
+
def _apply_self_prompt_if_needed(
|
|
352
|
+
self, argument, system: str, user_text: str, image_blocks: list[dict], user_prompt: dict
|
|
353
|
+
):
|
|
354
|
+
if not (
|
|
355
|
+
argument.prop.instance._kwargs.get("self_prompt", False) or argument.prop.self_prompt
|
|
356
|
+
):
|
|
358
357
|
return system, user_prompt
|
|
359
358
|
|
|
360
359
|
self_prompter = SelfPrompt()
|
|
361
|
-
res = self_prompter({
|
|
360
|
+
res = self_prompter({"user": user_text, "system": system})
|
|
362
361
|
if res is None:
|
|
363
362
|
msg = "Self-prompting failed!"
|
|
364
363
|
UserMessage(msg)
|
|
365
364
|
raise ValueError(msg)
|
|
366
365
|
|
|
367
|
-
updated_user_prompt = self._wrap_user_prompt_content(res[
|
|
368
|
-
return res[
|
|
366
|
+
updated_user_prompt = self._wrap_user_prompt_content(res["user"], image_blocks)
|
|
367
|
+
return res["system"], updated_user_prompt
|
|
369
368
|
|
|
370
369
|
def _prepare_request_payload(self, argument):
|
|
371
370
|
kwargs = argument.kwargs
|
|
372
|
-
model = kwargs.get(
|
|
373
|
-
max_tokens = kwargs.get(
|
|
374
|
-
stop = kwargs.get(
|
|
375
|
-
temperature = kwargs.get(
|
|
376
|
-
top_p = kwargs.get(
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
371
|
+
model = kwargs.get("model", self.model)
|
|
372
|
+
max_tokens = kwargs.get("max_tokens", self.max_response_tokens)
|
|
373
|
+
stop = kwargs.get("stop", NOT_GIVEN)
|
|
374
|
+
temperature = kwargs.get("temperature", 1)
|
|
375
|
+
top_p = kwargs.get(
|
|
376
|
+
"top_p", NOT_GIVEN if temperature is not None else 1
|
|
377
|
+
) # @NOTE:'You should either alter temperature or top_p, but not both.'
|
|
378
|
+
top_k = kwargs.get("top_k", NOT_GIVEN)
|
|
379
|
+
stream = kwargs.get(
|
|
380
|
+
"stream", True
|
|
381
|
+
) # Do NOT remove this default value! Getting tons of API errors because they can't process requests >10m
|
|
382
|
+
tools = kwargs.get("tools", NOT_GIVEN)
|
|
383
|
+
tool_choice = kwargs.get("tool_choice", NOT_GIVEN)
|
|
384
|
+
metadata_anthropic = kwargs.get("metadata", NOT_GIVEN)
|
|
382
385
|
|
|
383
386
|
if stop != NOT_GIVEN and not isinstance(stop, list):
|
|
384
387
|
stop = [stop]
|
|
385
388
|
|
|
386
|
-
|
|
389
|
+
# @NOTE: Anthropic fails if stop is not raw string, so cast it to r'…'
|
|
387
390
|
# E.g. when we use defaults in core.py, i.e. stop=['\n']
|
|
388
391
|
if stop != NOT_GIVEN:
|
|
389
|
-
stop = [r
|
|
392
|
+
stop = [r"{s}" for s in stop]
|
|
390
393
|
|
|
391
394
|
return {
|
|
392
395
|
"model": model,
|
|
@@ -398,7 +401,7 @@ class ClaudeXChatEngine(Engine, AnthropicMixin):
|
|
|
398
401
|
"stream": stream,
|
|
399
402
|
"metadata": metadata_anthropic,
|
|
400
403
|
"tools": tools,
|
|
401
|
-
"tool_choice": tool_choice
|
|
404
|
+
"tool_choice": tool_choice,
|
|
402
405
|
}
|
|
403
406
|
|
|
404
407
|
def _collect_response(self, res):
|
|
@@ -408,7 +411,9 @@ class ClaudeXChatEngine(Engine, AnthropicMixin):
|
|
|
408
411
|
if isinstance(res, Message):
|
|
409
412
|
return self._collect_message_response(res)
|
|
410
413
|
|
|
411
|
-
UserMessage(
|
|
414
|
+
UserMessage(
|
|
415
|
+
f"Unexpected response type from Anthropic API: {type(res)}", raise_with=ValueError
|
|
416
|
+
)
|
|
412
417
|
return {}
|
|
413
418
|
|
|
414
419
|
def _collect_streaming_response(self, res):
|
|
@@ -426,27 +431,24 @@ class ClaudeXChatEngine(Engine, AnthropicMixin):
|
|
|
426
431
|
if tool_call is not None:
|
|
427
432
|
tool_calls_raw.append(tool_call)
|
|
428
433
|
|
|
429
|
-
text_content =
|
|
434
|
+
text_content = "".join(text_parts)
|
|
430
435
|
function_call_data = self._build_function_call_data(tool_calls_raw)
|
|
431
436
|
|
|
432
|
-
return {
|
|
433
|
-
"text": text_content,
|
|
434
|
-
"function_call": function_call_data
|
|
435
|
-
}
|
|
437
|
+
return {"text": text_content, "function_call": function_call_data}
|
|
436
438
|
|
|
437
439
|
def _start_tool_call(self, chunk, active_tool_calls: dict):
|
|
438
440
|
if isinstance(chunk.content_block, ToolUseBlock):
|
|
439
441
|
active_tool_calls[chunk.index] = {
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
442
|
+
"id": chunk.content_block.id,
|
|
443
|
+
"name": chunk.content_block.name,
|
|
444
|
+
"input_json_str": "",
|
|
443
445
|
}
|
|
444
446
|
|
|
445
447
|
def _update_stream_chunk(self, chunk, text_parts: list, active_tool_calls: dict):
|
|
446
448
|
if isinstance(chunk.delta, TextDelta):
|
|
447
449
|
text_parts.append(chunk.delta.text)
|
|
448
450
|
elif isinstance(chunk.delta, InputJSONDelta) and chunk.index in active_tool_calls:
|
|
449
|
-
active_tool_calls[chunk.index][
|
|
451
|
+
active_tool_calls[chunk.index]["input_json_str"] += chunk.delta.partial_json
|
|
450
452
|
|
|
451
453
|
def _finish_tool_call(self, chunk, active_tool_calls: dict):
|
|
452
454
|
if chunk.index not in active_tool_calls:
|
|
@@ -454,10 +456,12 @@ class ClaudeXChatEngine(Engine, AnthropicMixin):
|
|
|
454
456
|
|
|
455
457
|
tool_call_info = active_tool_calls.pop(chunk.index)
|
|
456
458
|
try:
|
|
457
|
-
tool_call_info[
|
|
459
|
+
tool_call_info["input"] = json.loads(tool_call_info["input_json_str"])
|
|
458
460
|
except json.JSONDecodeError as e:
|
|
459
|
-
UserMessage(
|
|
460
|
-
|
|
461
|
+
UserMessage(
|
|
462
|
+
f"Failed to parse JSON for tool call {tool_call_info['name']}: {e}. Raw JSON: '{tool_call_info['input_json_str']}'"
|
|
463
|
+
)
|
|
464
|
+
tool_call_info["input"] = {}
|
|
461
465
|
return tool_call_info
|
|
462
466
|
|
|
463
467
|
def _build_function_call_data(self, tool_calls_raw: list | None) -> dict | None:
|
|
@@ -465,13 +469,12 @@ class ClaudeXChatEngine(Engine, AnthropicMixin):
|
|
|
465
469
|
return None
|
|
466
470
|
|
|
467
471
|
if len(tool_calls_raw) > 1:
|
|
468
|
-
UserMessage(
|
|
472
|
+
UserMessage(
|
|
473
|
+
"Multiple tool calls detected in the stream but only the first one will be processed."
|
|
474
|
+
)
|
|
469
475
|
|
|
470
476
|
tool_call = tool_calls_raw[0]
|
|
471
|
-
return {
|
|
472
|
-
'name': tool_call['name'],
|
|
473
|
-
'arguments': tool_call['input']
|
|
474
|
-
}
|
|
477
|
+
return {"name": tool_call["name"], "arguments": tool_call["input"]}
|
|
475
478
|
|
|
476
479
|
def _collect_message_response(self, res: Message):
|
|
477
480
|
text_parts = []
|
|
@@ -483,15 +486,14 @@ class ClaudeXChatEngine(Engine, AnthropicMixin):
|
|
|
483
486
|
text_parts.append(content_block.text)
|
|
484
487
|
elif isinstance(content_block, ToolUseBlock):
|
|
485
488
|
if hit_tool_use:
|
|
486
|
-
UserMessage(
|
|
489
|
+
UserMessage(
|
|
490
|
+
"Multiple tool use blocks detected in the response but only the first one will be processed."
|
|
491
|
+
)
|
|
487
492
|
else:
|
|
488
493
|
function_call_data = {
|
|
489
|
-
|
|
490
|
-
|
|
494
|
+
"name": content_block.name,
|
|
495
|
+
"arguments": content_block.input,
|
|
491
496
|
}
|
|
492
497
|
hit_tool_use = True
|
|
493
498
|
|
|
494
|
-
return {
|
|
495
|
-
"text": ''.join(text_parts),
|
|
496
|
-
"function_call": function_call_data
|
|
497
|
-
}
|
|
499
|
+
return {"text": "".join(text_parts), "function_call": function_call_data}
|