Current section
Files
Jump to
Current section
Files
lib/examples/tools/browser.ex
defmodule Jido.AI.Examples.Tools.Browser.ReadPage do
@moduledoc "Read a web page and return extracted content using a compatibility-safe fallback order."
use Jido.Action,
name: "read_page",
description: "Read a web page URL and return extracted content",
schema: [
url: [type: :string, required: true, doc: "URL to read"],
format: [type: {:in, [:html, :text, :markdown]}, default: :html, doc: "Preferred output format"],
max_chars: [type: :integer, default: 20_000, doc: "Maximum number of characters to return"]
]
@impl true
def run(%{url: url} = params, _context) do
preferred = Map.get(params, :format, :html)
max_chars = Map.get(params, :max_chars, 20_000)
with {:ok, browser} <- ensure_browser_loaded() do
case apply(browser, :start_session, [[headless: true]]) do
{:ok, session} ->
try do
with {:ok, session, _meta} <- apply(browser, :navigate, [session, url]),
{:ok, content, used_format} <- extract_with_fallback(session, preferred, browser) do
trimmed = String.slice(content, 0, max_chars)
{:ok,
%{
url: url,
format: used_format,
content: trimmed,
length: String.length(trimmed)
}}
end
after
apply(browser, :end_session, [session])
end
{:error, reason} ->
{:error, "Failed to start browser session: #{inspect(reason)}"}
end
end
end
defp extract_with_fallback(session, preferred_format, browser) do
formats =
[preferred_format, :html, :text, :markdown]
|> Enum.uniq()
Enum.reduce_while(formats, {:error, :no_content_extracted}, fn format, _acc ->
case apply(browser, :extract_content, [session, [format: format, selector: "body"]]) do
{:ok, _updated, %{content: content}} when is_binary(content) and byte_size(content) > 0 ->
{:halt, {:ok, content, format}}
{:ok, _updated, _} ->
{:cont, {:error, {:empty_content, format}}}
{:error, _reason} ->
{:cont, {:error, {:extract_failed, format}}}
end
end)
end
defp ensure_browser_loaded do
browser = Module.concat([JidoBrowser])
if Code.ensure_loaded?(browser) do
{:ok, browser}
else
{:error, "jido_browser dependency is not available"}
end
end
end
defmodule Jido.AI.Examples.Tools.Browser.SearchWeb do
@moduledoc "Search the web using Brave Search and return ranked result snippets."
use Jido.Action,
name: "search_web",
description: "Search the web via Brave Search API",
schema: [
query: [type: :string, required: true, doc: "Search query"],
max_results: [type: :integer, default: 5, doc: "Maximum number of results to return"]
]
@brave_endpoint "https://api.search.brave.com/res/v1/web/search"
@impl true
def run(%{query: query} = params, _context) do
max_results = Map.get(params, :max_results, 5)
with {:ok, token} <- brave_api_key(),
{:ok, response} <- request_search(token, query, max_results) do
results = normalize_results(response.body, max_results)
{:ok,
%{
query: query,
count: length(results),
results: results
}}
end
end
defp brave_api_key do
case System.get_env("BRAVE_SEARCH_API_KEY") do
value when is_binary(value) and value != "" -> {:ok, value}
_ -> {:error, "Missing BRAVE_SEARCH_API_KEY"}
end
end
defp request_search(token, query, max_results) do
Req.get(@brave_endpoint,
headers: [{"accept", "application/json"}, {"x-subscription-token", token}],
params: [q: query, count: max_results]
)
end
defp normalize_results(body, max_results) do
body
|> Map.get("web", %{})
|> Map.get("results", [])
|> Enum.take(max_results)
|> Enum.with_index(1)
|> Enum.map(fn {result, rank} ->
%{
rank: rank,
title: Map.get(result, "title", "(untitled)"),
url: Map.get(result, "url", ""),
snippet: Map.get(result, "description", "")
}
end)
end
end
defmodule Jido.AI.Examples.Tools.Browser.SnapshotUrl do
@moduledoc "Capture a structural page snapshot (title/content/headings/links/forms) for a URL."
use Jido.Action,
name: "snapshot_url",
description: "Capture a page snapshot for links/forms/headings",
schema: [
url: [type: :string, required: true, doc: "URL to snapshot"],
include_links: [type: :boolean, default: true, doc: "Include links in snapshot"],
include_forms: [type: :boolean, default: true, doc: "Include forms in snapshot"],
include_headings: [type: :boolean, default: true, doc: "Include headings in snapshot"]
]
@impl true
def run(%{url: url} = params, _context) do
with {:ok, browser} <- ensure_browser_loaded() do
case apply(browser, :start_session, [[headless: true]]) do
{:ok, session} ->
try do
with {:ok, nav_session, _meta} <- apply(browser, :navigate, [session, url]),
{:ok, content, _format} <- extract_content_with_fallback(nav_session, browser),
{:ok, title} <- extract_title(nav_session, url) do
{:ok,
%{
url: url,
title: title,
content: content,
headings: if(Map.get(params, :include_headings, true), do: [], else: []),
links: if(Map.get(params, :include_links, true), do: [], else: []),
forms: if(Map.get(params, :include_forms, true), do: [], else: [])
}}
end
after
apply(browser, :end_session, [session])
end
{:error, reason} ->
{:error, "Failed to start browser session: #{inspect(reason)}"}
end
end
end
defp extract_content_with_fallback(session, browser) do
[:html, :text]
|> Enum.reduce_while({:error, :no_content_extracted}, fn format, _acc ->
case apply(browser, :extract_content, [session, [format: format, selector: "body"]]) do
{:ok, _updated_session, %{content: content}} when is_binary(content) and byte_size(content) > 0 ->
{:halt, {:ok, content, format}}
_ ->
{:cont, {:error, {:extract_failed, format}}}
end
end)
end
defp extract_title(session, fallback) do
get_title = Module.concat([JidoBrowser, Actions, GetTitle])
case apply(get_title, :run, [%{}, %{session: session}]) do
{:ok, %{title: title}} when is_binary(title) and title != "" -> {:ok, title}
_ -> {:ok, fallback}
end
end
defp ensure_browser_loaded do
browser = Module.concat([JidoBrowser])
if Code.ensure_loaded?(browser) do
{:ok, browser}
else
{:error, "jido_browser dependency is not available"}
end
end
end