defmodule ArcanaWeb.AskLive do
@moduledoc """
LiveView for asking questions about documents in Arcana.
"""
use Phoenix.LiveView
import Ecto.Query
import ArcanaWeb.DashboardComponents
alias Arcana.Graph.Entity
@impl true
def mount(_params, session, socket) do
repo = get_repo_from_session(session)
{:ok,
socket
|> assign(repo: repo)
|> assign(
ask_mode: :simple,
ask_question: "",
ask_running: false,
ask_context: nil,
ask_error: nil,
stats: nil,
collections: [],
graph_context_expanded: true,
llm_select: false
)}
end
@impl true
def handle_params(_params, _uri, socket) do
{:noreply, load_data(socket)}
end
defp load_data(socket) do
repo = socket.assigns.repo
socket
|> assign(stats: load_stats(repo))
|> assign(collections: load_collections_with_graph_status(repo))
end
defp load_collections_with_graph_status(repo) do
collections = load_collections(repo)
# Get entity counts per collection
entity_counts =
repo.all(
from(e in Entity,
group_by: e.collection_id,
select: {e.collection_id, count(e.id)}
)
)
|> Map.new()
Enum.map(collections, fn c ->
entity_count = Map.get(entity_counts, c.id, 0)
Map.put(c, :graph_enabled, entity_count > 0)
end)
end
defp any_graph_enabled?(collections) do
Enum.any?(collections, & &1[:graph_enabled])
end
@impl true
def handle_event("ask_submit", params, socket) do
question = params["question"] || ""
case {Application.get_env(:arcana, :llm), question} do
{nil, _} ->
{:noreply,
assign(socket,
ask_error: "No LLM configured. Set :arcana, :llm in your config.",
ask_running: false
)}
{_, ""} ->
{:noreply, assign(socket, ask_error: "Please enter a question")}
{llm, question} ->
socket = assign(socket, ask_running: true, ask_error: nil, ask_question: question)
start_ask_task(socket, params, llm, question)
{:noreply, socket}
end
end
def handle_event("ask_clear", _params, socket) do
{:noreply, assign(socket, ask_context: nil, ask_error: nil, ask_question: "")}
end
def handle_event("ask_switch_mode", %{"mode" => mode}, socket) do
mode = String.to_existing_atom(mode)
# Reset llm_select when switching to simple mode
llm_select = if mode == :simple, do: false, else: socket.assigns.llm_select
{:noreply, assign(socket, ask_mode: mode, llm_select: llm_select)}
end
def handle_event("toggle_graph_context", _params, socket) do
{:noreply, assign(socket, graph_context_expanded: !socket.assigns.graph_context_expanded)}
end
def handle_event("toggle_llm_select", _params, socket) do
{:noreply, assign(socket, llm_select: !socket.assigns.llm_select)}
end
defp start_ask_task(socket, params, llm, question) do
repo = socket.assigns.repo
mode = params["mode"] || "simple"
selected_collections = params["collections"] || []
parent = self()
Arcana.TaskSupervisor.start_child(fn ->
result =
run_ask(
mode,
question,
repo,
llm,
socket.assigns.collections,
params,
selected_collections
)
send(parent, {:ask_complete, result})
end)
end
defp run_ask("simple", question, repo, llm, _all_collections, _params, selected_collections) do
run_simple_ask(question, repo, llm, selected_collections)
end
defp run_ask(_mode, question, repo, llm, all_collections, params, selected_collections) do
run_agentic_ask(
question,
repo,
llm,
all_collections,
collections: selected_collections,
use_llm_select: params["llm_select"] == "true",
use_expand: params["use_expand"] == "true",
use_decompose: params["use_decompose"] == "true",
use_rerank: params["use_rerank"] == "true",
self_correct: params["self_correct"] == "true"
)
end
@impl true
def handle_info({:ask_complete, result}, socket) do
socket =
case result do
{:ok, ctx} ->
assign(socket, ask_running: false, ask_context: ctx, ask_error: nil)
{:error, reason} ->
assign(socket, ask_running: false, ask_error: inspect(reason))
end
{:noreply, socket}
end
defp run_simple_ask(question, repo, llm, selected_collections) do
opts = [repo: repo, llm: llm]
# Add collection(s) option if user selected any
opts =
case selected_collections do
[] -> opts
[single] -> Keyword.put(opts, :collection, single)
multiple -> Keyword.put(opts, :collections, multiple)
end
case Arcana.ask(question, opts) do
{:ok, answer, results} ->
# Build a context-like struct for consistent UI display
{:ok,
%{
question: question,
answer: answer,
results: results,
expanded_query: nil,
sub_questions: nil,
selected_collections:
if(selected_collections == [], do: nil, else: selected_collections)
}}
{:error, reason} ->
{:error, reason}
end
rescue
e -> {:error, Exception.message(e)}
end
defp run_agentic_ask(question, repo, llm, all_collections, opts) do
alias Arcana.Agent
all_collection_names = Enum.map(all_collections, & &1.name)
search_opts = build_search_opts(opts, all_collection_names)
Agent.new(question, repo: repo, llm: llm)
|> maybe_select(opts, all_collection_names)
|> maybe_expand(opts)
|> maybe_decompose(opts)
|> Agent.search(search_opts)
|> maybe_rerank(opts)
|> Agent.answer()
|> format_agentic_result(question)
rescue
e -> {:error, Exception.message(e)}
end
defp maybe_select(ctx, opts, all_collection_names) do
if Keyword.get(opts, :use_llm_select, false) and length(all_collection_names) > 1 do
Arcana.Agent.select(ctx, collections: all_collection_names)
else
ctx
end
end
defp maybe_expand(ctx, opts) do
if Keyword.get(opts, :use_expand, false), do: Arcana.Agent.expand(ctx), else: ctx
end
defp maybe_decompose(ctx, opts) do
if Keyword.get(opts, :use_decompose, false), do: Arcana.Agent.decompose(ctx), else: ctx
end
defp maybe_rerank(ctx, opts) do
if Keyword.get(opts, :use_rerank, false), do: Arcana.Agent.rerank(ctx), else: ctx
end
defp build_search_opts(opts, all_collection_names) do
base = [self_correct: Keyword.get(opts, :self_correct, false)]
use_llm_select = Keyword.get(opts, :use_llm_select, false)
if use_llm_select and length(all_collection_names) > 1 do
base
else
add_collection_opts(base, Keyword.get(opts, :collections, []))
end
end
defp add_collection_opts(opts, []), do: opts
defp add_collection_opts(opts, [single]), do: Keyword.put(opts, :collection, single)
defp add_collection_opts(opts, multiple), do: Keyword.put(opts, :collections, multiple)
defp format_agentic_result(%{error: error}, _question) when not is_nil(error) do
{:error, error}
end
defp format_agentic_result(ctx, question) do
{:ok,
%{
question: question,
answer: ctx.answer,
results: ctx.results,
expanded_query: ctx.expanded_query,
sub_questions: ctx.sub_questions,
selected_collections: ctx.collections
}}
end
@impl true
def render(assigns) do
~H"""
<.dashboard_layout stats={@stats} current_tab={:ask}>
Ask
Ask questions about your documents. Choose Simple for basic RAG or Agentic for advanced pipeline features.
<%= if @ask_mode == :simple do %>
Basic RAG: search for relevant chunks and generate an answer.
<% else %>
Advanced RAG: query expansion, decomposition, self-correction, and reranking.
<% end %>