この記事は、Elixir Advent Calendar 2025その5 の3日目です
昨日は @t-yamanashi さんで、「KR260にUbuntu 24.04とElixirをインストールする」 でした
piacere です、ご覧いただいてありがとございます ![]()
LiveViewからDify API経由でLLMを使うDifyチャットアプリを呼び出すことで、LiveView上で動く生成AIチャットを作ります
LINEやChatGPT、Geminiのような会話履歴として、生成AIとのやり取りが記録されます
このコラムには、LiveView開発のレクチャーも付いています
Elixirアドベントカレンダー、応援お願いします
今年もやっています
①クラウド版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をエスケープせずに、そのまま出力するための関数です
<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キーに入れ替えてください
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に入れ替えます
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のプレビュー付きプロンプトで作りました)
<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の呼出と回答の返却待ちが行われています
②ローカル版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を叩く
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に付与させた」 です
