Packages
fnord
0.9.33
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/frobs/prompt.ex
defmodule Frobs.Prompt do
@moduledoc """
UI-driven prompting for frob parameters. This module uses `UI` to prompt the
user for each property defined in a frob's `spec.json` and returns a map of
coerced values. It relies on `AI.Tools.Params` for validation/coercion.
Supports JSON Schema composition keywords (`anyOf`, `oneOf`, `allOf`) via
schema resolution: nullable unions prompt for the non-null type, simple
multi-type unions offer a type chooser, `allOf` merges and prompts the merged
schema, and anything else falls back to raw JSON input.
"""
@doc """
Prompt the user for parameters described by `spec`.
Returns `{:ok, map}` or `{:error, reason}`.
"""
@spec prompt_for_params(map(), module()) :: {:ok, map()} | {:error, term()}
def prompt_for_params(spec, ui \\ UI) do
with {:ok, normalized} <- AI.Tools.Params.normalize_spec(spec),
{:ok, params} <- AI.Tools.Params.param_list(normalized) do
if not ui.is_tty?() or ui.quiet?() do
non_interactive_collect(params, normalized)
else
interactive_collect(params, normalized, ui)
end
end
end
defp non_interactive_collect(params, normalized) do
defaults = collect_defaults(params)
missing =
params
|> Enum.filter(fn {name, _schema, req} -> req and not Map.has_key?(defaults, name) end)
|> Enum.map(fn {name, _, _} -> name end)
if missing != [] do
{:error, {:non_interactive_missing_required, missing}}
else
AI.Tools.Params.validate_all_args(normalized, defaults)
end
end
defp collect_defaults(list) do
Enum.reduce(list, %{}, fn {name, schema, _req}, acc ->
case Map.get(schema, "default") do
nil -> acc
v -> Map.put(acc, name, v)
end
end)
end
defp interactive_collect(params, normalized, ui) do
result =
Enum.reduce_while(params, {:ok, %{}}, fn {name, schema, required?}, {:ok, acc} ->
case prompt_property(name, schema, required?, ui) do
{:ok, v} -> {:cont, {:ok, Map.put(acc, name, v)}}
{:error, :user_cancelled} -> {:halt, {:error, :user_cancelled}}
{:error, reason} -> {:halt, {:error, reason}}
end
end)
case result do
{:ok, answers} ->
case AI.Tools.Params.validate_all_args(normalized, answers) do
{:ok, coerced} -> {:ok, coerced}
{:error, reason} -> {:error, reason}
end
other ->
other
end
end
# ---------------------------------------------------------------------------
# Property prompting -- dispatches by schema resolution
# ---------------------------------------------------------------------------
defp prompt_property(name, schema, _required?, ui) do
label = name |> to_string() |> String.replace("_", " ")
desc = Map.get(schema, "description", "")
unless ui.quiet?() do
if desc != "", do: ui.puts(desc)
end
case AI.Tools.Params.resolve_schema_type(schema) do
{:ok, type} ->
prompt_by_type(type, name, label, schema, ui)
{:composition, keyword, sub_schemas} ->
prompt_composition(keyword, sub_schemas, name, label, schema, ui)
{:error, :unresolvable} ->
prompt_raw_json(label, schema, ui)
end
end
# ---------------------------------------------------------------------------
# Type-based prompting
# ---------------------------------------------------------------------------
defp prompt_by_type(_type, _name, label, %{"enum" => choices} = _schema, ui) do
display = Enum.map(choices, &to_string/1)
sel = ui.choose(label, display)
idx = Enum.find_index(display, &(&1 == sel)) || 0
{:ok, Enum.at(choices, idx)}
end
defp prompt_by_type("boolean", _name, label, _schema, ui) do
sel = ui.choose(label, ["Yes", "No"]) || "No"
{:ok, sel in ["Yes", "yes", "Y", "y"]}
end
defp prompt_by_type(type, _name, label, schema, ui)
when type in ["string", "integer", "number"] do
default = Map.get(schema, "default")
prompt_text = if default, do: "#{label} (default: #{inspect(default)})", else: label
raw = ui.prompt(prompt_text)
val = if raw in [nil, ""], do: default, else: raw
{:ok, val}
end
defp prompt_by_type("array", name, _label, schema, ui) do
items_schema = Map.get(schema, "items") || %{"type" => "string"}
collect_array_items(name, items_schema, ui, [])
end
defp prompt_by_type("object", _name, _label, schema, ui) do
nested_spec = %{
"parameters" => %{
"type" => "object",
"properties" => Map.get(schema, "properties", %{}),
"required" => Map.get(schema, "required", [])
}
}
case prompt_for_params(nested_spec, ui) do
{:ok, m} -> {:ok, m}
other -> other
end
end
defp prompt_by_type(_type, _name, label, schema, ui) do
prompt_raw_json(label, schema, ui)
end
# ---------------------------------------------------------------------------
# Composition prompting
# ---------------------------------------------------------------------------
defp prompt_composition(keyword, sub_schemas, name, label, schema, ui) do
case AI.Tools.Params.nullable_schema?(keyword, sub_schemas) do
{:nullable, non_null_schema} ->
prompt_nullable(name, label, non_null_schema, ui)
:not_nullable when keyword in ["anyOf", "oneOf"] ->
if AI.Tools.Params.all_simple_types?(sub_schemas) do
prompt_type_chooser(sub_schemas, label, ui)
else
prompt_raw_json(label, schema, ui)
end
:not_nullable when keyword == "allOf" ->
merged = AI.Tools.Params.merge_schemas(sub_schemas)
prompt_property(name, merged, false, ui)
_ ->
prompt_raw_json(label, schema, ui)
end
end
defp prompt_nullable(name, label, non_null_schema, ui) do
case AI.Tools.Params.resolve_schema_type(non_null_schema) do
{:ok, type} ->
prompt_text = "#{label} (optional, enter blank to skip)"
raw = ui.prompt(prompt_text)
if raw in [nil, ""] do
{:ok, nil}
else
case AI.Tools.Params.validate_and_coerce_param(non_null_schema, raw) do
{:ok, coerced} -> {:ok, coerced}
{:error, _, _} -> prompt_by_type(type, name, label, non_null_schema, ui)
end
end
_ ->
# Non-null sub-schema is itself complex; fall back to raw JSON
prompt_text = "#{label} (optional, enter blank to skip, or enter JSON)"
raw = ui.prompt(prompt_text)
if raw in [nil, ""] do
{:ok, nil}
else
parse_and_validate_json(raw, non_null_schema, label, ui)
end
end
end
defp prompt_type_chooser(sub_schemas, label, ui) do
types = Enum.map(sub_schemas, fn s -> Map.get(s, "type") end)
sel = ui.choose("#{label} (type)", types)
idx = Enum.find_index(types, &(&1 == sel)) || 0
chosen_schema = Enum.at(sub_schemas, idx)
prompt_text = "#{label} (#{sel})"
raw = ui.prompt(prompt_text)
default = Map.get(chosen_schema, "default")
val = if raw in [nil, ""], do: default, else: raw
case AI.Tools.Params.validate_and_coerce_param(chosen_schema, val) do
{:ok, coerced} -> {:ok, coerced}
{:error, _, msg} -> {:error, {:coercion_failed, msg}}
end
end
# ---------------------------------------------------------------------------
# Raw JSON fallback
# ---------------------------------------------------------------------------
defp prompt_raw_json(label, schema, ui) do
prompt_text = "#{label} (enter as JSON)"
raw = ui.prompt(prompt_text)
default = Map.get(schema, "default")
if raw in [nil, ""] and default != nil do
{:ok, default}
else
parse_and_validate_json(raw, schema, label, ui)
end
end
defp parse_and_validate_json(raw, schema, label, ui) do
case SafeJson.decode(raw || "") do
{:ok, parsed} ->
case AI.Tools.Params.validate_and_coerce_param(schema, parsed) do
{:ok, coerced} ->
{:ok, coerced}
{:error, _, msg} ->
ui.puts("Invalid value: #{msg}")
prompt_raw_json(label, schema, ui)
end
{:error, _} ->
ui.puts("Invalid JSON. Please enter a valid JSON value.")
prompt_raw_json(label, schema, ui)
end
end
# ---------------------------------------------------------------------------
# Array item collection
# ---------------------------------------------------------------------------
defp collect_array_items(_name, items_schema, ui, acc) do
idx = length(acc) + 1
prompt_text = "Item ##{idx} (enter blank to finish)"
raw = ui.prompt(prompt_text)
cond do
raw in [nil, ""] and acc == [] ->
{:ok, []}
raw in [nil, ""] ->
{:ok, acc}
true ->
case AI.Tools.Params.validate_and_coerce_param(items_schema, raw) do
{:ok, coerced} ->
collect_array_items(nil, items_schema, ui, acc ++ [coerced])
{:error, _reason, _msg} ->
ui.puts("Invalid item, try again")
collect_array_items(nil, items_schema, ui, acc)
end
end
end
end