symbolicai 0.21.0__py3-none-any.whl → 1.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. symai/__init__.py +269 -173
  2. symai/backend/base.py +123 -110
  3. symai/backend/engines/drawing/engine_bfl.py +45 -44
  4. symai/backend/engines/drawing/engine_gpt_image.py +112 -97
  5. symai/backend/engines/embedding/engine_llama_cpp.py +63 -52
  6. symai/backend/engines/embedding/engine_openai.py +25 -21
  7. symai/backend/engines/execute/engine_python.py +19 -18
  8. symai/backend/engines/files/engine_io.py +104 -95
  9. symai/backend/engines/imagecaptioning/engine_blip2.py +28 -24
  10. symai/backend/engines/imagecaptioning/engine_llavacpp_client.py +102 -79
  11. symai/backend/engines/index/engine_pinecone.py +124 -97
  12. symai/backend/engines/index/engine_qdrant.py +1011 -0
  13. symai/backend/engines/index/engine_vectordb.py +84 -56
  14. symai/backend/engines/lean/engine_lean4.py +96 -52
  15. symai/backend/engines/neurosymbolic/__init__.py +41 -13
  16. symai/backend/engines/neurosymbolic/engine_anthropic_claudeX_chat.py +330 -248
  17. symai/backend/engines/neurosymbolic/engine_anthropic_claudeX_reasoning.py +329 -264
  18. symai/backend/engines/neurosymbolic/engine_cerebras.py +328 -0
  19. symai/backend/engines/neurosymbolic/engine_deepseekX_reasoning.py +118 -88
  20. symai/backend/engines/neurosymbolic/engine_google_geminiX_reasoning.py +344 -299
  21. symai/backend/engines/neurosymbolic/engine_groq.py +173 -115
  22. symai/backend/engines/neurosymbolic/engine_huggingface.py +114 -84
  23. symai/backend/engines/neurosymbolic/engine_llama_cpp.py +144 -118
  24. symai/backend/engines/neurosymbolic/engine_openai_gptX_chat.py +415 -307
  25. symai/backend/engines/neurosymbolic/engine_openai_gptX_reasoning.py +394 -231
  26. symai/backend/engines/ocr/engine_apilayer.py +23 -27
  27. symai/backend/engines/output/engine_stdout.py +10 -13
  28. symai/backend/engines/{webscraping → scrape}/engine_requests.py +101 -54
  29. symai/backend/engines/search/engine_openai.py +100 -88
  30. symai/backend/engines/search/engine_parallel.py +665 -0
  31. symai/backend/engines/search/engine_perplexity.py +44 -45
  32. symai/backend/engines/search/engine_serpapi.py +37 -34
  33. symai/backend/engines/speech_to_text/engine_local_whisper.py +54 -51
  34. symai/backend/engines/symbolic/engine_wolframalpha.py +15 -9
  35. symai/backend/engines/text_to_speech/engine_openai.py +20 -26
  36. symai/backend/engines/text_vision/engine_clip.py +39 -37
  37. symai/backend/engines/userinput/engine_console.py +5 -6
  38. symai/backend/mixin/__init__.py +13 -0
  39. symai/backend/mixin/anthropic.py +48 -38
  40. symai/backend/mixin/deepseek.py +6 -5
  41. symai/backend/mixin/google.py +7 -4
  42. symai/backend/mixin/groq.py +2 -4
  43. symai/backend/mixin/openai.py +140 -110
  44. symai/backend/settings.py +87 -20
  45. symai/chat.py +216 -123
  46. symai/collect/__init__.py +7 -1
  47. symai/collect/dynamic.py +80 -70
  48. symai/collect/pipeline.py +67 -51
  49. symai/collect/stats.py +161 -109
  50. symai/components.py +707 -360
  51. symai/constraints.py +24 -12
  52. symai/core.py +1857 -1233
  53. symai/core_ext.py +83 -80
  54. symai/endpoints/api.py +166 -104
  55. symai/extended/.DS_Store +0 -0
  56. symai/extended/__init__.py +46 -12
  57. symai/extended/api_builder.py +29 -21
  58. symai/extended/arxiv_pdf_parser.py +23 -14
  59. symai/extended/bibtex_parser.py +9 -6
  60. symai/extended/conversation.py +156 -126
  61. symai/extended/document.py +50 -30
  62. symai/extended/file_merger.py +57 -14
  63. symai/extended/graph.py +51 -32
  64. symai/extended/html_style_template.py +18 -14
  65. symai/extended/interfaces/blip_2.py +2 -3
  66. symai/extended/interfaces/clip.py +4 -3
  67. symai/extended/interfaces/console.py +9 -1
  68. symai/extended/interfaces/dall_e.py +4 -2
  69. symai/extended/interfaces/file.py +2 -0
  70. symai/extended/interfaces/flux.py +4 -2
  71. symai/extended/interfaces/gpt_image.py +16 -7
  72. symai/extended/interfaces/input.py +2 -1
  73. symai/extended/interfaces/llava.py +1 -2
  74. symai/extended/interfaces/{naive_webscraping.py → naive_scrape.py} +4 -3
  75. symai/extended/interfaces/naive_vectordb.py +9 -10
  76. symai/extended/interfaces/ocr.py +5 -3
  77. symai/extended/interfaces/openai_search.py +2 -0
  78. symai/extended/interfaces/parallel.py +30 -0
  79. symai/extended/interfaces/perplexity.py +2 -0
  80. symai/extended/interfaces/pinecone.py +12 -9
  81. symai/extended/interfaces/python.py +2 -0
  82. symai/extended/interfaces/serpapi.py +3 -1
  83. symai/extended/interfaces/terminal.py +2 -4
  84. symai/extended/interfaces/tts.py +3 -2
  85. symai/extended/interfaces/whisper.py +3 -2
  86. symai/extended/interfaces/wolframalpha.py +2 -1
  87. symai/extended/metrics/__init__.py +11 -1
  88. symai/extended/metrics/similarity.py +14 -13
  89. symai/extended/os_command.py +39 -29
  90. symai/extended/packages/__init__.py +29 -3
  91. symai/extended/packages/symdev.py +51 -43
  92. symai/extended/packages/sympkg.py +41 -35
  93. symai/extended/packages/symrun.py +63 -50
  94. symai/extended/repo_cloner.py +14 -12
  95. symai/extended/seo_query_optimizer.py +15 -13
  96. symai/extended/solver.py +116 -91
  97. symai/extended/summarizer.py +12 -10
  98. symai/extended/taypan_interpreter.py +17 -18
  99. symai/extended/vectordb.py +122 -92
  100. symai/formatter/__init__.py +9 -1
  101. symai/formatter/formatter.py +51 -47
  102. symai/formatter/regex.py +70 -69
  103. symai/functional.py +325 -176
  104. symai/imports.py +190 -147
  105. symai/interfaces.py +57 -28
  106. symai/memory.py +45 -35
  107. symai/menu/screen.py +28 -19
  108. symai/misc/console.py +66 -56
  109. symai/misc/loader.py +8 -5
  110. symai/models/__init__.py +17 -1
  111. symai/models/base.py +395 -236
  112. symai/models/errors.py +1 -2
  113. symai/ops/__init__.py +32 -22
  114. symai/ops/measures.py +24 -25
  115. symai/ops/primitives.py +1149 -731
  116. symai/post_processors.py +58 -50
  117. symai/pre_processors.py +86 -82
  118. symai/processor.py +21 -13
  119. symai/prompts.py +764 -685
  120. symai/server/huggingface_server.py +135 -49
  121. symai/server/llama_cpp_server.py +21 -11
  122. symai/server/qdrant_server.py +206 -0
  123. symai/shell.py +100 -42
  124. symai/shellsv.py +700 -492
  125. symai/strategy.py +630 -346
  126. symai/symbol.py +368 -322
  127. symai/utils.py +100 -78
  128. {symbolicai-0.21.0.dist-info → symbolicai-1.1.0.dist-info}/METADATA +22 -10
  129. symbolicai-1.1.0.dist-info/RECORD +168 -0
  130. symbolicai-0.21.0.dist-info/RECORD +0 -162
  131. {symbolicai-0.21.0.dist-info → symbolicai-1.1.0.dist-info}/WHEEL +0 -0
  132. {symbolicai-0.21.0.dist-info → symbolicai-1.1.0.dist-info}/entry_points.txt +0 -0
  133. {symbolicai-0.21.0.dist-info → symbolicai-1.1.0.dist-info}/licenses/LICENSE +0 -0
  134. {symbolicai-0.21.0.dist-info → symbolicai-1.1.0.dist-info}/top_level.txt +0 -0
