9
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Elixir/LiveViewからDify API経由でLLM生成AIチャットを作る(LiveView開発レクチャー付き)

9
Last updated at Posted at 2025-12-23

この記事は、Elixir Advent Calendar 2025その5 の3日目です

昨日は @t-yamanashi さんで、「KR260にUbuntu 24.04とElixirをインストールする」 でした


piacere です、ご覧いただいてありがとございます :bow:

LiveViewからDify API経由でLLMを使うDifyチャットアプリを呼び出すことで、LiveView上で動く生成AIチャットを作ります

LINEやChatGPT、Geminiのような会話履歴として、生成AIとのやり取りが記録されます

このコラムには、LiveView開発のレクチャーも付いています

Elixirアドベントカレンダー、応援お願いします :bow:

今年もやっています

①クラウド版DifyをPhoenixから使う

Difyを呼ぶPhoenix PJ作成

下記コマンドでPhoenix PJを作成し、Phoenixを起動します

なお、PhoenixにはReqが内蔵されているので、自身で deps に追加する必要はありません

mix phx.new basic --no-ecto
cd basic
iex -S mix phx.server

Dify API準備/APIキー取得

Dify APIを作成し、APIキー取得するには、下記コラムをご参考ください

Difyチャットアプリから作成する必要がある方は、下記を行ってから、Dify API作成/APIキー取得してください

ReqでDify APIを叩く

Reqを使ってDify APIを呼んだ回答をレンダリングするLiveViewページを追加します

LiveView(~_live.ex)とHTML(~_live.html.heex)は、@ で始まる変数でフロントサイドの状態を保持したり、入力内容をサーバーサイドに渡すことができ、逆にサーバーサイドで書き換えると、フロント側への表示反映や状態更新を自動的に行うこともできます

Phoenix.HTML.raw() は、HTMLをエスケープせずに、そのまま出力するための関数です

basic/lib/basic_web/live/buddy_live.html.heex
<div class="chat-container">
  <div id="Scroll" phx-hook="Scroll" class="chat-body">
    <div :for={conversation <- @conversations}>
      <div class="message user" name="prompt">{conversation["prompt"]}</div>
      <div class="message assistant">{Phoenix.HTML.raw(conversation["answer"])}</div>
    </div>
  </div>

  <div class="input-area">
    <form phx-submit="predict">
      <input type="text" name="prompt" placeholder="プロンプトを入力" />
      <button type="submit" class="btn btn-success text-white">送信</button>
    </form>
  </div>
</div>

次にLiveViewのロジック部分で、mount() がページ初期時に行う処理、handle_info() はLiveView側処理を数珠繋ぎにするためのハンドラ(send() で遅延呼出しも可能)、assign() でサーバーサイド更新をフロントに反映させます

今回は、assigns.conversations という変数に、各会話回ごとの %{"prompt" => transcript, "answer" => "AI処理中…"} をリストし、それをHTML側で :for を使って全ての会話履歴を表示しています

フロント側での入力/変更内容は、handle_info() の第2引数 params に文字列キーのマップとして入っており、HTML側の name= で指定した変数名で取得できます

なお、下記コード中、緑帯の {"Authorization", "Bearer 【Dify APIキー】"}, のAPIキー部分は、前述で取得したAPIキーに入れ替えてください

basic/lib/basic_web/live/buddy_live.ex
defmodule BasicWeb.BuddyLive do
  use BasicWeb, :live_view

  @impl true
  def mount(_params, _session, socket) do
    socket = socket
      |> assign(prompt: "")
      |> assign(conversations: [])
      |> assign(conversation_id: "")
    {:ok, socket}
  end

  @impl true
  def handle_event("predict", %{"prompt" => prompt}, %{assigns: assigns} = socket) do
    body = %{
      "query" => prompt,
      "user" => "fromElixirToDify",
      "inputs" => %{},
      "response_mode" => "blocking",
      "conversation_id" => assigns.conversation_id
    }
    headers = [
+     {"Authorization", "Bearer 【Dify APIキー】"},
      {"Content-Type", "application/json"}
    ]
    response = Req.post!("https://api.dify.ai/v1/chat-messages", json: body, headers: headers) 

    socket = socket
      |> assign(prompt: "")
      |> assign(conversations: assigns.conversations ++ [%{"prompt" => prompt, "answer" => response.body["answer"]}])
      |> assign(conversation_id: response.body["conversation_id"])
    {:noreply, socket}
  end
end

ルートを上記LiveViewに入れ替えます

basic/lib/basic_web/router.ex
defmodule BasicWeb.Router do
  use BasicWeb, :router

  scope "/", BasicWeb do
    pipe_through :browser

-   get "/", PageController, :home
+   live "/", BuddyLive
  end

会話UIの吹き出しは、下記CSSで形成しており、buddy_live.html.heex に追加してください(ココはChatGPTのプレビュー付きプロンプトで作りました)

