summaryrefslogtreecommitdiff
path: root/src/ollama_web/tools
diff options
context:
space:
mode:
Diffstat (limited to 'src/ollama_web/tools')
-rw-r--r--src/ollama_web/tools/fetch.py23
-rw-r--r--src/ollama_web/tools/registry.py56
-rw-r--r--src/ollama_web/tools/scrape.py4
3 files changed, 65 insertions, 18 deletions
diff --git a/src/ollama_web/tools/fetch.py b/src/ollama_web/tools/fetch.py
index 6ab17bb..7e2d3b5 100644
--- a/src/ollama_web/tools/fetch.py
+++ b/src/ollama_web/tools/fetch.py
@@ -80,7 +80,7 @@ def store_fetched_file(
"mime": mime,
"text": text,
"url": url,
- }
+ },
)
@@ -91,7 +91,7 @@ def pop_fetched_files(session_id: str) -> list[_FetchedFile]:
def _normalize_file_type(file_type: str) -> str:
ft = file_type.strip().lower().lstrip(".")
- return _FILETYPE_SUFFIXES.get(ft, ft)
+ return _FILETYPE_SUFFIXES.get(ft, "")
def _filetype_query(query: str, file_type: str) -> str:
@@ -117,9 +117,9 @@ def _guess_mime(name: str, file_type: str) -> str:
return mime or f"application/{file_type}"
-def _fetch_bytes(url: str) -> bytes:
- data, _, _ = fetch_public_bytes(url, timeout=settings.scrape_timeout)
- return data
+def _fetch_bytes(url: str) -> tuple[bytes, str]:
+ data, _, content_type = fetch_public_bytes(url, timeout=settings.scrape_timeout)
+ return data, content_type
def _full_text_for_file(url: str, data: bytes, file_type: str) -> str:
@@ -212,9 +212,7 @@ def search_and_fetch(
except (TypeError, ValueError) as exc:
return f"Invalid continuation token: cursor is not an integer: {exc}"
legacy_pdf_offset = (
- file_offset
- if payload.get("v") == 1 and "pdf_page_index" not in payload
- else None
+ file_offset if payload.get("v") == 1 and "pdf_page_index" not in payload else None
)
if min(file_idx, file_offset, pdf_page_index, pdf_page_char_offset) < 0:
return "Invalid continuation token: cursor values must not be negative."
@@ -293,7 +291,10 @@ def _render_file_page(
p = get_prompts().get("fetch", {})
try:
- data = _fetch_bytes(url)
+ data, content_type = _fetch_bytes(url)
+ media_type = content_type.split(";", 1)[0].strip().lower()
+ if media_type.startswith(("image/", "audio/", "video/")):
+ raise ValueError(f"unsupported media content type: {media_type}")
except Exception as exc: # noqa: BLE001
# On fetch failure, advance to the next candidate rather than aborting
# the whole pagination chain.
@@ -439,9 +440,7 @@ def _render_file_page(
return f"File pagination failed: {exc}"
-def _make_fetch_token(
- matched: list[dict[str, str]], cursor: dict[str, int], file_type: str
-) -> str:
+def _make_fetch_token(matched: list[dict[str, str]], cursor: dict[str, int], file_type: str) -> str:
file_idx = cursor["file_idx"]
url = matched[file_idx].get("href", "") if file_idx < len(matched) else "completed"
return make_token(
diff --git a/src/ollama_web/tools/registry.py b/src/ollama_web/tools/registry.py
index e3b7824..51bdf29 100644
--- a/src/ollama_web/tools/registry.py
+++ b/src/ollama_web/tools/registry.py
@@ -14,6 +14,37 @@ from .search import web_search
@dataclass
+class ToolArtifact:
+ """Binary output produced by a tool, kept outside its text result."""
+
+ kind: str
+ mime: str
+ name: str
+ data: bytes
+ file_id: str | None = None
+ for_user: bool = True
+ for_assistant: bool = True
+
+ def public_dict(self) -> dict[str, Any]:
+ """Return browser-safe metadata without the binary payload."""
+ return {
+ "id": self.file_id,
+ "kind": self.kind,
+ "mime": self.mime,
+ "name": self.name,
+ "size": len(self.data),
+ }
+
+
+@dataclass
+class ToolExecutionResult:
+ """A tool's LLM-facing text and separately handled binary artifacts."""
+
+ text: str
+ artifacts: list[ToolArtifact] = field(default_factory=list)
+
+
+@dataclass
class ToolRegistry:
"""A simple registry mapping tool names to callable implementations."""
@@ -75,6 +106,20 @@ class ToolRegistry:
def get(self, name: str) -> Callable[..., object] | None:
return self._tools.get(name)
+ def execute_detailed(
+ self, name: str, arguments: dict[str, object] | str
+ ) -> ToolExecutionResult:
+ """Execute a tool while preserving any binary artifacts separately."""
+ result = self._execute_raw(name, arguments)
+ if isinstance(result, ToolExecutionResult):
+ return result
+ if isinstance(result, str):
+ return ToolExecutionResult(result)
+ try:
+ return ToolExecutionResult(json.dumps(result, ensure_ascii=False, default=str))
+ except (TypeError, ValueError):
+ return ToolExecutionResult(str(result))
+
def execute(self, name: str, arguments: dict[str, object] | str) -> str:
"""Execute a named tool and return a JSON-encoded string result.
@@ -87,6 +132,10 @@ class ToolRegistry:
A JSON string representation of the tool's return value, or an error
object when the tool is unknown or raises.
"""
+ return self.execute_detailed(name, arguments).text
+
+ def _execute_raw(self, name: str, arguments: dict[str, object] | str) -> object:
+ """Execute a named tool without flattening a structured result."""
func = self.get(name)
if func is None:
return json.dumps({"error": f"Unknown tool: {name}"})
@@ -106,12 +155,7 @@ class ToolRegistry:
except Exception as exc: # noqa: BLE001
return json.dumps({"error": f"{type(exc).__name__}: {exc}"})
- if isinstance(result, str):
- return result
- try:
- return json.dumps(result, ensure_ascii=False, default=str)
- except (TypeError, ValueError):
- return str(result)
+ return result
def default_registry() -> ToolRegistry:
diff --git a/src/ollama_web/tools/scrape.py b/src/ollama_web/tools/scrape.py
index 2f85c38..aa79793 100644
--- a/src/ollama_web/tools/scrape.py
+++ b/src/ollama_web/tools/scrape.py
@@ -80,6 +80,10 @@ def scrape_url(url: str, max_chars: int | None = None, continue_from: str | None
except Exception as exc: # noqa: BLE001
return f"Failed to fetch {url}: {exc}"
+ media_type = content_type.split(";", 1)[0].strip().lower()
+ if media_type.startswith(("image/", "audio/", "video/")):
+ return f"Unsupported content type for text scraping: {media_type}."
+
cleaned = extract_readable_text(data, content_type)
if not cleaned: