Compare commits

...
3 changed files with 111 additions and 7 deletions

No files matched your search

@@ -3,6 +3,7 @@ ImageAgent - A specialized agent class for generating images using AI models.
This class extends the base Agent class to provide specific functionality for image generation,
including support for different image models, sizes, and quality settings.
"""
import asyncio
from typing import Optional, Any, Dict, Union, List
from ..agent.agent import Agent
from pydantic import BaseModel, Field
@@ -180,8 +181,13 @@ class ImageAgent(Agent):
logging.error(error_msg)
raise
async def agenerate_image(self, prompt: str, **kwargs) -> Dict[str, Any]:
"""Async wrapper for generate_image."""
return self.generate_image(prompt, **kwargs)
"""Generate an image without blocking the event loop.
The underlying litellm image providers expose only synchronous APIs, so the
call is offloaded to a worker thread via ``asyncio.to_thread`` to keep other
async tasks responsive.
"""
return await asyncio.to_thread(self.generate_image, prompt, **kwargs)
# Aliases for consistency with other agents
def generate(self, prompt: str, **kwargs) -> Dict[str, Any]:
@@ -190,7 +196,7 @@ class ImageAgent(Agent):
async def agenerate(self, prompt: str, **kwargs) -> Dict[str, Any]:
"""Async alias for generate_image()."""
return self.generate_image(prompt, **kwargs)
return await self.agenerate_image(prompt, **kwargs)
def chat(self, prompt: str, **kwargs) -> Dict[str, Any]:
"""Generate an image from the prompt."""
@@ -302,8 +308,12 @@ class ImageAgent(Agent):
size: Optional[str] = None,
**kwargs
) -> Dict[str, Any]:
"""Async version of edit()."""
return self.edit(image, prompt, mask, n, size, **kwargs)
"""Edit an image without blocking the event loop.
``litellm.image_edit`` is synchronous, so it runs in a worker thread via
``asyncio.to_thread`` to keep concurrent async tasks responsive.
"""
return await asyncio.to_thread(self.edit, image, prompt, mask, n, size, **kwargs)
def variation(
self,
@@ -372,6 +382,9 @@ class ImageAgent(Agent):
size: Optional[str] = None,
**kwargs
) -> Dict[str, Any]:
"""Async version of variation()."""
return self.variation(image, n, size, **kwargs)
"""Generate image variations without blocking the event loop.
``litellm.image_variation`` is synchronous, so it runs in a worker thread via
``asyncio.to_thread`` to keep concurrent async tasks responsive.
"""
return await asyncio.to_thread(self.variation, image, n, size, **kwargs)
@@ -0,0 +1,41 @@
"""Opt-in real text inference alongside an image call with a local provider stub."""
import asyncio
import os
import threading
import pytest
@pytest.mark.live
@pytest.mark.asyncio
async def test_text_agent_runs_alongside_async_image_generation(monkeypatch):
from praisonaiagents import Agent, ImageAgent
loop = asyncio.get_running_loop()
released = threading.Event()
image_result = {"data": [{"url": "https://example.com/landscape.png"}]}
def image_provider(**kwargs):
assert kwargs["prompt"] == "A mountain landscape"
loop.call_soon_threadsafe(released.set)
if not released.wait(timeout=5):
raise RuntimeError("Image provider blocked the event loop")
return image_result
options = dict(
instructions="Give a brief answer.",
llm={"model": os.environ.get("PRAISONAI_TEST_MODEL", "openai/gpt-4o-mini"), "max_tokens": 96},
reflection=False, memory=False, rules=False, output="silent",
)
if os.environ.get("OPENAI_BASE_URL"):
options["base_url"] = os.environ["OPENAI_BASE_URL"]
with ImageAgent(llm="openai/dall-e-3", instructions="Create images", verbose=False) as artist, Agent(**options) as writer:
monkeypatch.setattr(artist, "_litellm", image_provider)
image, answer = await asyncio.gather(
artist.agenerate_image("A mountain landscape"),
asyncio.to_thread(writer.start, "Write one short caption for a mountain landscape."),
)
print("Full Agent.start output:", answer)
assert image == image_result
assert isinstance(answer, str) and answer.strip()
@@ -0,0 +1,50 @@
"""Async image calls must let other event-loop callbacks make progress."""
import asyncio
import threading
import pytest
from praisonaiagents import ImageAgent
CALLS = [
("agenerate_image", "generate_image", ("A landscape",), {"quality": "hd"}),
("agenerate", "generate_image", ("A landscape",), {"quality": "hd"}),
("achat", "generate_image", ("A landscape",), {"quality": "hd"}),
("aedit", "edit", ("source.png", "Add clouds", "mask.png", 2, "512x512"), {"quality": "hd"}),
("avariation", "variation", ("source.png", 2, "512x512"), {"response_format": "b64_json"}),
]
@pytest.mark.asyncio
@pytest.mark.parametrize("async_name,sync_name,args,kwargs", CALLS)
@pytest.mark.parametrize("provider_error", [False, True])
async def test_image_call_allows_loop_progress(monkeypatch, async_name, sync_name, args, kwargs, provider_error):
agent = ImageAgent(llm="openai/dall-e-3", instructions="Create images", verbose=False)
loop = asyncio.get_running_loop()
released = threading.Event()
result = {"data": [{"url": "https://example.com/image.png"}]}
received = []
def blocking_call(*call_args, **call_kwargs):
received.append((call_args, call_kwargs))
loop.call_soon_threadsafe(released.set)
if not released.wait(timeout=2):
raise RuntimeError("Image call blocked the event loop")
if provider_error:
raise ValueError("Image provider unavailable")
return result
monkeypatch.setattr(agent, sync_name, blocking_call)
try:
if provider_error and async_name != "achat":
with pytest.raises(ValueError, match="Image provider unavailable"):
await getattr(agent, async_name)(*args, **kwargs)
else:
answer = await getattr(agent, async_name)(*args, **kwargs)
assert answer == ({"error": "Image provider unavailable"} if provider_error else result)
assert received == [(args, kwargs)]
finally:
released.set()
agent.close()