basic/lib/basic_web/live/buddy_live.html.heex
<style>
  :root {
    --bg: #f5f7fb;
    --panel: #ffffff;
    --user: #00BAA6;
    --user-text: #ffffff;
    --assistant: #e5e7eb;
    --assistant-text: #111827;
    --muted: #6b7280;
    --radius: 14px;
  }

  * { box-sizing: border-box; }

  .chat-container {
    max-width: 1000px;
    margin: 0 auto;
    height: 100vh;
    display: flex;
    flex-direction: column;
    background: var(--panel);
    box-shadow: 0 20px 40px rgba(0,0,0,.08);
  }

  .chat-body {
    flex: 1;
    padding: 16px;
    overflow-y: auto;
    display: flex;
    flex-direction: column;
    gap: 10px;
  }

  .chat-body > div {
    display: flex;
    flex-direction: column;
  }

  .message {
    max-width: 70%;
    padding: 10px 14px;
    border-radius: var(--radius);
    font-size: 14px;
    line-height: 1.6;
    word-wrap: break-word;
  }

  .assistant {
    background: var(--assistant);
    color: var(--assistant-text);
    align-self: flex-start;
    border-top-left-radius: 0;
    position: relative;
    margin-left: 8px;
  }

  .assistant::before {
    content: "";
    position: absolute;
    left: -8px;
    top: 0;
    width: 8px;
    height: 9px;
    background: var(--assistant);
    clip-path: polygon(0 0, 100% 0, 100% 100%);
  }

  .user {
    background: var(--user);
    color: var(--user-text);
    align-self: flex-end;
    border-top-right-radius: 0;
    position: relative;
    margin-right: 8px;
  }

  .user::after {
    content: "";
    position: absolute;
    right: -8px;
    top: 0;
    width: 8px;
    height: 9px;
    background: var(--user);
    clip-path: polygon(100% 0, 0 0, 0 100%);
  }

  .input-area {
    display: flex;
    gap: 8px;
    padding: 12px;
    border-top: 1px solid #e5e7eb;
    background: var(--panel);
    margin-top: auto;
  }

  .input-area input {
    flex: 1;
    border: 1px solid #d1d5db;
    border-radius: 999px;
    padding: 10px 14px;
    font-size: 14px;
    outline: none;
  }

  .input-area form {
    display: flex;
    width: 100%;
    gap: 8px;
  }

  .input-area input {
    flex: 1;
  }

  .input-area button {
    border: none;
    background: var(--user);
    color: #fff;
    border-radius: 999px;
    padding: 0 16px;
    font-size: 14px;
    cursor: pointer;
  }
</style>

LiveView上からDify API+LLMを実行

ブラウザで http://localhost:4000 にアクセスすると、下記のようなLiveViewチャットアプリが表示され、プロンプトを入力して「送信」ボタンをクリックすると、内部ではDifyチャットアプリによってLLM処理された回答が返ってきます

なお現状コードは、Difyからのストリーミングをしていないため、送信から数秒、固まったように見えるかも知れませんが、裏側ではDifyの呼出と回答の返却待ちが行われています

image.png

②ローカル版DifyをPhoenixから使う

上記Phoeinx PJを流用して、ローカル版Difyを呼ぶコードも作ってみます

ローカル版Difyの構築

まず、ローカル版Difyがインストール済みで無ければ、下記コラムを見ながら構築してください

なおLLMは、オープン型でも、ローカルLLMでも、どちらでもOKです(両者のDify設定およびローカルLLM構築も下記コラムで解説していますのでご参考ください)

Dify API準備/APIキー取得

ローカル版Difyもクラウド版同様、Dify APIを作成し、APIキー取得するには、下記コラムをご参考ください

Difyチャットアプリから作成する必要がある方は、下記を行ってから、Dify API作成/APIキー取得してください

ReqでローカルDify APIを叩く

basic/lib/basic_web/live/buddy_live.ex
defmodule BasicWeb.BuddyLive do
  use BasicWeb, :live_view

  @impl true
  def mount(_params, _session, socket) do
    socket = socket
      |> assign(prompt: "")
      |> assign(conversations: [])
      |> assign(conversation_id: "")
    {:ok, socket}
  end

  @impl true
  def handle_event("predict", %{"prompt" => prompt}, %{assigns: assigns} = socket) do
    body = %{
      "query" => prompt,
      "user" => "fromElixirToDify",
      "inputs" => %{},
      "response_mode" => "blocking",
      "conversation_id" => assigns.conversation_id
    }
    headers = [
      {"Authorization", "Bearer 【Dify APIキー】"},
      {"Content-Type", "application/json"}
    ]
-   response = Req.post!("https://api.dify.ai/v1/chat-messages", json: body, headers: headers) 
+   response = Req.post!("http://localhost/v1/chat-messages", json: body, headers: headers)

    socket = socket
      |> assign(prompt: "")
      |> assign(conversations: assigns.conversations ++ [%{"prompt" => prompt, "answer" => response.body["answer"]}])
      |> assign(conversation_id: response.body["conversation_id"])
    {:noreply, socket}
  end
end

LiveView上からDify API+LLMを実行

ブラウザで http://localhost:4000 にアクセスすると、中身がローカル版Difyに切り替わったLiveViewチャットアプリによるLLM処理された回答が返ってきます

終わりに

LiveViewから、API化されたDifyチャットアプリ+LLMを叩いて、LiveView上で動く生成AIチャットをLiveView開発レクチャーしつつ作ってみました

Difyは、クラウド版とローカル版の両方を使いました

ノーコード/RPAでLLM利用アプリ部分を作り、Elixir/LiveViewから簡単に利用できることが伝わったかと思います

これは同時に、LLM周辺処理の改良はノーコードだけで済ませられ、Elixir側は改修しなくて良いという役割分担が魅力的です

また利用するLLMの変更も、Dify側で吸収できる点も利点です

最後に、ローカル版Difyを使っていたら、下記コマンドでコンテナ群を落として終了してください

docker compose down

明日も私で 「Gemini/Dify等にあるConversationIDはLLMが付与している訳では無いのでElixirに付与させた」 です

9
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?