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
symai/imports.py CHANGED
@@ -1,25 +1,24 @@
1
1
  import importlib
2
2
  import json
3
3
  import logging
4
- import os
5
4
  import shutil
6
5
  import stat
7
6
  import subprocess
8
7
  import sys
9
8
  from pathlib import Path
10
- from typing import List, Tuple, Union
11
9
 
12
10
  from loguru import logger
13
11
 
14
12
  from .backend.settings import HOME_PATH
15
13
  from .symbol import Expression
14
+ from .utils import UserMessage
16
15
 
17
16
  logging.getLogger("subprocess").setLevel(logging.ERROR)
18
17
 
19
18
 
20
19
  __root_dir__ = HOME_PATH / 'packages'
21
20
  BASE_PACKAGE_MODULE = '' # use relative path
22
- BASE_PACKAGE_PATH = str(__root_dir__)
21
+ BASE_PACKAGE_PATH = __root_dir__
23
22
  sys.path.append(str(__root_dir__))
24
23
 
25
24
 
@@ -33,10 +32,10 @@ class Import(Expression):
33
32
  @staticmethod
34
33
  def exists(module):
35
34
  # Check if module is a local path or a GitHub repo reference
36
- if os.path.exists(module) and os.path.isdir(module):
37
- return os.path.exists(f'{module}/package.json')
38
- else:
39
- return os.path.exists(f'{BASE_PACKAGE_PATH}/{module}/package.json')
35
+ module_path = Path(module)
36
+ if module_path.exists() and module_path.is_dir():
37
+ return (module_path / 'package.json').exists()
38
+ return (BASE_PACKAGE_PATH / module / 'package.json').exists()
40
39
 
41
40
  @staticmethod
42
41
  def get_from_local(module, local_path):
@@ -47,37 +46,39 @@ class Import(Expression):
47
46
  local_path: Path to local package directory
