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
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import base64
|
|
2
|
-
import io
|
|
3
2
|
import logging
|
|
4
3
|
import mimetypes
|
|
5
4
|
import re
|
|
6
|
-
import urllib.parse
|
|
7
5
|
from copy import deepcopy
|
|
8
6
|
from pathlib import Path
|
|
9
7
|
|
|
@@ -12,9 +10,7 @@ from google import genai
|
|
|
12
10
|
from google.genai import types
|
|
13
11
|
|
|
14
12
|
from ....components import SelfPrompt
|
|
15
|
-
from ....
|
|
16
|
-
from ....symbol import Symbol
|
|
17
|
-
from ....utils import CustomUserWarning, encode_media_frames
|
|
13
|
+
from ....utils import UserMessage, encode_media_frames
|
|
18
14
|
from ...base import Engine
|
|
19
15
|
from ...mixin.google import GoogleMixin
|
|
20
16
|
from ...settings import SYMAI_CONFIG
|
|
@@ -72,9 +68,8 @@ class GeminiXReasoningEngine(Engine, GoogleMixin):
|
|
|
72
68
|
api_contents: list[types.Content] = []
|
|
73
69
|
|
|
74
70
|
for msg in messages:
|
|
75
|
-
if
|
|
76
|
-
|
|
77
|
-
for part in msg:
|
|
71
|
+
msg_parts = msg if isinstance(msg, list) else [msg]
|
|
72
|
+
for part in msg_parts:
|
|
78
73
|
if isinstance(part, str):
|
|
79
74
|
role = 'user'
|
|
80
75
|
content_str = part
|
|
@@ -99,11 +94,11 @@ class GeminiXReasoningEngine(Engine, GoogleMixin):
|
|
|
99
94
|
count_response = self.client.models.count_tokens(model=self.model, contents=api_contents)
|
|
100
95
|
return count_response.total_tokens
|
|
101
96
|
except Exception as e:
|
|
102
|
-
|
|
103
|
-
|
|
97
|
+
UserMessage(f"Gemini count_tokens failed: {e}")
|
|
98
|
+
UserMessage(f"Error counting tokens for Gemini: {e!s}", raise_with=RuntimeError)
|
|
104
99
|
|
|
105
|
-
def compute_remaining_tokens(self,
|
|
106
|
-
|
|
100
|
+
def compute_remaining_tokens(self, _prompts: list) -> int:
|
|
101
|
+
UserMessage("Token counting not implemented for Gemini", raise_with=NotImplementedError)
|
|
107
102
|
|
|
108
103
|
def _handle_document_content(self, content: str):
|
|
109
104
|
"""Handle document content by uploading to Gemini"""
|
|
@@ -115,98 +110,103 @@ class GeminiXReasoningEngine(Engine, GoogleMixin):
|
|
|
115
110
|
|
|
116
111
|
doc_path = matches[0].strip()
|
|
117
112
|
if doc_path.startswith('http'):
|
|
118
|
-
|
|
113
|
+
UserMessage("URL documents not yet supported for Gemini")
|
|
119
114
|
return None
|
|
120
|
-
|
|
121
|
-
uploaded_file = genai.upload_file(doc_path)
|
|
122
|
-
return uploaded_file
|
|
115
|
+
return genai.upload_file(doc_path)
|
|
123
116
|
except Exception as e:
|
|
124
|
-
|
|
117
|
+
UserMessage(f"Failed to process document: {e}")
|
|
125
118
|
return None
|
|
126
119
|
|
|
127
|
-
def _handle_image_content(self, content: str) -> list:
|
|
120
|
+
def _handle_image_content(self, content: str) -> list[types.Part]:
|
|
128
121
|
"""Handle image content by processing and preparing google.generativeai.types.Part objects."""
|
|
129
|
-
image_parts = []
|
|
130
|
-
|
|
131
|
-
matches = re.findall(pattern, content) # re must be imported
|
|
132
|
-
|
|
133
|
-
for match in matches:
|
|
134
|
-
img_src = match.strip()
|
|
135
|
-
|
|
122
|
+
image_parts: list[types.Part] = []
|
|
123
|
+
for img_src in self._extract_image_sources(content):
|
|
136
124
|
try:
|
|
137
|
-
|
|
138
|
-
header, encoded = img_src.split(',', 1)
|
|
139
|
-
mime_type = header.split(';')[0].split(':')[1]
|
|
140
|
-
image_bytes = base64.b64decode(encoded)
|
|
141
|
-
image_parts.append(genai.types.Part(inline_data=genai.types.Blob(mime_type=mime_type, data=image_bytes)))
|
|
142
|
-
|
|
143
|
-
elif img_src.startswith('http://') or img_src.startswith('https://'):
|
|
144
|
-
response = requests.get(img_src, timeout=10) # 10 seconds timeout
|
|
145
|
-
response.raise_for_status()
|
|
146
|
-
|
|
147
|
-
image_bytes = response.content
|
|
148
|
-
mime_type = response.headers.get('Content-Type', 'application/octet-stream')
|
|
149
|
-
|
|
150
|
-
if not mime_type.startswith('image/'):
|
|
151
|
-
CustomUserWarning(f"URL content type '{mime_type}' does not appear to be an image for: {img_src}. Attempting to use anyway.")
|
|
152
|
-
|
|
153
|
-
image_parts.append(genai.types.Part(inline_data=genai.types.Blob(mime_type=mime_type, data=image_bytes)))
|
|
154
|
-
|
|
155
|
-
elif img_src.startswith('frames:'):
|
|
156
|
-
temp_path = img_src.replace('frames:', '')
|
|
157
|
-
parts = temp_path.split(':', 1)
|
|
158
|
-
if len(parts) != 2:
|
|
159
|
-
CustomUserWarning(f"Invalid 'frames:' format: {img_src}")
|
|
160
|
-
continue
|
|
161
|
-
max_used_frames_str, actual_path = parts
|
|
162
|
-
try:
|
|
163
|
-
max_used_frames = int(max_used_frames_str)
|
|
164
|
-
except ValueError:
|
|
165
|
-
CustomUserWarning(f"Invalid max_frames number in 'frames:' format: {img_src}")
|
|
166
|
-
continue
|
|
167
|
-
|
|
168
|
-
frame_buffers, ext = encode_media_frames(actual_path)
|
|
169
|
-
|
|
170
|
-
mime_type = f'image/{ext.lower()}' if ext else 'application/octet-stream'
|
|
171
|
-
if ext and ext.lower() == 'jpg':
|
|
172
|
-
mime_type = 'image/jpeg'
|
|
173
|
-
|
|
174
|
-
if not frame_buffers:
|
|
175
|
-
CustomUserWarning(f"encode_media_frames returned no frames for: {actual_path}")
|
|
176
|
-
continue
|
|
177
|
-
|
|
178
|
-
step = max(1, len(frame_buffers) // 50)
|
|
179
|
-
indices = list(range(0, len(frame_buffers), step))[:max_used_frames]
|
|
180
|
-
|
|
181
|
-
for i_idx in indices:
|
|
182
|
-
if i_idx < len(frame_buffers):
|
|
183
|
-
image_bytes = frame_buffers[i_idx]
|
|
184
|
-
image_parts.append(genai.types.Part(inline_data=genai.types.Blob(mime_type=mime_type, data=image_bytes)))
|
|
185
|
-
|
|
186
|
-
else:
|
|
187
|
-
# Handle local file paths
|
|
188
|
-
local_file_path = Path(img_src)
|
|
189
|
-
if not local_file_path.is_file():
|
|
190
|
-
CustomUserWarning(f"Local image file not found: {img_src}")
|
|
191
|
-
continue
|
|
192
|
-
|
|
193
|
-
image_bytes = local_file_path.read_bytes()
|
|
194
|
-
mime_type, _ = mimetypes.guess_type(local_file_path)
|
|
195
|
-
if mime_type is None: # Fallback MIME type determination
|
|
196
|
-
file_ext = local_file_path.suffix.lower().lstrip('.')
|
|
197
|
-
if file_ext == 'jpg': mime_type = 'image/jpeg'
|
|
198
|
-
elif file_ext == 'png': mime_type = 'image/png'
|
|
199
|
-
elif file_ext == 'gif': mime_type = 'image/gif'
|
|
200
|
-
elif file_ext == 'webp': mime_type = 'image/webp'
|
|
201
|
-
else: mime_type = 'application/octet-stream'
|
|
202
|
-
|
|
203
|
-
image_parts.append(genai.types.Part(inline_data=genai.types.Blob(mime_type=mime_type, data=image_bytes)))
|
|
204
|
-
|
|
125
|
+
image_parts.extend(self._create_parts_from_image_source(img_src))
|
|
205
126
|
except Exception as e:
|
|
206
|
-
|
|
207
|
-
|
|
127
|
+
UserMessage(f"Failed to process image source '{img_src}'. Error: {e!s}", raise_with=ValueError)
|
|
208
128
|
return image_parts
|
|
209
129
|
|
|
130
|
+
def _extract_image_sources(self, content: str) -> list[str]:
|
|
131
|
+
pattern = r'<<vision:(.*?):>>'
|
|
132
|
+
return [match.strip() for match in re.findall(pattern, content)]
|
|
133
|
+
|
|
134
|
+
def _create_parts_from_image_source(self, img_src: str) -> list[types.Part]:
|
|
135
|
+
if img_src.startswith('data:image'):
|
|
136
|
+
return self._create_parts_from_data_uri(img_src)
|
|
137
|
+
if img_src.startswith(('http://', 'https://')):
|
|
138
|
+
return self._create_parts_from_url(img_src)
|
|
139
|
+
if img_src.startswith('frames:'):
|
|
140
|
+
return self._create_parts_from_frames(img_src)
|
|
141
|
+
return self._create_parts_from_local_path(img_src)
|
|
142
|
+
|
|
143
|
+
def _create_parts_from_data_uri(self, img_src: str) -> list[types.Part]:
|
|
144
|
+
header, encoded = img_src.split(',', 1)
|
|
145
|
+
mime_type = header.split(';')[0].split(':')[1]
|
|
146
|
+
image_bytes = base64.b64decode(encoded)
|
|
147
|
+
part = genai.types.Part(inline_data=genai.types.Blob(mime_type=mime_type, data=image_bytes))
|
|
148
|
+
return [part]
|
|
149
|
+
|
|
150
|
+
def _create_parts_from_url(self, img_src: str) -> list[types.Part]:
|
|
151
|
+
response = requests.get(img_src, timeout=10)
|
|
152
|
+
response.raise_for_status()
|
|
153
|
+
image_bytes = response.content
|
|
154
|
+
mime_type = response.headers.get('Content-Type', 'application/octet-stream')
|
|
155
|
+
if not mime_type.startswith('image/'):
|
|
156
|
+
UserMessage(f"URL content type '{mime_type}' does not appear to be an image for: {img_src}. Attempting to use anyway.")
|
|
157
|
+
part = genai.types.Part(inline_data=genai.types.Blob(mime_type=mime_type, data=image_bytes))
|
|
158
|
+
return [part]
|
|
159
|
+
|
|
160
|
+
def _create_parts_from_frames(self, img_src: str) -> list[types.Part]:
|
|
161
|
+
temp_path = img_src.replace('frames:', '')
|
|
162
|
+
parts = temp_path.split(':', 1)
|
|
163
|
+
if len(parts) != 2:
|
|
164
|
+
UserMessage(f"Invalid 'frames:' format: {img_src}")
|
|
165
|
+
return []
|
|
166
|
+
max_used_frames_str, actual_path = parts
|
|
167
|
+
try:
|
|
168
|
+
max_used_frames = int(max_used_frames_str)
|
|
169
|
+
except ValueError:
|
|
170
|
+
UserMessage(f"Invalid max_frames number in 'frames:' format: {img_src}")
|
|
171
|
+
return []
|
|
172
|
+
frame_buffers, ext = encode_media_frames(actual_path)
|
|
173
|
+
mime_type = f'image/{ext.lower()}' if ext else 'application/octet-stream'
|
|
174
|
+
if ext and ext.lower() == 'jpg':
|
|
175
|
+
mime_type = 'image/jpeg'
|
|
176
|
+
if not frame_buffers:
|
|
177
|
+
UserMessage(f"encode_media_frames returned no frames for: {actual_path}")
|
|
178
|
+
return []
|
|
179
|
+
step = max(1, len(frame_buffers) // 50)
|
|
180
|
+
indices = list(range(0, len(frame_buffers), step))[:max_used_frames]
|
|
181
|
+
parts_list: list[types.Part] = []
|
|
182
|
+
for frame_idx in indices:
|
|
183
|
+
if frame_idx < len(frame_buffers):
|
|
184
|
+
image_bytes = frame_buffers[frame_idx]
|
|
185
|
+
parts_list.append(genai.types.Part(inline_data=genai.types.Blob(mime_type=mime_type, data=image_bytes)))
|
|
186
|
+
return parts_list
|
|
187
|
+
|
|
188
|
+
def _create_parts_from_local_path(self, img_src: str) -> list[types.Part]:
|
|
189
|
+
local_file_path = Path(img_src)
|
|
190
|
+
if not local_file_path.is_file():
|
|
191
|
+
UserMessage(f"Local image file not found: {img_src}")
|
|
192
|
+
return []
|
|
193
|
+
image_bytes = local_file_path.read_bytes()
|
|
194
|
+
mime_type, _ = mimetypes.guess_type(local_file_path)
|
|
195
|
+
if mime_type is None:
|
|
196
|
+
file_ext = local_file_path.suffix.lower().lstrip('.')
|
|
197
|
+
if file_ext == 'jpg':
|
|
198
|
+
mime_type = 'image/jpeg'
|
|
199
|
+
elif file_ext == 'png':
|
|
200
|
+
mime_type = 'image/png'
|
|
201
|
+
elif file_ext == 'gif':
|
|
202
|
+
mime_type = 'image/gif'
|
|
203
|
+
elif file_ext == 'webp':
|
|
204
|
+
mime_type = 'image/webp'
|
|
205
|
+
else:
|
|
206
|
+
mime_type = 'application/octet-stream'
|
|
207
|
+
part = genai.types.Part(inline_data=genai.types.Blob(mime_type=mime_type, data=image_bytes))
|
|
208
|
+
return [part]
|
|
209
|
+
|
|
210
210
|
def _handle_video_content(self, content: str):
|
|
211
211
|
"""Handle video content by uploading to Gemini"""
|
|
212
212
|
try:
|
|
@@ -217,14 +217,12 @@ class GeminiXReasoningEngine(Engine, GoogleMixin):
|
|
|
217
217
|
|
|
218
218
|
video_path = matches[0].strip()
|
|
219
219
|
if video_path.startswith('http'):
|
|
220
|
-
|
|
220
|
+
UserMessage("URL videos not yet supported for Gemini")
|
|
221
221
|
return None
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
uploaded_file = genai.upload_file(video_path)
|
|
225
|
-
return uploaded_file
|
|
222
|
+
# Upload local video
|
|
223
|
+
return genai.upload_file(video_path)
|
|
226
224
|
except Exception as e:
|
|
227
|
-
|
|
225
|
+
UserMessage(f"Failed to process video: {e}")
|
|
228
226
|
return None
|
|
229
227
|
|
|
230
228
|
def _handle_audio_content(self, content: str):
|
|
@@ -237,14 +235,12 @@ class GeminiXReasoningEngine(Engine, GoogleMixin):
|
|
|
237
235
|
|
|
238
236
|
audio_path = matches[0].strip()
|
|
239
237
|
if audio_path.startswith('http'):
|
|
240
|
-
|
|
238
|
+
UserMessage("URL audio not yet supported for Gemini")
|
|
241
239
|
return None
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
uploaded_file = genai.upload_file(audio_path)
|
|
245
|
-
return uploaded_file
|
|
240
|
+
# Upload local audio
|
|
241
|
+
return genai.upload_file(audio_path)
|
|
246
242
|
except Exception as e:
|
|
247
|
-
|
|
243
|
+
UserMessage(f"Failed to process audio: {e}")
|
|
248
244
|
return None
|
|
249
245
|
|
|
250
246
|
def _remove_media_patterns(self, text: str) -> str:
|
|
@@ -312,55 +308,17 @@ class GeminiXReasoningEngine(Engine, GoogleMixin):
|
|
|
312
308
|
|
|
313
309
|
def forward(self, argument):
|
|
314
310
|
kwargs = argument.kwargs
|
|
315
|
-
|
|
311
|
+
_system, prompt = argument.prop.prepared_input
|
|
316
312
|
payload = self._prepare_request_payload(argument)
|
|
317
313
|
except_remedy = kwargs.get('except_remedy')
|
|
318
314
|
|
|
319
|
-
contents =
|
|
320
|
-
for msg in prompt:
|
|
321
|
-
role = msg['role']
|
|
322
|
-
parts_list = msg['content']
|
|
323
|
-
contents.append(types.Content(role=role, parts=parts_list))
|
|
315
|
+
contents = self._build_contents_from_prompt(prompt)
|
|
324
316
|
|
|
325
317
|
try:
|
|
326
|
-
generation_config =
|
|
327
|
-
|
|
328
|
-
temperature=payload.get('temperature', 1.0),
|
|
329
|
-
top_p=payload.get('top_p', 0.95),
|
|
330
|
-
top_k=payload.get('top_k', 40),
|
|
331
|
-
stop_sequences=payload.get('stop_sequences'),
|
|
332
|
-
response_mime_type=payload.get('response_mime_type', 'text/plain'),
|
|
333
|
-
)
|
|
334
|
-
|
|
335
|
-
if payload.get('system_instruction'):
|
|
336
|
-
generation_config.system_instruction = payload['system_instruction']
|
|
337
|
-
|
|
338
|
-
if payload.get('thinking_config'):
|
|
339
|
-
generation_config.thinking_config = payload['thinking_config']
|
|
340
|
-
|
|
341
|
-
if payload.get('tools'):
|
|
342
|
-
generation_config.tools = payload['tools']
|
|
343
|
-
generation_config.automatic_function_calling=payload['automatic_function_calling']
|
|
344
|
-
|
|
345
|
-
res = self.client.models.generate_content(
|
|
346
|
-
model=kwargs.get('model', self.model),
|
|
347
|
-
contents=contents,
|
|
348
|
-
config=generation_config
|
|
349
|
-
)
|
|
350
|
-
|
|
318
|
+
generation_config = self._build_generation_config(payload)
|
|
319
|
+
res = self._generate_model_response(kwargs, contents, generation_config)
|
|
351
320
|
except Exception as e:
|
|
352
|
-
|
|
353
|
-
msg = 'Google API key is not set. Please set it in the config file or pass it as an argument to the command method.'
|
|
354
|
-
logging.error(msg)
|
|
355
|
-
if self.config['NEUROSYMBOLIC_ENGINE_API_KEY'] is None or self.config['NEUROSYMBOLIC_ENGINE_API_KEY'] == '':
|
|
356
|
-
CustomUserWarning(msg, raise_with=ValueError)
|
|
357
|
-
self.api_key = self.config['NEUROSYMBOLIC_ENGINE_API_KEY']
|
|
358
|
-
genai.configure(api_key=self.api_key)
|
|
359
|
-
|
|
360
|
-
if except_remedy is not None:
|
|
361
|
-
res = except_remedy(self, e, self.client.generate_content, argument)
|
|
362
|
-
else:
|
|
363
|
-
CustomUserWarning(f'Error during generation. Caused by: {e}', raise_with=ValueError)
|
|
321
|
+
res = self._handle_generation_error(e, except_remedy, argument)
|
|
364
322
|
|
|
365
323
|
metadata = {'raw_output': res}
|
|
366
324
|
if payload.get('tools'):
|
|
@@ -376,11 +334,59 @@ class GeminiXReasoningEngine(Engine, GoogleMixin):
|
|
|
376
334
|
|
|
377
335
|
processed_text = output['text']
|
|
378
336
|
if argument.prop.response_format:
|
|
379
|
-
# Safely remove JSON markdown formatting if present
|
|
380
337
|
processed_text = processed_text.replace('```json', '').replace('```', '')
|
|
381
338
|
|
|
382
339
|
return [processed_text], metadata
|
|
383
340
|
|
|
341
|
+
def _build_contents_from_prompt(self, prompt) -> list[types.Content]:
|
|
342
|
+
contents: list[types.Content] = []
|
|
343
|
+
for msg in prompt:
|
|
344
|
+
role = msg['role']
|
|
345
|
+
parts_list = msg['content']
|
|
346
|
+
contents.append(types.Content(role=role, parts=parts_list))
|
|
347
|
+
return contents
|
|
348
|
+
|
|
349
|
+
def _build_generation_config(self, payload: dict) -> types.GenerateContentConfig:
|
|
350
|
+
generation_config = types.GenerateContentConfig(
|
|
351
|
+
max_output_tokens=payload.get('max_output_tokens'),
|
|
352
|
+
temperature=payload.get('temperature', 1.0),
|
|
353
|
+
top_p=payload.get('top_p', 0.95),
|
|
354
|
+
top_k=payload.get('top_k', 40),
|
|
355
|
+
stop_sequences=payload.get('stop_sequences'),
|
|
356
|
+
response_mime_type=payload.get('response_mime_type', 'text/plain'),
|
|
357
|
+
)
|
|
358
|
+
self._apply_optional_config_fields(generation_config, payload)
|
|
359
|
+
return generation_config
|
|
360
|
+
|
|
361
|
+
def _apply_optional_config_fields(self, generation_config: types.GenerateContentConfig, payload: dict) -> None:
|
|
362
|
+
if payload.get('system_instruction'):
|
|
363
|
+
generation_config.system_instruction = payload['system_instruction']
|
|
364
|
+
if payload.get('thinking_config'):
|
|
365
|
+
generation_config.thinking_config = payload['thinking_config']
|
|
366
|
+
if payload.get('tools'):
|
|
367
|
+
generation_config.tools = payload['tools']
|
|
368
|
+
generation_config.automatic_function_calling = payload['automatic_function_calling']
|
|
369
|
+
|
|
370
|
+
def _generate_model_response(self, kwargs: dict, contents: list[types.Content], generation_config: types.GenerateContentConfig):
|
|
371
|
+
return self.client.models.generate_content(
|
|
372
|
+
model=kwargs.get('model', self.model),
|
|
373
|
+
contents=contents,
|
|
374
|
+
config=generation_config
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
def _handle_generation_error(self, exception: Exception, except_remedy, argument):
|
|
378
|
+
if self.api_key is None or self.api_key == '':
|
|
379
|
+
msg = 'Google API key is not set. Please set it in the config file or pass it as an argument to the command method.'
|
|
380
|
+
UserMessage(msg)
|
|
381
|
+
if self.config['NEUROSYMBOLIC_ENGINE_API_KEY'] is None or self.config['NEUROSYMBOLIC_ENGINE_API_KEY'] == '':
|
|
382
|
+
UserMessage(msg, raise_with=ValueError)
|
|
383
|
+
self.api_key = self.config['NEUROSYMBOLIC_ENGINE_API_KEY']
|
|
384
|
+
genai.configure(api_key=self.api_key)
|
|
385
|
+
if except_remedy is not None:
|
|
386
|
+
return except_remedy(self, exception, self.client.generate_content, argument)
|
|
387
|
+
UserMessage(f'Error during generation. Caused by: {exception}', raise_with=ValueError)
|
|
388
|
+
return None
|
|
389
|
+
|
|
384
390
|
def _process_function_calls(self, res, metadata):
|
|
385
391
|
hit = False
|
|
386
392
|
if hasattr(res, 'candidates') and res.candidates:
|
|
@@ -389,7 +395,7 @@ class GeminiXReasoningEngine(Engine, GoogleMixin):
|
|
|
389
395
|
for part in candidate.content.parts:
|
|
390
396
|
if hasattr(part, 'function_call') and part.function_call:
|
|
391
397
|
if hit:
|
|
392
|
-
|
|
398
|
+
UserMessage("Multiple function calls detected in the response but only the first one will be processed.")
|
|
393
399
|
break
|
|
394
400
|
func_call = part.function_call
|
|
395
401
|
metadata['function_call'] = {
|
|
@@ -401,60 +407,62 @@ class GeminiXReasoningEngine(Engine, GoogleMixin):
|
|
|
401
407
|
|
|
402
408
|
def _prepare_raw_input(self, argument):
|
|
403
409
|
if not argument.prop.processed_input:
|
|
404
|
-
|
|
410
|
+
UserMessage('Need to provide a prompt instruction to the engine if `raw_input` is enabled!', raise_with=ValueError)
|
|
405
411
|
|
|
406
412
|
raw_prompt_data = argument.prop.processed_input
|
|
407
|
-
|
|
408
|
-
system_instruction =
|
|
413
|
+
normalized_prompts = self._normalize_raw_prompt_data(raw_prompt_data)
|
|
414
|
+
system_instruction, non_system_messages = self._separate_system_instruction(normalized_prompts)
|
|
415
|
+
messages_for_api = self._build_raw_input_messages(non_system_messages)
|
|
416
|
+
return system_instruction, messages_for_api
|
|
409
417
|
|
|
418
|
+
def _normalize_raw_prompt_data(self, raw_prompt_data):
|
|
410
419
|
if isinstance(raw_prompt_data, str):
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
420
|
+
return [{'role': 'user', 'content': raw_prompt_data}]
|
|
421
|
+
if isinstance(raw_prompt_data, dict):
|
|
422
|
+
return [raw_prompt_data]
|
|
423
|
+
if isinstance(raw_prompt_data, list):
|
|
415
424
|
for item in raw_prompt_data:
|
|
416
425
|
if not isinstance(item, dict):
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
426
|
+
UserMessage(f"Invalid item in raw_input list: {item}. Expected dict.", raise_with=ValueError)
|
|
427
|
+
return raw_prompt_data
|
|
428
|
+
UserMessage(f"Unsupported type for raw_input: {type(raw_prompt_data)}. Expected str, dict, or list of dicts.", raise_with=ValueError)
|
|
429
|
+
return []
|
|
421
430
|
|
|
422
|
-
|
|
431
|
+
def _separate_system_instruction(self, normalized_prompts):
|
|
432
|
+
system_instruction = None
|
|
433
|
+
non_system_messages = []
|
|
423
434
|
for msg in normalized_prompts:
|
|
424
435
|
role = msg.get('role')
|
|
425
436
|
content = msg.get('content')
|
|
426
|
-
|
|
427
437
|
if role is None or content is None:
|
|
428
|
-
|
|
438
|
+
UserMessage(f"Message in raw_input is missing 'role' or 'content': {msg}", raise_with=ValueError)
|
|
429
439
|
if not isinstance(content, str):
|
|
430
|
-
|
|
440
|
+
UserMessage(f"Message content for role '{role}' in raw_input must be a string. Found type: {type(content)} for content: {content}", raise_with=ValueError)
|
|
431
441
|
if role == 'system':
|
|
432
442
|
if system_instruction is not None:
|
|
433
|
-
|
|
443
|
+
UserMessage('Only one system instruction is allowed in raw_input mode!', raise_with=ValueError)
|
|
434
444
|
system_instruction = content
|
|
435
445
|
else:
|
|
436
|
-
|
|
446
|
+
non_system_messages.append({'role': role, 'content': content})
|
|
447
|
+
return system_instruction, non_system_messages
|
|
437
448
|
|
|
438
|
-
|
|
449
|
+
def _build_raw_input_messages(self, messages):
|
|
450
|
+
messages_for_api = []
|
|
451
|
+
for msg in messages:
|
|
439
452
|
content_str = str(msg.get('content', ''))
|
|
440
|
-
|
|
441
453
|
current_message_api_parts: list[types.Part] = []
|
|
442
|
-
|
|
443
454
|
image_api_parts = self._handle_image_content(content_str)
|
|
444
455
|
if image_api_parts:
|
|
445
456
|
current_message_api_parts.extend(image_api_parts)
|
|
446
|
-
|
|
447
457
|
text_only_content = self._remove_media_patterns(content_str)
|
|
448
458
|
if text_only_content:
|
|
449
459
|
current_message_api_parts.append(types.Part(text=text_only_content))
|
|
450
|
-
|
|
451
460
|
if current_message_api_parts:
|
|
452
461
|
messages_for_api.append({
|
|
453
462
|
'role': msg['role'],
|
|
454
463
|
'content': current_message_api_parts
|
|
455
464
|
})
|
|
456
|
-
|
|
457
|
-
return system_instruction, messages_for_api
|
|
465
|
+
return messages_for_api
|
|
458
466
|
|
|
459
467
|
def prepare(self, argument):
|
|
460
468
|
#@NOTE: OpenAI compatibility at high level
|
|
@@ -462,79 +470,72 @@ class GeminiXReasoningEngine(Engine, GoogleMixin):
|
|
|
462
470
|
argument.prop.prepared_input = self._prepare_raw_input(argument)
|
|
463
471
|
return
|
|
464
472
|
|
|
465
|
-
|
|
473
|
+
processed_input_str = str(argument.prop.processed_input)
|
|
474
|
+
media_content = self._process_multimodal_content(processed_input_str)
|
|
475
|
+
system_content = self._compose_system_content(argument)
|
|
476
|
+
user_content = self._compose_user_content(argument)
|
|
477
|
+
system_content, user_content = self._apply_self_prompt_if_needed(argument, system_content, user_content)
|
|
466
478
|
|
|
467
|
-
|
|
468
|
-
|
|
479
|
+
user_prompt = self._build_user_prompt(media_content, user_content)
|
|
480
|
+
argument.prop.prepared_input = (system_content, [user_prompt])
|
|
469
481
|
|
|
482
|
+
def _compose_system_content(self, argument) -> str:
|
|
483
|
+
system_content = ""
|
|
484
|
+
_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"""
|
|
470
485
|
if argument.prop.suppress_verbose_output:
|
|
471
486
|
system_content += _non_verbose_output
|
|
472
487
|
system_content = f'{system_content}\n' if system_content and len(system_content) > 0 else ''
|
|
473
|
-
|
|
474
488
|
if argument.prop.response_format:
|
|
475
|
-
|
|
476
|
-
assert
|
|
477
|
-
if
|
|
478
|
-
system_content +=
|
|
479
|
-
|
|
489
|
+
response_format = argument.prop.response_format
|
|
490
|
+
assert response_format.get('type') is not None, 'Response format type is required!'
|
|
491
|
+
if response_format["type"] == "json_object":
|
|
492
|
+
system_content += '<RESPONSE_FORMAT/>\nYou are a helpful assistant designed to output JSON.\n\n'
|
|
480
493
|
ref = argument.prop.instance
|
|
481
494
|
static_ctxt, dyn_ctxt = ref.global_context
|
|
482
495
|
if len(static_ctxt) > 0:
|
|
483
496
|
system_content += f"<STATIC_CONTEXT/>\n{static_ctxt}\n\n"
|
|
484
|
-
|
|
485
497
|
if len(dyn_ctxt) > 0:
|
|
486
498
|
system_content += f"<DYNAMIC_CONTEXT/>\n{dyn_ctxt}\n\n"
|
|
487
|
-
|
|
488
499
|
payload = argument.prop.payload
|
|
489
500
|
if argument.prop.payload:
|
|
490
|
-
system_content += f"<ADDITIONAL_CONTEXT/>\n{
|
|
491
|
-
|
|
501
|
+
system_content += f"<ADDITIONAL_CONTEXT/>\n{payload!s}\n\n"
|
|
492
502
|
examples: list[str] = argument.prop.examples
|
|
493
503
|
if examples and len(examples) > 0:
|
|
494
|
-
system_content += f"<EXAMPLES/>\n{
|
|
495
|
-
|
|
496
|
-
# Handle multimodal content
|
|
497
|
-
processed_input_str = str(argument.prop.processed_input)
|
|
498
|
-
media_content = self._process_multimodal_content(processed_input_str)
|
|
499
|
-
|
|
504
|
+
system_content += f"<EXAMPLES/>\n{examples!s}\n\n"
|
|
500
505
|
if argument.prop.prompt is not None and len(argument.prop.prompt) > 0:
|
|
501
506
|
val = str(argument.prop.prompt)
|
|
502
507
|
val = self._remove_media_patterns(val)
|
|
503
508
|
system_content += f"<INSTRUCTION/>\n{val}\n\n"
|
|
509
|
+
if argument.prop.template_suffix:
|
|
510
|
+
system_content += f' You will only generate content for the placeholder `{argument.prop.template_suffix!s}` following the instructions and the provided context information.\n\n'
|
|
511
|
+
return system_content
|
|
504
512
|
|
|
513
|
+
def _compose_user_content(self, argument) -> str:
|
|
505
514
|
suffix = str(argument.prop.processed_input)
|
|
506
515
|
suffix = self._remove_media_patterns(suffix)
|
|
507
|
-
|
|
516
|
+
return f"{suffix}"
|
|
508
517
|
|
|
509
|
-
|
|
510
|
-
system_content += f' You will only generate content for the placeholder `{str(argument.prop.template_suffix)}` following the instructions and the provided context information.\n\n'
|
|
511
|
-
|
|
512
|
-
# Handle self-prompting
|
|
518
|
+
def _apply_self_prompt_if_needed(self, argument, system_content: str, user_content: str):
|
|
513
519
|
if argument.prop.instance._kwargs.get('self_prompt', False) or argument.prop.self_prompt:
|
|
514
520
|
self_prompter = SelfPrompt()
|
|
515
|
-
|
|
516
521
|
res = self_prompter(
|
|
517
522
|
{'user': user_content, 'system': system_content},
|
|
518
523
|
max_tokens=argument.kwargs.get('max_tokens', self.max_response_tokens),
|
|
519
524
|
thinking=argument.kwargs.get('thinking', None),
|
|
520
525
|
)
|
|
521
526
|
if res is None:
|
|
522
|
-
|
|
523
|
-
|
|
527
|
+
UserMessage("Self-prompting failed!", raise_with=ValueError)
|
|
524
528
|
user_content = res['user']
|
|
525
529
|
system_content = res['system']
|
|
530
|
+
return system_content, user_content
|
|
526
531
|
|
|
527
|
-
|
|
528
|
-
all_user_content
|
|
532
|
+
def _build_user_prompt(self, media_content, user_content: str) -> dict:
|
|
533
|
+
all_user_content = list(media_content)
|
|
529
534
|
if user_content.strip():
|
|
530
535
|
all_user_content.append(genai.types.Part(text=user_content.strip()))
|
|
531
|
-
|
|
532
536
|
if not all_user_content:
|
|
533
537
|
all_user_content = [genai.types.Part(text="N/A")]
|
|
534
|
-
|
|
535
|
-
user_prompt = {'role': 'user', 'content': all_user_content}
|
|
536
|
-
|
|
537
|
-
argument.prop.prepared_input = (system_content, [user_prompt])
|
|
538
|
+
return {'role': 'user', 'content': all_user_content}
|
|
538
539
|
|
|
539
540
|
def _prepare_request_payload(self, argument):
|
|
540
541
|
kwargs = argument.kwargs
|
|
@@ -584,7 +585,7 @@ class GeminiXReasoningEngine(Engine, GoogleMixin):
|
|
|
584
585
|
elif isinstance(tool_item, types.FunctionDeclaration):
|
|
585
586
|
processed_tools.append(types.Tool(function_declarations=[tool_item]))
|
|
586
587
|
else:
|
|
587
|
-
|
|
588
|
+
UserMessage(f"Ignoring invalid tool format. Expected a callable, google.genai.types.Tool, or google.genai.types.FunctionDeclaration: {tool_item}")
|
|
588
589
|
|
|
589
590
|
if not processed_tools:
|
|
590
591
|
return None
|