symbolicai 0.20.2__py3-none-any.whl → 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. symai/__init__.py +96 -64
  2. symai/backend/base.py +93 -80
  3. symai/backend/engines/drawing/engine_bfl.py +12 -11
  4. symai/backend/engines/drawing/engine_gpt_image.py +108 -87
  5. symai/backend/engines/embedding/engine_llama_cpp.py +25 -28
  6. symai/backend/engines/embedding/engine_openai.py +3 -5
  7. symai/backend/engines/execute/engine_python.py +6 -5
  8. symai/backend/engines/files/engine_io.py +74 -67
  9. symai/backend/engines/imagecaptioning/engine_blip2.py +3 -3
  10. symai/backend/engines/imagecaptioning/engine_llavacpp_client.py +54 -38
  11. symai/backend/engines/index/engine_pinecone.py +23 -24
  12. symai/backend/engines/index/engine_vectordb.py +16 -14
  13. symai/backend/engines/lean/engine_lean4.py +38 -34
  14. symai/backend/engines/neurosymbolic/__init__.py +41 -13
  15. symai/backend/engines/neurosymbolic/engine_anthropic_claudeX_chat.py +262 -182
  16. symai/backend/engines/neurosymbolic/engine_anthropic_claudeX_reasoning.py +263 -191
  17. symai/backend/engines/neurosymbolic/engine_deepseekX_reasoning.py +53 -49
  18. symai/backend/engines/neurosymbolic/engine_google_geminiX_reasoning.py +212 -211
  19. symai/backend/engines/neurosymbolic/engine_groq.py +87 -63
  20. symai/backend/engines/neurosymbolic/engine_huggingface.py +21 -24
  21. symai/backend/engines/neurosymbolic/engine_llama_cpp.py +117 -48
  22. symai/backend/engines/neurosymbolic/engine_openai_gptX_chat.py +256 -229
  23. symai/backend/engines/neurosymbolic/engine_openai_gptX_reasoning.py +270 -150
  24. symai/backend/engines/ocr/engine_apilayer.py +6 -8
  25. symai/backend/engines/output/engine_stdout.py +1 -4
  26. symai/backend/engines/search/engine_openai.py +7 -7
  27. symai/backend/engines/search/engine_perplexity.py +5 -5
  28. symai/backend/engines/search/engine_serpapi.py +12 -14
  29. symai/backend/engines/speech_to_text/engine_local_whisper.py +20 -27
  30. symai/backend/engines/symbolic/engine_wolframalpha.py +3 -3
  31. symai/backend/engines/text_to_speech/engine_openai.py +5 -7
  32. symai/backend/engines/text_vision/engine_clip.py +7 -11
  33. symai/backend/engines/userinput/engine_console.py +3 -3
  34. symai/backend/engines/webscraping/engine_requests.py +81 -48
  35. symai/backend/mixin/__init__.py +13 -0
  36. symai/backend/mixin/anthropic.py +4 -2
  37. symai/backend/mixin/deepseek.py +2 -0
  38. symai/backend/mixin/google.py +2 -0
  39. symai/backend/mixin/openai.py +11 -3
  40. symai/backend/settings.py +83 -16
  41. symai/chat.py +101 -78
  42. symai/collect/__init__.py +7 -1
  43. symai/collect/dynamic.py +77 -69
  44. symai/collect/pipeline.py +35 -27
  45. symai/collect/stats.py +75 -63
  46. symai/components.py +198 -169
  47. symai/constraints.py +15 -12
  48. symai/core.py +698 -359
  49. symai/core_ext.py +32 -34
  50. symai/endpoints/api.py +80 -73
  51. symai/extended/.DS_Store +0 -0
  52. symai/extended/__init__.py +46 -12
  53. symai/extended/api_builder.py +11 -8
  54. symai/extended/arxiv_pdf_parser.py +13 -12
  55. symai/extended/bibtex_parser.py +2 -3
  56. symai/extended/conversation.py +101 -90
  57. symai/extended/document.py +17 -10
  58. symai/extended/file_merger.py +18 -13
  59. symai/extended/graph.py +18 -13
  60. symai/extended/html_style_template.py +2 -4
  61. symai/extended/interfaces/blip_2.py +1 -2
  62. symai/extended/interfaces/clip.py +1 -2
  63. symai/extended/interfaces/console.py +7 -1
  64. symai/extended/interfaces/dall_e.py +1 -1
  65. symai/extended/interfaces/flux.py +1 -1
  66. symai/extended/interfaces/gpt_image.py +1 -1
  67. symai/extended/interfaces/input.py +1 -1
  68. symai/extended/interfaces/llava.py +0 -1
  69. symai/extended/interfaces/naive_vectordb.py +7 -8
  70. symai/extended/interfaces/naive_webscraping.py +1 -1
  71. symai/extended/interfaces/ocr.py +1 -1
  72. symai/extended/interfaces/pinecone.py +6 -5
  73. symai/extended/interfaces/serpapi.py +1 -1
  74. symai/extended/interfaces/terminal.py +2 -3
  75. symai/extended/interfaces/tts.py +1 -1
  76. symai/extended/interfaces/whisper.py +1 -1
  77. symai/extended/interfaces/wolframalpha.py +1 -1
  78. symai/extended/metrics/__init__.py +11 -1
  79. symai/extended/metrics/similarity.py +11 -13
  80. symai/extended/os_command.py +17 -16
  81. symai/extended/packages/__init__.py +29 -3
  82. symai/extended/packages/symdev.py +19 -16
  83. symai/extended/packages/sympkg.py +12 -9
  84. symai/extended/packages/symrun.py +21 -19
  85. symai/extended/repo_cloner.py +11 -10
  86. symai/extended/seo_query_optimizer.py +1 -2
  87. symai/extended/solver.py +20 -23
  88. symai/extended/summarizer.py +4 -3
  89. symai/extended/taypan_interpreter.py +10 -12
  90. symai/extended/vectordb.py +99 -82
  91. symai/formatter/__init__.py +9 -1
  92. symai/formatter/formatter.py +12 -16
  93. symai/formatter/regex.py +62 -63
  94. symai/functional.py +176 -122
  95. symai/imports.py +136 -127
  96. symai/interfaces.py +56 -27
  97. symai/memory.py +14 -13
  98. symai/misc/console.py +49 -39
  99. symai/misc/loader.py +5 -3
  100. symai/models/__init__.py +17 -1
  101. symai/models/base.py +269 -181
  102. symai/models/errors.py +0 -1
  103. symai/ops/__init__.py +32 -22
  104. symai/ops/measures.py +11 -15
  105. symai/ops/primitives.py +348 -228
  106. symai/post_processors.py +32 -28
  107. symai/pre_processors.py +39 -41
  108. symai/processor.py +6 -4
  109. symai/prompts.py +59 -45
  110. symai/server/huggingface_server.py +23 -20
  111. symai/server/llama_cpp_server.py +7 -5
  112. symai/shell.py +3 -4
  113. symai/shellsv.py +499 -375
  114. symai/strategy.py +517 -287
  115. symai/symbol.py +111 -116
  116. symai/utils.py +42 -36
  117. {symbolicai-0.20.2.dist-info → symbolicai-1.0.0.dist-info}/METADATA +4 -2
  118. symbolicai-1.0.0.dist-info/RECORD +163 -0
  119. symbolicai-0.20.2.dist-info/RECORD +0 -162
  120. {symbolicai-0.20.2.dist-info → symbolicai-1.0.0.dist-info}/WHEEL +0 -0
  121. {symbolicai-0.20.2.dist-info → symbolicai-1.0.0.dist-info}/entry_points.txt +0 -0
  122. {symbolicai-0.20.2.dist-info → symbolicai-1.0.0.dist-info}/licenses/LICENSE +0 -0
  123. {symbolicai-0.20.2.dist-info → symbolicai-1.0.0.dist-info}/top_level.txt +0 -0
