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.
- symai/__init__.py +269 -173
- symai/backend/base.py +123 -110
- symai/backend/engines/drawing/engine_bfl.py +45 -44
- symai/backend/engines/drawing/engine_gpt_image.py +112 -97
- symai/backend/engines/embedding/engine_llama_cpp.py +63 -52
- symai/backend/engines/embedding/engine_openai.py +25 -21
- symai/backend/engines/execute/engine_python.py +19 -18
- symai/backend/engines/files/engine_io.py +104 -95
- symai/backend/engines/imagecaptioning/engine_blip2.py +28 -24
- symai/backend/engines/imagecaptioning/engine_llavacpp_client.py +102 -79
- symai/backend/engines/index/engine_pinecone.py +124 -97
- symai/backend/engines/index/engine_qdrant.py +1011 -0
- symai/backend/engines/index/engine_vectordb.py +84 -56
- symai/backend/engines/lean/engine_lean4.py +96 -52
- symai/backend/engines/neurosymbolic/__init__.py +41 -13
- symai/backend/engines/neurosymbolic/engine_anthropic_claudeX_chat.py +330 -248
- symai/backend/engines/neurosymbolic/engine_anthropic_claudeX_reasoning.py +329 -264
- symai/backend/engines/neurosymbolic/engine_cerebras.py +328 -0
- symai/backend/engines/neurosymbolic/engine_deepseekX_reasoning.py +118 -88
- symai/backend/engines/neurosymbolic/engine_google_geminiX_reasoning.py +344 -299
- symai/backend/engines/neurosymbolic/engine_groq.py +173 -115
- symai/backend/engines/neurosymbolic/engine_huggingface.py +114 -84
- symai/backend/engines/neurosymbolic/engine_llama_cpp.py +144 -118
- symai/backend/engines/neurosymbolic/engine_openai_gptX_chat.py +415 -307
- symai/backend/engines/neurosymbolic/engine_openai_gptX_reasoning.py +394 -231
- symai/backend/engines/ocr/engine_apilayer.py +23 -27
- symai/backend/engines/output/engine_stdout.py +10 -13
- symai/backend/engines/{webscraping → scrape}/engine_requests.py +101 -54
- symai/backend/engines/search/engine_openai.py +100 -88
- symai/backend/engines/search/engine_parallel.py +665 -0
- symai/backend/engines/search/engine_perplexity.py +44 -45
- symai/backend/engines/search/engine_serpapi.py +37 -34
- symai/backend/engines/speech_to_text/engine_local_whisper.py +54 -51
- symai/backend/engines/symbolic/engine_wolframalpha.py +15 -9
- symai/backend/engines/text_to_speech/engine_openai.py +20 -26
- symai/backend/engines/text_vision/engine_clip.py +39 -37
- symai/backend/engines/userinput/engine_console.py +5 -6
- symai/backend/mixin/__init__.py +13 -0
- symai/backend/mixin/anthropic.py +48 -38
- symai/backend/mixin/deepseek.py +6 -5
- symai/backend/mixin/google.py +7 -4
- symai/backend/mixin/groq.py +2 -4
- symai/backend/mixin/openai.py +140 -110
- symai/backend/settings.py +87 -20
- symai/chat.py +216 -123
- symai/collect/__init__.py +7 -1
- symai/collect/dynamic.py +80 -70
- symai/collect/pipeline.py +67 -51
- symai/collect/stats.py +161 -109
- symai/components.py +707 -360
- symai/constraints.py +24 -12
- symai/core.py +1857 -1233
- symai/core_ext.py +83 -80
- symai/endpoints/api.py +166 -104
- symai/extended/.DS_Store +0 -0
- symai/extended/__init__.py +46 -12
- symai/extended/api_builder.py +29 -21
- symai/extended/arxiv_pdf_parser.py +23 -14
- symai/extended/bibtex_parser.py +9 -6
- symai/extended/conversation.py +156 -126
- symai/extended/document.py +50 -30
- symai/extended/file_merger.py +57 -14
- symai/extended/graph.py +51 -32
- symai/extended/html_style_template.py +18 -14
- symai/extended/interfaces/blip_2.py +2 -3
- symai/extended/interfaces/clip.py +4 -3
- symai/extended/interfaces/console.py +9 -1
- symai/extended/interfaces/dall_e.py +4 -2
- symai/extended/interfaces/file.py +2 -0
- symai/extended/interfaces/flux.py +4 -2
- symai/extended/interfaces/gpt_image.py +16 -7
- symai/extended/interfaces/input.py +2 -1
- symai/extended/interfaces/llava.py +1 -2
- symai/extended/interfaces/{naive_webscraping.py → naive_scrape.py} +4 -3
- symai/extended/interfaces/naive_vectordb.py +9 -10
- symai/extended/interfaces/ocr.py +5 -3
- symai/extended/interfaces/openai_search.py +2 -0
- symai/extended/interfaces/parallel.py +30 -0
- symai/extended/interfaces/perplexity.py +2 -0
- symai/extended/interfaces/pinecone.py +12 -9
- symai/extended/interfaces/python.py +2 -0
- symai/extended/interfaces/serpapi.py +3 -1
- symai/extended/interfaces/terminal.py +2 -4
- symai/extended/interfaces/tts.py +3 -2
- symai/extended/interfaces/whisper.py +3 -2
- symai/extended/interfaces/wolframalpha.py +2 -1
- symai/extended/metrics/__init__.py +11 -1
- symai/extended/metrics/similarity.py +14 -13
- symai/extended/os_command.py +39 -29
- symai/extended/packages/__init__.py +29 -3
- symai/extended/packages/symdev.py +51 -43
- symai/extended/packages/sympkg.py +41 -35
- symai/extended/packages/symrun.py +63 -50
- symai/extended/repo_cloner.py +14 -12
- symai/extended/seo_query_optimizer.py +15 -13
- symai/extended/solver.py +116 -91
- symai/extended/summarizer.py +12 -10
- symai/extended/taypan_interpreter.py +17 -18
- symai/extended/vectordb.py +122 -92
- symai/formatter/__init__.py +9 -1
- symai/formatter/formatter.py +51 -47
- symai/formatter/regex.py +70 -69
- symai/functional.py +325 -176
- symai/imports.py +190 -147
- symai/interfaces.py +57 -28
- symai/memory.py +45 -35
- symai/menu/screen.py +28 -19
- symai/misc/console.py +66 -56
- symai/misc/loader.py +8 -5
- symai/models/__init__.py +17 -1
- symai/models/base.py +395 -236
- symai/models/errors.py +1 -2
- symai/ops/__init__.py +32 -22
- symai/ops/measures.py +24 -25
- symai/ops/primitives.py +1149 -731
- symai/post_processors.py +58 -50
- symai/pre_processors.py +86 -82
- symai/processor.py +21 -13
- symai/prompts.py +764 -685
- symai/server/huggingface_server.py +135 -49
- symai/server/llama_cpp_server.py +21 -11
- symai/server/qdrant_server.py +206 -0
- symai/shell.py +100 -42
- symai/shellsv.py +700 -492
- symai/strategy.py +630 -346
- symai/symbol.py +368 -322
- symai/utils.py +100 -78
- {symbolicai-0.21.0.dist-info → symbolicai-1.1.0.dist-info}/METADATA +22 -10
- symbolicai-1.1.0.dist-info/RECORD +168 -0
- symbolicai-0.21.0.dist-info/RECORD +0 -162
- {symbolicai-0.21.0.dist-info → symbolicai-1.1.0.dist-info}/WHEEL +0 -0
- {symbolicai-0.21.0.dist-info → symbolicai-1.1.0.dist-info}/entry_points.txt +0 -0
- {symbolicai-0.21.0.dist-info → symbolicai-1.1.0.dist-info}/licenses/LICENSE +0 -0
- {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
|
-
|
|
14
|
+
_TIKA_STATE = {"initialized": False}
|
|
15
|
+
|
|
14
16
|
|
|
15
17
|
def _ensure_tika_vm():
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
try:
|
|
18
|
+
if not _TIKA_STATE["initialized"]:
|
|
19
|
+
with contextlib.suppress(Exception):
|
|
19
20
|
tika.initVM()
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
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(
|
|
45
|
-
|
|
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_ =
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
94
|
+
assert path_obj.exists(), f"File does not exist: {file_path}"
|
|
76
95
|
|
|
77
96
|
# verify if file is empty
|
|
78
|
-
if
|
|
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 =
|
|
83
|
-
if ext in {
|
|
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(
|
|
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(
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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(
|
|
105
|
-
if
|
|
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(
|
|
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
|
|
129
|
-
actual_line = len(pdf_stream_in)-i
|
|
130
|
-
|
|
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
|
-
|
|
139
|
-
|
|
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
|
|
146
|
-
with open(
|
|
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
|
-
|
|
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
|
|
155
|
-
with_metadata = argument.kwargs.get(
|
|
156
|
-
|
|
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(
|
|
169
|
+
extracted = extracted.encode("utf8", "ignore").decode("utf8", "ignore")
|
|
161
170
|
if with_metadata:
|
|
162
|
-
txt.append(TextContainer(
|
|
171
|
+
txt.append(TextContainer(file_id, str(i), extracted))
|
|
163
172
|
else:
|
|
164
173
|
txt.append(extracted)
|
|
165
174
|
|
|
166
|
-
return
|
|
175
|
+
return "\n".join(txt) if not with_metadata else txt
|
|
167
176
|
|
|
168
177
|
def forward(self, argument):
|
|
169
|
-
kwargs
|
|
170
|
-
path
|
|
178
|
+
kwargs = argument.kwargs
|
|
179
|
+
path = argument.prop.prepared_input
|
|
171
180
|
|
|
172
|
-
if
|
|
181
|
+
if ".pdf" in path:
|
|
173
182
|
page_range = None
|
|
174
|
-
if
|
|
175
|
-
page_range = kwargs[
|
|
176
|
-
if isinstance(page_range, tuple
|
|
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
|
|
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
|
-
|
|
187
|
-
if
|
|
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
|
-
|
|
205
|
+
UserMessage(f"Error reading empty file: {e} | {path}")
|
|
197
206
|
raise e
|
|
198
207
|
|
|
199
208
|
if rsp is None:
|
|
200
|
-
|
|
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[
|
|
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(
|
|
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
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|
42
|
-
self.model_id = kwargs[
|
|
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
|
-
|
|
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
|
|
49
|
-
|
|
50
|
-
|
|
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
|
|
55
|
-
except_remedy = kwargs
|
|
54
|
+
kwargs = argument.kwargs
|
|
55
|
+
except_remedy = kwargs.get("except_remedy")
|
|
56
56
|
|
|
57
|
-
if
|
|
58
|
-
image = Image.open(requests.get(image, stream=True).raw).convert(
|
|
59
|
-
elif
|
|
60
|
-
image = Image.open(image).convert(
|
|
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
|
|
64
|
-
prompt
|
|
65
|
-
res
|
|
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
|
-
|
|
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=
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
30
|
-
self.raw
|
|
29
|
+
self.error = None
|
|
30
|
+
self.raw = value
|
|
31
31
|
self._perse_result()
|
|
32
32
|
|
|
33
33
|
def _perse_result(self):
|
|
34
|
-
val
|
|
34
|
+
val = json.loads(self.value)
|
|
35
35
|
self.value = val
|
|
36
|
-
if
|
|
37
|
-
self.error = val[
|
|
38
|
-
if
|
|
39
|
-
self.value = val[
|
|
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 =
|
|
43
|
+
def __init__(self, host: str = "localhost", port: int = 8080, timeout: int = 240):
|
|
44
44
|
super().__init__()
|
|
45
|
-
logger = logging.getLogger(
|
|
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
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
|
65
|
-
kwargs
|
|
66
|
+
prompts = argument.prop.prepared_input
|
|
67
|
+
kwargs = argument.kwargs
|
|
66
68
|
system, user, image = prompts
|
|
67
69
|
# escape special characters
|
|
68
|
-
system
|
|
69
|
-
user
|
|
70
|
+
system = system["content"]
|
|
71
|
+
user = user["content"]
|
|
70
72
|
|
|
71
|
-
if isinstance(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
|
|
74
|
-
im_bytes = image_to_byte_array(image[
|
|
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
|
|
79
|
+
with Path(image["content"]).open("rb") as f:
|
|
78
80
|
im_bytes = f.read()
|
|
79
81
|
# Create multipart/form-data payload
|
|
80
|
-
payload
|
|
82
|
+
payload = MultipartEncoder(
|
|
81
83
|
fields={
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
|
89
|
-
api
|
|
90
|
-
except_remedy = kwargs
|
|
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
|
-
|
|
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
|
|
104
|
-
rsp
|
|
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
|
|
109
|
-
if argument.prop.raw_input:
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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{
|
|
135
|
+
system += f"[ADDITIONAL CONTEXT]\n{payload!s}\n\n"
|
|
130
136
|
|
|
131
|
-
examples:
|
|
137
|
+
examples: list[str] = argument.prop.examples
|
|
132
138
|
if examples and len(examples) > 0:
|
|
133
|
-
system += f"[EXAMPLES]\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
|
-
|
|
141
|
-
|
|
142
|
-
if
|
|
143
|
-
parts = suffix.split(
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
153
|
-
|
|
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{
|
|
158
|
-
user +=
|
|
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 = {
|
|
183
|
+
user_prompt = {"role": "user", "content": user}
|
|
161
184
|
argument.prop.prepared_input = [
|
|
162
|
-
{
|
|
185
|
+
{"role": "system", "content": system},
|
|
163
186
|
user_prompt,
|
|
164
|
-
{
|
|
187
|
+
{"role": "image", "content": argument.prop.image},
|
|
165
188
|
]
|