ヤドカリくんをDifyと連携させる
利用イメージ
チャット形式、フォームの自由記述欄に世帯情報を入れると、内容をもとにOpenFiscaへリクエスト
ヤドカリの絵文字が無かった... 🦀
概念実証として、「かんたん見積もり」の機能(本人の年収、居住地、配偶者の年収、子どもの年齢)だけ実装
実践的な例も検証したかったので、高校履修種別、運営種別のみ追加(高等学校等就学支援金、奨学支援金)
https://scrapbox.io/files/67a2043a18ed77e3642c2137.png
code:_
年収250万、妻は120万、横浜在住
高2の娘は私立、高1の息子は公立高校、中2の娘は私立中学に通っています。
code:_
こんな制度が受けられるかもしれないヤド
-----
一時生活再建費: 60.0万円
児童手当: 5.0万円
就学支度費: 50.0万円
教育支援費: 10.0万円
生活保護: 2.7万円
生活支援費: 20.0万円
福祉費: 580.0万円
緊急小口資金: 10.0万円
高等学校奨学給付金: 0.7~2.3万円
高等学校等就学支援金: 4.3~5.2万円
検証まとめ
自由記述欄からLLMによる「パラメータ抽出」で世帯情報を抽出し、OpenFisca-Japanへリクエスト
音声入力ができるともっと使える幅が広がりそう
(モデルによるかもしれないが)パラメータ抽出の精度が意外と高かった
パラメータが増えるとLLMの処理時間、料金が増えるかも
開発ボリュームはフロントエンドをもう1つ作るくらい(少しだけ簡単?)
API連携処理を作りこむのでノーコードだけでは難しかった
OpenFisca知らないと開発は難しい
Difyとは?
LLM開発プラットフォーム
LLMそのものではなく、LLMを使ったアプリを作りやすくするためのツール?
LLMの入出力に別の処理を繋げたりできる
モデルの差し替えもボタン一つで行える
環境構築
Difyのアカウントを作る
またはローカル環境にDockerでDifyを構築する
ワークフローを読み込む
左上「全て」のタブを選択し、左側「アプリを作成する」→「DSLファイルをインポート」を選択
リンク先のDSLファイル yadokarify.yml をダウンロードし、インポートの欄へドラッグ&ドロップ
この画面が出ればOK
https://scrapbox.io/files/67a20a54dae7936544e05ea6.png
LLMのモデルを利用できるようにする
上記プロジェクトでは Gemini 2.0 Flash Thinking Exp 1219 を使用
experimentalなのでAPI利用無料(2024/2/4現在)、性能もかなり高い
デフォルトで gpt-4o-mini のモデルも使用可能だが、10回くらい使うとエラーが出て呼び出せなくなってしまった
Dify無料版なので制限がある?
GeminiのAPIキーをDifyに登録
APIキーを取得
注意: APIキーは公開NG!(誤って公開してしまったらすぐに削除が必要)
APIキーをDifyに登録
右上の自分のアカウント名→「設定」→左側「モデルプロバイダー」→Geminiの「セットアップ」でAPI Keyの欄に入力
右上の「▶実行」で動作確認
トレースで各工程の入出力も確認可能
https://scrapbox.io/files/67a20c132229c28e02eb8921.png
https://scrapbox.io/files/67a20c35a5dd5f9d27090f10.png
https://scrapbox.io/files/67a20c2765669fa14c38985e.png
右上の「公開する」でインターネット上での公開も可能
注意:不特定多数の人が使えるようになるので、LLMの使用料金が跳ね上がる可能性あり!
念のためこのワークフローではAPI利用無料(2025/2現在)のモデルを使用しています
実装
技術スタック
コード: Python
出力文字列整形: Jinja2 (Pythonベースのテンプレート言語)
ワークフローは以下の工程からなる(各工程はDifyのブロックに対応)
開始:自由記述欄のテキスト
パラメータ抽出:LLM(Gemini)を使って、自由記述欄からOpenFiscaリクエストに必要な情報を抽出、生成
リクエストボディ作成:
OpenFiscaへリクエスト:「HTTPリクエスト」のブロックを使いOpenFisca-Japan(「ヤドカリくん」バックエンドAPI)へリクエスト
レスポンスのJSONをdict化:レスポンスのJSON文字列をデコードしPythonが扱えるdict形式に変換
結果を整形:Jinja2テンプレートで扱いやすいよう、不要な情報を削除
結果を表示:Jinja2テンプレートに結果データを流し込み結果文字列生成
終了:チャットの返信
ブロックの右上にある「▷」を押すと、ブロック単体での動作確認が可能
https://scrapbox.io/files/67a20d89a5dd5f9d270918f0.png
パラメータ抽出
ワークフロー唯一のLLMを使う箇所
プロンプトを使用して、テキストからパラメータを取り出す
パラメータ名、パラメータの説明、型を指定可能
例
table:作成したパラメータ
名前 型 説明
parent_income number 私(話者自身)の年収
spouse_income number 配偶者(話者の妻、夫)の年収
children_num number 子どもの人数
children_ages array[number] 子どもの年齢一覧、第一子から順に格納
address_pref string 住んでいる都道府県(正式名称) ...
address_city string 住んでいる市区町村(正式名称)
children_highschool_kind array[string] 子どもの高校履修種別一覧。各要素の取りうる値は以下のいずれか ...
children_highschool_management array[string] 子どもの高校運営種別一覧。各要素の取りうる値は以下のいずれか ...
https://scrapbox.io/files/67a20f2fe03c67ab20927eff.png
精度
Gemini 2.0 Flash Thinking Exp 1219 を使用したところかなりよい
私の年収と配偶者の年収を取り違えることがなかった
子どもの年齢も、学年から類推してくれた
入力
code:_
年収50万、妻は120万、横浜在住
長男は小5、長女は年長
抽出されたパラメータ
code:json
{
"__is_success": 1,
"__reason": null,
"parent_income": 500000,
"spouse_income": 1200000,
"children_num": 2,
"children_ages": [
10,
5
],
"address_pref": "神奈川県",
"address_city": "横浜市"
}
プロンプトで工夫した点
居住地は、「ヤドカリくん」の入力に合うよう「正式名称で」というプロンプトを加えた
「横浜」と書かれていてもパラメータを「横浜市」にするため
また、都道府県は市区町村から推定するというプロンプトを加えた
住んでいる都道府県(正式名称) ※市区町村しか記載がない場合は市区町村から推定する
「横浜市」なら入力に書かれていなくても「神奈川県」
enumで想定外の値が入らないようにした
デフォルトと取りうる値を列挙
code:_
子どもの高校履修種別一覧。各要素の取りうる値は以下のいずれか
- "全日制課程" (デフォルト)
- "定時制課程"
- "通信制課程"
- "専攻科"
- "無" (高校生でない場合)
課題
論理的に同じになるはずのarrayの長さが同じにならない
例:children_highschool_kindの長さが children_num と同じにならない
高校生でない場合、無 と入れるべきところが要素ごと欠落してしまうため
対策として、後続の「リクエストボディ作成」でバリデーションチェックや値の補完をしている
リクエストボディ作成
上記で得られた全パラメータを引数に取り、OpenFiscaリクエストボディのJSONを作成
「コード」機能で、Pythonコードを使って整形
実装の概観はヤドカリくんのフロントエンドとほぼ同じ
実装ボリュームとしてはフロントエンドをもう1つ作るイメージ
デザインがないだけ少し楽?
Difyの制約で5重以上の入れ子のdictを扱えないため、JSON化してからreturnする必要がある
code:python
import datetime
import json
allowance_names = [
"児童手当",
"児童育成手当",
"児童扶養手当_最大",
"児童扶養手当_最小",
"特別児童扶養手当_最小",
"特別児童扶養手当_最大",
"生活保護",
"障害児福祉手当",
"高等学校奨学給付金_最小",
"高等学校奨学給付金_最大",
"生活支援費",
"一時生活再建費",
"福祉費",
"緊急小口資金",
"教育支援費",
"就学支度費",
"不動産担保型生活資金",
"災害弔慰金",
"災害障害見舞金_最大",
"災害障害見舞金_最小",
"被災者生活再建支援制度",
"災害援護資金",
"高等学校等就学支援金_最大",
"高等学校等就学支援金_最小",
"健康管理費用_最大",
"健康管理費用_最小",
"健康管理支援事業_最大",
"健康管理支援事業_最小",
"先天性の傷病治療によるC型肝炎患者に係るQOL向上等のための調査研究事業_最大",
"先天性の傷病治療によるC型肝炎患者に係るQOL向上等のための調査研究事業_最小",
"障害基礎年金_最大",
"障害基礎年金_最小",
"特別障害者手当_最大",
"特別障害者手当_最小",
"特定疾病療養の対象者がいる",
"先天性血液凝固因子障害等治療研究事業の対象者がいる",
"重度心身障害者医療費助成制度の対象者がいる",
"傷病手当金_最大",
"傷病手当金_最小",
"被災している",
]
def today() -> str:
now = datetime.datetime.now()
return now.strftime("%Y-%m-%d")
def age_to_birthday(age: int) -> str:
now = datetime.datetime.now()
birth = now - datetime.timedelta(days=age * 365)
return birth.strftime("%Y-%m-%d")
def allowances() -> dict:
timestamp = today()
return {name: {timestamp: None} for name in allowance_names}
def main(
parent_income: int,
spouse_income: int,
children_num: int,
address_pref: str,
address_city: str,
children_highschool_kind: liststr, children_highschool_management: liststr, ) -> str:
req = {
"世帯員": {
"あなた": {
"収入": {today(): parent_income}
}
},
"世帯一覧": {
"世帯1": allowances(),
}
}
pref = "東京都"
if address_pref:
pref = address_pref
city = "新宿区"
if address_city:
city = address_city
if spouse_income != 0:
if len(children_highschool_kind) < children_num:
if len(children_highschool_management) < children_num:
if children_num and children_num > 0:
for i in range(children_num):
"誕生年月日": {"ETERNITY": age_to_birthday(children_agesi)}, "高校履修種別": {today(): children_highschool_kindi}, "高校運営種別": {today(): children_highschool_managementi} }
# HACK: 結果をJSONエンコードして文字列化してからreturn(Difyの制約上、5重以上の入れ子のdictを戻り値として扱えないため)
# Error: "Depth limit $5 reached, object too deep."
return {
"result": json.dumps(req),
}
JSON化すると日本語がエスケープされてしまい読めないので、トレースの際に使えるよう「リクエストボディ(デバッグ表示用)」のブロックも追加している
OpenfIscaへリクエスト
Difyの「HTTPリクエスト」の機能を使用
本番へ変なデータを送って過負荷になると困るので、開発環境のAPIへリクエストしている
ボディ形式はJSONではなく raw を使用
加工済みのJSONを使用するため(JSONだとJSON文字列がさらにエスケープされてしまい壊れてしまう)
リクエストヘッダ Content-Type: application/json 指定必須!
ないとOpenFisca側でJSONと認識できない
https://scrapbox.io/files/67a2152016290ded8c186eac.png
レスポンスのJSONをdict化
「コード」ブロックで、後続のPythonコードで扱えるようレスポンスをデコード
code:python
import json
def main(body: str) -> dict:
return {
"result": json.loads(body)
}
結果を整形
テンプレートで扱いやすいように不要な情報を捨てる
最大額、最小額の突合がしやすいようdictを以下の形式に整形
code:json
{
"児童手当": {
"min": 10000,
"max": 10000
},
...
}
code:py
def main(arg1: dict) -> dict:
見積もり結果 = {}
def 数値抽出(結果):
for 制度名, 結果 in 制度一覧.items():
if 制度名 in 制度以外のキー:
continue
if 制度名.endswith("_最小"):
最小額 = 数値抽出(結果)
elif 制度名.endswith("_最大"):
# 取得済みのため何もしない
pass
else:
金額 = 数値抽出(結果)
見積もり結果制度名 = {"min": 金額, "max": 金額} return {"result": 見積もり結果}
結果を表示
Jinja2のテンプレートに上記で得られた結果を流し込むことで結果の文字列を作成 Pythonに似ている
{{}} の中にオブジェクトを指定すると、引数に取ったオブジェクトが文字列化されはめ込まれる
変数定義やif文等プログラムのためだけに入れた行は %{- ではじめる
{% のままにすると無駄な改行が入ってしまうため
float の小数点が邪魔なので round(1) で小数点第一位で四捨五入
code:jinja2
こんな制度が受けられるかもしれないヤド
-----
{%- for name, amount in body.items() %}
{%- if amount.max > 0 %}
{%- if amount.min == amount.max %}
{{name}}: {{(amount.min / 10000) | round(1)}}万円
{%- else %}
{{name}}: {{(amount.min / 10000) | round(1)}}~{{(amount.max / 10000) | round(1)}}万円
{%- endif %}
{%- endif %}
{%- endfor %}