@@ -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 ....misc.console import ConsoleStyle
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 not isinstance(msg, list):
76
- msg = [msg]
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
- logging.error(f"Gemini count_tokens failed: {e}")
103
- CustomUserWarning(f"Error counting tokens for Gemini: {str(e)}", raise_with=RuntimeError)
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, prompts: list) -> int:
106
- CustomUserWarning("Token counting not implemented for Gemini", raise_with=NotImplementedError)
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
- CustomUserWarning("URL documents not yet supported for Gemini")
113
+ UserMessage("URL documents not yet supported for Gemini")
119
114
  return None
120
- else:
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
- CustomUserWarning(f"Failed to process document: {e}")
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
- pattern = r'<<vision:(.*?):>>'
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
- if img_src.startswith('data:image'):
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
- CustomUserWarning(f"Failed to process image source '{img_src}'. Error: {str(e)}", raise_with=ValueError)
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
- CustomUserWarning("URL videos not yet supported for Gemini")
220
+ UserMessage("URL videos not yet supported for Gemini")
221
221
  return None
222
- else:
223
- # Upload local video
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
- CustomUserWarning(f"Failed to process video: {e}")
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
- CustomUserWarning("URL audio not yet supported for Gemini")
238
+ UserMessage("URL audio not yet supported for Gemini")
241
239
  return None
