defmodule Ragex.Embeddings.TextGenerator do @moduledoc """ Generates embeddable text descriptions from code entities. Converts modules, functions, and other code entities into natural language descriptions suitable for embedding generation and semantic search. Functions enriched with MetaAST metadata (from `Ragex.Analyzers.Metastatic`) include additional semantic context in their text: - `async: true` -- annotated as "async function" - `is_macro: true` -- annotated as "macro" - `decorators: [...]` -- decorator names listed - `guards: ...` -- annotated as "with guards" - `type_annotations: [...]` -- type information listed """ @doc """ Generates text description for a module. Includes module name, documentation, and metadata. """ def module_text(module_data) do parts = [ "Module: #{module_name_to_string(module_data.name)}", if(module_data[:doc], do: "Documentation: #{module_data.doc}", else: nil), "File: #{module_data.file}", if(module_data[:metadata][:type], do: "Type: #{module_data.metadata.type}", else: nil), if(module_data[:metadata][:container_type], do: "Container type: #{module_data.metadata.container_type}", else: nil ) ] parts |> Enum.reject(&is_nil/1) |> Enum.join(". ") end @doc """ Generates text description for a function. Includes function signature, module context, documentation, visibility, and any MetaAST semantic metadata (async, macro, guards, decorators, annotations). """ def function_text(function_data) do signature = function_signature(function_data) meta = Map.get(function_data, :metadata, %{}) metastatic = Map.get(meta, :metastatic, %{}) # Deeper indexing enrichment: strings and keywords from metadata strings_hint = strings_hint(meta) keywords_hint = keywords_hint(meta) parts = [ "Function: #{signature}", "Module: #{module_name_to_string(function_data.module)}", if(function_data[:doc], do: "Documentation: #{function_data.doc}", else: nil), "Visibility: #{function_data.visibility}", "File: #{function_data.file}:#{function_data.line}", metastatic_hint(metastatic), strings_hint, keywords_hint ] parts |> Enum.reject(&is_nil/1) |> Enum.join(". ") end # Build a short semantic hint string from MetaAST enrichment data. # Returns nil when there is no enrichment to describe. defp metastatic_hint(metastatic) when is_map(metastatic) and map_size(metastatic) > 0 do hints = [] hints = if Map.get(metastatic, :async) == true do ["async" | hints] else hints end hints = if Map.get(metastatic, :is_macro) == true do ["macro" | hints] else hints end hints = case Map.get(metastatic, :guards) do nil -> hints _guard -> ["with guards" | hints] end hints = case Map.get(metastatic, :decorators) do nil -> hints [] -> hints decorators when is_list(decorators) -> ["decorators: #{Enum.join(decorators, ", ")}" | hints] end hints = case Map.get(metastatic, :multi_clause) do true -> clauses = Map.get(metastatic, :clauses, []) ["#{length(clauses)} clauses" | hints] _ -> hints end case hints do [] -> nil _ -> "Attributes: #{Enum.join(Enum.reverse(hints), ", ")}" end end defp metastatic_hint(_), do: nil # Build a hint from string literals found in the function body. defp strings_hint(meta) when is_map(meta) do case Map.get(meta, :strings) do strings when is_list(strings) and strings != [] -> # Take up to 5 strings, truncate each to 80 chars samples = strings |> Enum.take(5) |> Enum.map_join("; ", &String.slice(&1, 0, 80)) "Strings: #{samples}" _ -> nil end end # Build a hint from extracted keywords. defp keywords_hint(meta) when is_map(meta) do case Map.get(meta, :keywords) do kw when is_map(kw) and map_size(kw) > 0 -> top = kw |> Enum.sort_by(fn {_k, v} -> -v end) |> Enum.take(10) |> Enum.map_join(", ", &elem(&1, 0)) "Keywords: #{top}" _ -> nil end end @doc """ Generates text description for a function with its body/implementation. Includes signature and code snippet for more detailed semantic search. """ def function_with_code_text(function_data, code_snippet) do base_text = function_text(function_data) # Truncate code snippet to reasonable length code = String.slice(code_snippet || "", 0, 1000) if String.length(code) > 0 do base_text <> ". Code: " <> code else base_text end end @doc """ Generates text for a call relationship. Describes which function calls which other function. """ def call_text(call_data) do from_sig = "#{module_name_to_string(call_data.from_module)}.#{call_data.from_function}/#{call_data.from_arity}" to_sig = "#{module_name_to_string(call_data.to_module)}.#{call_data.to_function}/#{call_data.to_arity}" "Function call: #{from_sig} calls #{to_sig}" end @doc """ Generates text for an import relationship. Describes which module imports which other module. """ def import_text(import_data) do "Import: #{module_name_to_string(import_data.from_module)} imports #{module_name_to_string(import_data.to_module)}" end # Private helpers defp function_signature(function_data) do "#{module_name_to_string(function_data.name)}/#{function_data.arity}" end defp module_name_to_string(name) when is_atom(name), do: Atom.to_string(name) defp module_name_to_string(name) when is_binary(name), do: name defp module_name_to_string({mod, name, arity}) when is_atom(mod) and is_atom(name) do "#{Atom.to_string(mod)}.#{Atom.to_string(name)}/#{arity}" end defp module_name_to_string(name), do: inspect(name) end