48
47
  """
49
48
  # If base package does not exist, create it
50
- if not os.path.exists(BASE_PACKAGE_PATH):
51
- os.makedirs(BASE_PACKAGE_PATH)
49
+ BASE_PACKAGE_PATH.mkdir(parents=True, exist_ok=True)
52
50
 
53
51
  # Create module directory
54
- module_path = f'{BASE_PACKAGE_PATH}/{module}'
55
- os.makedirs(os.path.dirname(module_path), exist_ok=True)
52
+ module_path = BASE_PACKAGE_PATH / module
53
+ module_path.parent.mkdir(parents=True, exist_ok=True)
56
54
 
57
55
  # Copy files from local path to package directory
58
56
  try:
59
- if os.path.exists(module_path):
57
+ if module_path.exists():
60
58
  shutil.rmtree(module_path)
61
59
  shutil.copytree(local_path, module_path)
62
60
  logger.info(f"Copied local package from {local_path} to {module_path}")
63
61
 
64
62
  # Install dependencies
65
- if os.path.exists(f'{module_path}/package.json'):
66
- with open(f'{module_path}/package.json') as f:
63
+ package_json = module_path / 'package.json'
64
+ if package_json.exists():
65
+ with package_json.open() as f:
67
66
  pkg = json.load(f)
68
67
  for dependency in pkg.get('dependencies', []):
69
68
  # Update git_url for the dependency
70
69
  git_url_dependency = f'git@github.com:{dependency}.git'
71
- if not os.path.exists(f'{BASE_PACKAGE_PATH}/{dependency}'):
72
- subprocess.check_call(['git', 'clone', git_url_dependency, f'{BASE_PACKAGE_PATH}/{dependency}'])
70
+ dependency_path = BASE_PACKAGE_PATH / dependency
71
+ if not dependency_path.exists():
72
+ subprocess.check_call(['git', 'clone', git_url_dependency, str(dependency_path)])
73
73
 
74
74
  # Install requirements
75
- if os.path.exists(f'{module_path}/requirements.txt'):
76
- with open(f'{module_path}/requirements.txt') as f:
75
+ requirements_file = module_path / 'requirements.txt'
76
+ if requirements_file.exists():
77
+ with requirements_file.open() as f:
77
78
  for dependency in f.readlines():
78
- dependency = dependency.strip()
79
- if dependency:
80
- subprocess.check_call([sys.executable, '-m', 'pip', 'install', dependency])
79
+ dependency_name = dependency.strip()
80
+ if dependency_name:
81
+ subprocess.check_call([sys.executable, '-m', 'pip', 'install', dependency_name])
81
82
  except Exception as e:
82
83
  logger.error(f"Error installing from local path: {e}")
83
84
  raise
@@ -85,29 +86,31 @@ class Import(Expression):
85
86
  @staticmethod
86
87
  def get_from_github(module, submodules=False):
87
88
  # if base package does not exist, create it
88
- if not os.path.exists(BASE_PACKAGE_PATH):
89
- os.makedirs(BASE_PACKAGE_PATH)
89
+ BASE_PACKAGE_PATH.mkdir(parents=True, exist_ok=True)
90
90
 
91
91
  # Clone repository
92
92
  git_url = f'git@github.com:{module}.git'
93
93
  clone_cmd = ['git', 'clone']
94
94
  if submodules:
95
95
  clone_cmd.extend(['--recurse-submodules'])
96
- clone_cmd.extend([git_url, f'{BASE_PACKAGE_PATH}/{module}'])
96
+ clone_cmd.extend([git_url, str(BASE_PACKAGE_PATH / module)])
97
97
  subprocess.check_call(clone_cmd)
98
98
 
99
99
  # Install dependencies
100
- with open(f'{BASE_PACKAGE_PATH}/{module}/package.json') as f:
100
+ package_json = BASE_PACKAGE_PATH / module / 'package.json'
101
+ with package_json.open() as f:
101
102
  pkg = json.load(f)
102
103
  for dependency in pkg['dependencies']:
103
104
  # Update git_url for the dependency
104
105
  git_url_dependency = f'git@github.com:{dependency}.git'
105
- if not os.path.exists(f'{BASE_PACKAGE_PATH}/{dependency}'):
106
- subprocess.check_call(['git', 'clone', git_url_dependency, f'{BASE_PACKAGE_PATH}/{dependency}'])
106
+ dependency_path = BASE_PACKAGE_PATH / dependency
107
+ if not dependency_path.exists():
108
+ subprocess.check_call(['git', 'clone', git_url_dependency, str(dependency_path)])
107
109
 
108
110
  # Install requirements
109
- if os.path.exists(f'{BASE_PACKAGE_PATH}/{module}/requirements.txt'):
110
- with open(f'{BASE_PACKAGE_PATH}/{module}/requirements.txt') as f:
111
+ requirements_file = BASE_PACKAGE_PATH / module / 'requirements.txt'
112
+ if requirements_file.exists():
113
+ with requirements_file.open() as f:
111
114
  for dependency in f.readlines():
112
115
  subprocess.check_call(['pip', 'install', dependency])
113
116
 
@@ -115,31 +118,25 @@ class Import(Expression):
115
118
  def load_module_class(module):
116
119
  module_classes = []
117
120
  # Detect if module is a local path
118
- is_local_path = os.path.exists(module) and os.path.isdir(module)
121
+ module_path_obj = Path(module)
122
+ is_local_path = module_path_obj.exists() and module_path_obj.is_dir()
119
123
 
120
- if is_local_path:
121
- package_path = module
122
- else:
123
- package_path = f'{BASE_PACKAGE_PATH}/{module}'
124
+ package_path = module_path_obj if is_local_path else BASE_PACKAGE_PATH / module
124
125
 
125
- with open(f'{package_path}/package.json') as f:
126
+ with (package_path / 'package.json').open() as f:
126
127
  pkg = json.load(f)
127
128
  for expr in pkg['expressions']:
128
129
  if is_local_path:
129
- module_path = f'{package_path}/{expr["module"].replace("/", ".")}'
130
130
  # For local modules, we need to add the path to sys.path
131
- parent_dir = os.path.dirname(package_path)
132
- if parent_dir not in sys.path:
133
- sys.path.append(parent_dir)
131
+ parent_dir = package_path.parent
132
+ if str(parent_dir) not in sys.path:
133
+ sys.path.append(str(parent_dir))
134
134
  # Use local module's name from the directory structure
135
- module_name = os.path.basename(package_path)
135
+ module_name = package_path.name
136
136
  relative_module_path = f"{module_name}.{expr['module'].replace('/', '.')}"
137
137
  else:
138
- module_path = f'{BASE_PACKAGE_PATH}/{expr["module"].replace("/", ".")}'
139
- # Determine relative path and adjust namespace
140
- relative_module_path = module_path.replace(BASE_PACKAGE_PATH, '').replace('/', '.').lstrip('.')
141
- # Replace with actual username and package_name values
142
- relative_module_path = module.split('/')[0] + '.' + module.split('/')[1] + '.' + relative_module_path
138
+ module_parts = module.split('/')
139
+ relative_module_path = '.'.join([*module_parts, expr['module'].replace('/', '.')])
143
140
 
144
141
  try:
145
142
  module_class = getattr(importlib.import_module(relative_module_path), expr['type'])
@@ -150,65 +147,66 @@ class Import(Expression):
150
147
  return module_classes
151
148
 
152
149
  @staticmethod
153
- def load_expression(module, expressions: Union[List[str] | Tuple[str] | str]) -> Union[List[Expression] | Expression]:
150
+ def _normalize_expressions(expressions: list[str] | tuple[str] | str) -> tuple[list[str], bool]:
151
+ if isinstance(expressions, str):
152
+ return [expressions], True
153
+ if isinstance(expressions, (list, tuple)):
154
+ expression_list = list(expressions)
155
+ return expression_list, len(expression_list) == 1
156
+ msg = "Invalid type for 'expressions'. Must be str, list or tuple."
157
+ UserMessage(msg)
158
+ raise Exception(msg)
159
+
160
+ @staticmethod
161
+ def load_expression(module, expressions: list[str] | tuple[str] | str) -> list[Expression] | Expression:
162
+ expression_list, return_single = Import._normalize_expressions(expressions)
163
+ expected_count = len(expression_list)
164
+ expression_targets = set(expression_list)
154
165
  module_classes = []
155
166
  # Detect if module is a local path
156
- is_local_path = os.path.exists(module) and os.path.isdir(module)
167
+ module_path_obj = Path(module)
168
+ is_local_path = module_path_obj.exists() and module_path_obj.is_dir()
169
+
170
+ package_path = module_path_obj if is_local_path else BASE_PACKAGE_PATH / module
157
171
 
158
172
  if is_local_path:
159
- package_path = module
173
+ parent_dir = package_path.parent
174
+ if str(parent_dir) not in sys.path:
175
+ sys.path.append(str(parent_dir))
176
+ module_name = package_path.name
160
177
  else:
161
- package_path = f'{BASE_PACKAGE_PATH}/{module}'
178
+ module_parts = module.split('/')
162
179
 
163
- with open(f'{package_path}/package.json') as f:
180
+ with (package_path / 'package.json').open() as f:
164
181
  pkg = json.load(f)
165
182
  for expr in pkg['expressions']:
166
- if is_local_path:
167
- # For local paths, need to handle imports differently
168
- parent_dir = os.path.dirname(package_path)
169
- if parent_dir not in sys.path:
170
- sys.path.append(parent_dir)
183
+ relative_module_path = (
184
+ f"{module_name}.{expr['module'].replace('/', '.')}"
185
+ if is_local_path
186
+ else '.'.join([*module_parts, expr['module'].replace('/', '.')])
187
+ )
171
188
 
172
- # Use basename as module name
173
- module_name = os.path.basename(package_path)
174
- relative_module_path = f"{module_name}.{expr['module'].replace('/', '.')}"
175
- else:
176
- # For GitHub packages
177
- module_path = f'{BASE_PACKAGE_MODULE}/{expr["module"].replace("/", ".")}'
178
- # Determine relative path and adjust namespace
179
- relative_module_path = module_path.replace(BASE_PACKAGE_MODULE, '').replace('/', '.').lstrip('.')
180
- # Add the organization/repo prefix
181
- relative_module_path = f"{module.split('/')[0]}.{module.split('/')[1]}.{relative_module_path}"
182
-
183
- if isinstance(expressions, str):
184
- if expr['type'] == expressions:
185
- try:
186
- module_obj = importlib.import_module(relative_module_path)
187
- module_class = getattr(module_obj, expr['type'])
188
- return module_class
189
- except (ImportError, ModuleNotFoundError) as e:
190
- logger.error(f"Error importing module {relative_module_path}: {e}")
191
- raise
192
- elif isinstance(expressions, list) or isinstance(expressions, tuple):
193
- if expr['type'] in expressions:
194
- try:
195
- module_obj = importlib.import_module(relative_module_path)
196
- module_class = getattr(module_obj, expr['type'])
197
- if len(expressions) == 1:
198
- return module_class
199
- module_classes.append(module_class)
200
- except (ImportError, ModuleNotFoundError) as e:
201
- logger.error(f"Error importing module {relative_module_path}: {e}")
202
- raise
203
- else:
204
- raise Exception("Invalid type for 'expressions'. Must be str, list or tuple.")
189
+ if expr['type'] not in expression_targets:
190
+ continue
191
+
192
+ try:
193
+ module_obj = importlib.import_module(relative_module_path)
194
+ module_class = getattr(module_obj, expr['type'])
195
+ except (ImportError, ModuleNotFoundError) as e:
196
+ logger.error(f"Error importing module {relative_module_path}: {e}")
197
+ raise
198
+
199
+ if return_single:
200
+ return module_class
201
+ module_classes.append(module_class)
205
202
 
206
203
  assert len(module_classes) > 0, f"Expression '{expressions}' not found in module '{module}'"
207
204
  module_classes_names = [str(class_.__name__) for class_ in module_classes]
208
- assert len(module_classes) == len(expressions), f"Not all expressions found in module '{module}'. Could not load {[expr for expr in expressions if expr not in module_classes_names]}"
205
+ missing_expressions = [expr for expr in expression_list if expr not in module_classes_names]
206
+ assert len(module_classes) == expected_count, f"Not all expressions found in module '{module}'. Could not load {missing_expressions}"
209
207
  return module_classes
210
208
 
211
- def __new__(self, module, auto_clone: bool = True, verbose: bool = False, local_path: str | None = None,
209
+ def __new__(cls, module, auto_clone: bool = True, verbose: bool = False, local_path: str | None = None,
212
210
  submodules: bool = False, *args, **kwargs):
213
211
  """
