Skip to content

Commit 8c0437c

Browse files
committed
fix(creative): handle moderation_blocked correctly and avoid false image success
1 parent 7e2c4c4 commit 8c0437c

File tree

3 files changed

+143
-6
lines changed

3 files changed

+143
-6
lines changed

agent/agents/creative.py

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,38 @@ async def _execute_with_nemotron(self, image_prompt: str, size: str = "1024x1024
139139
"quality": quality,
140140
})
141141

142+
@staticmethod
143+
def _extract_tool_error(observation: dict) -> tuple[bool, str, str]:
144+
"""Normalisiert Fehlererkennung aus Tool-Responses."""
145+
if not isinstance(observation, dict):
146+
return True, "Unerwartetes Antwortformat vom Tool.", ""
147+
148+
status = str(observation.get("status", "") or "").strip().lower()
149+
error_code = str(observation.get("error_code", "") or "").strip().lower()
150+
151+
if "error" in observation:
152+
return True, str(observation.get("error") or "Unbekannter Fehler"), error_code
153+
if status in {"error", "failed", "failure"}:
154+
msg = str(
155+
observation.get("message")
156+
or observation.get("error")
157+
or "Unbekannter Fehler"
158+
)
159+
return True, msg, error_code
160+
return False, "", error_code
161+
162+
@staticmethod
163+
def _build_safe_retry_prompt(detailed_prompt: str) -> str:
164+
"""Erzeugt einen moderationsfreundlichen Retry-Prompt fuer Bildgenerierung."""
165+
base = str(detailed_prompt or "").strip()
166+
safety_clause = (
167+
"Original fictional character only, no copyrighted or trademarked characters, "
168+
"no logos, no real persons, no violence, no weapons, family-friendly."
169+
)
170+
if base:
171+
return f"{base}. {safety_clause}"
172+
return safety_clause
173+
142174
async def run(self, task: str) -> str:
143175
log.info(f"{self.__class__.__name__} - HYBRID MODE (GPT-5.1 + Nemotron)")
144176

@@ -156,12 +188,35 @@ async def run(self, task: str) -> str:
156188
self._handle_file_artifacts(observation)
157189

158190
if isinstance(observation, dict):
159-
if "error" in observation:
160-
return f"Fehler bei der Bildgenerierung: {observation['error']}"
191+
has_error, error_message, error_code = self._extract_tool_error(observation)
192+
if has_error and error_code == "moderation_blocked":
193+
safe_prompt = self._build_safe_retry_prompt(detailed_prompt)
194+
log.warning("Bildgenerierung moderiert geblockt. Versuche sicheren Retry.")
195+
observation_retry = await self._execute_with_nemotron(
196+
safe_prompt, size="1024x1024", quality="high"
197+
)
198+
self._handle_file_artifacts(observation_retry)
199+
if isinstance(observation_retry, dict):
200+
retry_error, retry_message, _ = self._extract_tool_error(observation_retry)
201+
if retry_error:
202+
return f"Fehler bei der Bildgenerierung: {retry_message}"
203+
observation = observation_retry
204+
detailed_prompt = safe_prompt
205+
else:
206+
return "Fehler bei der Bildgenerierung: Unerwartetes Antwortformat beim Retry."
207+
208+
elif has_error:
209+
return f"Fehler bei der Bildgenerierung: {error_message}"
161210

162211
saved_path = observation.get("saved_as", "")
163212
image_url = observation.get("image_url", "")
164213

214+
if not saved_path and not image_url:
215+
return (
216+
"Bildgenerierung abgeschlossen, aber es wurde weder Datei noch URL "
217+
"zurueckgegeben."
218+
)
219+
165220
final_answer = "Ich habe das Bild erfolgreich generiert!"
166221
if saved_path:
167222
final_answer += f"\n\nGespeichert unter: {saved_path}"
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import sys
2+
from pathlib import Path
3+
4+
import pytest
5+
6+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
7+
8+
from agent.agents.creative import CreativeAgent
9+
10+
11+
@pytest.mark.asyncio
12+
async def test_creative_agent_reports_status_error():
13+
agent = CreativeAgent.__new__(CreativeAgent)
14+
agent._handle_file_artifacts = lambda observation: None
15+
16+
async def _fake_prompt(_task: str) -> str:
17+
return "cinematic hero on rooftop"
18+
19+
async def _fake_exec(_prompt: str, size: str = "1024x1024", quality: str = "high"):
20+
return {
21+
"status": "error",
22+
"message": "moderation blocked",
23+
"error_code": "moderation_blocked",
24+
}
25+
26+
agent._generate_image_prompt_with_gpt = _fake_prompt
27+
agent._execute_with_nemotron = _fake_exec
28+
29+
result = await CreativeAgent.run(agent, "male ein bild von einem helden")
30+
assert "Fehler bei der Bildgenerierung" in result
31+
assert "moderation blocked" in result.lower()
32+
33+
34+
@pytest.mark.asyncio
35+
async def test_creative_agent_retries_once_on_moderation_and_succeeds():
36+
agent = CreativeAgent.__new__(CreativeAgent)
37+
agent._handle_file_artifacts = lambda observation: None
38+
calls = {"n": 0, "prompts": []}
39+
40+
async def _fake_prompt(_task: str) -> str:
41+
return "cinematic superhero scene"
42+
43+
async def _fake_exec(prompt: str, size: str = "1024x1024", quality: str = "high"):
44+
calls["n"] += 1
45+
calls["prompts"].append(prompt)
46+
if calls["n"] == 1:
47+
return {
48+
"status": "error",
49+
"error": "blocked by moderation",
50+
"error_code": "moderation_blocked",
51+
}
52+
return {
53+
"status": "success",
54+
"saved_as": "results/hero.png",
55+
"message": "ok",
56+
}
57+
58+
agent._generate_image_prompt_with_gpt = _fake_prompt
59+
agent._execute_with_nemotron = _fake_exec
60+
61+
result = await CreativeAgent.run(agent, "male ein bild von einem helden")
62+
63+
assert calls["n"] == 2
64+
assert "copyrighted" in calls["prompts"][1].lower()
65+
assert "erfolgreich generiert" in result.lower()
66+
assert "results/hero.png" in result
67+

tools/creative_tool/tool.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,12 +104,27 @@ async def generate_image(prompt: str, size: str = "1024x1024", quality: str = "s
104104
}
105105

106106
except Exception as e:
107-
# Verbesserte Fehlerbehandlung, um die OpenAI-Fehlermeldung zu extrahieren
107+
# Verbesserte Fehlerbehandlung inkl. strukturierter Error-Codes.
108108
error_message = str(e)
109-
if hasattr(e, 'body') and e.body and 'error' in e.body and 'message' in e.body['error']:
110-
error_message = f"OpenAI API Fehler: {e.body['error']['message']}"
109+
error_code = ""
110+
error_type = ""
111+
112+
body = getattr(e, "body", None)
113+
if isinstance(body, dict):
114+
err = body.get("error", {}) or {}
115+
if isinstance(err, dict):
116+
error_message = str(err.get("message") or error_message)
117+
error_code = str(err.get("code") or "")
118+
error_type = str(err.get("type") or "")
119+
111120
log.error(f"❌ {error_message}", exc_info=True)
112-
return {"status": "error", "message": error_message}
121+
return {
122+
"status": "error",
123+
"error": error_message,
124+
"message": error_message,
125+
"error_code": error_code,
126+
"error_type": error_type,
127+
}
113128

114129
# ... (deine anderen Methoden wie generate_code, generate_text bleiben unverändert) ...
115130

0 commit comments

Comments
 (0)