@@ -1,5 +1,5 @@
1
+ import contextlib
1
2
  import logging
2
- import os
3
3
  from dataclasses import dataclass
4
4
  from pathlib import Path
5
5
 
@@ -7,21 +7,60 @@ import pypdf
7
7
  import tika
8
8
  from tika import unpack
9
9
 
10
+ from ....utils import UserMessage
10
11
  from ...base import Engine
11
12
 
12
13
  # Initialize Tika lazily to avoid spawning JVMs prematurely for all workers
13
- _TIKA_INITIALIZED = False
14
+ _TIKA_STATE = {"initialized": False}
15
+
14
16
 
15
17
  def _ensure_tika_vm():
16
- global _TIKA_INITIALIZED
17
- if not _TIKA_INITIALIZED:
18
- try:
18
+ if not _TIKA_STATE["initialized"]:
19
+ with contextlib.suppress(Exception):
19
20
  tika.initVM()
20
- except Exception:
21
- # If initVM fails, we still attempt unpack.from_file which may auto-init
22
- pass
23
- logging.getLogger('tika').setLevel(logging.CRITICAL)
24
- _TIKA_INITIALIZED = True
21
+ logging.getLogger("tika").setLevel(logging.CRITICAL)
22
+ _TIKA_STATE["initialized"] = True
23
+
24
+
25
+ def _int_or_none(value):
26
+ return int(value) if value != "" else None
27
+
28
+
29
+ def _parse_slice_token(token):
30
+ if ":" not in token:
31
+ return int(token)
32
+ parts = token.split(":")
33
+ if len(parts) == 2:
34
+ start, end = parts
35
+ return slice(_int_or_none(start), _int_or_none(end), None)
36
+ if len(parts) == 3:
37
+ start, end, step = parts
38
+ return slice(_int_or_none(start), _int_or_none(end), _int_or_none(step))
39
+ return None
40
+
41
+
42
+ def _parse_slice_spec(file_path):
43
+ if "[" not in file_path or "]" not in file_path:
44
+ return file_path, None
45
+ path_part, remainder = file_path.split("[", 1)
46
+ slice_section = remainder.split("]", 1)[0]
47
+ slices = []
48
+ for token in slice_section.split(","):
49
+ if token == "":
50
+ continue
51
+ parsed = _parse_slice_token(token)
52
+ if parsed is not None:
53
+ slices.append(parsed)
54
+ return path_part, slices or None
55
+
56
+
57
+ def _apply_slices(lines, slices_):
58
+ if slices_ is None:
59
+ return lines
60
+ new_content = []
61
+ for slice_item in slices_:
62
+ new_content.extend(lines[slice_item])
63
+ return new_content
25
64
 