214
212
  Import a module from GitHub or local path.
@@ -222,15 +220,18 @@ class Import(Expression):
222
220
  *args, **kwargs: Additional arguments to pass to the module constructor
223
221
  """
224
222
  # Detect if module is a local path
225
- is_local_path = os.path.exists(module) and os.path.isdir(module)
223
+ module_path_obj = Path(module)
224
+ is_local_path = module_path_obj.exists() and module_path_obj.is_dir()
226
225
 
227
226
  if is_local_path:
228
227
  # If module is a local path
229
- package_path = module
230
- if not os.path.exists(f'{package_path}/package.json'):
231
- raise ValueError(f"No package.json found in {module}")
228
+ package_path = module_path_obj
229
+ if not (package_path / 'package.json').exists():
230
+ msg = f"No package.json found in {module}"
231
+ UserMessage(msg)
232
+ raise ValueError(msg)
232
233
 
233
- with open(f'{package_path}/package.json') as f:
234
+ with (package_path / 'package.json').open() as f:
234
235
  pkg = json.load(f)
235
236
  else:
236
237
  # Module is a GitHub reference
@@ -240,27 +241,29 @@ class Import(Expression):
240
241
  else:
241
242
  Import.get_from_github(module, submodules)
242
243
 
243
- with open(f'{BASE_PACKAGE_PATH}/{module}/package.json') as f:
244
+ with (BASE_PACKAGE_PATH / module / 'package.json').open() as f:
244
245
  pkg = json.load(f)
245
246
  if 'run' not in pkg:
246
- raise Exception(f"Module '{module}' has no 'run' expression defined.")
247
+ msg = f"Module '{module}' has no 'run' expression defined."
248
+ UserMessage(msg)
249
+ raise Exception(msg)
247
250
  expr = pkg['run']
248
- module_path = f'{expr["module"].replace("/", ".")}'
249
- # Determine relative path and adjust namespace
250
- relative_module_path = module_path.replace(BASE_PACKAGE_PATH, '').replace('/', '.').lstrip('.')
251
- # Replace with actual username and package_name values
252
- relative_module_path = module.split('/')[0] + '.' + module.split('/')[1] + '.' + relative_module_path
251
+ module_rel = expr['module'].replace('/', '.')
252
+ module_parts = module.split('/')
253
+ relative_module_path = '.'.join([*module_parts, module_rel])
253
254
  class_ = expr['type']
254
255
  if verbose:
255
256
  logger.info(f"Loading module '{relative_module_path}.{expr['type']}'")
256
257
  module_class = getattr(importlib.import_module(relative_module_path), class_)
257
258
  return module_class(*args, **kwargs)
258
259
 
259
- def __call__(self, *args, **kwargs):
260
- raise Exception("Cannot call Import class directly. Use Import.load_module_class(module) instead.")
260
+ def __call__(self, *_args, **_kwargs):
261
+ msg = "Cannot call Import class directly. Use Import.load_module_class(module) instead."
262
+ UserMessage(msg)
263
+ raise Exception(msg)
261
264
 
262
265
  @staticmethod
263
- def install(module: str, local_path: str = None, submodules: bool = False):
266
+ def install(module: str, local_path: str | None = None, submodules: bool = False):
264
267
  """Install a package from GitHub or a local path.
