diff options
Diffstat (limited to 'src/ollama_web/tools')
| -rw-r--r-- | src/ollama_web/tools/fetch.py | 23 | ||||
| -rw-r--r-- | src/ollama_web/tools/registry.py | 56 | ||||
| -rw-r--r-- | src/ollama_web/tools/scrape.py | 4 |
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: |
