Add image search with local cache (Phase 23)
Images found via SearXNG are fetched server-side, stored on disk, and served from /api/images/<id> — the user's browser never contacts the original image host. Original URLs are preserved for citation. New files: - alembic/versions/0016_add_image_cache.py — image_cache table - src/fabledassistant/models/image_cache.py — SQLAlchemy model - src/fabledassistant/services/images.py — fetch/store/serve logic - src/fabledassistant/routes/images.py — GET /api/images/<id> Modified: - config.py: IMAGE_CACHE_DIR (/data/images), IMAGE_MAX_BYTES (5 MB) - research.py: _search_searxng_images() — SearXNG categories=images - tools.py: _IMAGE_TOOLS def + search_images branch in execute_tool - intent.py: search_images routing rule (explicit visual language only) - app.py: register images_bp - docker-compose.yml: image_cache named volume mounted at /data/images - ToolCallCard.vue: "image_search" label Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -710,6 +710,33 @@ _RESEARCH_TOOLS = [
|
||||
}
|
||||
]
|
||||
|
||||
_IMAGE_TOOLS = [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "search_images",
|
||||
"description": (
|
||||
"Search the web for images and display them inline in the response. "
|
||||
"ONLY use this when the user explicitly asks to SEE, SHOW, VIEW, or DISPLAY "
|
||||
"an image or photo — e.g. 'show me a picture of X', 'what does X look like', "
|
||||
"'find a photo of X', 'display an image of X'. "
|
||||
"Do NOT use for general descriptions, factual questions, or when the user just "
|
||||
"wants information without visual content. Returns at most 2 images."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "The image search query",
|
||||
}
|
||||
},
|
||||
"required": ["query"],
|
||||
},
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
async def get_tools_for_user(user_id: int) -> list[dict]:
|
||||
"""Build the tool list for a user based on their configured integrations."""
|
||||
@@ -719,6 +746,7 @@ async def get_tools_for_user(user_id: int) -> list[dict]:
|
||||
if Config.searxng_enabled():
|
||||
tools.extend(_SEARCH_TOOLS)
|
||||
tools.extend(_RESEARCH_TOOLS)
|
||||
tools.extend(_IMAGE_TOOLS)
|
||||
logger.debug("User %d: %d tools available", user_id, len(tools))
|
||||
return tools
|
||||
|
||||
@@ -1215,6 +1243,41 @@ async def execute_tool(user_id: int, tool_name: str, arguments: dict) -> dict:
|
||||
"data": {"topic": topic},
|
||||
}
|
||||
|
||||
elif tool_name == "search_images":
|
||||
from fabledassistant.services.images import fetch_and_store_image
|
||||
from fabledassistant.services.research import _search_searxng_images
|
||||
query = arguments.get("query", "")
|
||||
if not query:
|
||||
return {"success": False, "error": "query is required"}
|
||||
raw_results = await _search_searxng_images(query)
|
||||
if not raw_results:
|
||||
return {"success": False, "error": f"No images found for '{query}'"}
|
||||
# Fetch and cache up to 2 images
|
||||
images = []
|
||||
for r in raw_results[:2]:
|
||||
img_url = r.get("img_src", "")
|
||||
if not img_url:
|
||||
continue
|
||||
record = await fetch_and_store_image(
|
||||
url=img_url,
|
||||
title=r.get("title"),
|
||||
source_domain=r.get("source_domain"),
|
||||
)
|
||||
if record:
|
||||
images.append({
|
||||
"local_url": f"/api/images/{record.id}",
|
||||
"page_url": r.get("page_url", ""),
|
||||
"source_domain": r.get("source_domain", ""),
|
||||
"title": record.title or query,
|
||||
})
|
||||
if not images:
|
||||
return {"success": False, "error": f"Could not retrieve images for '{query}'"}
|
||||
return {
|
||||
"success": True,
|
||||
"type": "image_search",
|
||||
"data": {"query": query, "images": images},
|
||||
}
|
||||
|
||||
else:
|
||||
return {"success": False, "error": f"Unknown tool: {tool_name}"}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user