lollms-client 0.17.0__py3-none-any.whl → 0.17.2__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.

Potentially problematic release.


This version of lollms-client might be problematic. Click here for more details.

@@ -0,0 +1,432 @@
1
+ # lollms_client/tti_bindings/dalle/__init__.py
2
+ import requests
3
+ import base64
4
+ from lollms_client.lollms_tti_binding import LollmsTTIBinding
5
+ from typing import Optional, List, Dict, Any, Union
6
+ from ascii_colors import trace_exception, ASCIIColors
7
+ import json # For json.JSONDecodeError in error handling, and general JSON operations
8
+ import os # Added for environment variable access
9
+
10
+ # Defines the binding name for the manager
11
+ BindingName = "DalleTTIBinding_Impl"
12
+
13
+ # DALL-E specific constants
14
+ DALLE_API_HOST = "https://api.openai.com/v1"
15
+ OPENAI_API_KEY_ENV_VAR = "OPENAI_API_KEY" # Environment variable name
16
+
17
+ # Supported models and their properties
18
+ DALLE_MODELS = {
19
+ "dall-e-2": {
20
+ "sizes": ["256x256", "512x512", "1024x1024"],
21
+ "default_size": "1024x1024",
22
+ "supports_quality": False,
23
+ "supports_style": False,
24
+ "max_prompt_length": 1000 # Characters
25
+ },
26
+ "dall-e-3": {
27
+ "sizes": ["1024x1024", "1792x1024", "1024x1792"],
28
+ "default_size": "1024x1024",
29
+ "qualities": ["standard", "hd"],
30
+ "default_quality": "standard",
31
+ "styles": ["vivid", "natural"],
32
+ "default_style": "vivid",
33
+ "supports_quality": True,
34
+ "supports_style": True,
35
+ "max_prompt_length": 4000 # Characters
36
+ }
37
+ }
38
+
39
+ class DalleTTIBinding_Impl(LollmsTTIBinding):
40
+ """
41
+ Concrete implementation of LollmsTTIBinding for OpenAI's DALL-E API.
42
+ """
43
+
44
+ def __init__(self,
45
+ api_key: Optional[str] = None, # Can be None to check env var
46
+ model_name: str = "dall-e-3", # Default to DALL-E 3
47
+ default_size: Optional[str] = None, # e.g. "1024x1024"
48
+ default_quality: Optional[str] = None, # "standard" or "hd" (DALL-E 3)
49
+ default_style: Optional[str] = None, # "vivid" or "natural" (DALL-E 3)
50
+ host_address: str = DALLE_API_HOST, # OpenAI API host
51
+ verify_ssl_certificate: bool = True,
52
+ **kwargs # To catch any other lollms_client specific params like service_key/client_id
53
+ ):
54
+ """
55
+ Initialize the DALL-E TTI binding.
56
+
57
+ Args:
58
+ api_key (Optional[str]): OpenAI API key. If None or empty, attempts to read
59
+ from the OPENAI_API_KEY environment variable.
60
+ model_name (str): Name of the DALL-E model to use (e.g., "dall-e-3", "dall-e-2").
61
+ default_size (Optional[str]): Default image size (e.g., "1024x1024").
62
+ If None, uses model's default.
63
+ default_quality (Optional[str]): Default image quality for DALL-E 3 ("standard", "hd").
64
+ If None, uses model's default if applicable.
65
+ default_style (Optional[str]): Default image style for DALL-E 3 ("vivid", "natural").
66
+ If None, uses model's default if applicable.
67
+ host_address (str): The API host address. Defaults to OpenAI's public API.
68
+ verify_ssl_certificate (bool): Whether to verify SSL certificates.
69
+ **kwargs: Catches other potential parameters like 'service_key' or 'client_id'.
70
+ """
71
+ super().__init__(binding_name="dalle")
72
+
73
+ resolved_api_key = api_key
74
+ if not resolved_api_key:
75
+ ASCIIColors.info(f"API key not provided directly, checking environment variable '{OPENAI_API_KEY_ENV_VAR}'...")
76
+ resolved_api_key = os.environ.get(OPENAI_API_KEY_ENV_VAR)
77
+
78
+ if not resolved_api_key:
79
+ raise ValueError(f"OpenAI API key is required. Provide it directly or set the '{OPENAI_API_KEY_ENV_VAR}' environment variable.")
80
+
81
+ self.api_key = resolved_api_key
82
+ self.host_address = host_address
83
+ self.verify_ssl_certificate = verify_ssl_certificate
84
+
85
+ if model_name not in DALLE_MODELS:
86
+ raise ValueError(f"Unsupported DALL-E model: {model_name}. Supported models: {list(DALLE_MODELS.keys())}")
87
+ self.model_name = model_name
88
+
89
+ model_props = DALLE_MODELS[self.model_name]
90
+
91
+ # Set defaults from model_props, overridden by user-provided defaults
92
+ self.current_size = default_size or model_props["default_size"]
93
+ if self.current_size not in model_props["sizes"]:
94
+ raise ValueError(f"Unsupported size '{self.current_size}' for model '{self.model_name}'. Supported sizes: {model_props['sizes']}")
95
+
96
+ if model_props["supports_quality"]:
97
+ self.current_quality = default_quality or model_props["default_quality"]
98
+ if self.current_quality not in model_props["qualities"]:
99
+ raise ValueError(f"Unsupported quality '{self.current_quality}' for model '{self.model_name}'. Supported qualities: {model_props['qualities']}")
100
+ else:
101
+ self.current_quality = None # Explicitly None if not supported
102
+
103
+ if model_props["supports_style"]:
104
+ self.current_style = default_style or model_props["default_style"]
105
+ if self.current_style not in model_props["styles"]:
106
+ raise ValueError(f"Unsupported style '{self.current_style}' for model '{self.model_name}'. Supported styles: {model_props['styles']}")
107
+ else:
108
+ self.current_style = None # Explicitly None if not supported
109
+
110
+ # For potential lollms client specific features, if `service_key` is passed as `client_id`
111
+ self.client_id = kwargs.get("service_key", kwargs.get("client_id", "dalle_client_user"))
112
+
113
+
114
+ def _get_model_properties(self, model_name: Optional[str] = None) -> Dict[str, Any]:
115
+ """Helper to get properties for a given model name, or the instance's current model."""
116
+ return DALLE_MODELS.get(model_name or self.model_name, {})
117
+
118
+ def generate_image(self,
119
+ prompt: str,
120
+ negative_prompt: Optional[str] = "",
121
+ width: int = 1024, # Default width
122
+ height: int = 1024, # Default height
123
+ **kwargs) -> bytes:
124
+ """
125
+ Generates image data using the DALL-E API.
126
+
127
+ Args:
128
+ prompt (str): The positive text prompt.
129
+ negative_prompt (Optional[str]): The negative prompt. For DALL-E 3, this is
130
+ appended to the main prompt. For DALL-E 2, it's ignored.
131
+ width (int): Image width.
132
+ height (int): Image height.
133
+ **kwargs: Additional parameters:
134
+ - model (str): Override the instance's default model for this call.
135
+ - quality (str): Override quality ("standard", "hd" for DALL-E 3).
136
+ - style (str): Override style ("vivid", "natural" for DALL-E 3).
137
+ - n (int): Number of images to generate (OpenAI supports >1, but this binding returns one). Default 1.
138
+ - user (str): A unique identifier for your end-user (OpenAI abuse monitoring).
139
+ Returns:
140
+ bytes: The generated image data (PNG format from DALL-E).
141
+
142
+ Raises:
143
+ Exception: If the request fails or image generation fails on the server.
144
+ """
145
+ model_override = kwargs.get("model")
146
+ active_model_name = model_override if model_override else self.model_name
147
+
148
+ model_props = self._get_model_properties(active_model_name)
149
+ if not model_props:
150
+ raise ValueError(f"Model {active_model_name} properties not found. Supported: {list(DALLE_MODELS.keys())}")
151
+
152
+ # Format size string and validate against the active model for this generation
153
+ size_str = f"{width}x{height}"
154
+ if size_str not in model_props["sizes"]:
155
+ raise ValueError(f"Unsupported size '{size_str}' for model '{active_model_name}'. Supported sizes: {model_props['sizes']}. Adjust width/height for this model.")
156
+
157
+ # Handle prompt and negative prompt based on the active model
158
+ final_prompt = prompt
159
+ if active_model_name == "dall-e-3" and negative_prompt:
160
+ final_prompt = f"{prompt}. Avoid: {negative_prompt}."
161
+ ASCIIColors.info(f"DALL-E 3: Appended negative prompt. Final prompt: '{final_prompt[:100]}...'")
162
+ elif active_model_name == "dall-e-2" and negative_prompt:
163
+ ASCIIColors.warning("DALL-E 2 does not support negative_prompt. It will be ignored.")
164
+
165
+ # Truncate prompt if too long for the active model
166
+ max_len = model_props.get("max_prompt_length", 4000)
167
+ if len(final_prompt) > max_len:
168
+ ASCIIColors.warning(f"Prompt for {active_model_name} is too long ({len(final_prompt)} chars). Truncating to {max_len} characters.")
169
+ final_prompt = final_prompt[:max_len]
170
+
171
+ endpoint = f"{self.host_address}/images/generations"
172
+ headers = {
173
+ "Authorization": f"Bearer {self.api_key}",
174
+ "Content-Type": "application/json"
175
+ }
176
+
177
+ payload = {
178
+ "model": active_model_name,
179
+ "prompt": final_prompt,
180
+ "n": kwargs.get("n", 1), # This binding expects to return one image
181
+ "size": size_str,
182
+ "response_format": "b64_json" # Request base64 encoded image
183
+ }
184
+
185
+ # Add model-specific parameters (quality, style)
186
+ # Use kwargs if provided, otherwise instance defaults, but only if the active model supports them.
187
+ if model_props["supports_quality"]:
188
+ payload["quality"] = kwargs.get("quality", self.current_quality)
189
+ if model_props["supports_style"]:
190
+ payload["style"] = kwargs.get("style", self.current_style)
191
+
192
+ if "user" in kwargs: # Pass user param if provided for moderation
193
+ payload["user"] = kwargs["user"]
194
+
195
+ try:
196
+ response = requests.post(endpoint, json=payload, headers=headers, verify=self.verify_ssl_certificate)
197
+ response.raise_for_status() # Raise HTTPError for bad responses (4xx or 5xx)
198
+
199
+ response_json = response.json()
200
+
201
+ if not response_json.get("data") or not response_json["data"][0].get("b64_json"):
202
+ raise Exception("Server did not return image data in expected b64_json format.")
203
+
204
+ img_base64 = response_json["data"][0]["b64_json"]
205
+ img_bytes = base64.b64decode(img_base64)
206
+ return img_bytes
207
+
208
+ except requests.exceptions.HTTPError as e:
209
+ error_detail = "Unknown server error"
210
+ if e.response is not None:
211
+ try:
212
+ err_json = e.response.json()
213
+ if "error" in err_json and isinstance(err_json["error"], dict) and "message" in err_json["error"]:
214
+ error_detail = err_json["error"]["message"]
215
+ elif "detail" in err_json: # Fallback for other error structures
216
+ error_detail = err_json["detail"]
217
+ else: # If no specific error message, use raw text (limited)
218
+ error_detail = e.response.text[:500]
219
+ except (json.JSONDecodeError, requests.exceptions.JSONDecodeError): # If response is not JSON
220
+ error_detail = e.response.text[:500]
221
+ trace_exception(e)
222
+ raise Exception(f"HTTP request failed: {e.response.status_code} {e.response.reason} - Detail: {error_detail}") from e
223
+ else: # HTTPError without a response object (less common)
224
+ trace_exception(e)
225
+ raise Exception(f"HTTP request failed without a response body: {e}") from e
226
+ except requests.exceptions.RequestException as e: # Catches other network errors (DNS, ConnectionError, etc.)
227
+ trace_exception(e)
228
+ raise Exception(f"Request failed due to network issue: {e}") from e
229
+ except Exception as e: # Catches other errors (e.g., base64 decoding, unexpected issues)
230
+ trace_exception(e)
231
+ raise Exception(f"Image generation process failed: {e}") from e
232
+
233
+
234
+ def list_services(self, **kwargs) -> List[Dict[str, str]]:
235
+ """
236
+ Lists available DALL-E models supported by this binding.
237
+ `client_id` from kwargs is ignored as DALL-E auth is via API key.
238
+ """
239
+ services = []
240
+ for model_name, props in DALLE_MODELS.items():
241
+ caption = f"OpenAI {model_name.upper()}"
242
+ help_text = f"Size options: {', '.join(props['sizes'])}. "
243
+ if props["supports_quality"]:
244
+ help_text += f"Qualities: {', '.join(props['qualities'])}. "
245
+ if props["supports_style"]:
246
+ help_text += f"Styles: {', '.join(props['styles'])}. "
247
+ services.append({
248
+ "name": model_name,
249
+ "caption": caption,
250
+ "help": help_text.strip()
251
+ })
252
+ return services
253
+
254
+ def get_settings(self, **kwargs) -> List[Dict[str, Any]]:
255
+ """
256
+ Retrieves the current configurable default settings for the DALL-E binding.
257
+ `client_id` from kwargs is ignored. Returns settings in a ConfigTemplate-like format.
258
+ """
259
+ model_props = self._get_model_properties(self.model_name) # Settings relative to current default model
260
+
261
+ settings = [
262
+ {
263
+ "name": "model_name",
264
+ "type": "str",
265
+ "value": self.model_name,
266
+ "description": "Default DALL-E model for generation.",
267
+ "options": list(DALLE_MODELS.keys()),
268
+ "category": "Model Configuration"
269
+ },
270
+ {
271
+ "name": "current_size",
272
+ "type": "str",
273
+ "value": self.current_size,
274
+ "description": "Default image size (e.g., 1024x1024). Format: widthxheight.",
275
+ "options": model_props.get("sizes", []), # Options relevant to the current default model
276
+ "category": "Image Generation Defaults"
277
+ }
278
+ ]
279
+
280
+ if model_props.get("supports_quality", False):
281
+ settings.append({
282
+ "name": "current_quality",
283
+ "type": "str",
284
+ "value": self.current_quality,
285
+ "description": "Default image quality (e.g., 'standard', 'hd' for DALL-E 3).",
286
+ "options": model_props.get("qualities", []),
287
+ "category": "Image Generation Defaults"
288
+ })
289
+
290
+ if model_props.get("supports_style", False):
291
+ settings.append({
292
+ "name": "current_style",
293
+ "type": "str",
294
+ "value": self.current_style,
295
+ "description": "Default image style (e.g., 'vivid', 'natural' for DALL-E 3).",
296
+ "options": model_props.get("styles", []),
297
+ "category": "Image Generation Defaults"
298
+ })
299
+
300
+ settings.append({
301
+ "name": "api_key_status",
302
+ "type": "str",
303
+ "value": "Set (loaded)" if self.api_key else "Not Set", # Indicate if API key is present
304
+ "description": f"OpenAI API Key status (set at initialization or via '{OPENAI_API_KEY_ENV_VAR}', not changeable here).",
305
+ "category": "Authentication",
306
+ "read_only": True # Custom attribute indicating it's informational
307
+ })
308
+
309
+ return settings
310
+
311
+
312
+ def set_settings(self, settings: Union[Dict[str, Any], List[Dict[str, Any]]], **kwargs) -> bool:
313
+ """
314
+ Applies new default settings to the DALL-E binding instance.
315
+ `client_id` from kwargs is ignored.
316
+
317
+ Args:
318
+ settings (Union[Dict[str, Any], List[Dict[str, Any]]]):
319
+ New settings to apply.
320
+ Can be a flat dict: `{"model_name": "dall-e-2", "current_size": "512x512"}`
321
+ Or a list of dicts (ConfigTemplate format):
322
+ `[{"name": "model_name", "value": "dall-e-2"}, ...]`
323
+
324
+ Returns:
325
+ bool: True if at least one setting was successfully applied, False otherwise.
326
+ """
327
+ applied_some_settings = False
328
+ original_model_name = self.model_name # To detect if model changes
329
+
330
+ # Normalize settings input to a flat dictionary
331
+ if isinstance(settings, list):
332
+ parsed_settings = {}
333
+ for item in settings:
334
+ if isinstance(item, dict) and "name" in item and "value" in item:
335
+ if item["name"] == "api_key_status": # This is read-only
336
+ continue
337
+ parsed_settings[item["name"]] = item["value"]
338
+ settings_dict = parsed_settings
339
+ elif isinstance(settings, dict):
340
+ settings_dict = settings
341
+ else:
342
+ ASCIIColors.error("Invalid settings format. Expected a dictionary or list of dictionaries.")
343
+ return False
344
+
345
+ try:
346
+ # Phase 1: Apply model_name change if present, as it affects other settings' validity
347
+ if "model_name" in settings_dict:
348
+ new_model_name = settings_dict["model_name"]
349
+ if new_model_name not in DALLE_MODELS:
350
+ ASCIIColors.warning(f"Invalid model_name '{new_model_name}' provided in settings. Keeping current model '{self.model_name}'.")
351
+ elif self.model_name != new_model_name:
352
+ self.model_name = new_model_name
353
+ ASCIIColors.info(f"Default model changed to: {self.model_name}")
354
+ applied_some_settings = True # Mark that model name was processed
355
+
356
+ # Phase 2: If model changed, or for initial setup, adjust dependent settings to be consistent
357
+ # Run this phase if model_name was specifically in settings_dict and changed, OR if this is the first time settings are processed.
358
+ # The 'applied_some_settings' flag after model_name processing indicates a change.
359
+ if "model_name" in settings_dict and applied_some_settings :
360
+ new_model_props = self._get_model_properties(self.model_name)
361
+
362
+ # Update current_size if invalid for new model or if model changed
363
+ if self.current_size not in new_model_props["sizes"]:
364
+ old_val = self.current_size
365
+ self.current_size = new_model_props["default_size"]
366
+ if old_val != self.current_size: ASCIIColors.info(f"Default size reset to '{self.current_size}' for model '{self.model_name}'.")
367
+
368
+ # Update current_quality
369
+ if new_model_props["supports_quality"]:
370
+ if self.current_quality not in new_model_props.get("qualities", []):
371
+ old_val = self.current_quality
372
+ self.current_quality = new_model_props["default_quality"]
373
+ if old_val != self.current_quality: ASCIIColors.info(f"Default quality reset to '{self.current_quality}' for model '{self.model_name}'.")
374
+ elif self.current_quality is not None: # New model doesn't support quality
375
+ self.current_quality = None
376
+ ASCIIColors.info(f"Quality setting removed as model '{self.model_name}' does not support it.")
377
+
378
+ # Update current_style
379
+ if new_model_props["supports_style"]:
380
+ if self.current_style not in new_model_props.get("styles", []):
381
+ old_val = self.current_style
382
+ self.current_style = new_model_props["default_style"]
383
+ if old_val != self.current_style: ASCIIColors.info(f"Default style reset to '{self.current_style}' for model '{self.model_name}'.")
384
+ elif self.current_style is not None: # New model doesn't support style
385
+ self.current_style = None
386
+ ASCIIColors.info(f"Style setting removed as model '{self.model_name}' does not support it.")
387
+
388
+ # Phase 3: Apply other specific settings from input, validating against the (potentially new) model
389
+ current_model_props = self._get_model_properties(self.model_name) # Re-fetch props if model changed
390
+
391
+ if "current_size" in settings_dict:
392
+ new_size = settings_dict["current_size"]
393
+ if new_size not in current_model_props["sizes"]:
394
+ ASCIIColors.warning(f"Invalid size '{new_size}' for model '{self.model_name}'. Keeping '{self.current_size}'. Supported: {current_model_props['sizes']}")
395
+ elif self.current_size != new_size:
396
+ self.current_size = new_size
397
+ ASCIIColors.info(f"Default size set to: {self.current_size}")
398
+ applied_some_settings = True
399
+
400
+ if "current_quality" in settings_dict:
401
+ if current_model_props["supports_quality"]:
402
+ new_quality = settings_dict["current_quality"]
403
+ if new_quality not in current_model_props["qualities"]:
404
+ ASCIIColors.warning(f"Invalid quality '{new_quality}' for model '{self.model_name}'. Keeping '{self.current_quality}'. Supported: {current_model_props['qualities']}")
405
+ elif self.current_quality != new_quality:
406
+ self.current_quality = new_quality
407
+ ASCIIColors.info(f"Default quality set to: {self.current_quality}")
408
+ applied_some_settings = True
409
+ elif "current_quality" in settings_dict: # Only warn if user explicitly tried to set it
410
+ ASCIIColors.warning(f"Model '{self.model_name}' does not support quality. Ignoring 'current_quality' setting.")
411
+
412
+ if "current_style" in settings_dict:
413
+ if current_model_props["supports_style"]:
414
+ new_style = settings_dict["current_style"]
415
+ if new_style not in current_model_props["styles"]:
416
+ ASCIIColors.warning(f"Invalid style '{new_style}' for model '{self.model_name}'. Keeping '{self.current_style}'. Supported: {current_model_props['styles']}")
417
+ elif self.current_style != new_style:
418
+ self.current_style = new_style
419
+ ASCIIColors.info(f"Default style set to: {self.current_style}")
420
+ applied_some_settings = True
421
+ elif "current_style" in settings_dict: # Only warn if user explicitly tried to set it
422
+ ASCIIColors.warning(f"Model '{self.model_name}' does not support style. Ignoring 'current_style' setting.")
423
+
424
+ if "api_key" in settings_dict: # Should not be settable here
425
+ ASCIIColors.warning("API key cannot be changed after initialization via set_settings. This setting was ignored.")
426
+
427
+ return applied_some_settings
428
+
429
+ except Exception as e:
430
+ trace_exception(e)
431
+ ASCIIColors.error(f"Failed to apply settings due to an unexpected error: {e}")
432
+ return False