defmodule AI.Tools.SelfHelp.Docs do @moduledoc """ Searches fnord's published documentation to answer questions about features, configuration, and usage patterns. Uses a web-search-capable model scoped to fnord's canonical documentation sites. """ @behaviour AI.Tools # Build canonical URL lists from the user-facing doc lanes at compile time # so adding a new guide or use-case doc automatically expands the search # scope without editing this module. Two lanes are in scope: # # - docs/user/ - feature/reference guides. # - docs/use-cases/ - end-to-end workflow runbooks. # # Both are published to hexdocs (see mix.exs, which globs the same two # directories), so a hexdocs URL resolves for every file here. Dev docs # under docs/dev/ are intentionally out of scope: they're architecture # notes for contributors/LLMs working on fnord itself, not answers to # end-user questions. # # Each lane's README.md is its hand-curated index and is NOT in hexdocs # extras (excluded in mix.exs to avoid a `readme.html` filename collision # with the top-level README.md), so a URL for either would 404. The root # README is already represented as the hardcoded `readme.html` entry # below; drop both README duplicates from the glob before building URLs. @doc_paths (Path.wildcard("docs/user/*.md") ++ Path.wildcard("docs/use-cases/*.md")) |> Enum.reject(&(&1 in ["docs/user/README.md", "docs/use-cases/README.md"])) |> Enum.sort() for path <- @doc_paths, do: @external_resource(path) @hexdocs_urls [ "- https://hexdocs.pm/fnord/readme.html" | Enum.map(@doc_paths, fn path -> "- https://hexdocs.pm/fnord/#{Path.basename(path, ".md")}.html" end) ] |> Enum.join("\n") @github_urls [ "- https://github.com/sysread/fnord/blob/main/README.md" | Enum.map(@doc_paths, fn path -> "- https://github.com/sysread/fnord/blob/main/#{path}" end) ] |> Enum.join("\n") @system_prompt """ You are a documentation lookup tool for fnord, an AI-powered CLI for codebase research and editing. Start with these canonical sources. Hexdocs (preferred): #{@hexdocs_urls} GitHub (fallback): #{@github_urls} Follow links from these pages when the answer requires it. GitHub's web UI URLs sometimes fail to load. When fetching content from GitHub, convert to raw URLs. For example: https://github.com/sysread/fnord/blob/main/docs/user/README.md becomes: https://raw.githubusercontent.com/sysread/fnord/refs/heads/main/docs/user/README.md Provide a direct, concise answer based on the documentation content. Include inline citations (page titles or URLs) when referencing specific information. If the documentation does not cover the question, say so explicitly. """ @impl AI.Tools def async?(), do: true @impl AI.Tools def is_available?(), do: true @impl AI.Tools def ui_note_on_request(%{"question" => q}), do: {"Researching fnord docs", q} def ui_note_on_request(_), do: "Researching fnord docs" @impl AI.Tools def ui_note_on_result(_, _), do: nil @impl AI.Tools def tool_call_failure_message(_, _), do: :default @impl AI.Tools def spec do %{ type: "function", name: "fnord_help_docs_tool", description: """ Search fnord's published documentation to answer questions about features, configuration, and usage. Use this tool when the user asks about how fnord works, what a feature does, or how to configure something. For CLI structure questions (flags, subcommands, command tree), prefer fnord_help_cli_tool instead. """, parameters: %{ type: "object", additionalProperties: false, required: ["question"], properties: %{ "question" => %{ type: "string", description: "The question to research in fnord's documentation." } } } } end @impl AI.Tools def read_args(args), do: {:ok, args} @impl AI.Tools def call(args) do with {:ok, question} <- AI.Tools.get_arg(args, "question") do search_docs(question) end end defp search_docs(question) do AI.Completion.get( model: AI.Model.web_search(), web_search?: true, messages: [ AI.Util.system_msg(@system_prompt), AI.Util.user_msg(question) ] ) |> case do {:ok, %{response: response}} -> {:ok, response} # Completion exposes the usage count alongside :context_length_exceeded; # collapsing it to an :error with a descriptive reason keeps the tool # caller contract single-shaped without dropping signal for logs. {:error, :context_length_exceeded, _usage} -> {:error, "documentation research exceeded the context window"} {:error, reason} -> {:error, reason} end end end