26
65
 
27
66
  @dataclass
@@ -37,97 +76,67 @@ class FileEngine(Engine):
37
76
  self.name = self.__class__.__name__
38
77
 
39
78
  def id(self) -> str:
40
- return 'files'
79
+ return "files"
41
80
 
42
81
  def _read_slice_file(self, file_path, argument):
43
82
  # check if file is empty
44
- with_metadata = argument.kwargs.get('with_metadata', False)
45
- id = Path(argument.prop.prepared_input).stem.replace(' ', '_')
46
- if file_path is None or file_path.strip() == '':
83
+ with_metadata = argument.kwargs.get("with_metadata", False)
84
+ file_id = Path(argument.prop.prepared_input).stem.replace(" ", "_")
85
+ if file_path is None or file_path.strip() == "":
47
86
  return None
48
87
 
49
88
  # check if file slice is used
50
- slices_ = None
51
- if '[' in file_path and ']' in file_path:
52
- file_parts = file_path.split('[')
53
- file_path = file_parts[0]
54
- # remove string up to '[' and after ']'
55
- slices_s = file_parts[1].split(']')[0].split(',')
56
- slices_ = []
57
- for s in slices_s:
58
- if s == '':
59
- continue
60
- elif ':' in s:
61
- s_split = s.split(':')
62
- if len(s_split) == 2:
63
- start_slice = int(s_split[0]) if s_split[0] != '' else None
64
- end_slice = int(s_split[1]) if s_split[1] != '' else None
65
- slices_.append(slice(start_slice, end_slice, None))
66
- elif len(s_split) == 3:
67
- start_slice = int(s_split[0]) if s_split[0] != '' else None
68
- end_slice = int(s_split[1]) if s_split[1] != '' else None
69
- step_slice = int(s_split[2]) if s_split[2] != '' else None
70
- slices_.append(slice(start_slice, end_slice, step_slice))
71
- else:
72
- slices_.append(int(s))
89
+ file_path, slices_ = _parse_slice_spec(file_path)
90
+
91
+ path_obj = Path(file_path)
73
92
 
74
93
  # check if file exists
75
- assert os.path.exists(file_path), f'File does not exist: {file_path}'
94
+ assert path_obj.exists(), f"File does not exist: {file_path}"
76
95
 
77
96
  # verify if file is empty
78
- if os.path.getsize(file_path) <= 0:
79
- return ''
97
+ if path_obj.stat().st_size <= 0:
98
+ return ""
80
99
 
81
100
  # For common plain-text extensions, avoid Tika overhead
82
- ext = Path(file_path).suffix.lower()
83
- if ext in {'.txt', '.md', '.py', '.json', '.yaml', '.yml', '.csv', '.tsv', '.log'}:
101
+ ext = path_obj.suffix.lower()
102
+ if ext in {".txt", ".md", ".py", ".json", ".yaml", ".yml", ".csv", ".tsv", ".log"}:
84
103
  try:
85
- with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
104
+ with path_obj.open(encoding="utf-8", errors="ignore") as f:
86
105
  content = f.read()
87
106
  if content is None:
88
107
  return None
89
108
  # Apply slicing by lines, mirroring the Tika branch
90
- lines = content.split('\n')
91
- if slices_ is not None:
92
- new_content = []
93
- for s in slices_:
94
- new_content.extend(lines[s])
95
- lines = new_content
96
- content = '\n'.join(lines)
97
- content = content.encode('utf8', 'ignore').decode('utf8', 'ignore')
98
- return content if not with_metadata else [TextContainer(id, None, content)]
109
+ lines = content.split("\n")
110
+ lines = _apply_slices(lines, slices_)
111
+ content = "\n".join(lines)
112
+ content = content.encode("utf8", "ignore").decode("utf8", "ignore")
113
+ return content if not with_metadata else [TextContainer(file_id, None, content)]
99
114
  except Exception:
100
115
  # Fallback to Tika if plain read fails
101
116
  pass
102
117
 
103
118
  _ensure_tika_vm()
104
- file_ = unpack.from_file(str(file_path))
105
- if 'content' in file_:
106
- content = file_['content']
107
- else:
108
- content = str(file_)
119
+ file_ = unpack.from_file(str(path_obj))
120
+ content = file_["content"] if "content" in file_ else str(file_)
109
121
 
110
122
  if content is None:
111
123
  return None
112
- content = content.split('\n')
113
-
114
- if slices_ is not None:
115
- new_content = []
116
- for s in slices_:
117
- new_content.extend(content[s])
118
- content = new_content
119
- content = '\n'.join(content)
120
- content = content.encode('utf8', 'ignore').decode('utf8', 'ignore')
121
- return content if not with_metadata else [TextContainer(id, None, content)]
124
+ content = content.split("\n")
122
125
 
126
+ content = _apply_slices(content, slices_)
127
+ content = "\n".join(content)
128
+ content = content.encode("utf8", "ignore").decode("utf8", "ignore")
129
+ return content if not with_metadata else [TextContainer(file_id, None, content)]
123
130
 
124
131
  def reset_eof_of_pdf_return_stream(self, pdf_stream_in: list):
125
132
  actual_line = len(pdf_stream_in) # Predefined value in case EOF not found
126
133
  # find the line position of the EOF
127
134
  for i, x in enumerate(pdf_stream_in[::-1]):
128
- if b'%%EOF' in x:
129
- actual_line = len(pdf_stream_in)-i
130
- print(f'EOF found at line position {-i} = actual {actual_line}, with value {x}')
135
+ if b"%%EOF" in x:
136
+ actual_line = len(pdf_stream_in) - i
137
+ UserMessage(
138
+ f"EOF found at line position {-i} = actual {actual_line}, with value {x}"
139
+ )
131
140
  break
132
141
 
133
142
  # return the list up to that point
@@ -135,56 +144,56 @@ class FileEngine(Engine):
135
144
 
136
145
  def fix_pdf(self, file_path: str):
137
146
  # opens the file for reading
138
- with open(file_path, 'rb') as p:
139
- txt = (p.readlines())
147
+ path_obj = Path(file_path)
148
+ with path_obj.open("rb") as p:
149
+ txt = p.readlines()
140
150
 
141
151
  # get the new list terminating correctly
142
152
  txtx = self.reset_eof_of_pdf_return_stream(txt)
143
153
 
144
154
  # write to new pdf
145
- new_file_path = f'{file_path}_fixed.pdf'
146
- with open(new_file_path, 'wb') as f:
155
+ new_file_path = Path(f"{file_path}_fixed.pdf")
156
+ with new_file_path.open("wb") as f:
147
157
  f.writelines(txtx)
148
158
 
149
- fixed_pdf = pypdf.PdfReader(new_file_path)
150
- return fixed_pdf
159
+ return pypdf.PdfReader(str(new_file_path))
151
160
 
152
161
  def read_text(self, pdf_reader, page_range, argument):
153
162
  txt = []
154
- n_pages = len(pdf_reader.pages)
155
- with_metadata = argument.kwargs.get('with_metadata', False)
156
- id = Path(argument.prop.prepared_input).stem.replace(' ', '_')
163
+ n_pages = len(pdf_reader.pages)
164
+ with_metadata = argument.kwargs.get("with_metadata", False)
165
+ file_id = Path(argument.prop.prepared_input).stem.replace(" ", "_")
157
166
  for i in range(n_pages)[slice(0, n_pages) if page_range is None else page_range]:
158
167
  page = pdf_reader.pages[i]
159
168
  extracted = page.extract_text()
160
- extracted = extracted.encode('utf8', 'ignore').decode('utf8', 'ignore')
169
+ extracted = extracted.encode("utf8", "ignore").decode("utf8", "ignore")
161
170
  if with_metadata:
