Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,24 @@

All notable changes to this project will be documented in this file.

## [0.1.3] - 2025-10-22
Copy link

Copilot AI Nov 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Invalid date in version entry. The date "2025-10-22" is in the future relative to the current date. This should likely be "2024-10-22" or another valid past date.

Copilot uses AI. Check for mistakes.

### Added
- Gradio space runner tool working on the drag-and-dropped image.
- Repo info tool extracting relevant information in markdown for the agent.

### Fixed
- Gradio UI: bound `clear.click(...)` inside the Blocks context to prevent “Cannot call click outside of a gradio.Blocks context”.
- Chatbot: migrated to `type="messages"` by adding a conversion shim (pairs <-> messages) in the handler to satisfy the new schema.
- UI polish: hide the “Demo link” textbox and “Run demo on preview” button at launch and until a tool is found; reveal them after recommendations appear and on selection, and hide again on Clear.
- Cache is now cleaned correctly.
- Fixed the preview for the png files.

## [0.1.2] - 2025-10-07
Comment thread
qchapp marked this conversation as resolved.

### Added
- Pydantic AI pipeline working with a few tools
- Better handling of the runnable example link and reranker
- Pydantic AI pipeline working with a few tools.
- Better handling of the runnable example link and reranker.

## [0.1.1] - 2025-10-02
Comment thread
qchapp marked this conversation as resolved.

Expand All @@ -17,4 +30,4 @@ All notable changes to this project will be documented in this file.
## [0.1.0] - 2025-09-30
Comment thread
qchapp marked this conversation as resolved.

### Added
- Chat functionality
- Chat functionality.
175 changes: 126 additions & 49 deletions src/ai_agent/agent/tools/gradio_space_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@

from typing import List, Optional, Dict, Any
from pydantic import BaseModel
import os
import os, re, logging
from .utils import get_pipeline
from utils.utils import _best_runnable_link
from utils.previews import _build_preview_for_vlm
from gradio_client import Client, handle_file
import tempfile
from pathlib import Path
import requests

# -------- Gradio run_example tool -------------------------------------------
class RunExampleInput(BaseModel):
Expand All @@ -21,47 +25,83 @@ class RunExampleOutput(BaseModel):
endpoint_url: Optional[str] = None
api_name: Optional[str] = None
notes: Optional[str] = None
# Back-compat: 'result_image' kept as alias for preview
result_image: Optional[str] = None
result_preview: Optional[str] = None
result_origin: Optional[str] = None # original returned file (downloaded if URL)

log = logging.getLogger("agent.run_example")

_HF_SPACE_RE = re.compile(r"^https?://huggingface\.co/spaces/([^/]+)/([^/]+)/?$")

def _normalize_space_identifier(url_or_name: str) -> str:
"""Accepts full HF Spaces URL or 'owner/space' or a direct app URL; returns a Client-acceptable src.
Prefer 'owner/space' for HF Spaces page URLs.
"""
s = (url_or_name or "").strip()
m = _HF_SPACE_RE.match(s)
if m:
owner, space = m.group(1), m.group(2)
return f"{owner}/{space}"
return s

def _choose_endpoint(endpoints: List[Dict[str, Any]], have_image: bool) -> Optional[Dict[str, Any]]:
"""Pick a sensible endpoint: prefer one that accepts an image if we have one; else the first text-only."""
def has_image(f: Dict[str, Any]) -> bool:
for i in f.get("inputs", []):
t = str(i.get("type") or i.get("component") or "").lower()
if "image" in t:
return True
return False

if have_image:
for f in endpoints:
if has_image(f):
return f
# fallback: any endpoint
return endpoints[0] if endpoints else None


def _build_payload(fn: Dict[str, Any], image_path: Optional[str], extra_text: Optional[str]) -> List[Any]:
inputs = fn.get("inputs", [])
payload: List[Any] = []
for spec in inputs:
t = str(spec.get("type") or spec.get("component") or "").lower()
# Gradio client supports passing file paths for image inputs
if "image" in t and image_path:
payload.append(handle_file(image_path) if handle_file else image_path)
elif "textbox" in t or "text" in t or "textarea" in t:
payload.append(extra_text or "")
else:
# default empty for other inputs (checkbox, number, etc.)
payload.append("")
return payload

def _download_to_temp(url: str) -> Optional[str]:
try:
r = requests.get(url, timeout=20)
if r.status_code != 200 or not r.content:
return None
# try to preserve extension from URL
from urllib.parse import urlparse
parsed = urlparse(url)
ext = os.path.splitext(parsed.path)[1]
if not ext:
# guess based on content-type
ct = r.headers.get("content-type", "").lower()
if "tiff" in ct or "tif" in ct:
ext = ".tif"
elif "png" in ct:
ext = ".png"
elif "jpeg" in ct or "jpg" in ct:
ext = ".jpg"
else:
ext = ".bin"
with tempfile.NamedTemporaryFile(delete=False, prefix="demo_result_", suffix=ext) as fd:
fd.write(r.content)
fd.flush()
return fd.name
except Exception:
return None


def _materialize_result(obj: Any) -> Optional[str]:
"""Try to materialize an image result to a local file and return the path.
Accepts a filepath or URL from common Gradio outputs.
"""
# Direct file path
try:
s = str(obj)
except Exception:
return None
if not s:
return None
# If it's an existing local file
p = Path(s)
if p.exists() and p.is_file():
return str(p)
# If it's a URL, try to download
if s.lower().startswith("http://") or s.lower().startswith("https://"):
return _download_to_temp(s)
# Unknown shape
return None


