ai-microcore 5.0.0.dev7__tar.gz → 5.0.1__tar.gz

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.
Files changed (47) hide show
  1. {ai_microcore-5.0.0.dev7 → ai_microcore-5.0.1}/PKG-INFO +51 -13
  2. {ai_microcore-5.0.0.dev7 → ai_microcore-5.0.1}/README.md +50 -12
  3. {ai_microcore-5.0.0.dev7 → ai_microcore-5.0.1}/microcore/__init__.py +2 -2
  4. {ai_microcore-5.0.0.dev7 → ai_microcore-5.0.1}/microcore/_llm_functions.py +14 -4
  5. {ai_microcore-5.0.0.dev7 → ai_microcore-5.0.1}/microcore/ai_func/__init__.py +2 -1
  6. {ai_microcore-5.0.0.dev7 → ai_microcore-5.0.1}/microcore/file_storage.py +17 -14
  7. {ai_microcore-5.0.0.dev7 → ai_microcore-5.0.1}/microcore/images.py +1 -1
  8. {ai_microcore-5.0.0.dev7 → ai_microcore-5.0.1}/microcore/llm/google_genai.py +5 -3
  9. {ai_microcore-5.0.0.dev7 → ai_microcore-5.0.1}/microcore/llm/openai.py +64 -16
  10. {ai_microcore-5.0.0.dev7 → ai_microcore-5.0.1}/LICENSE +0 -0
  11. {ai_microcore-5.0.0.dev7 → ai_microcore-5.0.1}/microcore/_env.py +0 -0
  12. {ai_microcore-5.0.0.dev7 → ai_microcore-5.0.1}/microcore/_prepare_llm_args.py +0 -0
  13. {ai_microcore-5.0.0.dev7 → ai_microcore-5.0.1}/microcore/ai_func/ai-func.json.j2 +0 -0
  14. {ai_microcore-5.0.0.dev7 → ai_microcore-5.0.1}/microcore/ai_func/ai-func.pythonic.j2 +0 -0
  15. {ai_microcore-5.0.0.dev7 → ai_microcore-5.0.1}/microcore/ai_func/ai-func.tag.j2 +0 -0
  16. {ai_microcore-5.0.0.dev7 → ai_microcore-5.0.1}/microcore/ai_modules.py +0 -0
  17. {ai_microcore-5.0.0.dev7 → ai_microcore-5.0.1}/microcore/configuration.py +0 -0
  18. {ai_microcore-5.0.0.dev7 → ai_microcore-5.0.1}/microcore/embedding_db/__init__.py +0 -0
  19. {ai_microcore-5.0.0.dev7 → ai_microcore-5.0.1}/microcore/embedding_db/chromadb.py +0 -0
  20. {ai_microcore-5.0.0.dev7 → ai_microcore-5.0.1}/microcore/embedding_db/qdrant.py +0 -0
  21. {ai_microcore-5.0.0.dev7 → ai_microcore-5.0.1}/microcore/file_cache.py +0 -0
  22. {ai_microcore-5.0.0.dev7 → ai_microcore-5.0.1}/microcore/interactive_setup.py +0 -0
  23. {ai_microcore-5.0.0.dev7 → ai_microcore-5.0.1}/microcore/json_parsing.py +0 -0
  24. {ai_microcore-5.0.0.dev7 → ai_microcore-5.0.1}/microcore/llm/__init__.py +0 -0
  25. {ai_microcore-5.0.0.dev7 → ai_microcore-5.0.1}/microcore/llm/anthropic.py +0 -0
  26. {ai_microcore-5.0.0.dev7 → ai_microcore-5.0.1}/microcore/llm/local_llm.py +0 -0
  27. {ai_microcore-5.0.0.dev7 → ai_microcore-5.0.1}/microcore/llm/local_transformers.py +0 -0
  28. {ai_microcore-5.0.0.dev7 → ai_microcore-5.0.1}/microcore/llm/shared.py +0 -0
  29. {ai_microcore-5.0.0.dev7 → ai_microcore-5.0.1}/microcore/llm_backends.py +0 -0
  30. {ai_microcore-5.0.0.dev7 → ai_microcore-5.0.1}/microcore/lm_client.py +0 -0
  31. {ai_microcore-5.0.0.dev7 → ai_microcore-5.0.1}/microcore/logging.py +0 -0
  32. {ai_microcore-5.0.0.dev7 → ai_microcore-5.0.1}/microcore/mcp.py +0 -0
  33. {ai_microcore-5.0.0.dev7 → ai_microcore-5.0.1}/microcore/message_types.py +0 -0
  34. {ai_microcore-5.0.0.dev7 → ai_microcore-5.0.1}/microcore/metrics.py +0 -0
  35. {ai_microcore-5.0.0.dev7 → ai_microcore-5.0.1}/microcore/presets.py +0 -0
  36. {ai_microcore-5.0.0.dev7 → ai_microcore-5.0.1}/microcore/python.py +0 -0
  37. {ai_microcore-5.0.0.dev7 → ai_microcore-5.0.1}/microcore/templating/__init__.py +0 -0
  38. {ai_microcore-5.0.0.dev7 → ai_microcore-5.0.1}/microcore/templating/jinja2.py +0 -0
  39. {ai_microcore-5.0.0.dev7 → ai_microcore-5.0.1}/microcore/text2speech/elevenlabs.py +0 -0
  40. {ai_microcore-5.0.0.dev7 → ai_microcore-5.0.1}/microcore/tokenizing.py +0 -0
  41. {ai_microcore-5.0.0.dev7 → ai_microcore-5.0.1}/microcore/types.py +0 -0
  42. {ai_microcore-5.0.0.dev7 → ai_microcore-5.0.1}/microcore/ui.py +0 -0
  43. {ai_microcore-5.0.0.dev7 → ai_microcore-5.0.1}/microcore/utils.py +0 -0
  44. {ai_microcore-5.0.0.dev7 → ai_microcore-5.0.1}/microcore/wrappers/__init__.py +0 -0
  45. {ai_microcore-5.0.0.dev7 → ai_microcore-5.0.1}/microcore/wrappers/llm_response_wrapper.py +0 -0
  46. {ai_microcore-5.0.0.dev7 → ai_microcore-5.0.1}/microcore/wrappers/prompt_wrapper.py +0 -0
  47. {ai_microcore-5.0.0.dev7 → ai_microcore-5.0.1}/pyproject.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ai-microcore