162
- txt.append(TextContainer(id, str(i), extracted))
171
+ txt.append(TextContainer(file_id, str(i), extracted))
163
172
  else:
164
173
  txt.append(extracted)
165
174
 
166
- return '\n'.join(txt) if not with_metadata else txt
175
+ return "\n".join(txt) if not with_metadata else txt
167
176
 
168
177
  def forward(self, argument):
169
- kwargs = argument.kwargs
170
- path = argument.prop.prepared_input
178
+ kwargs = argument.kwargs
179
+ path = argument.prop.prepared_input
171
180
 
172
- if '.pdf' in path:
181
+ if ".pdf" in path:
173
182
  page_range = None
174
- if 'slice' in kwargs:
175
- page_range = kwargs['slice']
176
- if isinstance(page_range, tuple) or isinstance(page_range, list):
183
+ if "slice" in kwargs:
184
+ page_range = kwargs["slice"]
185
+ if isinstance(page_range, (tuple, list)):
177
186
  page_range = slice(*page_range)
178
187
 
179
- rsp = ''
188
+ rsp = ""
180
189
  try:
181
- with open(str(path), 'rb') as f:
190
+ with Path(path).open("rb") as f:
182
191
  # creating a pdf reader object
183
192
  pdf_reader = pypdf.PdfReader(f)
184
193
  rsp = self.read_text(pdf_reader, page_range, argument)
185
194
  except Exception as e:
186
- print(f'Error reading PDF: {e} | {path}')
187
- if 'fix_pdf' not in kwargs or not kwargs['fix_pdf']:
195
+ UserMessage(f"Error reading PDF: {e} | {path}")
196
+ if "fix_pdf" not in kwargs or not kwargs["fix_pdf"]:
188
197
  raise e
189
198
  fixed_pdf = self.fix_pdf(str(path))
190
199
  pdf_reader_fixed = pypdf.PdfReader(fixed_pdf)
@@ -193,11 +202,11 @@ class FileEngine(Engine):
193
202
  try:
194
203
  rsp = self._read_slice_file(path, argument)
195
204
  except Exception as e:
196
- print(f'Error reading empty file: {e} | {path}')
205
+ UserMessage(f"Error reading empty file: {e} | {path}")
197
206
  raise e
198
207
 
199
208
  if rsp is None:
200
- raise Exception(f'Error reading file - empty result: {path}')
209
+ UserMessage(f"Error reading file - empty result: {path}", raise_with=Exception)
201
210
 
202
211
  metadata = {}
203
212
 
@@ -206,5 +215,5 @@ class FileEngine(Engine):
206
215
  def prepare(self, argument):
207
216
  assert not argument.prop.processed_input, "FileEngine does not support processed_input."
208
217
  path = argument.prop.path
209
- path = path.replace('\\', '')
218
+ path = path.replace("\\", "")
210
219
  argument.prop.prepared_input = path
@@ -1,5 +1,3 @@
1
- from typing import List
2
-
3
1
  import requests
4
2
  import torch
5
3
 
@@ -10,6 +8,7 @@ except ImportError:
10
8
 
11
9
  from PIL import Image
12
10
 
11
+ from ....utils import UserMessage
13
12
  from ...base import Engine
14
13
  from ...settings import SYMAI_CONFIG
15
14
 
@@ -18,7 +17,7 @@ class Blip2Engine(Engine):
18
17
  def __init__(self):
19
18
  super().__init__()
20
19
  self.config = SYMAI_CONFIG
21
- ids = self.config['CAPTION_ENGINE_MODEL'].split('/')
20
+ ids = self.config["CAPTION_ENGINE_MODEL"].split("/")
22
21
  if len(ids) != 2:
23
22
  # return unregistered engine
24
23
  return
@@ -27,42 +26,47 @@ class Blip2Engine(Engine):
27
26
  self.model = None # lazy loading
28
27
  self.vis_processors = None # lazy loading
29
28
  self.txt_processors = None # lazy loading
30
- self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
29
+ self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
31
30
  self.name = self.__class__.__name__
32
31
 
33
32
  def id(self) -> str:
34
- if self.config['CAPTION_ENGINE_MODEL'] and \
35
- 'blip2' in self.config['CAPTION_ENGINE_MODEL']:
36
- return 'imagecaptioning'
37
- return super().id() # default to unregistered
33
+ if self.config["CAPTION_ENGINE_MODEL"] and "blip2" in self.config["CAPTION_ENGINE_MODEL"]:
34
+ return "imagecaptioning"
35
+ return super().id() # default to unregistered
38
36
 
39
37
  def command(self, *args, **kwargs):
40
38
  super().command(*args, **kwargs)
