Packages
fnord
0.9.13
0.9.40
0.9.39
0.9.38
0.9.37
0.9.36
0.9.35
0.9.34
0.9.33
0.9.32
0.9.31
0.9.30
0.9.29
0.9.28
0.9.27
0.9.26
0.9.25
0.9.24
0.9.23
0.9.22
0.9.21
0.9.20
0.9.19
0.9.18
0.9.17
0.9.16
0.9.15
0.9.14
0.9.13
0.9.12
0.9.11
0.9.10
0.9.9
0.9.8
0.9.7
0.9.6
0.9.5
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.99
0.8.98
0.8.97
0.8.96
0.8.95
0.8.94
0.8.93
0.8.92
0.8.91
0.8.90
0.8.89
0.8.88
0.8.87
0.8.86
0.8.85
0.8.84
0.8.83
0.8.82
0.8.81
0.8.80
0.8.79
0.8.78
0.8.77
0.8.76
0.8.75
0.8.74
0.8.73
0.8.72
0.8.71
0.8.70
0.8.69
0.8.68
0.8.67
0.8.66
0.8.65
0.8.64
0.8.63
0.8.62
0.8.61
0.8.60
0.8.59
0.8.58
0.8.57
0.8.56
0.8.55
0.8.54
0.8.53
0.8.52
0.8.51
0.8.50
0.8.49
0.8.48
0.8.47
0.8.46
0.8.45
0.8.44
0.8.43
0.8.42
0.8.41
0.8.40
0.8.39
0.8.38
0.8.37
0.8.36
0.8.35
0.8.34
0.8.33
0.8.32
0.8.31
0.8.30
0.8.29
0.8.27
0.8.26
0.8.25
0.8.24
0.8.23
0.8.22
0.8.21
0.8.20
0.8.19
0.8.18
0.8.17
0.8.16
0.8.15
0.8.14
0.8.13
0.8.12
0.8.11
0.8.1
0.8.0
0.7.24
0.7.23
0.7.22
0.7.21
0.7.20
0.7.19
0.7.18
0.7.17
0.7.16
0.7.15
0.7.14
0.7.13
0.7.12
0.7.11
0.7.10
0.7.9
0.7.8
0.7.7
0.7.6
0.7.5
0.7.3
0.7.2
0.7.1
0.7.0
0.6.9
0.6.8
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.1
0.6.0
0.5.9
0.5.8
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.44
0.4.43
0.4.42
0.4.41
0.4.40
0.4.39
0.4.38
0.4.37
0.4.36
0.4.35
0.4.34
0.4.33
0.4.32
0.4.30
0.4.29
0.4.28
0.4.27
0.4.26
0.4.25
0.4.24
0.4.23
0.4.22
0.4.21
0.4.20
0.4.19
0.4.18
0.4.17
0.4.16
0.4.15
0.4.14
0.4.13
0.4.12
0.4.11
0.4.10
0.4.9
0.4.8
0.4.7
0.4.6
0.4.5
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.0
0.2.0
0.1.0
AI code archaeology
Current section
Files
Jump to
Current section
Files
lib/ai/tools/file/search.ex
defmodule AI.Tools.File.Search do
@max_search_results 25
@behaviour AI.Tools
@impl AI.Tools
def async?, do: true
@doc """
This tool requires that the project has been indexed to use. If the project
has not been indexed, the tool should not be made available.
"""
@impl AI.Tools
def is_available?(), do: AI.Tools.has_indexed_project()
@impl AI.Tools
def ui_note_on_request(args), do: {"Semantic search", args["query"]}
@impl AI.Tools
def ui_note_on_result(%{"query" => query}, result) do
re = ~r/Semantic search found (\d+) matching files in (\d+) ms:/
case Regex.run(re, result) do
[_, count_str, took_ms] ->
count = String.to_integer(count_str)
{"Semantic search", "#{query} -> #{count} file matches in #{took_ms} ms"}
_ ->
{"Semantic search", "#{query} -> #{result}"}
end
end
@impl AI.Tools
def tool_call_failure_message(_args, _reason), do: :default
@impl AI.Tools
def read_args(%{"query" => query}), do: {:ok, %{"query" => query}}
@impl AI.Tools
def spec() do
%{
type: "function",
function: %{
name: "file_search_tool",
description: """
Semantic search over project files. Your query is converted to an
embedding and compared against pre-computed file embeddings using
cosine similarity. This means your query must be **natural language**
describing the concept or topic you're looking for.
Your query is embedded as-is. Regex, syntax fragments, function
signatures, and boolean operators will NOT work - they will be
interpreted as literal text about those topics, not as patterns.
For exact string or pattern matching, use `rg` via `shell_tool`.
Good queries: "user authentication", "database connection settings",
"error handling for HTTP requests", "modules that parse JSON"
Bad queries: "def.*auth", "conn |> put_status", "TODO|FIXME"
Note that this tool does not have access to historical data or commit
messages. It only searches the most recently indexed version of the
project.
""",
parameters: %{
additionalProperties: false,
type: "object",
required: ["query"],
properties: %{
query: %{
type: "string",
description: """
A natural language query describing what you're looking for.
Write it as you would describe the topic to a person, not as
a code pattern or regex.
Examples:
- "user authentication"
- "feature flag definitions"
- "settings related to database connections"
- "functions that call the payment API"
- "unit tests related to the parser"
- "modules that read or write json data"
"""
}
}
}
}
}
end
@impl AI.Tools
def call(args) do
with {:ok, query} <- Map.fetch(args, "query"),
start_time = DateTime.utc_now(),
{:ok, matches} <- search(query),
took_ms = DateTime.diff(DateTime.utc_now(), start_time, :millisecond),
{:ok, index_state_msg} <- index_state() do
{:ok, build_response(matches, took_ms, index_state_msg)}
end
end
# Build the complete tool response by formatting matches and the index state banner.
defp build_response(matches, took_ms, index_state_msg) do
header = "[search_tool]"
body =
matches
|> Enum.map(&format_entry/1)
|> Enum.join("\n-----\n")
|> then(fn body ->
"""
Semantic search found #{length(matches)} matching files in #{took_ms} ms:
-----
#{body}
"""
end)
[header, body, "-----", index_state_msg]
|> Enum.join("\n")
end
# Format a single search result entry as markdown with file, similarity, and summary.
defp format_entry({file, score, data}) do
"""
# `#{file}` (cosine similarity: #{score})
#{data["summary"]}
"""
end
# -----------------------------------------------------------------------------
# Returns the current state of the project index, including new, stale, and
# deleted files.
# -----------------------------------------------------------------------------
defp index_state do
with {:ok, project} <- Store.get_project() do
%{new: new, stale: stale, deleted: deleted} = Store.Project.index_status(project)
msg =
"""
The results of this search may be affected by the state of the project index.
The current index state is:
- New files (not yet indexed): #{length(new)}
- Stale files (outdated index): #{length(stale)}
- Deleted files (indexed but deleted in project): #{length(deleted)}
If you are seeing unexpected search results, try reindexing with the `file_reindex_tool` tool.
"""
{:ok, msg}
end
end
# -----------------------------------------------------------------------------
# Searches the database for matches to the search query. Returns a list of
# `{file, score, data}` tuples.
# -----------------------------------------------------------------------------
defp search(query) do
%{
detail: true,
limit: @max_search_results,
query: query
}
|> Search.Files.new()
|> Search.Files.get_results()
|> case do
{:ok, results} ->
results
|> Enum.map(fn {entry, score, data} ->
{entry.rel_path, score, data}
end)
|> then(&{:ok, &1})
{:error, reason} ->
{:error, reason}
end
end
end