nebu 0.1.29__py3-none-any.whl → 0.1.31__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/cache.py +24 -11
- nebu/chatx/convert.py +120 -36
- nebu/processors/consumer.py +3 -1
- nebu/processors/processor.py +8 -2
- {nebu-0.1.29.dist-info → nebu-0.1.31.dist-info}/METADATA +1 -1
- {nebu-0.1.29.dist-info → nebu-0.1.31.dist-info}/RECORD +9 -9
- {nebu-0.1.29.dist-info → nebu-0.1.31.dist-info}/WHEEL +0 -0
- {nebu-0.1.29.dist-info → nebu-0.1.31.dist-info}/licenses/LICENSE +0 -0
- {nebu-0.1.29.dist-info → nebu-0.1.31.dist-info}/top_level.txt +0 -0
nebu/cache.py
CHANGED
@@ -25,29 +25,42 @@ class Cache:
|
|
25
25
|
Initializes the Redis connection.
|
26
26
|
Pulls connection details from environment variables REDIS_HOST,
|
27
27
|
REDIS_PORT, and REDIS_DB if available, otherwise uses defaults.
|
28
|
+
Also checks for REDIS_URL and prefers that if set.
|
28
29
|
"""
|
29
|
-
|
30
|
-
redis_port = int(os.environ.get("REDIS_PORT", port))
|
31
|
-
redis_db = int(os.environ.get("REDIS_DB", db))
|
30
|
+
redis_url = os.environ.get("REDIS_URL")
|
32
31
|
namespace = os.environ.get("NEBU_NAMESPACE")
|
33
32
|
if not namespace:
|
34
33
|
raise ValueError("NEBU_NAMESPACE environment variable is not set")
|
35
34
|
|
35
|
+
self.redis_client = None
|
36
|
+
connection_info = ""
|
37
|
+
|
36
38
|
try:
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
39
|
+
if redis_url:
|
40
|
+
# Use REDIS_URL if available
|
41
|
+
self.redis_client = redis.StrictRedis.from_url(
|
42
|
+
redis_url, decode_responses=True
|
43
|
+
)
|
44
|
+
connection_info = f"URL {redis_url}"
|
45
|
+
else:
|
46
|
+
# Fallback to individual host, port, db
|
47
|
+
redis_host = os.environ.get("REDIS_HOST", host)
|
48
|
+
redis_port = int(os.environ.get("REDIS_PORT", port))
|
49
|
+
redis_db = int(os.environ.get("REDIS_DB", db))
|
50
|
+
self.redis_client = redis.StrictRedis(
|
51
|
+
host=redis_host, port=redis_port, db=redis_db, decode_responses=True
|
52
|
+
)
|
53
|
+
connection_info = f"{redis_host}:{redis_port}/{redis_db}"
|
54
|
+
|
41
55
|
# Ping the server to ensure connection is established
|
42
56
|
self.redis_client.ping()
|
43
|
-
print(
|
44
|
-
f"Successfully connected to Redis at {redis_host}:{redis_port}/{redis_db}"
|
45
|
-
)
|
57
|
+
print(f"Successfully connected to Redis using {connection_info}")
|
46
58
|
|
47
59
|
self.prefix = f"cache:{namespace}"
|
48
60
|
except Exception as e:
|
49
61
|
print(f"Error connecting to Redis: {e}")
|
50
|
-
|
62
|
+
# Ensure client is None if connection fails at any point
|
63
|
+
self.redis_client = None
|
51
64
|
|
52
65
|
def get(self, key: str) -> str | None:
|
53
66
|
"""
|
nebu/chatx/convert.py
CHANGED
@@ -74,36 +74,49 @@ def convert_to_unsloth_inference(
|
|
74
74
|
# regardless of how many images were found, and merges all text into one block.
|
75
75
|
new_content = []
|
76
76
|
if image_urls:
|
77
|
-
|
77
|
+
# Add image placeholders for each image found
|
78
|
+
for _ in image_urls:
|
79
|
+
new_content.append({"type": "image"})
|
78
80
|
if merged_text:
|
79
81
|
new_content.append({"type": "text", "text": merged_text})
|
80
82
|
|
81
|
-
|
83
|
+
# Check if there's any content to add
|
84
|
+
if new_content:
|
85
|
+
new_schema.append({"role": role, "content": new_content})
|
86
|
+
# else: Optionally handle cases where a message might become empty
|
82
87
|
|
83
88
|
return new_schema, all_images
|
84
89
|
|
85
90
|
|
86
91
|
def oai_to_unsloth(
|
87
|
-
messages_input:
|
92
|
+
messages_input: Dict[
|
93
|
+
str, Any
|
94
|
+
], # Assume input is always dict like {'messages': [...]}
|
88
95
|
) -> Dict[str, List[Dict[str, Any]]]:
|
89
96
|
"""
|
90
|
-
Converts
|
97
|
+
Converts messages from a JSON object containing a 'messages' key
|
98
|
+
(typical in JSON Lines format) to the Nebulous conversation format.
|
91
99
|
Images specified by URLs or base64 strings are loaded into PIL.Image objects.
|
92
100
|
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
101
|
+
{
|
102
|
+
"messages": [
|
103
|
+
{
|
104
|
+
"role": "user",
|
105
|
+
"content": [
|
106
|
+
{
|
107
|
+
"type": "text",
|
108
|
+
"text": "Who is this an image of?"
|
109
|
+
},
|
110
|
+
{
|
111
|
+
"type": "image_url",
|
112
|
+
"image_url": {
|
113
|
+
"url": "https://upload.wikimedia.org/wikipedia/commons/5/57/Abraham_Lincoln_1863_Portrait_%283x4_cropped%29.jpg"
|
114
|
+
}
|
115
|
+
}
|
116
|
+
]
|
117
|
+
}
|
118
|
+
]
|
119
|
+
}
|
107
120
|
|
108
121
|
Output format example:
|
109
122
|
{
|
@@ -122,10 +135,26 @@ def oai_to_unsloth(
|
|
122
135
|
]
|
123
136
|
}
|
124
137
|
"""
|
138
|
+
# Directly extract the list of messages, assuming input structure
|
139
|
+
messages_to_process = messages_input.get("messages", [])
|
140
|
+
|
141
|
+
# Validate that 'messages' key contained a list
|
142
|
+
if not isinstance(messages_to_process, list):
|
143
|
+
print(
|
144
|
+
f"Warning: Input dict provided, but 'messages' key does not contain a list: {type(messages_to_process)}. Returning empty."
|
145
|
+
)
|
146
|
+
return {"messages": []}
|
147
|
+
|
125
148
|
nebu_conversation = []
|
126
|
-
for message in
|
149
|
+
for message in messages_to_process: # Use the extracted list
|
150
|
+
# Add check here for robustness against malformed items *within* the list
|
151
|
+
if not isinstance(message, dict):
|
152
|
+
print(f"Warning: Skipping non-dictionary item in message list: {message!r}")
|
153
|
+
continue
|
154
|
+
|
127
155
|
role = message.get("role")
|
128
156
|
input_content = message.get("content") # Can be list or string
|
157
|
+
image_url_top_level = message.get("image_url") # Check for top-level image_url
|
129
158
|
|
130
159
|
processed_content = []
|
131
160
|
|
@@ -143,41 +172,59 @@ def oai_to_unsloth(
|
|
143
172
|
"image",
|
144
173
|
): # Accept 'image' as source key too
|
145
174
|
# Use "image_url" first, then fallback to "image" if needed
|
146
|
-
|
147
|
-
|
175
|
+
image_source_value = item.get("image_url", item.get("image"))
|
176
|
+
image_url_to_load = None
|
177
|
+
pil_image_to_load = None
|
178
|
+
|
179
|
+
if item_type == "image_url" and isinstance(
|
180
|
+
image_source_value, dict
|
181
|
+
):
|
182
|
+
# Handle nested {"url": "..."}
|
183
|
+
image_url_to_load = image_source_value.get("url")
|
184
|
+
elif isinstance(image_source_value, str):
|
185
|
+
# Handle direct URL string or base64 string
|
186
|
+
image_url_to_load = image_source_value # Could be URL or base64
|
187
|
+
elif isinstance(image_source_value, Image.Image):
|
188
|
+
# Handle direct PIL image
|
189
|
+
pil_image_to_load = image_source_value
|
190
|
+
|
191
|
+
if pil_image_to_load: # If we already have a PIL image
|
192
|
+
processed_content.append(
|
193
|
+
{"type": "image", "image": pil_image_to_load}
|
194
|
+
)
|
195
|
+
elif (
|
196
|
+
image_url_to_load
|
197
|
+
): # If we have a URL or base64 string to process
|
148
198
|
pil_image = None
|
149
199
|
try:
|
150
|
-
if
|
151
|
-
image_source, str
|
152
|
-
) and image_source.startswith(("http://", "https://")):
|
200
|
+
if image_url_to_load.startswith(("http://", "https://")):
|
153
201
|
# Handle URL
|
154
|
-
response = requests.get(
|
202
|
+
response = requests.get(image_url_to_load, stream=True)
|
155
203
|
response.raise_for_status() # Raise an exception for bad status codes
|
156
204
|
pil_image = Image.open(response.raw)
|
157
|
-
|
205
|
+
else: # Assume base64
|
158
206
|
# Handle base64 string
|
159
207
|
# Remove potential data URI prefix (e.g., "data:image/png;base64,")
|
160
|
-
if "," in
|
161
|
-
|
162
|
-
|
208
|
+
if "," in image_url_to_load:
|
209
|
+
image_url_to_load = image_url_to_load.split(",", 1)[
|
210
|
+
1
|
211
|
+
]
|
212
|
+
image_bytes = base64.b64decode(image_url_to_load)
|
163
213
|
pil_image = Image.open(io.BytesIO(image_bytes))
|
164
214
|
|
165
|
-
elif isinstance(image_source, Image.Image):
|
166
|
-
# Handle direct PIL.Image input
|
167
|
-
pil_image = image_source
|
168
|
-
|
169
215
|
if pil_image:
|
170
216
|
processed_content.append(
|
171
217
|
{"type": "image", "image": pil_image}
|
172
218
|
)
|
173
219
|
else:
|
220
|
+
# This condition might be less likely now with the separated logic
|
174
221
|
print(
|
175
|
-
f"Warning: Could not load image from source: {type(
|
222
|
+
f"Warning: Could not load image from source: {type(image_url_to_load)}"
|
176
223
|
)
|
177
224
|
|
178
225
|
except requests.exceptions.RequestException as e:
|
179
226
|
print(
|
180
|
-
f"Warning: Failed to fetch image from URL {
|
227
|
+
f"Warning: Failed to fetch image from URL {image_url_to_load}: {e}"
|
181
228
|
)
|
182
229
|
except (binascii.Error, ValueError) as e:
|
183
230
|
print(f"Warning: Failed to decode base64 image string: {e}")
|
@@ -190,7 +237,7 @@ def oai_to_unsloth(
|
|
190
237
|
|
191
238
|
else:
|
192
239
|
print(
|
193
|
-
"Warning: Image item provided but
|
240
|
+
"Warning: Image item provided but could not resolve image source (URL, base64, or PIL Image)."
|
194
241
|
)
|
195
242
|
|
196
243
|
# Add handling for other potential input types if necessary
|
@@ -199,6 +246,43 @@ def oai_to_unsloth(
|
|
199
246
|
processed_content.append({"type": "text", "text": input_content})
|
200
247
|
# else: Handle unexpected content format (e.g., log warning, skip message)
|
201
248
|
|
249
|
+
# Handle potential top-level image_url (after processing content)
|
250
|
+
if image_url_top_level and isinstance(image_url_top_level, str):
|
251
|
+
pil_image = None
|
252
|
+
try:
|
253
|
+
if image_url_top_level.startswith(("http://", "https://")):
|
254
|
+
# Handle URL
|
255
|
+
response = requests.get(image_url_top_level, stream=True)
|
256
|
+
response.raise_for_status() # Raise an exception for bad status codes
|
257
|
+
pil_image = Image.open(response.raw)
|
258
|
+
else:
|
259
|
+
# Assume base64 string if not URL (could refine this check)
|
260
|
+
# Remove potential data URI prefix
|
261
|
+
if "," in image_url_top_level:
|
262
|
+
image_url_top_level = image_url_top_level.split(",", 1)[1]
|
263
|
+
image_bytes = base64.b64decode(image_url_top_level)
|
264
|
+
pil_image = Image.open(io.BytesIO(image_bytes))
|
265
|
+
|
266
|
+
if pil_image:
|
267
|
+
processed_content.append({"type": "image", "image": pil_image})
|
268
|
+
else:
|
269
|
+
print(
|
270
|
+
f"Warning: Could not load image from top-level source: {type(image_url_top_level)}"
|
271
|
+
)
|
272
|
+
|
273
|
+
except requests.exceptions.RequestException as e:
|
274
|
+
print(
|
275
|
+
f"Warning: Failed to fetch top-level image from URL {image_url_top_level}: {e}"
|
276
|
+
)
|
277
|
+
except (binascii.Error, ValueError) as e:
|
278
|
+
print(f"Warning: Failed to decode top-level base64 image string: {e}")
|
279
|
+
except (IOError, UnidentifiedImageError) as e:
|
280
|
+
print(f"Warning: Failed to open top-level image: {e}")
|
281
|
+
except Exception as e:
|
282
|
+
print(
|
283
|
+
f"Warning: An unexpected error occurred while processing top-level image: {e}"
|
284
|
+
)
|
285
|
+
|
202
286
|
if role and processed_content:
|
203
287
|
nebu_conversation.append({"role": role, "content": processed_content})
|
204
288
|
# else: Handle missing role or empty content if needed
|
nebu/processors/consumer.py
CHANGED
@@ -342,7 +342,9 @@ def process_message(message_id: str, message_data: Dict[str, str]) -> None:
|
|
342
342
|
if content_type_name and content_type_name in local_namespace:
|
343
343
|
# Try to create the content type model first
|
344
344
|
try:
|
345
|
-
content_model = local_namespace[content_type_name](
|
345
|
+
content_model = local_namespace[content_type_name].model_validate(
|
346
|
+
content
|
347
|
+
)
|
346
348
|
print(f"Content model: {content_model}")
|
347
349
|
input_obj = local_namespace["Message"](
|
348
350
|
kind=kind,
|
nebu/processors/processor.py
CHANGED
@@ -12,6 +12,7 @@ from nebu.processors.models import (
|
|
12
12
|
V1Processors,
|
13
13
|
V1ProcessorScaleRequest,
|
14
14
|
V1Scale,
|
15
|
+
V1StreamData,
|
15
16
|
V1UpdateProcessor,
|
16
17
|
)
|
17
18
|
|
@@ -140,7 +141,7 @@ class Processor:
|
|
140
141
|
patch_response.raise_for_status()
|
141
142
|
print(f"Updated Processor {self.processor.metadata.name}")
|
142
143
|
|
143
|
-
def send(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
144
|
+
def send(self, data: Dict[str, Any], wait: bool = False) -> Dict[str, Any]:
|
144
145
|
"""
|
145
146
|
Send data to the processor.
|
146
147
|
"""
|
@@ -149,9 +150,14 @@ class Processor:
|
|
149
150
|
|
150
151
|
url = f"{self.processors_url}/{self.processor.metadata.namespace}/{self.processor.metadata.name}/messages"
|
151
152
|
|
153
|
+
stream_data = V1StreamData(
|
154
|
+
content=data,
|
155
|
+
wait=wait,
|
156
|
+
)
|
157
|
+
|
152
158
|
response = requests.post(
|
153
159
|
url,
|
154
|
-
json=
|
160
|
+
json=stream_data.model_dump(exclude_none=True),
|
155
161
|
headers={"Authorization": f"Bearer {self.api_key}"},
|
156
162
|
)
|
157
163
|
response.raise_for_status()
|
@@ -1,26 +1,26 @@
|
|
1
1
|
nebu/__init__.py,sha256=5sepbzdAdoA_8TIxws60S4ugFY1apQd_savzn20a4cY,465
|
2
2
|
nebu/adapter.py,sha256=yWQCpAn2lJxXPSyQacmFpfVzAL3RrKtKgrSMws0BoT8,211
|
3
3
|
nebu/auth.py,sha256=N_v6SPFD9HU_UoRDTaouH03g2Hmo9C-xxqInE1FweXE,1471
|
4
|
-
nebu/cache.py,sha256=
|
4
|
+
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=S_0Q7OTZEeGQZTfqCmshp0UaXJbAR_w9dIBTcyqnlBo,12107
|
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=becQxjQoerlaDdr2_wxzIux3wl8SVmo2LA93RZn0Jk8,19694
|
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
|
18
|
-
nebu/processors/processor.py,sha256=
|
18
|
+
nebu/processors/processor.py,sha256=EQ3-dBf432fSAQE2A_9ATX-cG5LkJ4fjVaOtlxCoXvc,9719
|
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.31.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
23
|
+
nebu-0.1.31.dist-info/METADATA,sha256=gXoUbLOIX6fDnn90IDvX3-fnFhBWRZrVxbJr0ZNbEf0,1786
|
24
|
+
nebu-0.1.31.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
25
|
+
nebu-0.1.31.dist-info/top_level.txt,sha256=uLIbEKJeGSHWOAJN5S0i5XBGwybALlF9bYoB1UhdEgQ,5
|
26
|
+
nebu-0.1.31.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|