41
- if 'CAPTION_ENGINE_MODEL' in kwargs:
42
- self.model_id = kwargs['CAPTION_ENGINE_MODEL']
39
+ if "CAPTION_ENGINE_MODEL" in kwargs:
40
+ self.model_id = kwargs["CAPTION_ENGINE_MODEL"]
43
41
 
44
42
  def forward(self, argument):
45
43
  if load_model_and_preprocess is None:
46
- raise ImportError('Blip2 is not installed. Please install it with `pip install symbolicai[blip2]`')
44
+ UserMessage(
45
+ "Blip2 is not installed. Please install it with `pip install symbolicai[blip2]`",
46
+ raise_with=ImportError,
47
+ )
47
48
  if self.model is None:
48
- self.model, self.vis_processors, self.txt_processors = load_model_and_preprocess(name = self.name_id,
49
- model_type = self.model_id,
50
- is_eval = True,
51
- device = self.device)
49
+ self.model, self.vis_processors, self.txt_processors = load_model_and_preprocess(
50
+ name=self.name_id, model_type=self.model_id, is_eval=True, device=self.device
51
+ )
52
52
 
53
53
  image, prompt = argument.prop.prepared_input
54
- kwargs = argument.kwargs
55
- except_remedy = kwargs['except_remedy'] if 'except_remedy' in kwargs else None
54
+ kwargs = argument.kwargs
55
+ except_remedy = kwargs.get("except_remedy")
56
56
 
57
- if 'http' in image:
58
- image = Image.open(requests.get(image, stream=True).raw).convert('RGB')
59
- elif '/' in image or '\\' in image:
60
- image = Image.open(image).convert('RGB')
57
+ if "http" in image:
58
+ image = Image.open(requests.get(image, stream=True).raw).convert("RGB")
59
+ elif "/" in image or "\\" in image:
60
+ image = Image.open(image).convert("RGB")
61
61
 
62
62
  try:
63
- image = self.vis_processors['eval'](image).unsqueeze(0).to(self.device)
64
- prompt = self.txt_processors['eval'](prompt)
65
- res = self.model.generate(samples={"image": image, "prompt": prompt}, use_nucleus_sampling=True, num_captions=3)
63
+ image = self.vis_processors["eval"](image).unsqueeze(0).to(self.device)
64
+ prompt = self.txt_processors["eval"](prompt)
65
+ res = self.model.generate(
66
+ samples={"image": image, "prompt": prompt},
67
+ use_nucleus_sampling=True,
68
+ num_captions=3,
69
+ )
66
70
  except Exception as e:
67
71
  if except_remedy is None:
68
72
  raise e
@@ -1,48 +1,48 @@
1
- import logging
2
- import requests
3
- import json
4
1
  import io
2
+ import json
3
+ import logging
4
+ from pathlib import Path
5
5
 
6
- from typing import List
7
- from requests_toolbelt.multipart.encoder import MultipartEncoder
6
+ import requests
8
7
  from PIL.Image import Image
8
+ from requests_toolbelt.multipart.encoder import MultipartEncoder
9
9
 
10
+ from ....symbol import Result
11
+ from ....utils import UserMessage
10
12
  from ...base import Engine
11
13
  from ...settings import SYMAI_CONFIG
12
- from ....symbol import Result
13
14
 
14
15
 
15
- def image_to_byte_array(image: Image, format='PNG') -> bytes:
16
- # BytesIO is a file-like buffer stored in memory
17
- imgByteArr = io.BytesIO()
18
- # image.save expects a file-like as a argument
19
- image.save(imgByteArr, format=format)
20
- # Turn the BytesIO object back into a bytes object
21
- imgByteArr = imgByteArr.getvalue()
22
- return imgByteArr
16
+ def image_to_byte_array(image: Image, format="PNG") -> bytes:
17
+ # BytesIO is a file-like buffer stored in memory
18
+ imgByteArr = io.BytesIO()
19
+ # image.save expects a file-like as a argument
20
+ image.save(imgByteArr, format=format)
21
+ # Turn the BytesIO object back into a bytes object
22
+ return imgByteArr.getvalue()
23
23
 
24
24
 
25
25
  class LLaMAResult(Result):
26
26
  def __init__(self, value=None, *args, **kwargs):
27
27
  super().__init__(value, *args, **kwargs)
28
28
  self._value = value
29
- self.error = None
30
- self.raw = value
29
+ self.error = None
30
+ self.raw = value
31
31
  self._perse_result()
32
32
 
33
33
  def _perse_result(self):
34
- val = json.loads(self.value)
34
+ val = json.loads(self.value)
35
35
  self.value = val
36
- if 'error' in val:
37
- self.error = val['error']
38
- if 'content' in val:
39
- self.value = val['content']
36
+ if "error" in val:
37
+ self.error = val["error"]
38
+ if "content" in val:
39
+ self.value = val["content"]
40
40
 
41
41
 
