Current section
Files
Jump to
Current section
Files
lib/raxol/mcp/adaptive_tools.ex
defmodule Raxol.MCP.AdaptiveTools do
@moduledoc """
MCP tool definitions for the adaptive UI subsystem.
Exposes layout recommendations, behavior aggregates, and feedback
loop controls as MCP tools for AI agent interaction.
Tools are only registered when the adaptive modules are loaded.
"""
@compile {:no_warn_undefined,
[
Raxol.Adaptive.BehaviorTracker,
Raxol.Adaptive.LayoutRecommender,
Raxol.Adaptive.FeedbackLoop
]}
@doc "Returns adaptive MCP tool definitions, or empty list if adaptive modules unavailable."
@spec tools() :: [Raxol.MCP.Registry.tool_def()]
def tools do
if available?() do
[
get_recommendation_tool(),
accept_recommendation_tool(),
reject_recommendation_tool(),
get_behavior_summary_tool(),
get_accuracy_tool()
]
else
[]
end
end
@doc "Returns true if the adaptive modules are loaded."
@spec available?() :: boolean()
def available? do
Code.ensure_loaded?(Raxol.Adaptive.LayoutRecommender) and
Code.ensure_loaded?(Raxol.Adaptive.BehaviorTracker) and
Code.ensure_loaded?(Raxol.Adaptive.FeedbackLoop)
end
# -- Tool Definitions --
defp get_recommendation_tool do
%{
name: "adaptive_get_recommendation",
description: """
Returns the most recent layout recommendation from the adaptive UI system.
Includes layout changes (hide/show/expand/shrink), confidence score, and reasoning.
Returns null if no recommendation has been generated yet.
""",
inputSchema: %{type: "object", properties: %{}},
callback: &get_recommendation/1
}
end
defp accept_recommendation_tool do
%{
name: "adaptive_accept_recommendation",
description: """
Accepts a pending layout recommendation by ID. This feeds positive signal
into the feedback loop for model improvement.
""",
inputSchema: %{
type: "object",
properties: %{
recommendation_id: %{
type: "string",
description: "The recommendation ID to accept"
}
},
required: ["recommendation_id"]
},
callback: &accept_recommendation/1
}
end
defp reject_recommendation_tool do
%{
name: "adaptive_reject_recommendation",
description: """
Rejects a pending layout recommendation by ID. This feeds negative signal
into the feedback loop for model improvement.
""",
inputSchema: %{
type: "object",
properties: %{
recommendation_id: %{
type: "string",
description: "The recommendation ID to reject"
}
},
required: ["recommendation_id"]
},
callback: &reject_recommendation/1
}
end
defp get_behavior_summary_tool do
%{
name: "adaptive_get_behavior_summary",
description: """
Returns recent behavior aggregates from the BehaviorTracker.
Shows pane dwell times, command frequency, scroll metrics,
alert response times, and takeover duration.
""",
inputSchema: %{
type: "object",
properties: %{
window_count: %{
type: "integer",
description:
"Number of recent aggregate windows to return (default: 3)"
}
}
},
callback: &get_behavior_summary/1
}
end
defp get_accuracy_tool do
%{
name: "adaptive_get_accuracy",
description: """
Returns the feedback loop acceptance accuracy as a percentage.
Tracks how often layout recommendations are accepted vs rejected.
""",
inputSchema: %{type: "object", properties: %{}},
callback: &get_accuracy/1
}
end
@doc "Registers adaptive tools with the MCP registry."
@spec register(GenServer.server()) :: :ok
def register(registry \\ Raxol.MCP.Registry) do
if Code.ensure_loaded?(Raxol.MCP.Registry) do
Raxol.MCP.Registry.register_tools(registry, tools())
end
:ok
end
# -- Callbacks --
defp get_recommendation(_args) do
rec = Raxol.Adaptive.LayoutRecommender.get_last_recommendation()
if rec do
changes =
Enum.map(rec.layout_changes, fn c ->
%{
pane_id: to_string(c.pane_id),
action: to_string(c.action),
params: c.params
}
end)
%{
id: rec.id,
confidence: rec.confidence,
reasoning: rec.reasoning,
layout_changes: changes,
timestamp: rec.timestamp
}
else
%{recommendation: nil, message: "No recommendation generated yet"}
end
end
defp accept_recommendation(%{"recommendation_id" => rec_id}) do
case Raxol.Adaptive.FeedbackLoop.accept(rec_id) do
:ok ->
%{status: "accepted", recommendation_id: rec_id}
{:error, :not_found} ->
%{status: "error", message: "Recommendation not found: #{rec_id}"}
end
end
defp accept_recommendation(_),
do: %{status: "error", message: "recommendation_id required"}
defp reject_recommendation(%{"recommendation_id" => rec_id}) do
case Raxol.Adaptive.FeedbackLoop.reject(rec_id) do
:ok ->
%{status: "rejected", recommendation_id: rec_id}
{:error, :not_found} ->
%{status: "error", message: "Recommendation not found: #{rec_id}"}
end
end
defp reject_recommendation(_),
do: %{status: "error", message: "recommendation_id required"}
defp get_behavior_summary(args) do
count = Map.get(args, "window_count", 3)
aggregates = Raxol.Adaptive.BehaviorTracker.get_aggregates(count)
Enum.map(aggregates, fn agg ->
%{
window_start: agg.window_start,
pane_dwell_times: stringify_keys(agg.pane_dwell_times),
command_frequency: agg.command_frequency,
avg_alert_response_ms: agg.avg_alert_response_ms,
most_used_panes: Enum.map(agg.most_used_panes, &to_string/1),
least_used_panes: Enum.map(agg.least_used_panes, &to_string/1),
scroll_frequency: stringify_keys(Map.get(agg, :scroll_frequency, %{})),
takeover_duration_ms:
stringify_keys(Map.get(agg, :takeover_duration_ms, %{})),
layout_override_count: Map.get(agg, :layout_override_count, 0),
command_concentration:
stringify_keys(Map.get(agg, :command_concentration, %{}))
}
end)
end
defp get_accuracy(_args) do
accuracy = Raxol.Adaptive.FeedbackLoop.get_accuracy()
%{accuracy: Float.round(accuracy * 100, 1), unit: "percent"}
end
defp stringify_keys(map) when is_map(map) do
Map.new(map, fn {k, v} -> {to_string(k), v} end)
end
end