nebu 0.1.30__py3-none-any.whl → 0.1.32__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.
- nebu/chatx/convert.py +103 -28
- nebu/processors/consumer.py +1 -0
- {nebu-0.1.30.dist-info → nebu-0.1.32.dist-info}/METADATA +1 -1
- {nebu-0.1.30.dist-info → nebu-0.1.32.dist-info}/RECORD +7 -7
- {nebu-0.1.30.dist-info → nebu-0.1.32.dist-info}/WHEEL +0 -0
- {nebu-0.1.30.dist-info → nebu-0.1.32.dist-info}/licenses/LICENSE +0 -0
- {nebu-0.1.30.dist-info → nebu-0.1.32.dist-info}/top_level.txt +0 -0
nebu/chatx/convert.py
CHANGED
@@ -7,9 +7,14 @@ from typing import Any, Dict, List, Tuple
|
|
7
7
|
import requests
|
8
8
|
from PIL import Image, UnidentifiedImageError
|
9
9
|
|
10
|
+
# Define a standard User-Agent header
|
11
|
+
REQUESTS_HEADERS = {
|
12
|
+
"User-Agent": "Nebulous-py/0.1 (https://github.com/agentsea/nebulous-py; contact@agentsea.ai)"
|
13
|
+
}
|
14
|
+
|
10
15
|
|
11
16
|
def convert_to_unsloth_inference(
|
12
|
-
old_schema: Dict[str, Any],
|
17
|
+
old_schema: List[Dict[str, Any]],
|
13
18
|
) -> Tuple[List[Dict[str, Any]], List[Image.Image]]:
|
14
19
|
"""
|
15
20
|
Convert from an old OpenAI message format that may look like:
|
@@ -63,7 +68,7 @@ def convert_to_unsloth_inference(
|
|
63
68
|
# Convert each URL into a PIL image
|
64
69
|
for url in image_urls:
|
65
70
|
# Download the image
|
66
|
-
response = requests.get(url)
|
71
|
+
response = requests.get(url, headers=REQUESTS_HEADERS)
|
67
72
|
response.raise_for_status()
|
68
73
|
image_data = BytesIO(response.content)
|
69
74
|
pil_img = Image.open(image_data).convert("RGB")
|
@@ -74,11 +79,16 @@ def convert_to_unsloth_inference(
|
|
74
79
|
# regardless of how many images were found, and merges all text into one block.
|
75
80
|
new_content = []
|
76
81
|
if image_urls:
|
77
|
-
|
82
|
+
# Add image placeholders for each image found
|
83
|
+
for _ in image_urls:
|
84
|
+
new_content.append({"type": "image"})
|
78
85
|
if merged_text:
|
79
86
|
new_content.append({"type": "text", "text": merged_text})
|
80
87
|
|
81
|
-
|
88
|
+
# Check if there's any content to add
|
89
|
+
if new_content:
|
90
|
+
new_schema.append({"role": role, "content": new_content})
|
91
|
+
# else: Optionally handle cases where a message might become empty
|
82
92
|
|
83
93
|
return new_schema, all_images
|
84
94
|
|
@@ -93,19 +103,22 @@ def oai_to_unsloth(
|
|
93
103
|
(typical in JSON Lines format) to the Nebulous conversation format.
|
94
104
|
Images specified by URLs or base64 strings are loaded into PIL.Image objects.
|
95
105
|
|
96
|
-
Input format example (as dict from JSON line):
|
97
106
|
{
|
98
107
|
"messages": [
|
99
108
|
{
|
100
109
|
"role": "user",
|
101
110
|
"content": [
|
102
|
-
{
|
103
|
-
|
111
|
+
{
|
112
|
+
"type": "text",
|
113
|
+
"text": "Who is this an image of?"
|
114
|
+
},
|
115
|
+
{
|
116
|
+
"type": "image_url",
|
117
|
+
"image_url": {
|
118
|
+
"url": "https://upload.wikimedia.org/wikipedia/commons/5/57/Abraham_Lincoln_1863_Portrait_%283x4_cropped%29.jpg"
|
119
|
+
}
|
120
|
+
}
|
104
121
|
]
|
105
|
-
},
|
106
|
-
{
|
107
|
-
"role": "assistant",
|
108
|
-
"content": [{"type": "text", "text": "This is an image of..."}] # Or potentially just a string
|
109
122
|
}
|
110
123
|
]
|
111
124
|
}
|
@@ -146,6 +159,7 @@ def oai_to_unsloth(
|
|
146
159
|
|
147
160
|
role = message.get("role")
|
148
161
|
input_content = message.get("content") # Can be list or string
|
162
|
+
image_url_top_level = message.get("image_url") # Check for top-level image_url
|
149
163
|
|
150
164
|
processed_content = []
|
151
165
|
|
@@ -163,41 +177,63 @@ def oai_to_unsloth(
|
|
163
177
|
"image",
|
164
178
|
): # Accept 'image' as source key too
|
165
179
|
# Use "image_url" first, then fallback to "image" if needed
|
166
|
-
|
167
|
-
|
180
|
+
image_source_value = item.get("image_url", item.get("image"))
|
181
|
+
image_url_to_load = None
|
182
|
+
pil_image_to_load = None
|
183
|
+
|
184
|
+
if item_type == "image_url" and isinstance(
|
185
|
+
image_source_value, dict
|
186
|
+
):
|
187
|
+
# Handle nested {"url": "..."}
|
188
|
+
image_url_to_load = image_source_value.get("url")
|
189
|
+
elif isinstance(image_source_value, str):
|
190
|
+
# Handle direct URL string or base64 string
|
191
|
+
image_url_to_load = image_source_value # Could be URL or base64
|
192
|
+
elif isinstance(image_source_value, Image.Image):
|
193
|
+
# Handle direct PIL image
|
194
|
+
pil_image_to_load = image_source_value
|
195
|
+
|
196
|
+
if pil_image_to_load: # If we already have a PIL image
|
197
|
+
processed_content.append(
|
198
|
+
{"type": "image", "image": pil_image_to_load}
|
199
|
+
)
|
200
|
+
elif (
|
201
|
+
image_url_to_load
|
202
|
+
): # If we have a URL or base64 string to process
|
168
203
|
pil_image = None
|
169
204
|
try:
|
170
|
-
if
|
171
|
-
image_source, str
|
172
|
-
) and image_source.startswith(("http://", "https://")):
|
205
|
+
if image_url_to_load.startswith(("http://", "https://")):
|
173
206
|
# Handle URL
|
174
|
-
response = requests.get(
|
207
|
+
response = requests.get(
|
208
|
+
image_url_to_load,
|
209
|
+
stream=True,
|
210
|
+
headers=REQUESTS_HEADERS,
|
211
|
+
)
|
175
212
|
response.raise_for_status() # Raise an exception for bad status codes
|
176
213
|
pil_image = Image.open(response.raw)
|
177
|
-
|
214
|
+
else: # Assume base64
|
178
215
|
# Handle base64 string
|
179
216
|
# Remove potential data URI prefix (e.g., "data:image/png;base64,")
|
180
|
-
if "," in
|
181
|
-
|
182
|
-
|
217
|
+
if "," in image_url_to_load:
|
218
|
+
image_url_to_load = image_url_to_load.split(",", 1)[
|
219
|
+
1
|
220
|
+
]
|
221
|
+
image_bytes = base64.b64decode(image_url_to_load)
|
183
222
|
pil_image = Image.open(io.BytesIO(image_bytes))
|
184
223
|
|
185
|
-
elif isinstance(image_source, Image.Image):
|
186
|
-
# Handle direct PIL.Image input
|
187
|
-
pil_image = image_source
|
188
|
-
|
189
224
|
if pil_image:
|
190
225
|
processed_content.append(
|
191
226
|
{"type": "image", "image": pil_image}
|
192
227
|
)
|
193
228
|
else:
|
229
|
+
# This condition might be less likely now with the separated logic
|
194
230
|
print(
|
195
|
-
f"Warning: Could not load image from source: {type(
|
231
|
+
f"Warning: Could not load image from source: {type(image_url_to_load)}"
|
196
232
|
)
|
197
233
|
|
198
234
|
except requests.exceptions.RequestException as e:
|
199
235
|
print(
|
200
|
-
f"Warning: Failed to fetch image from URL {
|
236
|
+
f"Warning: Failed to fetch image from URL {image_url_to_load}: {e}"
|
201
237
|
)
|
202
238
|
except (binascii.Error, ValueError) as e:
|
203
239
|
print(f"Warning: Failed to decode base64 image string: {e}")
|
@@ -210,7 +246,7 @@ def oai_to_unsloth(
|
|
210
246
|
|
211
247
|
else:
|
212
248
|
print(
|
213
|
-
"Warning: Image item provided but
|
249
|
+
"Warning: Image item provided but could not resolve image source (URL, base64, or PIL Image)."
|
214
250
|
)
|
215
251
|
|
216
252
|
# Add handling for other potential input types if necessary
|
@@ -219,6 +255,45 @@ def oai_to_unsloth(
|
|
219
255
|
processed_content.append({"type": "text", "text": input_content})
|
220
256
|
# else: Handle unexpected content format (e.g., log warning, skip message)
|
221
257
|
|
258
|
+
# Handle potential top-level image_url (after processing content)
|
259
|
+
if image_url_top_level and isinstance(image_url_top_level, str):
|
260
|
+
pil_image = None
|
261
|
+
try:
|
262
|
+
if image_url_top_level.startswith(("http://", "https://")):
|
263
|
+
# Handle URL
|
264
|
+
response = requests.get(
|
265
|
+
image_url_top_level, stream=True, headers=REQUESTS_HEADERS
|
266
|
+
)
|
267
|
+
response.raise_for_status() # Raise an exception for bad status codes
|
268
|
+
pil_image = Image.open(response.raw)
|
269
|
+
else:
|
270
|
+
# Assume base64 string if not URL (could refine this check)
|
271
|
+
# Remove potential data URI prefix
|
272
|
+
if "," in image_url_top_level:
|
273
|
+
image_url_top_level = image_url_top_level.split(",", 1)[1]
|
274
|
+
image_bytes = base64.b64decode(image_url_top_level)
|
275
|
+
pil_image = Image.open(io.BytesIO(image_bytes))
|
276
|
+
|
277
|
+
if pil_image:
|
278
|
+
processed_content.append({"type": "image", "image": pil_image})
|
279
|
+
else:
|
280
|
+
print(
|
281
|
+
f"Warning: Could not load image from top-level source: {type(image_url_top_level)}"
|
282
|
+
)
|
283
|
+
|
284
|
+
except requests.exceptions.RequestException as e:
|
285
|
+
print(
|
286
|
+
f"Warning: Failed to fetch top-level image from URL {image_url_top_level}: {e}"
|
287
|
+
)
|
288
|
+
except (binascii.Error, ValueError) as e:
|
289
|
+
print(f"Warning: Failed to decode top-level base64 image string: {e}")
|
290
|
+
except (IOError, UnidentifiedImageError) as e:
|
291
|
+
print(f"Warning: Failed to open top-level image: {e}")
|
292
|
+
except Exception as e:
|
293
|
+
print(
|
294
|
+
f"Warning: An unexpected error occurred while processing top-level image: {e}"
|
295
|
+
)
|
296
|
+
|
222
297
|
if role and processed_content:
|
223
298
|
nebu_conversation.append({"role": role, "content": processed_content})
|
224
299
|
# else: Handle missing role or empty content if needed
|
nebu/processors/consumer.py
CHANGED
@@ -479,6 +479,7 @@ while True:
|
|
479
479
|
# Read from stream with blocking
|
480
480
|
streams = {REDIS_STREAM: ">"} # '>' means read only new messages
|
481
481
|
# The type checker still struggles here, but the runtime types are asserted.
|
482
|
+
print("reading from stream...")
|
482
483
|
messages = r.xreadgroup( # type: ignore[arg-type]
|
483
484
|
REDIS_CONSUMER_GROUP, consumer_name, streams, count=1, block=5000
|
484
485
|
)
|
@@ -5,13 +5,13 @@ nebu/cache.py,sha256=1aY1plIXWOPmUY6GGq_s_QDXzIi5UMuG34XYBA8PpW8,3803
|
|
5
5
|
nebu/config.py,sha256=aZzQltkobtOLHFCGcIkpKoE3ITn3Z11Dp0E72w84TA0,5769
|
6
6
|
nebu/data.py,sha256=kIH9-JJ1-iO7P2t28bku6Gn0Y5tgQszGeTW_rpmO03A,38725
|
7
7
|
nebu/meta.py,sha256=CzFHMND9seuewzq9zNNx9WTr6JvrCBExe7BLqDSr7lM,745
|
8
|
-
nebu/chatx/convert.py,sha256=
|
8
|
+
nebu/chatx/convert.py,sha256=jIOuGXPfLkYf_cRqh739gsKKg7cBB2zEYm3Jpmf6Xvo,12535
|
9
9
|
nebu/chatx/openai.py,sha256=LLSPvVGnauUViAXY7OifwoWlkUGZWfgxEjxR4mjSqZg,44961
|
10
10
|
nebu/containers/container.py,sha256=yb7KaPTVXnEEAlrpdlUi4HNqF6P7z9bmwAILGlq6iqU,13502
|
11
11
|
nebu/containers/decorator.py,sha256=uFtzlAXRHYZECJ-NPusY7oN9GXvdHrHDd_JNrIGr8aQ,3244
|
12
12
|
nebu/containers/models.py,sha256=0j6NGy4yto-enRDh_4JH_ZTbHrLdSpuMOqNQPnIrwC4,6815
|
13
13
|
nebu/containers/server.py,sha256=yFa2Y9PzBn59E1HftKiv0iapPonli2rbGAiU6r-wwe0,2513
|
14
|
-
nebu/processors/consumer.py,sha256=
|
14
|
+
nebu/processors/consumer.py,sha256=mSgHm6ANcsM1KtmH9Nu-D6X52d6we0Au8m29rcvb57s,19734
|
15
15
|
nebu/processors/decorate.py,sha256=9mf25RawOX6_6WyQcRLBIHQC3oCDtofQZijJ13aQM9E,34576
|
16
16
|
nebu/processors/default.py,sha256=W4slJenG59rvyTlJ7gRp58eFfXcNOTT2Hfi6zzJAobI,365
|
17
17
|
nebu/processors/models.py,sha256=y40HoW-MEzDWB2dm_tsYlUy3Nf3s6eiLC0iGO9BoNog,3956
|
@@ -19,8 +19,8 @@ nebu/processors/processor.py,sha256=EQ3-dBf432fSAQE2A_9ATX-cG5LkJ4fjVaOtlxCoXvc,
|
|
19
19
|
nebu/processors/remote.py,sha256=TeAIPGEMqnDIb7H1iett26IEZrBlcbPB_-DSm6jcH1E,1285
|
20
20
|
nebu/redis/models.py,sha256=coPovAcVXnOU1Xh_fpJL4PO3QctgK9nBe5QYoqEcnxg,1230
|
21
21
|
nebu/services/service.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
22
|
-
nebu-0.1.
|
23
|
-
nebu-0.1.
|
24
|
-
nebu-0.1.
|
25
|
-
nebu-0.1.
|
26
|
-
nebu-0.1.
|
22
|
+
nebu-0.1.32.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
23
|
+
nebu-0.1.32.dist-info/METADATA,sha256=zjHxnA7sDhRyFcO7z3l6MnBjuox-ydeKL4TR09J_R1w,1786
|
24
|
+
nebu-0.1.32.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
25
|
+
nebu-0.1.32.dist-info/top_level.txt,sha256=uLIbEKJeGSHWOAJN5S0i5XBGwybALlF9bYoB1UhdEgQ,5
|
26
|
+
nebu-0.1.32.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|