42
42
  class LLaMACppClientEngine(Engine):
43
- def __init__(self, host: str = 'localhost', port: int = 8080, timeout: int = 240):
43
+ def __init__(self, host: str = "localhost", port: int = 8080, timeout: int = 240):
44
44
  super().__init__()
45
- logger = logging.getLogger('nesy_client')
45
+ logger = logging.getLogger("nesy_client")
46
46
  logger.setLevel(logging.WARNING)
47
47
  self.config = SYMAI_CONFIG
48
48
  self.host = host
@@ -51,43 +51,45 @@ class LLaMACppClientEngine(Engine):
51
51
  self.name = self.__class__.__name__
52
52
 
53
53
  def id(self) -> str:
54
- if self.config['CAPTION_ENGINE_MODEL'] and \
55
- 'llavacpp' in self.config['CAPTION_ENGINE_MODEL']:
56
- return 'imagecaptioning'
57
- return super().id() # default to unregistered
54
+ if (
55
+ self.config["CAPTION_ENGINE_MODEL"]
56
+ and "llavacpp" in self.config["CAPTION_ENGINE_MODEL"]
57
+ ):
58
+ return "imagecaptioning"
59
+ return super().id() # default to unregistered
58
60
 
59
61
  @property
60
62
  def max_tokens(self):
61
63
  return 4096
62
64
 
63
65
  def forward(self, argument):
64
- prompts = argument.prop.prepared_input
65
- kwargs = argument.kwargs
66
+ prompts = argument.prop.prepared_input
67
+ kwargs = argument.kwargs
66
68
  system, user, image = prompts
67
69
  # escape special characters
68
- system = system['content']
69
- user = user['content']
70
+ system = system["content"]
71
+ user = user["content"]
70
72
 
71
- if isinstance(image['content'], Image):
73
+ if isinstance(image["content"], Image):
72
74
  # format image to bytes
73
- format_ = argument.prop.image_format if argument.prop.image_format else 'PNG'
74
- im_bytes = image_to_byte_array(image['content'], format=format_)
75
+ format_ = argument.prop.image_format if argument.prop.image_format else "PNG"
76
+ im_bytes = image_to_byte_array(image["content"], format=format_)
75
77
  else:
76
78
  # Convert image to bytes, open as binary
77
- with open(image['content'], 'rb') as f:
79
+ with Path(image["content"]).open("rb") as f:
78
80
  im_bytes = f.read()
79
81
  # Create multipart/form-data payload
