Pado Chat
Mix.install([
{:pado, path: Path.expand("../../pado", __DIR__)},
{:pado_kino, path: Path.expand("..", __DIR__)}
])
Authenticate
cd pado && mix pado.llm.login --output ~/.config/pado/openai_codex.json
Configure Agent
alias Pado.Agent.LLM
alias Pado.LLM.Catalog.OpenAICodex
alias Pado.LLM.Credential.FileLoader
alias Pado.LLM.Message.User
# 1. 크레덴셜 로드
creds_path = Path.expand("~/.config/pado/openai_codex.json")
{:ok, creds} = FileLoader.load(creds_path)
# 2. 에이전트와 세션
agent = %Pado.Agent{
llm: %Pado.Agent.LLM{
provider: :openai_codex,
credentials: creds,
model: OpenAICodex.default()
},
harness: %Pado.Agent.Harness{tools: [Pado.Agent.Tools.Bash.tool(timeout: 10)]}
}
session_id = "session-#{System.unique_integer([:positive])}"
# 3. 누적되는 메시지 상태 (Elixir Agent로 보관)
{:ok, state_pid} = Elixir.Agent.start_link(fn -> [] end)
# 4. UI 위젯
chat_frame = Kino.Frame.new()
model_options =
Pado.LLM.Catalog.OpenAICodex.all()
|> Enum.map(fn m -> {m.id, m.name} end)
model_input =
Kino.Input.select("모델", model_options,
default: Pado.LLM.Catalog.OpenAICodex.default().id
)
effort_input =
Kino.Input.select(
"효과",
[{:none, "none"}, {:low, "low"}, {:medium, "medium"}, {:high, "high"}, {:xhigh, "xhigh"}],
default: :medium
)
input = Kino.Input.textarea("메시지")
form =
Kino.Control.form(
[model: model_input, effort: effort_input, prompt: input],
submit: "보내기",
reset_on_submit: [:prompt]
)
# 5. 입력 처리
Kino.listen(form, fn %{data: %{model: model_id, effort: effort, prompt: text}} ->
text = String.trim(text)
if text != "" do
model = Pado.LLM.Catalog.OpenAICodex.get(model_id)
opts = Keyword.put(agent.llm.opts, :reasoning_effort, effort)
current_agent = %{agent | llm: %{agent.llm | model: model, opts: opts}}
msgs0 = Elixir.Agent.get(state_pid, & &1) ++ [User.new(text)]
Kino.Frame.append(chat_frame, Kino.Markdown.new("**🙂 나**\n\n" <> text))
job = %Pado.Agent.Job{
messages: msgs0,
session_id: session_id,
job_id: "job-#{System.unique_integer([:positive])}"
}
tool_result_text = fn %Pado.LLM.Message.ToolResult{content: parts} ->
parts
|> Enum.map(fn
{:text, t} -> t
other -> inspect(other)
end)
|> Enum.join("\n")
end
render_reply = fn frame, thinking, text ->
thinking_block =
if thinking == "" do
""
else
"> 💭 " <> String.replace(thinking, "\n", "\n> ") <> "\n\n"
end
Kino.Frame.render(
frame,
Kino.Markdown.new("**🤖 봇**\n\n" <> thinking_block <> text)
)
end
ensure_frame = fn frame ->
case frame do
nil ->
f = Kino.Frame.new()
Kino.Frame.append(chat_frame, f)
f
f ->
f
end
end
{_frame, _tbuf, _buf, final_msgs} =
Pado.Agent.stream(current_agent, job)
|> Enum.reduce({nil, "", "", msgs0}, fn
{:message_update, %{llm_event: {:thinking_delta, %{delta: d}}}},
{frame, tbuf, buf, ms} ->
frame = ensure_frame.(frame)
new_tbuf = tbuf <> d
render_reply.(frame, new_tbuf, buf)
{frame, new_tbuf, buf, ms}
{:message_update, %{llm_event: {:text_delta, %{delta: d}}}},
{frame, tbuf, buf, ms} ->
frame = ensure_frame.(frame)
new_buf = buf <> d
render_reply.(frame, tbuf, new_buf)
{frame, tbuf, new_buf, ms}
{:tool_execution_start, %{tool_name: name, args: args}}, {_frame, _tbuf, _buf, ms} ->
block =
"**🔧 " <> name <> "**\n\n" <>
"```elixir\n" <> inspect(args, pretty: true) <> "\n```"
Kino.Frame.append(chat_frame, Kino.Markdown.new(block))
{nil, "", "", ms}
{:tool_execution_end, %{tool_name: name, result: result, is_error: is_error}},
{_frame, _tbuf, _buf, ms} ->
emoji = if is_error, do: "⚠️", else: "📤"
output = tool_result_text.(result)
block =
"**" <> emoji <> " " <> name <> " 결과**\n\n" <>
"```\n" <> output <> "\n```"
Kino.Frame.append(chat_frame, Kino.Markdown.new(block))
{nil, "", "", ms}
{:job_end, %{turns: turns}}, {frame, tbuf, buf, ms} ->
new_ms = ms ++ Enum.flat_map(turns, &Pado.Agent.Turn.as_llm_messages/1)
{frame, tbuf, buf, new_ms}
_other, acc ->
acc
end)
Elixir.Agent.update(state_pid, fn _ -> final_msgs end)
end
end)
:ok
Load UI
Kino.Layout.grid([chat_frame, form], boxed: true)