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 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
- new_content.append({"type": "image"})
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
- new_schema.append({"role": role, "content": new_content})
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
- {"type": "input_text", "text": "Describe the image."},
103
- {"type": "input_image", "image_url": "http://... or base64 string"},
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
- image_source = item.get("image_url", item.get("image"))
167
- if image_source:
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 isinstance(
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(image_source, stream=True)
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
- elif isinstance(image_source, str):
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 image_source:
181
- image_source = image_source.split(",", 1)[1]
182
- image_bytes = base64.b64decode(image_source)
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(image_source)}"
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 {image_source}: {e}"
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 'image_url' or 'image' key is missing or empty."
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
@@ -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
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nebu
3
- Version: 0.1.30
3
+ Version: 0.1.32
4
4
  Summary: A globally distributed container runtime
5
5
  Requires-Python: >=3.10.14
6
6
  Description-Content-Type: text/markdown
@@ -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=4rjccr9bI0xmCAumKlxyFpVzW0up9Yj-gJYvDjH6C54,8798
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=becQxjQoerlaDdr2_wxzIux3wl8SVmo2LA93RZn0Jk8,19694
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.30.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
23
- nebu-0.1.30.dist-info/METADATA,sha256=Y_o6hvOdihVgFJMRQPIpALA2V6JfaXybtrlSaQyMNtI,1786
24
- nebu-0.1.30.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
25
- nebu-0.1.30.dist-info/top_level.txt,sha256=uLIbEKJeGSHWOAJN5S0i5XBGwybALlF9bYoB1UhdEgQ,5
26
- nebu-0.1.30.dist-info/RECORD,,
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