import base64
import os
from agents import (Agent, ImageGenerationTool, Runner,
set_default_openai_api, trace)
from dotenv import load_dotenv
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import Response
from pydantic import BaseModel, Field
# 从 .env 读取 OPENAI_BASE_URL、OPENAI_API_KEY、OPENAI_DEFAULT_MODEL
load_dotenv()
set_default_openai_api("chat_completions") # 仓库原文没有,接第三方模型用
# ----- 写死的配置 -----
# 书里写死的是 "gpt-5-mini",这里换成环境变量
CONTROLLER_MODEL = os.environ["OPENAI_DEFAULT_MODEL"] # 跑这个智能体的大模型
TOOL_CONFIG = {
"type": "image_generation",
"quality": "high",
"model": "gpt-image-1", # ← 这个第三方服务基本没有,见开头说明
"size": "1536x1024",
}
STYLE_GUIDELINES = """
## 所有图像的风格规范:
- **一致性**:每张图都要保持同样的摄影质感,3D 渲染元素要无缝融入
- **配色**:统一使用蓝、紫、暖金和绿色,点缀少量明亮的强调色
- **光照**:专业摄影级布光,戏剧性但色调温暖
- **信息图**:半透明的全息投影效果,不要喧宾夺主
- **角色**:AI 机器人要可爱、亲切,彼此有区别但又像一家人
- **图标**:用通用易懂的符号(灯泡代表想法、齿轮代表处理、爱心代表对齐等)
- **氛围**:乐观、有教育感、略带未来感,但不能冷漠或有压迫感
"""
# ----- FastAPI -----
app = FastAPI(title="Fixed-Config Image Generator API", version="1.0.0")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # 演示用,生产要收紧成具体域名
allow_credentials=False, # 用 "*" 时必须保持 False
allow_methods=["POST", "OPTIONS"],
allow_headers=["Content-Type"], # 浏览器预检会问这个
max_age=600, # 预检结果缓存 10 分钟
)
class GenerateIn(BaseModel):
# 调用方唯一能改的东西
input: str = Field(
..., description="大白话的需求(提示词由智能体自己组织)。"
)
def build_agent() -> Agent:
"""
每个请求都新建一个智能体,避免请求之间串状态。
工具配置在这里写死,不允许用户覆盖。
"""
return Agent(
name="Image generator",
instructions=STYLE_GUIDELINES,
model=CONTROLLER_MODEL,
tools=[ImageGenerationTool(tool_config=TOOL_CONFIG)],
)
def extract_image_b64(result) -> str | None:
"""
从运行结果里翻出图像生成工具产出的 base64 数据。
"""
for item in getattr(result, "new_items", []) or []:
if (
getattr(item, "type", None) == "tool_call_item"
and getattr(item, "raw_item", None) is not None
and getattr(item.raw_item, "type", None) == "image_generation_call"
and getattr(item.raw_item, "result", None)
):
return item.raw_item.result
return None
@app.post("/generate", response_class=Response)
async def generate(body: GenerateIn):
# 唯一的变量就是输入文本,其他全在代码里固定死
agent = build_agent()
print(f"正在为输入生成图像:{body.input}")
with trace("Image generation"):
result = await Runner.run(agent, body.input)
b64 = extract_image_b64(result)
if not b64:
raise HTTPException(
status_code=500, detail="图像生成工具没有产出任何结果。"
)
print("生成成功,返回 PNG 字节。")
png_bytes = base64.b64decode(b64)
# 直接返回 PNG 字节。没有文件名,没有 JSON —— 就是一张图。
return Response(content=png_bytes, media_type="image/png")
这 101 行里藏着三个真正的部署课
一、build_agent() 每次请求新建一个智能体。
注释写得很直白:避免请求之间串状态。前七章你都是全局建一个 agent
然后一直用——那是因为只有你一个人在用。一旦并发,
共享的 agent 对象就可能把 A 用户的上下文泄给 B 用户。
这是从「自己跑」到「别人用」最容易踩的坑。
二、调用方只能改 input 一个字段。
模型、图片尺寸、质量、风格规范全写死在代码里。
为什么不做成参数?因为凡是用户能改的,用户就能拿来烧你的钱——
把 quality 调到最高、尺寸拉到最大,账单是你的。
「固定配置」是个安全决策,不是偷懒。
三、返回的是裸 PNG 字节,不是 JSON。
Response(content=png_bytes, media_type="image/png")——
所以你不能用 response.json() 去解析它,
curl 得加 --output image.png。
这让前端能直接把它塞进 <img src>,省掉一次 base64 编解码。
怎么跑起来
模块名以数字开头,得用字符串形式导入:
cd chapter_08
uv run uvicorn 02_app:app --host 0.0.0.0 --port 8000 --reload
# 试一下(注意 --output,因为返回的是裸 PNG 不是 JSON)
curl -X POST "http://localhost:8000/generate" \
-H "Content-Type: application/json" \
-d '{"input":"一个教小朋友太阳能的友好机器人"}' \
--output image.png
注意 8000 端口——第三章的 SSE 示例也用它,别同时开。
另外 03_realtime_image_agent.html 默认就连
http://localhost:8000,先起这个服务,再开那个网页。