242
- else:
243
- # Upload local audio
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
- CustomUserWarning(f"Failed to process audio: {e}")
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
- system, prompt = argument.prop.prepared_input
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 = types.GenerateContentConfig(
327
- max_output_tokens=payload.get('max_output_tokens'),
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
- if self.api_key is None or self.api_key == '':
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
- CustomUserWarning("Multiple function calls detected in the response but only the first one will be processed.")
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
- CustomUserWarning('Need to provide a prompt instruction to the engine if `raw_input` is enabled!', raise_with=ValueError)
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
- messages_for_api = []
408
- system_instruction = None
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
- normalized_prompts = [{'role': 'user', 'content': raw_prompt_data}]
412
- elif isinstance(raw_prompt_data, dict):
413
- normalized_prompts = [raw_prompt_data]
414
- elif isinstance(raw_prompt_data, list):
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
- CustomUserWarning(f"Invalid item in raw_input list: {item}. Expected dict.", raise_with=ValueError)
418
- normalized_prompts = raw_prompt_data
419
- else:
420
- CustomUserWarning(f"Unsupported type for raw_input: {type(raw_prompt_data)}. Expected str, dict, or list of dicts.", raise_with=ValueError)
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
- temp_non_system_messages = []
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
- CustomUserWarning(f"Message in raw_input is missing 'role' or 'content': {msg}", raise_with=ValueError)
438
+ UserMessage(f"Message in raw_input is missing 'role' or 'content': {msg}", raise_with=ValueError)
429
439
  if not isinstance(content, str):
430
- CustomUserWarning(f"Message content for role '{role}' in raw_input must be a string. Found type: {type(content)} for content: {content}", raise_with=ValueError)
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
- CustomUserWarning('Only one system instruction is allowed in raw_input mode!', raise_with=ValueError)
443
+ UserMessage('Only one system instruction is allowed in raw_input mode!', raise_with=ValueError)
434
444
  system_instruction = content
435
445
  else:
436
- temp_non_system_messages.append({'role': role, 'content': content})
446
+ non_system_messages.append({'role': role, 'content': content})
447
+ return system_instruction, non_system_messages
437
448
 
438
- for msg in temp_non_system_messages:
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
- _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"""
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
- user_content = ""
468
- system_content = ""
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
- _rsp_fmt = argument.prop.response_format
476
- assert _rsp_fmt.get('type') is not None, 'Response format type is required!'
477
- if _rsp_fmt["type"] == "json_object":
478
- system_content += f'<RESPONSE_FORMAT/>\nYou are a helpful assistant designed to output JSON.\n\n'
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{str(payload)}\n\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{str(examples)}\n\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
- user_content += f"{suffix}"
516
+ return f"{suffix}"
508
517
 
509
- if argument.prop.template_suffix:
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
- CustomUserWarning("Self-prompting failed!", raise_with=ValueError)
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
- all_user_content = []
528
- all_user_content.extend(media_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
- CustomUserWarning(f"Ignoring invalid tool format. Expected a callable, google.genai.types.Tool, or google.genai.types.FunctionDeclaration: {tool_item}")
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