265
268
 
266
269
  Args:
@@ -269,11 +272,12 @@ class Import(Expression):
269
272
  submodules: Whether to initialize submodules for GitHub repos
270
273
  """
271
274
  # Determine if module is a local path
272
- is_local_path = local_path is not None and os.path.exists(local_path) and os.path.isdir(local_path)
275
+ local_path_obj = Path(local_path) if local_path is not None else None
276
+ is_local_path = local_path_obj is not None and local_path_obj.exists() and local_path_obj.is_dir()
273
277
 
274
278
  if not Import.exists(module):
275
279
  if is_local_path:
276
- Import.get_from_local(module, local_path)
280
+ Import.get_from_local(module, str(local_path_obj))
277
281
  logger.success(f"Module '{module}' installed from local path.")
278
282
  else:
279
283
  Import.get_from_github(module, submodules)
@@ -284,44 +288,48 @@ class Import(Expression):
284
288
  @staticmethod
285
289
  def remove(module: str):
286
290
  # Determine if module is a local path
287
- is_local_path = os.path.exists(module) and os.path.isdir(module)
291
+ module_path_obj = Path(module)
292
+ is_local_path = module_path_obj.exists() and module_path_obj.is_dir()
288
293
 
289
294
  if is_local_path:
290
295
  # For local path, remove directly
291
- if os.path.exists(module):
292
- def del_rw(action, name, exc):
293
- os.chmod(name, stat.S_IWRITE)
294
- os.remove(name)
295
- shutil.rmtree(module, onerror=del_rw)
296
+ if module_path_obj.exists():
297
+ def del_rw(_action, name, _exc):
298
+ path_obj = Path(name)
299
+ path_obj.chmod(stat.S_IWRITE)
300
+ path_obj.unlink()
301
+ shutil.rmtree(module_path_obj, onerror=del_rw)
296
302
  logger.success(f"Removed local module at '{module}'")
297
303
  else:
298
304
  logger.error(f"Local module '{module}' not found.")
299
305
  else:
300
306
  # For GitHub modules, remove from packages directory
301
- module_path = f'{BASE_PACKAGE_PATH}/{module}'
302
- if os.path.exists(module_path):
303
- def del_rw(action, name, exc):
304
- os.chmod(name, stat.S_IWRITE)
305
- os.remove(name)
307
+ module_path = BASE_PACKAGE_PATH / module
308
+ if module_path.exists():
309
+ def del_rw(_action, name, _exc):
310
+ path_obj = Path(name)
311
+ path_obj.chmod(stat.S_IWRITE)
312
+ path_obj.unlink()
306
313
  shutil.rmtree(module_path, onerror=del_rw)
307
314
  logger.success(f"Removed module '{module}'")
308
315
 
309
316
  # Check if folder is empty and remove it
310
- parent_path = f'{BASE_PACKAGE_PATH}/{module.split("/")[0]}'
311
- if os.path.exists(parent_path) and not os.listdir(parent_path):
312
- os.rmdir(parent_path)
317
+ parent_path = BASE_PACKAGE_PATH / module.split('/')[0]
318
+ if parent_path.exists() and not any(parent_path.iterdir()):
319
+ parent_path.rmdir()
313
320
  logger.info(f"Removed empty parent folder '{parent_path}'")
314
321
  else:
315
322
  logger.error(f"Module '{module}' not found.")
316
323
 
317
324
  @staticmethod
318
325
  def list_installed():
319
- base_dirs = [dir for dir in os.listdir(BASE_PACKAGE_PATH) if os.path.isdir(f'{BASE_PACKAGE_PATH}/{dir}')]
326
+ if not BASE_PACKAGE_PATH.exists():
327
+ return []
328
+ base_dirs = [entry for entry in BASE_PACKAGE_PATH.iterdir() if entry.is_dir()]
320
329
 
321
330
  sub_dirs = []
322
331
  for base_dir in base_dirs:
323
- full_path = f'{BASE_PACKAGE_PATH}/{base_dir}'
324
- sub_dirs.extend([f'{base_dir}/{dir}' for dir in os.listdir(full_path) if os.path.isdir(f'{full_path}/{dir}')])
332
+ sub_dirs.extend([f'{base_dir.name}/{entry.name}' for entry in base_dir.iterdir() if entry.is_dir()])
325
333
 
326
334
  return sub_dirs
327
335
 
@@ -335,13 +343,14 @@ class Import(Expression):
335
343
  """