3
- Version: 5.0.0.dev7
3
+ Version: 5.0.1
4
4
  Summary: # Minimalistic Foundation for AI Applications
5
5
  Keywords: llm,large language models,ai,similarity search,ai search,gpt,openai,framework,adapter,anthropic,google gemini,google vertex ai
6
6
  Author-email: Vitalii Stepanenko <mail@vitaliy.in>
@@ -116,21 +116,14 @@ For the full list of available configuration options, you may also check [`micro
116
116
 
117
117
  ### Installing vendor-specific packages
118
118
  For models working not via OpenAI API, you may need to install additional packages:
119
- #### Anthropic Claude 3
119
+ #### Anthropic Claude
120
120
  ```bash
121
121
  pip install anthropic
122
122
  ```
123
- #### Google Gemini via AI Studio
123
+ #### Google Gemini via AI Studio or Vertex AI
124
124
  ```bash
125
- pip install google-generativeai
125
+ pip install google-genai
126
126
  ```
127
- #### Google Gemini via Vertex AI
128
- ```bash
129
- pip install vertexai
130
- ```
131
- 📌Additionally for working through [Vertex AI](https://cloud.google.com/vertex-ai) you need to
132
- [install the Google Cloud CLI](https://cloud.google.com/sdk/docs/install)
133
- and [configure the authorization](https://cloud.google.com/sdk/docs/authorizing).
134
127
 
135
128
  #### Local language models via Hugging Face Transformers
136
129
 
@@ -207,7 +200,7 @@ ai_response = llm('What is your model name?')
207
200
  # - For chat completion models elements are treated as separate messages
208
201
  # - For completion LLMs elements are treated as text lines
209
202
  llm(['1+2', '='])
210
- llm('1+2=', model='gpt-4')
203
+ llm('1+2=', model='gpt-5.2')
211
204
 
212
205
  # To specify a message role, you can use dictionary or classes
213
206
  llm(dict(role='system', content='1+2='))
@@ -314,8 +307,53 @@ Text generation using HF/Transformers model locally (example with Qwen 3 0.6B).
314
307
  #### [Other examples](https://github.com/llm-microcore/microcore/tree/main/examples)
315
308
 
316
309
  ## Python functions as AI tools
310
+ *Usage Example*:
311
+ ```python
312
+ from microcore.ai_func import ai_func
313
+
314
+ @ai_func
315
+ def search_products(
316
+ query: str,
317
+ category: str = "all",
318
+ max_results: int = 10,
319
+ in_stock_only: bool = False
320
+ ):
321
+ """
322
+ Search for products in the catalog.
323
+
324
+ Args:
325
+ query: Search terms to find matching products
326
+ category: Product category to filter by (e.g., "electronics", "clothing")
327
+ max_results: Maximum number of results to return
328
+ in_stock_only: If True, only return products currently in stock
329
+
330
+ Returns:
331
+ List of matching products with name, price, and availability
332
+ """
333
+ # Implementation would go here
334
+ pass
335
+ ```
336
+ *Output*:
337
+ ```
338
+ # Search for products in the catalog.
339
+
340
+ Args:
341
+ query: Search terms to find matching products
342
+ category: Product category to filter by (e.g., "electronics", "clothing")
343
+ max_results: Maximum number of results to return
344
+ in_stock_only: If True, only return products currently in stock
345
+
346
+ Returns:
347
+ List of matching products with name, price, and availability
348
+ {
349
+ "call": "search_products",
350
+ "query": <str>,
351
+ "category": <str> (default = "all"),
352
+ "max_results": <int> (default = 10),
353
+ "in_stock_only": <bool> (default = False)
354
+ }
317
355
 
318
- @TODO
356
+ ```
319
357
 
320
358
  ## 🤖 AI Modules
321
359
  **This is an experimental feature.**
@@ -81,21 +81,14 @@ For the full list of available configuration options, you may also check [`micro
81
81
 
82
82
  ### Installing vendor-specific packages
83
83
  For models working not via OpenAI API, you may need to install additional packages:
84
- #### Anthropic Claude 3
84
+ #### Anthropic Claude
85
85
  ```bash
86
86
  pip install anthropic
87
87
  ```
88
- #### Google Gemini via AI Studio
88
+ #### Google Gemini via AI Studio or Vertex AI
89
89
  ```bash
90
- pip install google-generativeai
90
+ pip install google-genai
91
91
  ```
92
- #### Google Gemini via Vertex AI
93
- ```bash
94
- pip install vertexai
95
- ```
96
- 📌Additionally for working through [Vertex AI](https://cloud.google.com/vertex-ai) you need to
97
- [install the Google Cloud CLI](https://cloud.google.com/sdk/docs/install)
98
- and [configure the authorization](https://cloud.google.com/sdk/docs/authorizing).
99
92
 
100
93
  #### Local language models via Hugging Face Transformers
101
94
 
@@ -172,7 +165,7 @@ ai_response = llm('What is your model name?')
172
165
  # - For chat completion models elements are treated as separate messages
173
166
  # - For completion LLMs elements are treated as text lines
174
167
  llm(['1+2', '='])
175
- llm('1+2=', model='gpt-4')
168
+ llm('1+2=', model='gpt-5.2')
176
169
 
177
170
  # To specify a message role, you can use dictionary or classes
178
171
  llm(dict(role='system', content='1+2='))
@@ -279,8 +272,53 @@ Text generation using HF/Transformers model locally (example with Qwen 3 0.6B).
279
272
  #### [Other examples](https://github.com/llm-microcore/microcore/tree/main/examples)
280
273
 
281
274
  ## Python functions as AI tools
275
+ *Usage Example*:
276
+ ```python
277
+ from microcore.ai_func import ai_func
278
+
279
+ @ai_func
280
+ def search_products(
281
+ query: str,
282
+ category: str = "all",
283
+ max_results: int = 10,
284
+ in_stock_only: bool = False
285
+ ):
286
+ """
287
+ Search for products in the catalog.
288
+
289
+ Args:
290
+ query: Search terms to find matching products
291
+ category: Product category to filter by (e.g., "electronics", "clothing")
292
+ max_results: Maximum number of results to return
293
+ in_stock_only: If True, only return products currently in stock
294
+
295
+ Returns:
296
+ List of matching products with name, price, and availability
297
+ """
298
+ # Implementation would go here
299
+ pass
300
+ ```
301
+ *Output*:
302
+ ```
303
+ # Search for products in the catalog.
304
+
305
+ Args:
306
+ query: Search terms to find matching products
307
+ category: Product category to filter by (e.g., "electronics", "clothing")
308
+ max_results: Maximum number of results to return
309
+ in_stock_only: If True, only return products currently in stock
310
+
311
+ Returns:
312
+ List of matching products with name, price, and availability
313
+ {
314
+ "call": "search_products",
315
+ "query": <str>,
316
+ "category": <str> (default = "all"),
317
+ "max_results": <int> (default = 10),
318
+ "in_stock_only": <bool> (default = False)
319
+ }
282
320
 
283
- @TODO
321
+ ```
284
322
 
285
323
  ## 🤖 AI Modules
286
324
  **This is an experimental feature.**
@@ -79,7 +79,7 @@ def model_names() -> list[str]:
79
79
  Return a list of available model names from the default LLM client.
80
80
  """
81
81
  if env().default_client is None:
82
- raise ValueError("No default LLM client supporting models list configured.")
82
+ raise ValueError("No default LLM client is configured.")
83
83
  return env().default_client.model_names()
84
84
 
85
85
 
@@ -231,4 +231,4 @@ __all__ = [
231
231
  # "wrappers",
232
232
  ]
233
233
 
234
- __version__ = "5.0.0.dev7"
234
+ __version__ = "5.0.1"
@@ -267,9 +267,7 @@ def llm(
267
267
  save_cache(cache_name, response)
268
268
  [h(response) for h in env().llm_after_handlers]
269
269
  if tries > 0:
270
- retry_params = dict(**kwargs)
271
- retry_params["retries"] = tries - 1
272
- setattr(response, "_retry_callback", lambda: llm(prompt, **retry_params))
270
+ setattr(response, "_retry_callback", lambda: llm(prompt, retries=tries - 1, **kwargs))
273
271
  if parse_json:
274
272
  parsing_params = parse_json if isinstance(parse_json, dict) else {}
275
273
  return response.parse_json(**parsing_params)
@@ -294,6 +292,13 @@ async def allm(
294
292
  parse_json (bool|dict):
295
293
  If True, parses response as JSON,
296
294
  alternatively non-empty dict can be used as parse_json arguments.
295
+ Default is False (no parsing).
296
+ file_cache (bool | str):
297
+ If True or non-empty string, enables file caching of LLM responses.
298
+ If string, it will be used as cache prefix.
299
+ When enabled, identical requests with identical parameters
300
+ will return cached responses instead of making new API calls.
301
+ Default is False (no caching).
297
302
  **kwargs: Parameters supported by the LLM API.
298
303
 
299
304
  See parameters supported by the OpenAI:
@@ -374,8 +379,13 @@ async def allm(
374
379
  if file_cache:
375
380
  delete_cache(cache_name)
376
381
  return await allm(
377
- prompt, retries=tries - 1, parse_json=parse_json, **kwargs
382
+ prompt,
383
+ retries=tries - 1,
384
+ parse_json=parse_json,
385
+ file_cache=file_cache,
386
+ **kwargs
378
387
  )
388
+ raise e
379
389
  return response
380
390
 
381
391
 
@@ -80,7 +80,7 @@ def func_metadata(func, name=None) -> Dict[str, Any]:
80
80
 
81
81
  # Add descriptions from parsed docstring to parameters
82
82
  for param in parsed_docstring.params:
83
- if param.arg_name in metadata.get("args", []):
83
+ if param.arg_name in metadata.get("args", {}):
84
84
  metadata["args"][param.arg_name]["docstr"] = param.description
85
85
 
86
86
  return metadata
@@ -190,6 +190,7 @@ def extract_tag_tool_params(
190
190
  if len(tags) > 1:
191
191
  if raise_errors:
192
192
  raise ValueError("Response contains multiple tags when only one expected")
193
+ logging.warning("Response contains multiple tags, but only the first one will be used.")
193
194
  tag, attrs, content = tags[0]
194
195
  return tag, [content], attrs
195
196
 
@@ -1,5 +1,15 @@
1
1
  """
2
- File storage functions
2
+ File storage functionality.
3
+
4
+
5
+ Provides a Storage class for file operations within a configured storage directory.
6
+ Supports automatic file numbering, backups, JSON serialization, encoding detection,
7
+ file copying, directory listing, etc.
8
+
9
+ Usage:
10
+ from microcore import storage
11
+ storage.write("data.txt", "content")
12
+ content = storage.read("data.txt")
3
13
  """
4
14
 
5
15
  import fnmatch
@@ -15,6 +25,7 @@ from ._env import config
15
25
  from .utils import file_link, list_files
16
26
 
17
27
  _missing = object()
28
+ """Sentinel value to distinguish between None and 'not provided'."""
18
29
 
19
30
 
20
31
  @dataclass
@@ -224,8 +235,6 @@ class Storage:
224
235
  if isinstance(content, bytes):
225
236
  if encoding is not None:
226
237
  logging.warning("Encoding is ignored when writing bytes content")
227
- if append:
228
- raise ValueError("Cannot append bytes content")
229
238
 
230
239
  if rewrite_existing is None:
231
240
  rewrite_existing = True
@@ -268,18 +277,12 @@ class Storage:
268
277
  if file_name != fn_incremented:
269
278
  os.rename(self.path / file_name, self.path / fn_incremented)
270
279
  (self.path / file_name).parent.mkdir(parents=True, exist_ok=True)
271
- if append:
272
- with (self.path / file_name).open(
273
- mode="a",
274
- encoding=encoding if not isinstance(content, bytes) else None,
275
- ) as file:
276
- file.write(content)
280
+ if isinstance(content, bytes):
281
+ with (self.path / file_name).open(mode="ab" if append else "wb") as f:
282
+ f.write(content)
277
283
  else:
278
- if isinstance(content, bytes):
279
- with (self.path / file_name).open(mode="wb") as file:
280
- file.write(content)
281
- else:
282
- (self.path / file_name).write_text(content, encoding=encoding)
284
+ with (self.path / file_name).open(mode="a" if append else "w", encoding=encoding) as f:
285
+ f.write(content)
283
286
  return file_name
284
287
 
285
288
  def clean(self, path: str | Path):
@@ -87,7 +87,7 @@ class FileImage(ImageInterface):
87
87
 
88
88
 
89
89
  class FileImageList(ImageListInterface):
90
- files: list[str] | None
90
+ files: list[str] | None = None
91
91
 
92
92
  def images(self) -> list[FileImage]:
93
93
  if not self.files:
@@ -163,8 +163,9 @@ class AsyncGoogleClient(BaseAsyncAIClient):
163
163
  def __init__(self, client: GoogleClient):
164
164
  self.sync_client = client
165
165
 
166
- async def load_models(self) -> dict:
167
- raise NotImplementedError
166
+ async def load_models(self, **kwargs) -> dict:
167
+ models = await self.sync_client.genai_client.aio.models.list(**kwargs)
168
+ return {model.name: model for model in models}
168
169
 
169
170
  async def generate(
170
171
  self,
@@ -221,6 +222,7 @@ class _GenerationContext:
221
222
  model_name = kwargs.pop("model", client.config.MODEL)
222
223
  callbacks = prepare_callbacks(client.config, kwargs, set_stream=False)
223
224
  is_image = is_image_model(model_name)
225
+ stream = kwargs.pop("stream", False) or (callbacks and not is_image)
224
226
  return _GenerationContext(
225
227
  model_name=model_name,
226
228
  save=kwargs.pop("save", True),
@@ -230,7 +232,7 @@ class _GenerationContext:
230
232
  genai_client=client.genai_client,
231
233
  config=client.config,
232
234
  is_image_model=is_image,
233
- stream=callbacks and not is_image
235
+ stream=stream,
234
236
  )
235
237
 
236
238
 
@@ -1,11 +1,12 @@
1
1
  import asyncio
2
2
  import base64
3
+ from typing import Any
3
4
 
4
5
  import openai
5
6
  from openai.types import CompletionChoice, ImagesResponse
6
7
 
7
8
  from ..lm_client import BaseAIChatClient, BaseAsyncAIClient
8
- from ..message_types import TMsgContentPart
9
+ from ..message_types import TMsgContentPart, TMsgContent
9
10
  from ..configuration import Config
10
11
  from ..llm_backends import ApiPlatform
11
12
  from .._prepare_llm_args import prepare_prompt
@@ -42,8 +43,7 @@ class AsyncOpenAIClient(BaseAsyncAIClient):
42
43
  config = self.sync_client.config
43
44
  args, options = _prepare_llm_arguments(config, kwargs)
44
45
  if is_image_model(args["model"]):
45
- # Note: image generation is synchronous
46
- return _generate_image(
46
+ return await _generate_image_async(
47
47
  prompt,
48
48
  args,
49
49
  self.oai_client,
@@ -133,6 +133,18 @@ class OpenAIClient(BaseAIChatClient):
133
133
  return image_to_oai(img)
134
134
  return content_part
135
135
 
136
+ def _convert_message_content(self, message_content: TMsgContent) -> Any:
137
+ """
138
+ Convert the message content into a format suitable for the LLM inference chat API.
139
+ """
140
+ if isinstance(message_content, str):
141
+ # Prevent conversion of string content into dict(type=text, text=...)
142
+ # because Azure OpenAI fails with Error 400
143
+ # when passing "azure_search" data source like following:
144
+ # llm(..., extra_body={"data_sources"=[{"type": "azure_search",...}]})
145
+ return message_content
146
+ return super()._convert_message_content(message_content)
147
+
136
148
  def load_models(self, **kwargs) -> dict:
137
149
  models_iter = self.oai_client.models.list(**kwargs)
138
150
  return {model.id: model for model in models_iter}
@@ -294,12 +306,8 @@ def _oai_image_response_to_images(response: ImagesResponse) -> list[Image]:
294
306
  return images
295
307
 
296
308
 
297
- def _generate_image(
298
- prompt,
299
- args,
300
- connection: openai.OpenAI | openai.AsyncOpenAI,
301
- options
302
- ) -> ImageGenerationResponse | None:
309
+ def _prepare_image_generation(prompt, args):
310
+ """Prepare prompt and images for image generation (shared logic)."""
303
311
  def convert_input_image(image: ImageInterface):
304
312
  if isinstance(image, FileImage):
305
313
  return open(image.file, "rb")
@@ -333,6 +341,32 @@ def _generate_image(
333
341
  if save and args.get("response_format", "b64_json") != "b64_json":
334
342
  raise ValueError("Only 'b64_json' response format is supported.")
335
343
 
344
+ return prompt, images, save
345
+
346
+
347
+ def _image_generation_response(
348
+ response: ImagesResponse,
349
+ save: bool,
350
+ options: dict,
351
+ ) -> ImageGenerationResponse | None:
352
+ check_for_errors(response)
353
+ images = _oai_image_response_to_images(response)
354
+ response_attrs = response.__dict__.copy()
355
+ result = make_image_generation_response(images, save, response_attrs)
356
+ for cb in options["callbacks"]:
357
+ cb(result)
358
+ return result
359
+
360
+
361
+ def _generate_image(
362
+ prompt,
363
+ args,
364
+ connection: openai.OpenAI,
365
+ options
366
+ ) -> ImageGenerationResponse | None:
367
+ """Synchronous version of image generation."""
368
+ prompt, images, save = _prepare_image_generation(prompt, args)
369
+
336
370
  if not images:
337
371
  response: ImagesResponse = connection.images.generate(prompt=prompt, **args)
338
372
  else:
@@ -341,10 +375,24 @@ def _generate_image(
341
375
  prompt=prompt,
342
376
  **args
343
377
  )
344
- check_for_errors(response)
345
- images = _oai_image_response_to_images(response)
346
- response_attrs = response.__dict__.copy()
347
- result = make_image_generation_response(images, save, response_attrs)
348
- for cb in options["callbacks"]:
349
- cb(result)
350
- return result
378
+ return _image_generation_response(response, save, options)
379
+
380
+
381
+ async def _generate_image_async(
382
+ prompt,
383
+ args,
384
+ connection: openai.AsyncOpenAI,
385
+ options
386
+ ) -> ImageGenerationResponse | None:
387
+ """Asynchronous version of image generation."""
388
+ prompt, images, save = _prepare_image_generation(prompt, args)
389
+
390
+ if not images:
391
+ response: ImagesResponse = await connection.images.generate(prompt=prompt, **args)
392
+ else:
393
+ response: ImagesResponse = await connection.images.edit(
394
+ image=images,
395
+ prompt=prompt,
396
+ **args
397
+ )
398
+ return _image_generation_response(response, save, options)
File without changes