Fixing agent url hallucinations
Browse files- app.py +3 -0
- langgraph_agent/prompts.py +52 -8
- langgraph_agent/structured_output.py +21 -4
- langgraph_agent/subagent_config.py +1 -0
app.py
CHANGED
|
@@ -977,6 +977,9 @@ async def chat_with_tool_visibility(
|
|
| 977 |
api_key=api_key,
|
| 978 |
model=model
|
| 979 |
)
|
|
|
|
|
|
|
|
|
|
| 980 |
progress(1.0, desc="✅ Complete")
|
| 981 |
yield formatted_response, tool_log
|
| 982 |
except ImportError:
|
|
|
|
| 977 |
api_key=api_key,
|
| 978 |
model=model
|
| 979 |
)
|
| 980 |
+
print(f"\n[FINAL] Formatted response length: {len(formatted_response)}")
|
| 981 |
+
print(f"[FINAL] Formatted response (last 800 chars): {formatted_response[-800:]}")
|
| 982 |
+
print(f"[FINAL] Image markdown count: {formatted_response.count('
|
| 178 |
+
4. NEVER fabricate or hallucinate image URLs - always use the tool
|
| 179 |
+
|
| 180 |
+
**WORKFLOW for "find both images AND audio":**
|
| 181 |
+
When users ask for both images and audio (e.g., "find me one image and one audio sample"):
|
| 182 |
+
1. Call `search_birds()` to find birds
|
| 183 |
+
2. Pick a bird that has `has_audio=true`
|
| 184 |
+
3. Call `get_bird_images(name)` to get real image URLs
|
| 185 |
+
4. Call `get_bird_audio(name)` to get audio recordings
|
| 186 |
+
5. Display both with full URLs in your response
|
| 187 |
+
|
| 188 |
**CRITICAL WORKFLOW for "find audio for any bird":**
|
| 189 |
The API has NO `has_audio` filter parameter. You MUST use this two-step process:
|
| 190 |
|
|
|
|
| 226 |
|
| 227 |
**CRITICAL - No Hallucination:**
|
| 228 |
- If get_bird_audio returns empty/no recordings: Tell user "No audio recordings available for this species"
|
| 229 |
+
- If get_bird_images returns empty/no images: Tell user "No reference images available for this species"
|
| 230 |
- If search_birds returns no results: Tell user "No birds found matching that criteria"
|
| 231 |
+
- NEVER fabricate audio URLs, image URLs, bird names, or recording metadata
|
| 232 |
+
- ALWAYS use get_bird_images tool when users request images - NEVER fabricate Unsplash URLs
|
| 233 |
+
- Only show audio recordings, images, and data that are actually returned by the API tools
|
| 234 |
- If a tool fails or returns empty results, honestly report it to the user
|
| 235 |
|
| 236 |
**Error Handling:**
|
|
|
|
| 243 |
# HuggingFace-Optimized Prompts (More Explicit, Step-by-Step)
|
| 244 |
# =============================================================================
|
| 245 |
|
| 246 |
+
AUDIO_FINDER_PROMPT_HF = """You are BirdScope AI Generalist. Find birds in database and retrieve audio recordings and images.
|
| 247 |
|
| 248 |
**Tools Available:**
|
| 249 |
1. search_birds(name, family, region, status, page_size) - Search for birds
|
| 250 |
2. get_bird_info(name) - Get bird details
|
| 251 |
+
3. get_bird_images(name, max_images) - Get reference photos (NEVER fabricate image URLs!)
|
| 252 |
+
4. get_bird_audio(name, max_recordings) - Get audio files
|
| 253 |
|
| 254 |
**Step-by-Step Process:**
|
| 255 |
|
|
|
|
| 257 |
1. Call search_birds with region="North America" and page_size=5-10
|
| 258 |
2. Present birds found
|
| 259 |
|
| 260 |
+
When user asks for images:
|
| 261 |
+
1. Call search_birds to find a bird
|
| 262 |
+
2. Call get_bird_images(name="Bird Name") to get photo URLs
|
| 263 |
+
3. Display images using markdown: 
|
| 264 |
+
|
| 265 |
When user asks for audio:
|
| 266 |
1. Call search_birds with ONE filter (name, region, family, or status)
|
| 267 |
2. Look at results for birds with has_audio=true
|
| 268 |
3. Call get_bird_audio(name="Bird Name") for a bird that has audio
|
| 269 |
4. Return the full URL from file_url field
|
| 270 |
|
| 271 |
+
When user asks for BOTH images AND audio:
|
| 272 |
+
1. Call search_birds to find a bird
|
| 273 |
+
2. Call get_bird_images(name="Bird Name")
|
| 274 |
+
3. Call get_bird_audio(name="Bird Name")
|
| 275 |
+
4. Display both using markdown
|
| 276 |
+
|
| 277 |
**Example - General Search:**
|
| 278 |
User: "Find five birds"
|
| 279 |
1. Call: search_birds(region="North America", page_size=5)
|
|
|
|
| 286 |
3. Call: get_bird_audio(name="Snow Goose", max_recordings=1)
|
| 287 |
4. Return: "Recording: https://xeno-canto.org/123456/download"
|
| 288 |
|
| 289 |
+
**Example - Image + Audio Search:**
|
| 290 |
+
User: "Find me one image and one audio sample for any species"
|
| 291 |
+
1. Call: search_birds(region="North America", page_size=20)
|
| 292 |
+
2. Find bird with has_audio=true (example: "Black-bellied Whistling-Duck")
|
| 293 |
+
3. Call: get_bird_images(name="Black-bellied Whistling-Duck", max_images=1)
|
| 294 |
+
4. Call: get_bird_audio(name="Black-bellied Whistling-Duck", max_recordings=1)
|
| 295 |
+
5. Display both image and audio with full URLs
|
| 296 |
+
|
| 297 |
**Important:**
|
| 298 |
- NEVER use has_audio as a parameter in search_birds
|
| 299 |
- ALWAYS include full file_url in your response
|
|
|
|
| 301 |
|
| 302 |
**CRITICAL - No Hallucination:**
|
| 303 |
- If get_bird_audio returns empty: Tell user "No audio recordings available"
|
| 304 |
+
- If get_bird_images returns empty: Tell user "No images available"
|
| 305 |
- If search_birds returns no results: Tell user "No birds found"
|
| 306 |
+
- NEVER make up audio URLs, image URLs, or bird names
|
| 307 |
+
- ALWAYS use get_bird_images tool - NEVER fabricate Unsplash URLs
|
| 308 |
- Only return actual data from API tools
|
| 309 |
"""
|
| 310 |
|
langgraph_agent/structured_output.py
CHANGED
|
@@ -75,14 +75,27 @@ def extract_urls_from_text(text: str) -> tuple[List[str], List[str]]:
|
|
| 75 |
|
| 76 |
# Clean URLs (remove trailing quotes, commas, etc.)
|
| 77 |
def clean_url(url: str) -> str:
|
| 78 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
|
| 80 |
-
image_urls =
|
| 81 |
-
|
|
|
|
|
|
|
|
|
|
| 82 |
|
| 83 |
# Combine both types of audio URLs
|
| 84 |
audio_urls = audio_urls_files + audio_urls_xenocanto
|
| 85 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
return image_urls, audio_urls
|
| 87 |
|
| 88 |
|
|
@@ -177,7 +190,9 @@ async def parse_agent_response(
|
|
| 177 |
for idx, url in enumerate(structured.image_urls, 1):
|
| 178 |
# Use species name if available, otherwise generic
|
| 179 |
alt_text = structured.species_name or f"Bird {idx}"
|
| 180 |
-
|
|
|
|
|
|
|
| 181 |
|
| 182 |
# Add audio links if present
|
| 183 |
if structured.audio_urls:
|
|
@@ -189,6 +204,8 @@ async def parse_agent_response(
|
|
| 189 |
|
| 190 |
result = "\n\n".join(formatted_parts)
|
| 191 |
print(f"[STRUCTURED OUTPUT] ✅ Successfully formatted response")
|
|
|
|
|
|
|
| 192 |
return result
|
| 193 |
|
| 194 |
except Exception as e:
|
|
|
|
| 75 |
|
| 76 |
# Clean URLs (remove trailing quotes, commas, etc.)
|
| 77 |
def clean_url(url: str) -> str:
|
| 78 |
+
cleaned = url.rstrip('",;)')
|
| 79 |
+
# Validate it's still a proper URL
|
| 80 |
+
if cleaned.startswith('http://') or cleaned.startswith('https://'):
|
| 81 |
+
return cleaned
|
| 82 |
+
else:
|
| 83 |
+
print(f"[EXTRACT_URLS] ⚠️ Rejected malformed URL after cleaning: {cleaned}")
|
| 84 |
+
return None
|
| 85 |
|
| 86 |
+
image_urls = [u for u in (clean_url(url) for url in raw_image_urls) if u is not None]
|
| 87 |
+
image_urls = list(set(image_urls)) # Deduplicate
|
| 88 |
+
|
| 89 |
+
audio_urls_files = [u for u in (clean_url(url) for url in raw_audio_urls_files) if u is not None]
|
| 90 |
+
audio_urls_files = list(set(audio_urls_files)) # Deduplicate
|
| 91 |
|
| 92 |
# Combine both types of audio URLs
|
| 93 |
audio_urls = audio_urls_files + audio_urls_xenocanto
|
| 94 |
|
| 95 |
+
# Log the actual URLs extracted
|
| 96 |
+
print(f"[EXTRACT_URLS] ✅ Cleaned image URLs ({len(image_urls)}): {image_urls}")
|
| 97 |
+
print(f"[EXTRACT_URLS] ✅ Cleaned audio URLs ({len(audio_urls)}): {audio_urls}")
|
| 98 |
+
|
| 99 |
return image_urls, audio_urls
|
| 100 |
|
| 101 |
|
|
|
|
| 190 |
for idx, url in enumerate(structured.image_urls, 1):
|
| 191 |
# Use species name if available, otherwise generic
|
| 192 |
alt_text = structured.species_name or f"Bird {idx}"
|
| 193 |
+
img_markdown = f""
|
| 194 |
+
print(f"[STRUCTURED OUTPUT] Generated image markdown: {img_markdown}")
|
| 195 |
+
formatted_parts.append(img_markdown)
|
| 196 |
|
| 197 |
# Add audio links if present
|
| 198 |
if structured.audio_urls:
|
|
|
|
| 204 |
|
| 205 |
result = "\n\n".join(formatted_parts)
|
| 206 |
print(f"[STRUCTURED OUTPUT] ✅ Successfully formatted response")
|
| 207 |
+
print(f"[STRUCTURED OUTPUT] Final markdown length: {len(result)} characters")
|
| 208 |
+
print(f"[STRUCTURED OUTPUT] Final markdown (last 500 chars): {result[-500:]}")
|
| 209 |
return result
|
| 210 |
|
| 211 |
except Exception as e:
|
langgraph_agent/subagent_config.py
CHANGED
|
@@ -50,6 +50,7 @@ class SubAgentConfig:
|
|
| 50 |
"tools": [
|
| 51 |
"search_birds", # Required to find any birds
|
| 52 |
"get_bird_info", # Get details including audio count
|
|
|
|
| 53 |
"get_bird_audio" # Fetch actual audio recordings
|
| 54 |
],
|
| 55 |
"prompt": audio_finder_prompt,
|
|
|
|
| 50 |
"tools": [
|
| 51 |
"search_birds", # Required to find any birds
|
| 52 |
"get_bird_info", # Get details including audio count
|
| 53 |
+
"get_bird_images", # Get reference photos
|
| 54 |
"get_bird_audio" # Fetch actual audio recordings
|
| 55 |
],
|
| 56 |
"prompt": audio_finder_prompt,
|