336
344
  if Import.exists(module):
337
345
  # Determine if module is a local path
338
- is_local_path = os.path.exists(module) and os.path.isdir(module)
346
+ module_path_obj = Path(module)
347
+ is_local_path = module_path_obj.exists() and module_path_obj.is_dir()
339
348
 
340
349
  # Use the appropriate path based on whether it's local or not
341
- module_path = module if is_local_path else f'{BASE_PACKAGE_PATH}/{module.replace(".","/")}'
350
+ module_path = module_path_obj if is_local_path else BASE_PACKAGE_PATH / module.replace(".", "/")
342
351
 
343
352
  # Construct the git pull command based on whether submodules should be included
344
- pull_cmd = ['git', '-C', module_path]
353
+ pull_cmd = ['git', '-C', str(module_path)]
345
354
  if submodules:
346
355
  pull_cmd.extend(['pull', '--recurse-submodules'])
347
356
  subprocess.check_call(pull_cmd)
symai/interfaces.py CHANGED
@@ -5,7 +5,7 @@ from .backend.mixin import OPENAI_CHAT_MODELS, OPENAI_REASONING_MODELS
5
5
  from .backend.settings import SYMAI_CONFIG
6
6
  from .imports import Import
7
7
  from .symbol import Expression
8
- from .utils import CustomUserWarning
8
+ from .utils import UserMessage
9
9
 
