Powered by AppSignal & Oban Pro

Pado Chat

pado_kino/livebooks/pado_chat.livemd

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, &amp;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)