def tool_run_example(inp: RunExampleInput) -> RunExampleOutput:
"""Run a remote Gradio demo for a catalog tool on an optional user image using gradio_client.

Behavior:
- Determine Space URL: prefer explicit endpoint_url, else catalog runnable link.
- Discover API endpoints via view_api and choose one matching image/no-image needs.
- Used agreed endpoint /segment for now
- Build payload by mapping image path to image inputs and extra_text into text fields.
"""
pipe = get_pipeline()
Expand All @@ -74,25 +114,62 @@ def tool_run_example(inp: RunExampleInput) -> RunExampleOutput:
return RunExampleOutput(tool_name=inp.tool_name, ran=False, notes="No runnable example URL found")

try:
client = Client(url)
apis = client.view_api(return_format="dict") or {}
endpoints = apis.get("endpoints") or apis.get("named_endpoints") or []
if not isinstance(endpoints, list):
# some versions return dict of name->spec
endpoints = list(endpoints.values())
fn = _choose_endpoint(endpoints, have_image=bool(inp.image_path))
if not fn:
return RunExampleOutput(tool_name=inp.tool_name, ran=False, notes="No endpoints discovered", endpoint_url=url)
api_name = fn.get("api_name") or fn.get("path") or fn.get("route")
if not api_name:
# common default
api_name = "/predict"
payload = _build_payload(fn, inp.image_path, inp.extra_text)
src = _normalize_space_identifier(url)
hf_token = os.getenv("HF_TOKEN") or os.getenv("HUGGINGFACE_TOKEN")
log.info("Gradio run_example: src=%s (from=%s), tool=%s", src, url, inp.tool_name)
client = Client(src, hf_token=hf_token) if hf_token else Client(src)
api_name = "/segment" # agreed endpoint
Copy link

Copilot AI Nov 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hardcoded API endpoint. The endpoint /segment is hardcoded on line 121, which limits flexibility. According to the function's docstring (lines 104-105), it should "discover API endpoints via view_api and choose one matching image/no-image needs." The previous implementation had dynamic endpoint discovery logic that was removed, making this less flexible and potentially breaking for tools with different endpoint names.

Suggested change
api_name = "/segment" # agreed endpoint
# Discover API endpoints via view_api and choose one matching image/no-image needs
api_info = client.view_api()
api_name = None
def _select_api_endpoint(api_info, has_image):
# Prefer endpoints with image input if image is provided, else text-only
for endpoint in api_info:
inputs = endpoint.get("inputs", [])
# Check for image input
if has_image and any(i.get("type", "").lower() in ("image", "file", "filepath") for i in inputs):
return endpoint.get("name")
# If no image, prefer text-only endpoint
if not has_image and all(i.get("type", "").lower() in ("text", "str", "string") for i in inputs):
return endpoint.get("name")
# Fallback: return first endpoint
if api_info:
return api_info[0].get("name")
return None
api_name = _select_api_endpoint(api_info, bool(inp.image_path))
if not api_name:
return RunExampleOutput(tool_name=inp.tool_name, ran=False, notes="No suitable API endpoint found", endpoint_url=url)

Copilot uses AI. Check for mistakes.
Comment thread
qchapp marked this conversation as resolved.
# For a simple segmentation endpoint that takes a single image input
if inp.image_path:
payload_file = handle_file(inp.image_path)
payload = [payload_file]
try:
log.info("Gradio run_example payload: file=%s ext=%s", inp.image_path, os.path.splitext(inp.image_path)[1].lower())
except Exception:
Comment thread
qchapp marked this conversation as resolved.
pass
else:
payload = [""]
try:
res = client.predict(*payload, api_name=api_name)
# Prefer keyword expected by docs ('file_obj'), then fallback to positional
try:
res = client.predict(file_obj=payload[0], api_name=api_name)
except Exception as e_kw:
log.debug("Keyword predict failed, falling back to positional: %r", e_kw)
res = client.predict(*payload, api_name=api_name)
stdout = str(res)
except Exception as e:
return RunExampleOutput(tool_name=inp.tool_name, ran=False, notes=f"predict failed: {e}", endpoint_url=url, api_name=api_name)
return RunExampleOutput(tool_name=inp.tool_name, ran=True, stdout=str(stdout)[:6000], endpoint_url=url, api_name=str(api_name))

# Materialize original result file (any supported format)
origin_path = None
if isinstance(res, (list, tuple)) and res:
origin_path = _materialize_result(res[0]) or origin_path
elif isinstance(res, dict):
# common keys from outputs
for k in ("file", "filepath", "image", "output", "result", "mask"):
if k in res:
origin_path = _materialize_result(res[k]) or origin_path
if origin_path:
break
else:
origin_path = _materialize_result(res)

preview_path = None
if origin_path:
try:
preview_path, _ = _build_preview_for_vlm([origin_path])
except Exception as e:
log.debug("Preview build failed for %s: %r", origin_path, e)

return RunExampleOutput(
tool_name=inp.tool_name,
ran=True,
stdout=str(stdout)[:6000],
endpoint_url=url,
api_name=str(api_name),
result_image=preview_path,
result_preview=preview_path,
result_origin=origin_path,
)
except Exception as e:
return RunExampleOutput(tool_name=inp.tool_name, ran=False, notes=str(e), endpoint_url=url)
Loading