10
10
 
11
11
  class Interface(Expression):
@@ -13,19 +13,19 @@ class Interface(Expression):
13
13
  super().__init__(*args, **kwargs)
14
14
  self.logger = logging.getLogger(__name__)
15
15
 
16
- def __new__(self, module: str, *args, **kwargs):
16
+ def __new__(cls, module: str, *args, **kwargs):
17
17
  module = str(module)
18
18
  # if `/` in module, assume github repo; else assume local module
19
19
  if "/" in module:
20
20
  return Import(module)
21
21
  module = module.lower()
22
22
  module = module.replace("-", "_")
23
- self._module = module
24
- self.module_path = f"symai.extended.interfaces.{module}"
25
- return Interface.load_module_class(self.module_path, self._module)(*args, **kwargs)
23
+ cls._module = module
24
+ cls.module_path = f"symai.extended.interfaces.{module}"
25
+ return Interface.load_module_class(cls.module_path, cls._module)(*args, **kwargs)
26
26
 
27
- def __call__(self, *args, **kwargs):
28
- CustomUserWarning(f"Interface {self._module} is not callable.", raise_with=NotImplementedError)
27
+ def __call__(self, *_args, **_kwargs):
28
+ UserMessage(f"Interface {self._module} is not callable.", raise_with=NotImplementedError)
29
29
 
