Objective: Add a custom AI agent that can create PPM images using natural language instructions.
This guide walks through adding a new Image Artist agent to the modular agent supervisor system. The agent will understand human-readable prompts like:
"Draw a PPM image of size 200 by 200, add a square at coordinate 10,10 with a size of 20 pixels in red color, then add 10 circles of varying sizes and colors randomly around the image, and write the file as image.ppm"
- Overview
- Create the PPM Drawing Tools Module
- Register Tools with the ToolRegistry
- Create the Image Artist Agent
- Export the Agent from Implementations
- Add Agent Configuration to YAML
- Register the Agent in SupervisorGraph
- Test the Complete Implementation
- Complete File Reference
We're adding:
- PPM Drawing Tools - A set of tools for pixel manipulation and PPM file output
- Image Artist Agent - An AI agent that interprets drawing instructions
app/
├── tools/
│ ├── registry.py ← We'll register new tools here
│ └── ppm_tools.py ← NEW: Our drawing tools
├── agents/
│ └── implementations/
│ └── image_artist.py ← NEW: Our agent
└── config/
└── agents.yaml ← Add agent configuration
PPM (Portable Pixmap) P6 is a simple binary image format:
- Header:
P6\n{width} {height}\n255\n - Body: Raw RGB bytes (3 bytes per pixel, row by row)
File: app/tools/ppm_tools.py
This module provides all the drawing primitives our agent needs.
Create a new file at app/tools/ppm_tools.py:
"""
PPM Image Drawing Tools.
This module provides tools for creating PPM P6 format images,
including primitive drawing operations and file output.
"""
from typing import Tuple
from langchain_core.tools import tool
# Global canvas state (shared between tools)
_canvas = {
"width": 0,
"height": 0,
"pixels": None, # Will be a bytearray
}
def _rgb_tuple(color: str) -> Tuple[int, int, int]:
"""
Convert a color name or hex value to RGB tuple.
Args:
color: Color name ('red', 'blue', etc.) or hex ('#FF0000')
Returns:
Tuple of (r, g, b) values 0-255
"""
color_map = {
"red": (255, 0, 0),
"green": (0, 255, 0),
"blue": (0, 0, 255),
"yellow": (255, 255, 0),
"cyan": (0, 255, 255),
"magenta": (255, 0, 255),
"white": (255, 255, 255),
"black": (0, 0, 0),
"orange": (255, 165, 0),
"purple": (128, 0, 128),
"pink": (255, 192, 203),
"gray": (128, 128, 128),
"grey": (128, 128, 128),
}
color_lower = color.lower().strip()
if color_lower in color_map:
return color_map[color_lower]
# Try hex format
if color_lower.startswith("#") and len(color_lower) == 7:
try:
r = int(color_lower[1:3], 16)
g = int(color_lower[3:5], 16)
b = int(color_lower[5:7], 16)
return (r, g, b)
except ValueError:
pass
# Default to white if unknown
return (255, 255, 255)Add this tool to initialize the pixel buffer:
@tool
def init_canvas(width: int, height: int, background_color: str = "black") -> str:
"""
Initialize a new canvas with the given dimensions and background color.
This must be called before any drawing operations.
Args:
width: Canvas width in pixels (e.g., 200)
height: Canvas height in pixels (e.g., 200)
background_color: Background color name or hex value (default: 'black')
Returns:
Confirmation message with canvas dimensions.
"""
_canvas["width"] = width
_canvas["height"] = height
# Initialize pixel buffer (3 bytes per pixel: RGB)
total_pixels = width * height
_canvas["pixels"] = bytearray(total_pixels * 3)
# Fill with background color
r, g, b = _rgb_tuple(background_color)
for i in range(total_pixels):
_canvas["pixels"][i * 3] = r
_canvas["pixels"][i * 3 + 1] = g
_canvas["pixels"][i * 3 + 2] = b
return f"Canvas initialized: {width}x{height} pixels with {background_color} background."Add this tool to set individual pixels:
@tool
def put_pixel(x: int, y: int, color: str) -> str:
"""
Set a single pixel at the specified coordinates.
Args:
x: X coordinate (0 = left edge)
y: Y coordinate (0 = top edge)
color: Color name ('red', 'blue', etc.) or hex value ('#FF0000')
Returns:
Confirmation message or error if out of bounds.
"""
if _canvas["pixels"] is None:
return "Error: Canvas not initialized. Call init_canvas first."
width = _canvas["width"]
height = _canvas["height"]
if x < 0 or x >= width or y < 0 or y >= height:
return f"Error: Coordinates ({x}, {y}) out of bounds for {width}x{height} canvas."
r, g, b = _rgb_tuple(color)
# Calculate pixel index (row-major order)
index = (y * width + x) * 3
_canvas["pixels"][index] = r
_canvas["pixels"][index + 1] = g
_canvas["pixels"][index + 2] = b
return f"Pixel set at ({x}, {y}) with color {color}."Add this tool to draw filled squares:
@tool
def draw_square(x: int, y: int, size: int, color: str) -> str:
"""
Draw a filled square on the canvas.
Args:
x: X coordinate of the top-left corner
y: Y coordinate of the top-left corner
size: Side length of the square in pixels
color: Color name ('red', 'blue', etc.) or hex value ('#FF0000')
Returns:
Confirmation message with the square details.
"""
if _canvas["pixels"] is None:
return "Error: Canvas not initialized. Call init_canvas first."
width = _canvas["width"]
height = _canvas["height"]
r, g, b = _rgb_tuple(color)
pixels_drawn = 0
for py in range(y, min(y + size, height)):
if py < 0:
continue
for px in range(x, min(x + size, width)):
if px < 0:
continue
index = (py * width + px) * 3
_canvas["pixels"][index] = r
_canvas["pixels"][index + 1] = g
_canvas["pixels"][index + 2] = b
pixels_drawn += 1
return f"Square drawn at ({x}, {y}) with size {size}px in {color}. ({pixels_drawn} pixels)"Add this tool to draw filled circles:
@tool
def draw_circle(x: int, y: int, radius: int, color: str) -> str:
"""
Draw a filled circle on the canvas.
Args:
x: X coordinate of the circle center
y: Y coordinate of the circle center
radius: Radius of the circle in pixels
color: Color name ('red', 'blue', etc.) or hex value ('#FF0000')
Returns:
Confirmation message with the circle details.
"""
if _canvas["pixels"] is None:
return "Error: Canvas not initialized. Call init_canvas first."
width = _canvas["width"]
height = _canvas["height"]
r, g, b = _rgb_tuple(color)
pixels_drawn = 0
radius_squared = radius * radius
# Iterate over bounding box of the circle
for py in range(max(0, y - radius), min(height, y + radius + 1)):
for px in range(max(0, x - radius), min(width, x + radius + 1)):
# Check if point is inside circle using distance formula
dx = px - x
dy = py - y
if dx * dx + dy * dy <= radius_squared:
index = (py * width + px) * 3
_canvas["pixels"][index] = r
_canvas["pixels"][index + 1] = g
_canvas["pixels"][index + 2] = b
pixels_drawn += 1
return f"Circle drawn at center ({x}, {y}) with radius {radius}px in {color}. ({pixels_drawn} pixels)"Add this tool to save the canvas as a PPM file:
@tool
def write_ppm(filename: str) -> str:
"""
Write the current canvas to a PPM P6 (binary) file.
Args:
filename: Output filename (e.g., 'image.ppm')
Returns:
Confirmation message with file details.
"""
if _canvas["pixels"] is None:
return "Error: Canvas not initialized. Call init_canvas first."
width = _canvas["width"]
height = _canvas["height"]
# Ensure .ppm extension
if not filename.lower().endswith(".ppm"):
filename += ".ppm"
try:
with open(filename, "wb") as f:
# Write PPM P6 header
header = f"P6\n{width} {height}\n255\n"
f.write(header.encode("ascii"))
# Write pixel data
f.write(_canvas["pixels"])
file_size = len(_canvas["pixels"]) + len(header)
return f"PPM file written: {filename} ({width}x{height}, {file_size} bytes)"
except IOError as e:
return f"Error writing file: {e}"At the end of the file, add the export list:
# Export all tools for registration
PPM_TOOLS = [
init_canvas,
put_pixel,
draw_square,
draw_circle,
write_ppm,
]File: app/tools/registry.py
Modify the registry to include our new PPM tools.
At the top of registry.py, add the import:
from app.tools.ppm_tools import PPM_TOOLSIn the _register_default_tools method, add registration for each PPM tool:
def _register_default_tools(self) -> None:
"""Register the default set of available tools."""
self._tools["tavily_search"] = TavilySearchResults(max_results=5)
self._tools["python_repl"] = PythonREPLTool()
# Register PPM drawing tools
for tool in PPM_TOOLS:
self._tools[tool.name] = toolHere's the complete file after modifications:
"""
Tool registry for managing available tools.
This module provides a centralized registry for tool instances,
enabling configuration-driven tool assignment to agents.
"""
from typing import Any
from langchain_community.tools.tavily_search import TavilySearchResults
from langchain_experimental.tools import PythonREPLTool
from app.tools.ppm_tools import PPM_TOOLS
class ToolRegistry:
"""
Registry for managing and retrieving tool instances.
The registry maintains a mapping of tool names to their instances,
allowing agents to be configured with tools by name.
"""
def __init__(self):
"""Initialize the registry with default tools."""
self._tools: dict[str, Any] = {}
self._register_default_tools()
def _register_default_tools(self) -> None:
"""Register the default set of available tools."""
self._tools["tavily_search"] = TavilySearchResults(max_results=5)
self._tools["python_repl"] = PythonREPLTool()
# Register PPM drawing tools
for tool in PPM_TOOLS:
self._tools[tool.name] = tool
def register(self, name: str, tool: Any) -> None:
"""
Register a new tool with the given name.
Args:
name: Unique identifier for the tool.
tool: The tool instance to register.
"""
self._tools[name] = tool
def get(self, name: str) -> Any:
"""
Retrieve a tool by name.
Args:
name: The name of the tool to retrieve.
Returns:
The tool instance.
Raises:
KeyError: If the tool name is not registered.
"""
if name not in self._tools:
raise KeyError(f"Tool '{name}' not found in registry")
return self._tools[name]
def get_tools(self, names: list[str]) -> list[Any]:
"""
Retrieve multiple tools by their names.
Args:
names: List of tool names to retrieve.
Returns:
List of tool instances in the same order as names.
"""
return [self.get(name) for name in names]
def list_available(self) -> list[str]:
"""
List all available tool names.
Returns:
List of registered tool names.
"""
return list(self._tools.keys())File: app/agents/implementations/image_artist.py
Create a new agent that specializes in image generation.
Create app/agents/implementations/image_artist.py:
"""
Image Artist agent implementation.
This agent specializes in creating PPM images based on
natural language descriptions of what to draw.
"""
from langchain_openai import ChatOpenAI
from app.agents.base import BaseAgent
class ImageArtistAgent(BaseAgent):
"""
Agent specialized in creating PPM images from descriptions.
The Image Artist interprets human-readable drawing instructions
and uses PPM drawing tools to create images. It can handle
requests like "draw a red square" or "create an image with
random circles".
"""
def __init__(
self,
name: str,
llm: ChatOpenAI,
tools: list,
system_prompt: str,
):
"""
Initialize the image artist agent.
Args:
name: Agent identifier.
llm: Language model for understanding instructions.
tools: PPM drawing tools.
system_prompt: Prompt defining drawing behavior.
"""
super().__init__(name, llm, tools, system_prompt)
def get_description(self) -> str:
"""Return a description of the image artist's capabilities."""
return (
"An image creation agent that generates PPM images from "
"natural language descriptions. It can draw shapes like "
"squares and circles, set individual pixels, and save "
"images to PPM P6 format files."
)File: app/agents/implementations/__init__.py
Update the init file to export our new agent.
"""Agent implementations module containing concrete agent classes."""
from app.agents.implementations.researcher import ResearcherAgent
from app.agents.implementations.coder import CoderAgent
from app.agents.implementations.reviewer import ReviewerAgent
from app.agents.implementations.qa_tester import QATesterAgent
from app.agents.implementations.image_artist import ImageArtistAgent
__all__ = [
"ResearcherAgent",
"CoderAgent",
"ReviewerAgent",
"QATesterAgent",
"ImageArtistAgent",
]File: app/config/agents.yaml
Add the configuration for our Image Artist agent.
Add this block to the agents: section in agents.yaml:
image_artist:
name: "Image Artist"
tools:
- "init_canvas"
- "put_pixel"
- "draw_square"
- "draw_circle"
- "write_ppm"
system_prompt: >
You are an Image Artist agent that creates PPM images based on user descriptions.
IMPORTANT WORKFLOW:
1. ALWAYS call init_canvas FIRST with the requested dimensions
2. Then use draw_square, draw_circle, or put_pixel to add shapes
3. ALWAYS call write_ppm at the end to save the file
When the user asks for random shapes, generate reasonable random values:
- Random positions within canvas bounds
- Random sizes appropriate for the canvas
- Random colors from: red, green, blue, yellow, cyan, magenta, orange, purple, pink
Be creative and make visually interesting compositions.
Always confirm what you drew after completing the image.agents:
researcher:
name: "Researcher"
tools:
- "tavily_search"
system_prompt: "You are a web researcher agent."
coder:
name: "Coder"
tools:
- "python_repl"
system_prompt: >
You may generate safe Python code to analyze data with pandas
and generate charts using matplotlib.
reviewer:
name: "Reviewer"
tools:
- "tavily_search"
system_prompt: >
You are a senior developer. You excel at code reviews.
You give detailed and specific actionable feedback.
You are not rude, but you don't worry about being polite either.
Instead you just communicate directly about the technical review.
qa_tester:
name: "QA Tester"
tools:
- "python_repl"
system_prompt: >
You may generate safe Python code to test functions and classes
using unittest or pytest.
image_artist:
name: "Image Artist"
tools:
- "init_canvas"
- "put_pixel"
- "draw_square"
- "draw_circle"
- "write_ppm"
system_prompt: >
You are an Image Artist agent that creates PPM images based on user descriptions.
IMPORTANT WORKFLOW:
1. ALWAYS call init_canvas FIRST with the requested dimensions
2. Then use draw_square, draw_circle, or put_pixel to add shapes
3. ALWAYS call write_ppm at the end to save the file
When the user asks for random shapes, generate reasonable random values:
- Random positions within canvas bounds
- Random sizes appropriate for the canvas
- Random colors from: red, green, blue, yellow, cyan, magenta, orange, purple, pink
Be creative and make visually interesting compositions.
Always confirm what you drew after completing the image.File: app/graphs/supervisor.py
Update the supervisor graph to recognize our new agent class.
Add ImageArtistAgent to the imports:
from app.agents.implementations import (
CoderAgent,
ImageArtistAgent,
QATesterAgent,
ResearcherAgent,
ReviewerAgent,
)In the _register_agent_classes method, add registration for the Image Artist:
def _register_agent_classes(self) -> None:
"""Register the default agent classes with the factory."""
self._agent_factory.register_agent_class("researcher", ResearcherAgent)
self._agent_factory.register_agent_class("coder", CoderAgent)
self._agent_factory.register_agent_class("reviewer", ReviewerAgent)
self._agent_factory.register_agent_class("qa_tester", QATesterAgent)
self._agent_factory.register_agent_class("image_artist", ImageArtistAgent)poetry run uvicorn app.server:app --host 0.0.0.0 --port 8080Open http://localhost:8080/playground/ and try this prompt:
Draw a PPM image of size 200 by 200. Add a square at coordinate 10,10 with a size of 20 pixels in red color. Then add 10 circles of varying sizes and colors randomly around the image. Write the PPM file called image.ppm
curl -X POST "http://localhost:8080/invoke" \
-H "Content-Type: application/json" \
-d '{
"input": {
"messages": [
{
"type": "human",
"content": "Draw a PPM image of size 200 by 200. Add a square at coordinate 10,10 with a size of 20 pixels in red color. Then add 10 circles of varying sizes and colors randomly around the image. Write the PPM file called image.ppm"
}
],
"next": ""
}
}'The PPM file will be created in the working directory. You can view it with:
- GIMP - Open directly
- ImageMagick -
display image.ppmor convert:convert image.ppm image.png - Python - Use Pillow:
from PIL import Image; Image.open('image.ppm').show()
Below are all the complete files for reference.
"""
PPM Image Drawing Tools.
This module provides tools for creating PPM P6 format images,
including primitive drawing operations and file output.
"""
from typing import Tuple
from langchain_core.tools import tool
# Global canvas state (shared between tools)
_canvas = {
"width": 0,
"height": 0,
"pixels": None, # Will be a bytearray
}
def _rgb_tuple(color: str) -> Tuple[int, int, int]:
"""
Convert a color name or hex value to RGB tuple.
Args:
color: Color name ('red', 'blue', etc.) or hex ('#FF0000')
Returns:
Tuple of (r, g, b) values 0-255
"""
color_map = {
"red": (255, 0, 0),
"green": (0, 255, 0),
"blue": (0, 0, 255),
"yellow": (255, 255, 0),
"cyan": (0, 255, 255),
"magenta": (255, 0, 255),
"white": (255, 255, 255),
"black": (0, 0, 0),
"orange": (255, 165, 0),
"purple": (128, 0, 128),
"pink": (255, 192, 203),
"gray": (128, 128, 128),
"grey": (128, 128, 128),
}
color_lower = color.lower().strip()
if color_lower in color_map:
return color_map[color_lower]
# Try hex format
if color_lower.startswith("#") and len(color_lower) == 7:
try:
r = int(color_lower[1:3], 16)
g = int(color_lower[3:5], 16)
b = int(color_lower[5:7], 16)
return (r, g, b)
except ValueError:
pass
# Default to white if unknown
return (255, 255, 255)
@tool
def init_canvas(width: int, height: int, background_color: str = "black") -> str:
"""
Initialize a new canvas with the given dimensions and background color.
This must be called before any drawing operations.
Args:
width: Canvas width in pixels (e.g., 200)
height: Canvas height in pixels (e.g., 200)
background_color: Background color name or hex value (default: 'black')
Returns:
Confirmation message with canvas dimensions.
"""
_canvas["width"] = width
_canvas["height"] = height
# Initialize pixel buffer (3 bytes per pixel: RGB)
total_pixels = width * height
_canvas["pixels"] = bytearray(total_pixels * 3)
# Fill with background color
r, g, b = _rgb_tuple(background_color)
for i in range(total_pixels):
_canvas["pixels"][i * 3] = r
_canvas["pixels"][i * 3 + 1] = g
_canvas["pixels"][i * 3 + 2] = b
return f"Canvas initialized: {width}x{height} pixels with {background_color} background."
@tool
def put_pixel(x: int, y: int, color: str) -> str:
"""
Set a single pixel at the specified coordinates.
Args:
x: X coordinate (0 = left edge)
y: Y coordinate (0 = top edge)
color: Color name ('red', 'blue', etc.) or hex value ('#FF0000')
Returns:
Confirmation message or error if out of bounds.
"""
if _canvas["pixels"] is None:
return "Error: Canvas not initialized. Call init_canvas first."
width = _canvas["width"]
height = _canvas["height"]
if x < 0 or x >= width or y < 0 or y >= height:
return f"Error: Coordinates ({x}, {y}) out of bounds for {width}x{height} canvas."
r, g, b = _rgb_tuple(color)
# Calculate pixel index (row-major order)
index = (y * width + x) * 3
_canvas["pixels"][index] = r
_canvas["pixels"][index + 1] = g
_canvas["pixels"][index + 2] = b
return f"Pixel set at ({x}, {y}) with color {color}."
@tool
def draw_square(x: int, y: int, size: int, color: str) -> str:
"""
Draw a filled square on the canvas.
Args:
x: X coordinate of the top-left corner
y: Y coordinate of the top-left corner
size: Side length of the square in pixels
color: Color name ('red', 'blue', etc.) or hex value ('#FF0000')
Returns:
Confirmation message with the square details.
"""
if _canvas["pixels"] is None:
return "Error: Canvas not initialized. Call init_canvas first."
width = _canvas["width"]
height = _canvas["height"]
r, g, b = _rgb_tuple(color)
pixels_drawn = 0
for py in range(y, min(y + size, height)):
if py < 0:
continue
for px in range(x, min(x + size, width)):
if px < 0:
continue
index = (py * width + px) * 3
_canvas["pixels"][index] = r
_canvas["pixels"][index + 1] = g
_canvas["pixels"][index + 2] = b
pixels_drawn += 1
return f"Square drawn at ({x}, {y}) with size {size}px in {color}. ({pixels_drawn} pixels)"
@tool
def draw_circle(x: int, y: int, radius: int, color: str) -> str:
"""
Draw a filled circle on the canvas.
Args:
x: X coordinate of the circle center
y: Y coordinate of the circle center
radius: Radius of the circle in pixels
color: Color name ('red', 'blue', etc.) or hex value ('#FF0000')
Returns:
Confirmation message with the circle details.
"""
if _canvas["pixels"] is None:
return "Error: Canvas not initialized. Call init_canvas first."
width = _canvas["width"]
height = _canvas["height"]
r, g, b = _rgb_tuple(color)
pixels_drawn = 0
radius_squared = radius * radius
# Iterate over bounding box of the circle
for py in range(max(0, y - radius), min(height, y + radius + 1)):
for px in range(max(0, x - radius), min(width, x + radius + 1)):
# Check if point is inside circle using distance formula
dx = px - x
dy = py - y
if dx * dx + dy * dy <= radius_squared:
index = (py * width + px) * 3
_canvas["pixels"][index] = r
_canvas["pixels"][index + 1] = g
_canvas["pixels"][index + 2] = b
pixels_drawn += 1
return f"Circle drawn at center ({x}, {y}) with radius {radius}px in {color}. ({pixels_drawn} pixels)"
@tool
def write_ppm(filename: str) -> str:
"""
Write the current canvas to a PPM P6 (binary) file.
Args:
filename: Output filename (e.g., 'image.ppm')
Returns:
Confirmation message with file details.
"""
if _canvas["pixels"] is None:
return "Error: Canvas not initialized. Call init_canvas first."
width = _canvas["width"]
height = _canvas["height"]
# Ensure .ppm extension
if not filename.lower().endswith(".ppm"):
filename += ".ppm"
try:
with open(filename, "wb") as f:
# Write PPM P6 header
header = f"P6\n{width} {height}\n255\n"
f.write(header.encode("ascii"))
# Write pixel data
f.write(_canvas["pixels"])
file_size = len(_canvas["pixels"]) + len(header)
return f"PPM file written: {filename} ({width}x{height}, {file_size} bytes)"
except IOError as e:
return f"Error writing file: {e}"
# Export all tools for registration
PPM_TOOLS = [
init_canvas,
put_pixel,
draw_square,
draw_circle,
write_ppm,
]"""
Tool registry for managing available tools.
This module provides a centralized registry for tool instances,
enabling configuration-driven tool assignment to agents.
"""
from typing import Any
from langchain_community.tools.tavily_search import TavilySearchResults
from langchain_experimental.tools import PythonREPLTool
from app.tools.ppm_tools import PPM_TOOLS
class ToolRegistry:
"""
Registry for managing and retrieving tool instances.
The registry maintains a mapping of tool names to their instances,
allowing agents to be configured with tools by name.
"""
def __init__(self):
"""Initialize the registry with default tools."""
self._tools: dict[str, Any] = {}
self._register_default_tools()
def _register_default_tools(self) -> None:
"""Register the default set of available tools."""
self._tools["tavily_search"] = TavilySearchResults(max_results=5)
self._tools["python_repl"] = PythonREPLTool()
# Register PPM drawing tools
for tool in PPM_TOOLS:
self._tools[tool.name] = tool
def register(self, name: str, tool: Any) -> None:
"""
Register a new tool with the given name.
Args:
name: Unique identifier for the tool.
tool: The tool instance to register.
"""
self._tools[name] = tool
def get(self, name: str) -> Any:
"""
Retrieve a tool by name.
Args:
name: The name of the tool to retrieve.
Returns:
The tool instance.
Raises:
KeyError: If the tool name is not registered.
"""
if name not in self._tools:
raise KeyError(f"Tool '{name}' not found in registry")
return self._tools[name]
def get_tools(self, names: list[str]) -> list[Any]:
"""
Retrieve multiple tools by their names.
Args:
names: List of tool names to retrieve.
Returns:
List of tool instances in the same order as names.
"""
return [self.get(name) for name in names]
def list_available(self) -> list[str]:
"""
List all available tool names.
Returns:
List of registered tool names.
"""
return list(self._tools.keys())"""
Image Artist agent implementation.
This agent specializes in creating PPM images based on
natural language descriptions of what to draw.
"""
from langchain_openai import ChatOpenAI
from app.agents.base import BaseAgent
class ImageArtistAgent(BaseAgent):
"""
Agent specialized in creating PPM images from descriptions.
The Image Artist interprets human-readable drawing instructions
and uses PPM drawing tools to create images. It can handle
requests like "draw a red square" or "create an image with
random circles".
"""
def __init__(
self,
name: str,
llm: ChatOpenAI,
tools: list,
system_prompt: str,
):
"""
Initialize the image artist agent.
Args:
name: Agent identifier.
llm: Language model for understanding instructions.
tools: PPM drawing tools.
system_prompt: Prompt defining drawing behavior.
"""
super().__init__(name, llm, tools, system_prompt)
def get_description(self) -> str:
"""Return a description of the image artist's capabilities."""
return (
"An image creation agent that generates PPM images from "
"natural language descriptions. It can draw shapes like "
"squares and circles, set individual pixels, and save "
"images to PPM P6 format files."
)"""Agent implementations module containing concrete agent classes."""
from app.agents.implementations.researcher import ResearcherAgent
from app.agents.implementations.coder import CoderAgent
from app.agents.implementations.reviewer import ReviewerAgent
from app.agents.implementations.qa_tester import QATesterAgent
from app.agents.implementations.image_artist import ImageArtistAgent
__all__ = [
"ResearcherAgent",
"CoderAgent",
"ReviewerAgent",
"QATesterAgent",
"ImageArtistAgent",
]agents:
researcher:
name: "Researcher"
tools:
- "tavily_search"
system_prompt: "You are a web researcher agent."
coder:
name: "Coder"
tools:
- "python_repl"
system_prompt: >
You may generate safe Python code to analyze data with pandas
and generate charts using matplotlib.
reviewer:
name: "Reviewer"
tools:
- "tavily_search"
system_prompt: >
You are a senior developer. You excel at code reviews.
You give detailed and specific actionable feedback.
You are not rude, but you don't worry about being polite either.
Instead you just communicate directly about the technical review.
qa_tester:
name: "QA Tester"
tools:
- "python_repl"
system_prompt: >
You may generate safe Python code to test functions and classes
using unittest or pytest.
image_artist:
name: "Image Artist"
tools:
- "init_canvas"
- "put_pixel"
- "draw_square"
- "draw_circle"
- "write_ppm"
system_prompt: >
You are an Image Artist agent that creates PPM images based on user descriptions.
IMPORTANT WORKFLOW:
1. ALWAYS call init_canvas FIRST with the requested dimensions
2. Then use draw_square, draw_circle, or put_pixel to add shapes
3. ALWAYS call write_ppm at the end to save the file
When the user asks for random shapes, generate reasonable random values:
- Random positions within canvas bounds
- Random sizes appropriate for the canvas
- Random colors from: red, green, blue, yellow, cyan, magenta, orange, purple, pink
Be creative and make visually interesting compositions.
Always confirm what you drew after completing the image.Updated imports:
from app.agents.implementations import (
CoderAgent,
ImageArtistAgent,
QATesterAgent,
ResearcherAgent,
ReviewerAgent,
)Updated _register_agent_classes method:
def _register_agent_classes(self) -> None:
"""Register the default agent classes with the factory."""
self._agent_factory.register_agent_class("researcher", ResearcherAgent)
self._agent_factory.register_agent_class("coder", CoderAgent)
self._agent_factory.register_agent_class("reviewer", ReviewerAgent)
self._agent_factory.register_agent_class("qa_tester", QATesterAgent)
self._agent_factory.register_agent_class("image_artist", ImageArtistAgent)You've now added a complete Image Artist agent to the system! Here's what we created:
| Component | File | Purpose |
|---|---|---|
| PPM Tools | app/tools/ppm_tools.py |
Drawing primitives and file output |
| Tool Registry | app/tools/registry.py |
Register tools for agent use |
| Agent Class | app/agents/implementations/image_artist.py |
Agent implementation |
| Agent Export | app/agents/implementations/__init__.py |
Export for imports |
| Agent Config | app/config/agents.yaml |
YAML configuration |
| Graph Registration | app/graphs/supervisor.py |
Factory registration |
The agent can now interpret natural language like:
- "Create a 100x100 image with a blue circle in the center"
- "Draw a checkerboard pattern with red and white squares"
- "Make abstract art with random shapes"
-
Simple shapes:
Create a 100x100 PPM image with a white background, draw a red square of size 30 at position 35,35, and save it as simple.ppm -
Multiple shapes:
Make a 300x200 PPM with black background. Draw a yellow sun (circle radius 25) at 50,50. Draw green ground (square 300x50) at bottom. Save as landscape.ppm -
Random art:
Create a 256x256 abstract art PPM. Add 5 random squares and 8 random circles with various colors. Save as abstract.ppm -
Pixel art:
Create a tiny 16x16 PPM. Draw a red heart shape using individual pixels. Save as heart.ppm