Newer
Older
navi-1 / navi / tools / share_file.py
@Eugene Sukhodolskiy Eugene Sukhodolskiy on 17 Apr 3 KB Webclient UI improvements + backend fixes
"""share_file tool — expose a local file to the user as a download link."""

import shutil
from pathlib import Path

from navi.config import settings
from navi.session_files import ensure_session_dir

from .base import Tool, ToolResult, current_session_id


def _fmt_size(n: int) -> str:
    if n < 1024:
        return f"{n} B"
    if n < 1024 ** 2:
        return f"{n / 1024:.1f} KB"
    return f"{n / 1024 ** 2:.1f} MB"


class ShareFileTool(Tool):
    name = "share_file"
    description = (
        "Make a file available for the user to download via a direct HTTP link. "
        "Use after generating or producing a file the user will want to keep (report, archive, export, etc.). "
        "IMPORTANT — path must be an ABSOLUTE path (e.g. /home/user/file.zip). "
        "If you only know a relative path, resolve it first: use filesystem(action='info') or "
        "terminal('realpath <relative_path>') to get the absolute path, then call share_file. "
        "After a successful call the result contains a URL — you MUST include that URL in your reply "
        "to the user as a clickable markdown link: [filename](url)."
    )
    parameters = {
        "type": "object",
        "properties": {
            "path": {
                "type": "string",
                "description": "Absolute path to the file to share",
            },
            "filename": {
                "type": "string",
                "description": (
                    "Download filename shown to the user. "
                    "Defaults to the original filename. "
                    "Use this to give the file a clean, descriptive name."
                ),
            },
        },
        "required": ["path"],
    }

    async def execute(self, params: dict) -> ToolResult:
        session_id = current_session_id.get()
        if not session_id:
            return ToolResult(success=False, output="No active session context.", error="no_session")

        src = Path(params["path"]).expanduser().resolve()
        if not src.exists():
            return ToolResult(success=False, output=f"File not found: {src}", error="not_found")
        if not src.is_file():
            return ToolResult(success=False, output=f"Path is not a file: {src}", error="not_a_file")

        # Determine the download filename
        download_name = Path(params.get("filename") or src.name).name or src.name

        dest_dir = ensure_session_dir(session_id)
        dest = dest_dir / download_name

        # If it's already the same file in the session dir, skip copy
        if dest.resolve() == src.resolve():
            pass
        else:
            # Avoid clobbering an existing different file with the same name
            if dest.exists():
                stem = Path(download_name).stem
                suffix = Path(download_name).suffix
                i = 1
                while dest.exists():
                    dest = dest_dir / f"{stem}_{i}{suffix}"
                    i += 1
            shutil.copy2(str(src), str(dest))

        size = dest.stat().st_size
        base_url = settings.public_url.rstrip("/")
        url = f"{base_url}/sessions/{session_id}/files/{dest.name}"

        return ToolResult(
            success=True,
            output=f"Download ready: {dest.name} ({_fmt_size(size)})\nURL: {url}",
            metadata={"url": url, "filename": dest.name, "size": size},
        )