30
30
  @staticmethod
31
31
  def load_module_class(module_path, class_name):
@@ -33,37 +33,66 @@ class Interface(Expression):
33
33
  return getattr(module_, class_name)
34
34
 
35
35
 
36
- def cfg_to_interface():
37
- """Maps configuration to interface."""
38
- mapping = {}
36
+ def _add_symbolic_interface(mapping):
39
37
  symbolic_api_key = SYMAI_CONFIG.get("SYMBOLIC_ENGINE_API_KEY")
40
38
  if symbolic_api_key is not None:
41
39
  mapping["symbolic"] = Interface("wolframalpha")
42
40
 
41
+
42
+ def _resolve_drawing_interface_name(drawing_engine_model):
43
+ if drawing_engine_model.startswith("flux"):
44
+ return "flux"
45
+ if drawing_engine_model.startswith("dall-e-"):
46
+ return "dall_e"
47
+ if drawing_engine_model.startswith("gpt-image-"):
48
+ return "gpt_image"
49
+ return None
50
+
51
+
52
+ def _add_drawing_interface(mapping):
43
53
  drawing_engine_api_key = SYMAI_CONFIG.get("DRAWING_ENGINE_API_KEY")
44
- if drawing_engine_api_key is not None:
45
- drawing_engine_model = SYMAI_CONFIG.get("DRAWING_ENGINE_MODEL")
46
- if drawing_engine_model.startswith("flux"):
47
- mapping["drawing"] = Interface("flux")
48
- elif drawing_engine_model.startswith("dall-e-"):
49
- mapping["drawing"] = Interface("dall_e")
50
- elif drawing_engine_model.startswith("gpt-image-"):
51
- mapping["drawing"] = Interface("gpt_image")
54
+ if drawing_engine_api_key is None:
55
+ return
56
+ drawing_engine_model = SYMAI_CONFIG.get("DRAWING_ENGINE_MODEL")
57
+ interface_name = _resolve_drawing_interface_name(drawing_engine_model)
58
+ if interface_name is not None:
59
+ mapping["drawing"] = Interface(interface_name)
60
+
61
+
62
+ def _resolve_search_interface_name(search_engine_model):
63
+ if search_engine_model.startswith("google"):
64
+ return "serpapi"
65
+ if search_engine_model.startswith("sonar"):
66
+ return "perplexity"
67
+ if search_engine_model in OPENAI_REASONING_MODELS + OPENAI_CHAT_MODELS:
68
+ return "openai_search"
69
+ return None
52
70
 
71
+
72
+ def _add_search_interface(mapping):
53
73
  search_engine_api_key = SYMAI_CONFIG.get("SEARCH_ENGINE_API_KEY")
54
- if search_engine_api_key is not None:
55
- search_engine_model = SYMAI_CONFIG.get("SEARCH_ENGINE_MODEL")
56
- if search_engine_model.startswith("google"):
57
- mapping["search"] = Interface("serpapi")
58
- elif search_engine_model.startswith("sonar"):
59
- mapping["search"] = Interface("perplexity")
60
- elif search_engine_model in OPENAI_REASONING_MODELS + OPENAI_CHAT_MODELS:
61
- mapping["search"] = Interface("openai_search")
74
+ if search_engine_api_key is None:
75
+ return
76
+ search_engine_model = SYMAI_CONFIG.get("SEARCH_ENGINE_MODEL")
77
+ interface_name = _resolve_search_interface_name(search_engine_model)
78
+ if interface_name is not None:
79
+ mapping["search"] = Interface(interface_name)
80
+
62
81
 
