Newer
Older
navi-1 / navi / tools / content_publish.py
"""content_publish tool — publish a session file for inline viewing in chat.

The file must already exist in the session's file directory.
Publishing only registers metadata; the file itself is NOT copied.
The user sees the file via the existing /api/sessions/{id}/files/{name} endpoint,
so edits made by the agent are immediately visible.
"""

from pathlib import Path

from navi.content_store import publish
from navi.config import settings
from navi.session_files import session_dir

from ._internal.base import Tool, ToolContext, ToolResult, current_session_id


class ContentPublishTool(Tool):
    name = "content_publish"
    description = (
        "Publish a file for inline viewing in the chat client. "
        "Use this when you generate or produce content the user will want to see interactively "
        "(3D models, HTML pages, SVG graphics, images, videos, PDFs, etc.).\n\n"
        "There are two different file areas: workspace/ is for persistent private working files; "
        "the session directory is for files visible to the user in this chat. "
        "IMPORTANT — the file MUST already be inside the current session directory. "
        "The default path is session_files/{session_id}/, but the root is configured by "
        "SESSION_FILES_DIR. This tool does NOT copy from workspace/. "
        "Before publishing, write, copy, or move the final file into the session folder. "
        "If a file with the same name already exists in the session directory, choose a different name "
        "or check the directory contents first with `filesystem list <session_dir>`.\n\n"
        "Best practices:\n"
        "- Use workspace/ for drafts and reusable work files\n"
        "- Use the session directory for final artifacts the user should view now\n"
        "- If a file already exists elsewhere and only needs a download link, use share_file instead\n"
        "- If a file exists elsewhere but needs an inline viewer, copy it into the session directory first\n"
        "- Use descriptive filenames (e.g., 'sales_chart.svg' not 'file.svg')\n"
        "- After publishing, you can edit the file directly and the user will see changes immediately\n"
        "- For images, use PNG or JPEG; for interactive content, use HTML or SVG\n"
        "- For STL created from OpenSCAD, pass source_filename only if the .scad file exists in the session directory; "
        "omit it for downloaded STL files or when no source exists"
    )
    parameters = {
        "type": "object",
        "properties": {
            "filename": {
                "type": "string",
                "description": (
                    "Name of the file to publish. "
                    "The file must already exist in the session directory. "
                    "Example: 'chart.svg' or 'report.html'."
                ),
            },
            "title": {
                "type": "string",
                "description": "Human-readable title shown on the content card",
            },
            "content_type": {
                "type": "string",
                "enum": ["stl", "html", "svg", "pdf", "image", "video", "unknown"],
                "description": "Content type for viewer selection. Auto-detected from extension if omitted.",
            },
            "source_filename": {
                "type": "string",
                "description": (
                    "Optional source file in the same session directory. "
                    "Use this mainly for STL models when an OpenSCAD .scad source file exists. "
                    "Do not invent it; omit it if the model was downloaded or no source file exists."
                ),
            },
        },
        "required": ["filename"],
    }

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

        filename = Path(params["filename"]).name  # strip any path components
        sess_dir = session_dir(session_id)
        src = sess_dir / filename

        if not src.exists():
            return ToolResult(
                success=False,
                output=(
                    f"File '{filename}' not found in the session directory: {sess_dir}\n"
                    f"SESSION_FILES_DIR is configured as: {settings.session_files_dir}\n"
                    f"Make sure the file was written, copied, or moved there before publishing. "
                    f"`workspace/` is separate and is not publishable directly. "
                    f"You can check the directory contents with `filesystem list {sess_dir}`."
                ),
                error="not_found",
            )
        if not src.is_file():
            return ToolResult(
                success=False,
                output=f"Path is not a file: {src}",
                error="not_a_file",
            )

        try:
            info = await publish(
                session_id=session_id,
                filename=filename,
                title=params.get("title"),
                content_type=params.get("content_type"),
                source_filename=params.get("source_filename"),
            )
        except FileNotFoundError as e:
            return ToolResult(success=False, output=str(e), error="not_found")
        except IsADirectoryError as e:
            return ToolResult(success=False, output=str(e), error="is_directory")
        except Exception as e:
            return ToolResult(success=False, output=f"Publish failed: {e}", error="publish_failed")

        return ToolResult(
            success=True,
            output=(
                f"Published: {info['title']} ({info['content_type']})\n"
                f"URL: {info['url']}\n"
                f"ID: {info['id']}\n"
                + (f"Source: {info['source_filename']}\n" if info.get("source_filename") else "")
                + f"If you need to edit this file later, edit it at: {src}"
            ),
            metadata=info,
        )