nebu 0.1.27__py3-none-any.whl → 0.1.30__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/__init__.py +5 -1
- nebu/adapter.py +11 -0
- nebu/auth.py +15 -0
- nebu/cache.py +103 -0
- nebu/{convert.py → chatx/convert.py} +111 -15
- nebu/chatx/openai.py +976 -0
- nebu/data.py +682 -779
- nebu/processors/consumer.py +4 -2
- nebu/processors/decorate.py +1 -1
- nebu/processors/processor.py +10 -8
- nebu/processors/remote.py +47 -0
- {nebu-0.1.27.dist-info → nebu-0.1.30.dist-info}/METADATA +2 -1
- nebu-0.1.30.dist-info/RECORD +26 -0
- nebu-0.1.27.dist-info/RECORD +0 -22
- {nebu-0.1.27.dist-info → nebu-0.1.30.dist-info}/WHEEL +0 -0
- {nebu-0.1.27.dist-info → nebu-0.1.30.dist-info}/licenses/LICENSE +0 -0
- {nebu-0.1.27.dist-info → nebu-0.1.30.dist-info}/top_level.txt +0 -0
nebu/__init__.py
CHANGED
@@ -3,10 +3,14 @@
|
|
3
3
|
# ruff: noqa: F401
|
4
4
|
# ruff: noqa: F403
|
5
5
|
|
6
|
+
from .adapter import *
|
7
|
+
from .auth import is_allowed
|
8
|
+
from .cache import Cache, OwnedValue
|
9
|
+
from .chatx.convert import *
|
10
|
+
from .chatx.openai import *
|
6
11
|
from .config import *
|
7
12
|
from .containers.container import Container
|
8
13
|
from .containers.models import *
|
9
|
-
from .convert import convert_to_unsloth
|
10
14
|
from .data import *
|
11
15
|
from .meta import *
|
12
16
|
from .processors.decorate import *
|
nebu/adapter.py
ADDED
nebu/auth.py
CHANGED
@@ -33,3 +33,18 @@ def get_user_profile(api_key: str) -> V1UserProfile:
|
|
33
33
|
response.raise_for_status()
|
34
34
|
|
35
35
|
return V1UserProfile.model_validate(response.json())
|
36
|
+
|
37
|
+
|
38
|
+
def is_allowed(
|
39
|
+
resource_owner: str,
|
40
|
+
user_id: Optional[str] = None,
|
41
|
+
orgs: Optional[Dict[str, Dict[str, str]]] = None,
|
42
|
+
) -> bool:
|
43
|
+
if orgs is None:
|
44
|
+
orgs = {}
|
45
|
+
owners = []
|
46
|
+
for org_id, _ in orgs.items():
|
47
|
+
owners.append(org_id)
|
48
|
+
if user_id:
|
49
|
+
owners.append(user_id)
|
50
|
+
return resource_owner in owners
|
nebu/cache.py
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
import os
|
2
|
+
import time
|
3
|
+
from typing import Any, Optional, cast
|
4
|
+
|
5
|
+
import redis
|
6
|
+
from pydantic import BaseModel, Field
|
7
|
+
|
8
|
+
|
9
|
+
class OwnedValue(BaseModel):
|
10
|
+
created_at: int = Field(default_factory=lambda: int(time.time()))
|
11
|
+
value: str
|
12
|
+
user_id: Optional[str] = None
|
13
|
+
orgs: Optional[Any] = None
|
14
|
+
handle: Optional[str] = None
|
15
|
+
owner: Optional[str] = None
|
16
|
+
|
17
|
+
|
18
|
+
class Cache:
|
19
|
+
"""
|
20
|
+
A simple cache class that connects to Redis.
|
21
|
+
"""
|
22
|
+
|
23
|
+
def __init__(self, host: str = "localhost", port: int = 6379, db: int = 0):
|
24
|
+
"""
|
25
|
+
Initializes the Redis connection.
|
26
|
+
Pulls connection details from environment variables REDIS_HOST,
|
27
|
+
REDIS_PORT, and REDIS_DB if available, otherwise uses defaults.
|
28
|
+
Also checks for REDIS_URL and prefers that if set.
|
29
|
+
"""
|
30
|
+
redis_url = os.environ.get("REDIS_URL")
|
31
|
+
namespace = os.environ.get("NEBU_NAMESPACE")
|
32
|
+
if not namespace:
|
33
|
+
raise ValueError("NEBU_NAMESPACE environment variable is not set")
|
34
|
+
|
35
|
+
self.redis_client = None
|
36
|
+
connection_info = ""
|
37
|
+
|
38
|
+
try:
|
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
|
+
|
55
|
+
# Ping the server to ensure connection is established
|
56
|
+
self.redis_client.ping()
|
57
|
+
print(f"Successfully connected to Redis using {connection_info}")
|
58
|
+
|
59
|
+
self.prefix = f"cache:{namespace}"
|
60
|
+
except Exception as e:
|
61
|
+
print(f"Error connecting to Redis: {e}")
|
62
|
+
# Ensure client is None if connection fails at any point
|
63
|
+
self.redis_client = None
|
64
|
+
|
65
|
+
def get(self, key: str) -> str | None:
|
66
|
+
"""
|
67
|
+
Gets the value associated with a key from Redis.
|
68
|
+
Returns None if the key does not exist or connection failed.
|
69
|
+
"""
|
70
|
+
if not self.redis_client:
|
71
|
+
print("Redis client not connected.")
|
72
|
+
return None
|
73
|
+
try:
|
74
|
+
key = f"{self.prefix}:{key}"
|
75
|
+
# Cast the result to str | None as expected
|
76
|
+
result = self.redis_client.get(key)
|
77
|
+
return cast(str | None, result)
|
78
|
+
except Exception as e:
|
79
|
+
print(f"Error getting key '{key}' from Redis: {e}")
|
80
|
+
return None
|
81
|
+
|
82
|
+
def set(self, key: str, value: str, expiry_seconds: int | None = None) -> bool:
|
83
|
+
"""
|
84
|
+
Sets a key-value pair in Redis.
|
85
|
+
Optionally sets an expiry time for the key in seconds.
|
86
|
+
Returns True if successful, False otherwise (e.g., connection failed).
|
87
|
+
"""
|
88
|
+
if not self.redis_client:
|
89
|
+
print("Redis client not connected.")
|
90
|
+
return False
|
91
|
+
try:
|
92
|
+
key = f"{self.prefix}:{key}"
|
93
|
+
if expiry_seconds:
|
94
|
+
# Cast the result to bool
|
95
|
+
result = self.redis_client.setex(key, expiry_seconds, value)
|
96
|
+
return cast(bool, result)
|
97
|
+
else:
|
98
|
+
# Cast the result to bool
|
99
|
+
result = self.redis_client.set(key, value)
|
100
|
+
return cast(bool, result)
|
101
|
+
except Exception as e:
|
102
|
+
print(f"Error setting key '{key}' in Redis: {e}")
|
103
|
+
return False
|
@@ -1,34 +1,115 @@
|
|
1
1
|
import base64
|
2
2
|
import binascii
|
3
3
|
import io
|
4
|
-
from
|
4
|
+
from io import BytesIO
|
5
|
+
from typing import Any, Dict, List, Tuple
|
5
6
|
|
6
7
|
import requests
|
7
8
|
from PIL import Image, UnidentifiedImageError
|
8
9
|
|
9
10
|
|
10
|
-
def
|
11
|
-
|
12
|
-
) ->
|
11
|
+
def convert_to_unsloth_inference(
|
12
|
+
old_schema: Dict[str, Any],
|
13
|
+
) -> Tuple[List[Dict[str, Any]], List[Image.Image]]:
|
13
14
|
"""
|
14
|
-
|
15
|
-
Images specified by URLs or base64 strings are loaded into PIL.Image objects.
|
16
|
-
|
17
|
-
Input format example:
|
15
|
+
Convert from an old OpenAI message format that may look like:
|
18
16
|
[
|
19
17
|
{
|
20
18
|
"role": "user",
|
21
19
|
"content": [
|
22
|
-
{"type": "
|
23
|
-
{"type": "
|
24
|
-
|
25
|
-
|
20
|
+
{"type": "text", "text": "some text"},
|
21
|
+
{"type": "image_url", "image_url": {"url": "https://..."}},
|
22
|
+
...
|
23
|
+
],
|
24
|
+
}
|
25
|
+
]
|
26
|
+
|
27
|
+
to a new format:
|
28
|
+
[
|
26
29
|
{
|
27
|
-
"role": "
|
28
|
-
"content": [
|
30
|
+
"role": "user",
|
31
|
+
"content": [
|
32
|
+
{"type": "image"},
|
33
|
+
{"type": "text", "text": "merged user text"}
|
34
|
+
],
|
29
35
|
}
|
30
36
|
]
|
31
37
|
|
38
|
+
Along with the new format, return a list of downloaded PIL Image objects.
|
39
|
+
"""
|
40
|
+
|
41
|
+
new_schema = []
|
42
|
+
all_images = [] # Will store PIL images as we convert them
|
43
|
+
|
44
|
+
for message in old_schema:
|
45
|
+
role = message.get("role", "user")
|
46
|
+
|
47
|
+
# Collect all text pieces and all image URLs
|
48
|
+
text_chunks = []
|
49
|
+
image_urls = []
|
50
|
+
|
51
|
+
for content_item in message.get("content", []):
|
52
|
+
content_type = content_item.get("type")
|
53
|
+
if content_type == "text":
|
54
|
+
text_chunks.append(content_item.get("text", ""))
|
55
|
+
elif content_type == "image_url":
|
56
|
+
image_url = content_item.get("image_url", {}).get("url")
|
57
|
+
if image_url:
|
58
|
+
image_urls.append(image_url)
|
59
|
+
|
60
|
+
# Merge text chunks into one
|
61
|
+
merged_text = " ".join(text_chunks).strip()
|
62
|
+
|
63
|
+
# Convert each URL into a PIL image
|
64
|
+
for url in image_urls:
|
65
|
+
# Download the image
|
66
|
+
response = requests.get(url)
|
67
|
+
response.raise_for_status()
|
68
|
+
image_data = BytesIO(response.content)
|
69
|
+
pil_img = Image.open(image_data).convert("RGB")
|
70
|
+
all_images.append(pil_img)
|
71
|
+
|
72
|
+
# Construct new message format
|
73
|
+
# For simplicity, this example only places one {"type": "image"} placeholder
|
74
|
+
# regardless of how many images were found, and merges all text into one block.
|
75
|
+
new_content = []
|
76
|
+
if image_urls:
|
77
|
+
new_content.append({"type": "image"})
|
78
|
+
if merged_text:
|
79
|
+
new_content.append({"type": "text", "text": merged_text})
|
80
|
+
|
81
|
+
new_schema.append({"role": role, "content": new_content})
|
82
|
+
|
83
|
+
return new_schema, all_images
|
84
|
+
|
85
|
+
|
86
|
+
def oai_to_unsloth(
|
87
|
+
messages_input: Dict[
|
88
|
+
str, Any
|
89
|
+
], # Assume input is always dict like {'messages': [...]}
|
90
|
+
) -> Dict[str, List[Dict[str, Any]]]:
|
91
|
+
"""
|
92
|
+
Converts messages from a JSON object containing a 'messages' key
|
93
|
+
(typical in JSON Lines format) to the Nebulous conversation format.
|
94
|
+
Images specified by URLs or base64 strings are loaded into PIL.Image objects.
|
95
|
+
|
96
|
+
Input format example (as dict from JSON line):
|
97
|
+
{
|
98
|
+
"messages": [
|
99
|
+
{
|
100
|
+
"role": "user",
|
101
|
+
"content": [
|
102
|
+
{"type": "input_text", "text": "Describe the image."},
|
103
|
+
{"type": "input_image", "image_url": "http://... or base64 string"},
|
104
|
+
]
|
105
|
+
},
|
106
|
+
{
|
107
|
+
"role": "assistant",
|
108
|
+
"content": [{"type": "text", "text": "This is an image of..."}] # Or potentially just a string
|
109
|
+
}
|
110
|
+
]
|
111
|
+
}
|
112
|
+
|
32
113
|
Output format example:
|
33
114
|
{
|
34
115
|
"messages": [
|
@@ -46,8 +127,23 @@ def convert_to_unsloth(
|
|
46
127
|
]
|
47
128
|
}
|
48
129
|
"""
|
130
|
+
# Directly extract the list of messages, assuming input structure
|
131
|
+
messages_to_process = messages_input.get("messages", [])
|
132
|
+
|
133
|
+
# Validate that 'messages' key contained a list
|
134
|
+
if not isinstance(messages_to_process, list):
|
135
|
+
print(
|
136
|
+
f"Warning: Input dict provided, but 'messages' key does not contain a list: {type(messages_to_process)}. Returning empty."
|
137
|
+
)
|
138
|
+
return {"messages": []}
|
139
|
+
|
49
140
|
nebu_conversation = []
|
50
|
-
for message in
|
141
|
+
for message in messages_to_process: # Use the extracted list
|
142
|
+
# Add check here for robustness against malformed items *within* the list
|
143
|
+
if not isinstance(message, dict):
|
144
|
+
print(f"Warning: Skipping non-dictionary item in message list: {message!r}")
|
145
|
+
continue
|
146
|
+
|
51
147
|
role = message.get("role")
|
52
148
|
input_content = message.get("content") # Can be list or string
|
53
149
|
|