82
+ def _add_tts_interface(mapping):
63
83
  tts_engine_api_key = SYMAI_CONFIG.get("TEXT_TO_SPEECH_ENGINE_API_KEY")
64
- if tts_engine_api_key is not None: # TODO: add tests for this engine
84
+ if tts_engine_api_key is not None: # TODO: add tests for this engine
65
85
  mapping["tts"] = Interface("tts")
66
86
 
87
+
88
+ def cfg_to_interface():
89
+ """Maps configuration to interface."""
90
+ mapping = {}
91
+ _add_symbolic_interface(mapping)
92
+ _add_drawing_interface(mapping)
93
+ _add_search_interface(mapping)
94
+ _add_tts_interface(mapping)
95
+
67
96
  mapping["indexing"] = Interface("naive_vectordb")
68
97
  mapping["scraper"] = Interface("naive_webscraping")
69
98
  mapping["stt"] = Interface("whisper")
symai/memory.py CHANGED
@@ -1,8 +1,7 @@
1
- from typing import List
2
1
 
3
2
  from . import core_ext
4
- from .symbol import Expression, Symbol
5
3
  from .components import Function
4
+ from .symbol import Expression, Symbol
6
5
 
7
6
 
8
7
  class Memory(Expression):
@@ -25,24 +24,24 @@ class Memory(Expression):
25
24
  class SlidingWindowListMemory(Memory):
26
25
  def __init__(self, window_size: int = 10, max_size: int = 1000, **kwargs):
27
26
  super().__init__(**kwargs)
28
- self._memory: List[str] = []
27
+ self._memory: list[str] = []
29
28
  self._window_size: int = window_size
30
29
  self._max_size: int = max_size
31
30
 
32
- def store(self, query: str, *args, **kwargs):
31
+ def store(self, query: str, *_args, **_kwargs):
33
32
  self._memory.append(query)
34
33
  if len(self._memory) > self._max_size:
35
34
  self._memory = self._memory[-self._max_size:]
36
35
 
37
- def forget(self, query: Symbol, *args, **kwargs):
36
+ def forget(self, query: Symbol, *_args, **_kwargs):
38
37
  self._memory.remove(query)
39
38
 
40
- def recall(self, *args, **kwargs):
39
+ def recall(self, *_args, **_kwargs):
41
40
  return self._memory[-self._window_size:]
42
41
 
43
42
 
44
43
  class SlidingWindowStringConcatMemory(Memory):
45
- def __init__(self, *args, **kwargs):
44
+ def __init__(self, *_args, **kwargs):
46
45
  super().__init__(**kwargs)
47
46
  self._memory: str = ''
48
47
  self.marker: str = '[--++=|=++--]'
@@ -71,9 +70,9 @@ class SlidingWindowStringConcatMemory(Memory):
71
70
 
72
71
  def store(self, query: str):
73
72
  # append to string to memory
74
- self._memory += f'{str(query)}{self.marker}'
73
+ self._memory += f'{query!s}{self.marker}'
75
74
 
76
- def forget(self, query: Symbol, *args, **kwargs):
75
+ def forget(self, query: Symbol, *_args, **_kwargs):
77
76
  # remove substring from memory
78
77
  sym = Symbol(self._memory)
79
78
  self._memory = str(sym - query)
@@ -101,13 +100,15 @@ class VectorDatabaseMemory(Memory):
101
100
  self.top_k: int = top_k
102
101
  self.index_name = index_name
103
102
 
104
- def store(self, query: str , *args, **kwargs):
105
- if not self.enabled: return
103
+ def store(self, query: str , *_args, **_kwargs):
104
+ if not self.enabled:
105
+ return
106
106
 
107
107
  self.add(Symbol(query).zip(), index_name=self.index_name)
108
108
 
109
- def recall(self, query: str, *args, **kwargs):
110
- if not self.enabled: return
109
+ def recall(self, query: str, *_args, **_kwargs):
110
+ if not self.enabled:
111
+ return None
111
112
 
112
113
  res = self.get(Symbol(query).embed().value, index_top_k=self.top_k, index_name=self.index_name).ast()
113
114