1.はじめに
本記事では、当社「DNPドキュメント構造化AI(※1)」で生成された構造化データを活用する方法の一例として、Difyのチャットボット機能をバックエンドで活用し、関連画像をフロントエンドで表示させる簡易的なアプリの実装内容について共有したいとします。
※1「DNPドキュメント構造化AI」は、企業内の非構造化文書を、生成AIが正確に理解・活用できる“AIリーダブルなデータ”に変換する、DNP独自開発のAIソリューションです。詳細は以下サービスページをご覧ください。
👉 DNPドキュメント構造化AI(AI-Ready Data)
2.事前準備
2-1.今回実装するアプリ
今回実装するのは、チャットボット機能周りをバックエンドで実行・連携させ、関連画像をフロント側で表示させる簡易的なアプリです。
Difyでは「ナレッジ」という機能を使えば、LLMが関連ドキュメントを参照して回答する、いわゆる「RAG環境」が作れますが、現時点(2025年7月時点)では「関連画像の表示」機能はまだ実装されていない(※2)ようなので、上記の方法で実装してみたいと思います。
※2 LLMが生成した画像の出力にはver1.4.0で対応しています。リンク
2-2.ナレッジ用データ
ナレッジに登録する元データは添付の通り、サンプルの災害報告書を作成してみました。
こちら元のデータはエクセル形式なので、まずは我々の技術でXML形式に変換します。
<structual-data>
<article>
<title>災害報告サンプル</title>
<text>サンプル事例</text>
<section>発生日時</section>
<text>2025年6月2日(月)10時47分頃</text>
<section>発生事業場</section>
<section>事業部・グループ会社</section>
<text>清掃部</text>
<section>事業場</section>
<text>東京工場</text>
(中略)
<text>・クリーニング工場内にて、Yシャツの包装機から包装され流れ出てくる商品を取ろうとした際、床面に置いてあった段ボールにつまずき、付近にあった台車に足が乗り、台車が動き、後方に転倒した。転倒した際、咄嗟に手をつき、手首を骨折した。</text>
<img src="images/figure_0.png"/>
見ていただければわかる通り、意味のある区切りで<title>
や<section>
などで構造化されています。画像については画像ファイルが格納されているパス情報を記載しております。
以下のようにimagesフォルダが作られ、その中に上記XMLデータ内に記載されているパスと名前で画像データが格納されております。(この処理は当社での構造化処理の際に実施されます。)
格納されている画像(figure_0.png)
3.開発
3-1.バックエンド(Dify)
まずはDify上での実装です。最初に上記で作ったxmlデータをtxtデータに変換し、ナレッジ登録画面にアップロードします。
チャンクの設定は以下の通り。(「親子モード」で作成。)
- 親チャンク
- チャンク識別子:
\n\n
(デフォルト) - 最大チャンク長:1100 (少し長めに設定)
- チャンク識別子:
- 子チャンク
- チャンク識別子:
</section>
- 最大チャンク長:512 (デフォルト)
- チャンク識別子:
子チャンクは、今回元のxmlデータに含まれる</section>
タグがちょうど良さそうだったので、それをそのまま識別子として設定しています。
作られたチャンクはこんな感じです。※元データに多少誤字などあったので手直し入れてます。
知識探索ブロックでは、先ほど作ったナレッジを設定。LLMでのプロンプトは以下の通りです。
今回のインプットデータがメーカーを想定したものだったので、役割にもその旨を記載しています。画像も出力したいので、画像の情報(=パス情報)は正しく回答してもらうよう指示を出しておきます。
この時点でDify上のチャットボットアプリの検証をしてみます。
内容自体はナレッジから正しく抽出して回答できているようです。画像は表示できませんが、パスは回答してくれています。
3-2.フロントエンド(Streamlit)
バックエンドはこれで良さそうなので次はフロントエンドの実装です。今回はStreamlitを用いて簡易的なものを作っていきます。
まずはDifyとの認証処理部分など。
※import文などは長くなるので割愛いたします。またAPI_KEYやBASE_URLもマスキングしています。
import streamlit as st
# その他import文は記載を割愛
# --- Dify APIの認証キーとエンドポイント設定 ---
API_KEY = 'app-****'
BASE_URL = 'http://****'
# --- セッション情報の初期化 ---
if 'user_id' not in st.session_state:
st.session_state.user_id = str(uuid.uuid4())
if 'conversation_id' not in st.session_state:
st.session_state.conversation_id = ""
# --- ユーザー名取得 ---
userName = st.session_state.user_id
タイトル周りは以下の通り。
# --- ロゴ画像をbase64で読み込み ---
with open("img/logo_powered_by_dify_.png", "rb") as f:
logo_base64 = base64.b64encode(f.read()).decode()
# --- タイトルとロゴの表示 ---
st.markdown(
f'''
<div style="display: flex; flex-direction: column;">
<span style="font-size:2em; line-height:1; display: flex; align-items: center;">
<b>📝 画像付き簡易ナレッジナビ</b>
<img src="data:image/png;base64,{logo_base64}" style="max-width:200px; height:auto; vertical-align:middle; margin-left:0.5em;">
</span>
</div>
''',
unsafe_allow_html=True
)
続いてチャット部分回り。
# --- チャット履歴の初期化 ---
if "messages" not in st.session_state:
st.session_state.messages = [
{"role": "assistant", "content": "こんにちは!\n質問内容を教えて頂けますか?"}
]
if "response" not in st.session_state:
st.session_state["response"] = None
# --- チャット履歴の表示 ---
messages = st.session_state.messages
for msg in messages:
st.chat_message(msg["role"]).write(msg["content"])
Difyとの連携処理部分は以下の通りです。
# --- ユーザー入力受付とAPIリクエスト ---
if prompt := st.chat_input(placeholder="input"):
messages.append({"role": "user", "content": prompt})
st.chat_message("user").write(prompt)
# --- APIリクエスト用データ作成 ---
headers = {
'Authorization': f'Bearer {API_KEY}',
'Content-Type': 'application/json'
}
query = "\n ### ユーザーからの入力 ### \n" + f"{prompt}"
data: Dict[str, any] = {
"inputs": {},
"query": query,
"response_mode": "streaming",
"conversation_id": st.session_state.conversation_id,
"user": userName,
}
# --- Dify APIへリクエスト送信 ---
try:
response = requests.post(BASE_URL, headers=headers, data=json.dumps(data), stream=True)
except requests.RequestException as e:
print(f"エラーが発生しました: {e}")
res = ""
# --- ストリーミングレスポンスの処理 ---
for line in response.iter_lines():
if line:
decoded_line = line.decode('utf-8')
if decoded_line.startswith("data:"):
json_str = decoded_line[5:].strip()
else:
json_str = decoded_line.strip()
try:
json_data = json.loads(json_str)
except json.JSONDecodeError as e:
print(f"JSONデコードエラー: {e}")
json_data = None
# --- 各イベントごとにレスポンスを処理 ---
if json_data['event'] == 'agent_message':
res += json_data['answer']
elif json_data.get('event') == 'workflow_finished':
# outputs.answerがあれば追加
answer = json_data.get('data', {}).get('outputs', {}).get('answer', '')
res += answer
if json_data['event'] == 'agent_thought':
st.session_state.conversation_id = json_data['conversation_id']
# --- レスポンスをセッションに保存 ---
st.session_state["response"] = res
最後は画像出力処理部分。簡単な方法ですが、Difyからの回答に「images/xxx.png」というような文字列があった場合、別途格納しているフォルダ内から対応する画像を取得してウェブ画面上に表示させます。
# --- アシスタントの返答と画像表示 ---
with st.chat_message("assistant"):
messages.append({"role": "assistant", "content": st.session_state["response"]})
st.write(st.session_state["response"])
# --- 回答に画像パスが含まれていれば画像表示 ---
image_paths = re.findall(r'images/[\w\-/\.]+', st.session_state["response"])
for img_path in image_paths:
# img/chunk/images/以下の画像ファイルパスを作成
full_path = os.path.join("img", "chunk", img_path)
if os.path.exists(full_path):
st.image(Image.open(full_path), caption=img_path)
else:
st.warning(f"画像が見つかりません: {img_path}")
これで完了です。続いて検証に入ります!
4.検証
できた画面はこちらになります。上記のDify上チャットボットで聞いたように、ナレッジに登録した情報に関する質問をしてみます。
無事画像が出力されました!回答内容も問題なさそうです。
5.まとめ
今回は、Difyの回答を元に、別途格納している画像データを表示させるフロントエンドアプリを実装しました。Streamlitのようなフレームワークと組み合わせることで、比較的容易に実現可能できたかと思います。
ただし今回の実装はあくまで一例であり、本格的なアプリを構築する際には、画像格納ストレージの準備、LLMの精度比較などの点を考慮する必要があります。
また、Difyは活発に開発が進んでいるため、将来的にはナレッジ機能に画像表示機能が実装される可能性もあります。最新情報を常に確認し、柔軟に対応できるようにしておきたいと思います。
ここまで読んでいただき、ありがとうございました。
今回の記事が、皆様の生成AIアプリ構築の一助となれば幸いです。