80
- payload = MultipartEncoder(
82
+ payload = MultipartEncoder(
81
83
  fields={
82
- 'user_prompt': ('user_prompt', user, 'text/plain'),
83
- 'image_file': ('image_file', im_bytes, 'application/octet-stream'),
84
- 'system_prompt': ('system_prompt', system, 'text/plain')
84
+ "user_prompt": ("user_prompt", user, "text/plain"),
85
+ "image_file": ("image_file", im_bytes, "application/octet-stream"),
86
+ "system_prompt": ("system_prompt", system, "text/plain"),
85
87
  }
86
88
  )
87
89
  # Update the headers for multipart/form-data
88
- headers = {'Content-Type': payload.content_type}
89
- api = f'http://{self.host}:{self.port}/llava'
90
- except_remedy = kwargs['except_remedy'] if 'except_remedy' in kwargs else None
90
+ headers = {"Content-Type": payload.content_type}
91
+ api = f"http://{self.host}:{self.port}/llava"
92
+ except_remedy = kwargs.get("except_remedy")
91
93
  try:
92
94
  # use http localhost 8000 to send a request to the server
93
95
  rsp = requests.post(api, data=payload, headers=headers, timeout=self.timeout)
@@ -95,27 +97,31 @@ class LLaMACppClientEngine(Engine):
95
97
  except Exception as e:
96
98
  if except_remedy is None:
97
99
  raise e
98
- callback = lambda: requests.post(api, data=payload, headers=headers, timeout=self.timeout)
100
+
101
+ def callback():
102
+ return requests.post(api, data=payload, headers=headers, timeout=self.timeout)
103
+
99
104
  res = except_remedy(self, e, callback, argument)
100
105
 
101
106
  metadata = {}
102
107
 
103
- res = LLaMAResult(res)
104
- rsp = [res]
108
+ res = LLaMAResult(res)
109
+ rsp = [res]
105
110
  output = rsp if isinstance(prompts, list) else rsp[0]
106
111
  return output, metadata
107
112
 
108
- def prepare(self, argument):
109
- if argument.prop.raw_input:
110
- if not argument.prop.processed_input:
111
- raise ValueError('Need to provide a prompt instruction to the engine if raw_input is enabled.')
112
- argument.prop.prepared_input = argument.prop.processed_input
113
- return
114
-
115
- user: str = ""
116
- system: str = ""
117
- system = f'{system}\n' if system and len(system) > 0 else ''
118
-
113
+ def _handle_raw_input(self, argument) -> bool:
114
+ if not argument.prop.raw_input:
115
+ return False
116
+ if not argument.prop.processed_input:
117
+ UserMessage(
118
+ "Need to provide a prompt instruction to the engine if raw_input is enabled.",
119
+ raise_with=ValueError,
120
+ )
121
+ argument.prop.prepared_input = argument.prop.processed_input
122
+ return True
123
+
124
+ def _append_context_sections(self, system: str, argument) -> str:
119
125
  ref = argument.prop.instance
120
126
  static_ctxt, dyn_ctxt = ref.global_context
121
127
  if len(static_ctxt) > 0:
@@ -126,40 +132,57 @@ class LLaMACppClientEngine(Engine):
126
132
 
127
133
  payload = argument.prop.payload
128
134
  if argument.prop.payload:
129
- system += f"[ADDITIONAL CONTEXT]\n{str(payload)}\n\n"
135
+ system += f"[ADDITIONAL CONTEXT]\n{payload!s}\n\n"
130
136
 
131
- examples: List[str] = argument.prop.examples
137
+ examples: list[str] = argument.prop.examples
132
138
  if examples and len(examples) > 0:
133
- system += f"[EXAMPLES]\n{str(examples)}\n\n"
139
+ system += f"[EXAMPLES]\n{examples!s}\n\n"
134
140
 
141
+ return system
142
+
143
+ def _build_user_instruction(self, argument) -> str:
144
+ user = ""
135
145
  if argument.prop.prompt is not None and len(argument.prop.prompt) > 0:
136
146
  val = str(argument.prop.prompt)
137
- # in this engine, instructions are considered as user prompts
138
147
  user += f"[INSTRUCTION]\n{val}"
139
-
140
- suffix: str = str(argument.prop.processed_input)
141
-
142
- if '[SYSTEM_INSTRUCTION::]: <<<' in suffix and argument.prop.parse_system_instructions:
143
- parts = suffix.split('\n>>>\n')
144
- # first parts are the system instructions
145
- c = 0
146
- for i, p in enumerate(parts):
147
- if 'SYSTEM_INSTRUCTION' in p:
148
- system += f"{p}\n"
149
- c += 1
148
+ return user
149
+
150
+ def _extract_system_instructions(self, argument, system: str, suffix: str) -> tuple[str, str]:
151
+ if "[SYSTEM_INSTRUCTION::]: <<<" in suffix and argument.prop.parse_system_instructions:
152
+ parts = suffix.split("\n>>>\n")
153
+ consumed = 0
154
+ for part in parts:
155
+ if "SYSTEM_INSTRUCTION" in part:
156
+ system += f"{part}\n"
157
+ consumed += 1
150
158
  else:
151
159
  break
152
- # last part is the user input
153
- suffix = '\n>>>\n'.join(parts[c:])
154
- user += f"{suffix}"
160
+ suffix = "\n>>>\n".join(parts[consumed:])
161
+ return system, suffix
155
162
 
163
+ def _append_template_suffix(self, user: str, argument) -> str:
156
164
  if argument.prop.template_suffix:
157
- user += f"\n[[PLACEHOLDER]]\n{str(argument.prop.template_suffix)}\n\n"
158
- user += f"Only generate content for the placeholder `[[PLACEHOLDER]]` following the instructions and context information. Do NOT write `[[PLACEHOLDER]]` or anything else in your output.\n\n"
165
+ user += f"\n[[PLACEHOLDER]]\n{argument.prop.template_suffix!s}\n\n"
166
+ user += "Only generate content for the placeholder `[[PLACEHOLDER]]` following the instructions and context information. Do NOT write `[[PLACEHOLDER]]` or anything else in your output.\n\n"
167
+ return user
168
+
169
+ def prepare(self, argument):
170
+ if self._handle_raw_input(argument):
171
+ return
172
+
173
+ system: str = ""
174
+ system = f"{system}\n" if system and len(system) > 0 else ""
175
+ system = self._append_context_sections(system, argument)
176
+
177
+ user = self._build_user_instruction(argument)
178
+ suffix: str = str(argument.prop.processed_input)
179
+ system, suffix = self._extract_system_instructions(argument, system, suffix)
180
+ user += f"{suffix}"
181
+ user = self._append_template_suffix(user, argument)
159
182
 
160
- user_prompt = { "role": "user", "content": user }
183
+ user_prompt = {"role": "user", "content": user}
161
184
  argument.prop.prepared_input = [
162
- { "role": "system", "content": system },
185
+ {"role": "system", "content": system},
163
186
  user_prompt,
164
- { "role": "image", "content": argument.prop.image }
187
+ {"role": "image", "content": argument.prop.image},
165
188
  ]