router: enforce direct image inputs for plant tools and inject runtime image_data
This commit is contained in:
@@ -2007,6 +2007,19 @@ async def agent_infer(agent_id: str, request: InferRequest):
|
||||
tool_args = {"params": {"count": 3, "timezone": "Europe/Kyiv"}}
|
||||
logger.info("🛠️ oneok: auto-filled schedule_propose_slots.params")
|
||||
|
||||
# Plant tools: inject runtime image payload from current request to avoid
|
||||
# hallucinated page URLs (e.g. t.me/<chat>/<msg>) that are not direct images.
|
||||
if tool_name in {"nature_id_identify", "plantnet_lookup"}:
|
||||
if not isinstance(tool_args, dict):
|
||||
tool_args = {}
|
||||
runtime_image_data = None
|
||||
if isinstance(request.images, list) and request.images:
|
||||
first_image = request.images[0]
|
||||
if isinstance(first_image, str) and first_image.startswith("data:image/") and ";base64," in first_image:
|
||||
runtime_image_data = first_image
|
||||
if runtime_image_data:
|
||||
tool_args["_runtime_image_data"] = runtime_image_data
|
||||
|
||||
result = await tool_manager.execute_tool(
|
||||
tool_name,
|
||||
tool_args,
|
||||
|
||||
@@ -19,6 +19,7 @@ from typing import Dict, List, Any, Optional
|
||||
from dataclasses import dataclass
|
||||
from io import BytesIO, StringIO
|
||||
from pathlib import PurePath
|
||||
from urllib.parse import urlparse
|
||||
import xml.etree.ElementTree as ET
|
||||
from xml.sax.saxutils import escape as xml_escape
|
||||
from zipfile import ZIP_DEFLATED, ZipFile
|
||||
@@ -164,8 +165,7 @@ TOOL_DEFINITIONS = [
|
||||
"description": "Поріг confidence для fallback на GBIF",
|
||||
"default": 0.65
|
||||
}
|
||||
},
|
||||
"required": ["image_url"]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -791,6 +791,27 @@ class ToolManager:
|
||||
tool_names = [t.get("function", {}).get("name") for t in filtered]
|
||||
logger.debug(f"Agent {agent_id} has {len(filtered)} tools: {tool_names}")
|
||||
return filtered
|
||||
|
||||
@staticmethod
|
||||
def _is_image_data_url(value: str) -> bool:
|
||||
v = str(value or "").strip()
|
||||
return bool(v.startswith("data:image/") and ";base64," in v)
|
||||
|
||||
@staticmethod
|
||||
def _is_known_non_direct_image_url(url: str) -> bool:
|
||||
u = str(url or "").strip()
|
||||
if not u:
|
||||
return False
|
||||
try:
|
||||
p = urlparse(u)
|
||||
except Exception:
|
||||
return True
|
||||
host = (p.netloc or "").lower()
|
||||
if host in {"t.me", "telegram.me"}:
|
||||
return True
|
||||
if "web.telegram.org" in host:
|
||||
return True
|
||||
return False
|
||||
|
||||
async def execute_tool(
|
||||
self,
|
||||
@@ -2652,6 +2673,10 @@ class ToolManager:
|
||||
"""Plant identification via Pl@ntNet API (skeleton adapter)."""
|
||||
query = str(args.get("query", "") or "").strip()
|
||||
image_url = str(args.get("image_url", "") or "").strip()
|
||||
image_data = str(args.get("image_data", "") or "").strip()
|
||||
runtime_image_data = str(args.get("_runtime_image_data", "") or "").strip()
|
||||
if not image_data and self._is_image_data_url(runtime_image_data):
|
||||
image_data = runtime_image_data
|
||||
organ = str(args.get("organ", "auto") or "auto").strip().lower()
|
||||
top_k = max(1, min(int(args.get("top_k", 3)), 5))
|
||||
|
||||
@@ -2687,8 +2712,15 @@ class ToolManager:
|
||||
except Exception as e:
|
||||
return ToolResult(success=False, result=None, error=f"plantnet_error: {e}")
|
||||
|
||||
if image_url:
|
||||
ni = await self._nature_id_identify({"image_url": image_url, "top_k": top_k})
|
||||
if image_url or image_data:
|
||||
ni_args: Dict[str, Any] = {"top_k": top_k}
|
||||
if image_data:
|
||||
ni_args["image_data"] = image_data
|
||||
else:
|
||||
ni_args["image_url"] = image_url
|
||||
if runtime_image_data:
|
||||
ni_args["_runtime_image_data"] = runtime_image_data
|
||||
ni = await self._nature_id_identify(ni_args)
|
||||
if ni.success:
|
||||
return ni
|
||||
|
||||
@@ -2705,9 +2737,23 @@ class ToolManager:
|
||||
"""Open-source plant identification via self-hosted nature-id compatible endpoint."""
|
||||
image_url = str(args.get("image_url", "") or "").strip()
|
||||
image_data = str(args.get("image_data", "") or "").strip()
|
||||
runtime_image_data = str(args.get("_runtime_image_data", "") or "").strip()
|
||||
if not image_data and self._is_image_data_url(runtime_image_data):
|
||||
image_data = runtime_image_data
|
||||
top_k = max(1, min(int(args.get("top_k", 3)), 10))
|
||||
min_confidence = float(args.get("min_confidence", os.getenv("NATURE_ID_MIN_CONFIDENCE", "0.65")))
|
||||
|
||||
if image_url and self._is_known_non_direct_image_url(image_url):
|
||||
if image_data:
|
||||
logger.info("nature_id_identify: replacing non-direct image_url with runtime image_data")
|
||||
image_url = ""
|
||||
else:
|
||||
return ToolResult(
|
||||
success=False,
|
||||
result=None,
|
||||
error="image_url is not direct image URL; provide image_data or direct Telegram file URL",
|
||||
)
|
||||
|
||||
if not image_url and not image_data:
|
||||
return ToolResult(success=False, result=None, error="image_url or image_data is required")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user