はじめに
前回は、pandasによるExcelインポートとopenpyxlによるExcelエクスポートを実装しました。
前回の記事:Python × Dify × RAGで学ぶ業務システム開発入門【第7回 Excel業務自動化編】
第8回では、社員の研修実績をもとにDify APIで評価コメントを生成する機能を実装します。本記事は、以下の最新ソースコードに合わせています。
社員ごとに研修履歴を集計
↓
総合スコアを計算
↓
Dify APIへ研修データを送信
↓
評価コメントを生成してSQLiteへ保存
↓
評価管理画面へ表示
現行実装では研修を1件選択して評価するのではなく、社員ごとの全研修履歴を平均して評価対象データを作ります。
本記事で学ぶこと
| 項目 | 内容 |
|---|---|
| 評価データ集計 | 社員ごとに研修履歴の平均値を取得する |
| 総合スコア計算 | 出席率・理解度・課題スコアを重み付けする |
| Dify API連携 |
requests.post でChat APIを呼び出す |
| フォールバック | APIキー未設定時や通信失敗時もコメントを返す |
| 評価履歴保存 | 生成結果を evaluation テーブルへ追加する |
| 生成中UI | スピナーを表示し、二重送信を防止する |
Difyとは
Difyは、LLM、プロンプト、Knowledge、ワークフローなどを組み合わせて生成AIアプリケーションを構築できるプラットフォームです。
| 用途 | 呼び出し方式 | 実装回 |
|---|---|---|
| AI評価コメント生成 | Chat API | 第8回(本記事) |
| 社内文書検索 | Chat API / Knowledge | 第9回 |
| FAQチャット | Chat API・会話ID | 第10回 |
第8回では、PythonからDifyの /chat-messages エンドポイントを呼び出し、評価コメントを取得します。
評価管理画面の仕様
評価管理画面は admin と staff が利用できます。有効な社員を一覧表示し、次の情報を確認できます。
| 表示項目 | 内容 |
|---|---|
| 社員番号・氏名・部署 | 社員マスタの情報 |
| 出席率 | 全研修履歴の平均出席率 |
| 理解度 | 全研修履歴の平均理解度 |
| 課題 | 全研修履歴の平均課題スコア |
| 総合 | 重み付けして計算した総合スコア |
| AIコメント | 最新コメント。未生成の場合は「未生成」 |
| 操作 |
AI生成 または 再生成
|
画面上部には、評価対象者数、平均出席率、平均理解度、AIコメント生成済み件数も表示します。
Dify APIの設定
プロジェクト直下の .env にDifyの設定を記述します。
DIFY_API_KEY=取得したDify APIキー
DIFY_API_URL=https://api.dify.ai/v1
APIキーはソースコードへ直接書かず、Gitにも登録しません。
.env
APIキーや SECRET_KEY などの秘密情報は、GitHubへコミットしないでください。
config.py では、python-dotenv を使って環境変数を読み込みます。
import os
from dotenv import load_dotenv
load_dotenv()
class Config:
DIFY_API_KEY = os.getenv("DIFY_API_KEY", "")
DIFY_API_URL = os.getenv("DIFY_API_URL", "https://api.dify.ai/v1")
DIFY_API_KEY の初期値は空文字です。APIキーが空の場合はアプリを停止させず、確認用のローカルコメントを返します。
評価データを集計する
評価関連のビジネスロジックは services/evaluation_service.py にまとめています。
総合スコアの計算
総合スコアは、出席率30%、理解度40%、課題スコア30%で計算します。
def calc_score(attendance_rate: float,
understanding_level: int,
report_score: int) -> float:
"""出席率30%、理解度40%、課題30%で総合スコアを計算する。"""
return round(
(attendance_rate * 0.3)
+ (understanding_level * 0.4)
+ (report_score * 0.3),
1,
)
出席率95%、理解度82%、課題88点なら、総合スコアは 87.7 です。
社員ごとの評価対象データを取得する
from database import get_db
def get_evaluation_targets() -> list:
conn = get_db()
rows = conn.execute(
"""
SELECT
e.id AS employee_id,
e.employee_no,
e.name,
e.department,
ROUND(AVG(COALESCE(th.attendance_rate, 0)), 1) AS attendance_rate,
ROUND(AVG(COALESCE(th.understanding_level, 0)), 1) AS understanding_level,
ROUND(AVG(COALESCE(th.report_score, 0)), 1) AS report_score,
ev.id AS evaluation_id,
ev.score,
ev.ai_comment,
ev.created_at
FROM employee e
LEFT JOIN training_history th ON th.employee_id = e.id
LEFT JOIN (
SELECT v1.*
FROM evaluation v1
JOIN (
SELECT employee_id, MAX(id) AS max_id
FROM evaluation
GROUP BY employee_id
) v2 ON v1.id = v2.max_id
) ev ON ev.employee_id = e.id
WHERE e.is_active = 1
GROUP BY e.id
ORDER BY e.employee_no
"""
).fetchall()
conn.close()
result = []
for row in rows:
item = dict(row)
item["calculated_score"] = calc_score(
float(item.get("attendance_rate") or 0),
int(item.get("understanding_level") or 0),
int(item.get("report_score") or 0),
)
result.append(item)
return result
実装上のポイントは次のとおりです。
-
employeeを基準にLEFT JOINし、研修履歴がない有効社員も表示する -
AVGで社員ごとの全研修履歴を平均する -
COALESCEで履歴がない項目を0として扱う -
evaluationは社員ごとの最大ID、つまり最新の1件だけを表示する - 現在の研修履歴から
calculated_scoreを計算する
Dify APIでコメントを生成する
API連携は services/dify_service.py に実装します。
認証ヘッダー
import requests
from config import Config
def _headers() -> dict:
return {
"Authorization": f"Bearer {Config.DIFY_API_KEY}",
"Content-Type": "application/json",
}
プロンプトとAPI呼び出し
def generate_evaluation_comment(name: str,
understanding: float,
attendance: float,
submission: float) -> str:
if not Config.DIFY_API_KEY:
return _fallback_comment(name, understanding, attendance, submission)
prompt = (
"あなたは人事評価の専門家です。以下の研修データをもとに、"
"200字以内・前向きな表現・具体的な改善点1つを含めた"
"評価コメントを作成してください。\n"
f"対象者:{name}\n"
f"理解度:{understanding}%\n"
f"出席率:{attendance}%\n"
f"課題スコア:{submission}%"
)
payload = {
"inputs": {},
"query": prompt,
"response_mode": "blocking",
"user": "human-resource-app",
}
try:
res = requests.post(
f"{Config.DIFY_API_URL}/chat-messages",
headers=_headers(),
json=payload,
timeout=30,
)
res.raise_for_status()
data = res.json()
return data.get("answer") or _fallback_comment(
name, understanding, attendance, submission
)
except requests.RequestException:
return _fallback_comment(name, understanding, attendance, submission)
| 項目 | 値 | 役割 |
|---|---|---|
inputs |
{} |
追加変数。現行実装では未使用 |
query |
プロンプト | 指示と社員の研修データ |
response_mode |
blocking |
回答完了までレスポンスを待つ |
user |
human-resource-app |
Dify側の固定利用者ID |
timeout=30 で30秒を超えた通信を終了し、raise_for_status() で4xx・5xxを例外として扱います。
フォールバックコメント
APIキー未設定、通信失敗、または空回答の場合は、研修データからローカルコメントを作ります。
def _fallback_comment(name: str,
understanding: float,
attendance: float,
submission: float) -> str:
score = round(
attendance * 0.3 + understanding * 0.4 + submission * 0.3,
1,
)
if score >= 80:
level = "非常に良好です"
advice = "今後は周囲への共有や応用課題への挑戦を期待します。"
elif score >= 60:
level = "概ね良好です"
advice = "理解が浅い部分を復習し、課題の精度を高めるとさらに伸びます。"
else:
level = "基礎の定着に追加支援が必要です"
advice = "出席状況と課題提出を確認し、短い単位で復習を進めましょう。"
return (
f"{name}さんの研修状況は{level}。"
f"出席率{attendance}%、理解度{understanding}%、"
f"課題スコア{submission}%です。{advice}"
)
これにより、Difyの設定前でも画面遷移とDB保存を確認できます。
現行実装はAPI障害の詳細を画面へ表示せず、フォールバックコメントを保存します。本番では例外をログへ記録し、AI生成結果とフォールバック結果を区別できる項目を追加すると運用しやすくなります。
評価結果を保存する
生成のたびに evaluation テーブルへ新しい行を追加します。
def save_evaluation(employee_id: int,
score: float,
ai_comment: str) -> int:
conn = get_db()
cur = conn.execute(
"INSERT INTO evaluation (employee_id, score, ai_comment) "
"VALUES (?, ?, ?)",
(employee_id, score, ai_comment),
)
conn.commit()
evaluation_id = cur.lastrowid
conn.close()
return evaluation_id
再生成時も既存行を更新せず、評価履歴として追加します。一覧画面では最大IDの評価だけを取得するため、常に最新コメントが表示されます。
Flaskのルーティング
評価一覧
GET /evaluations では評価対象を取得し、画面上部の集計値を作ります。
@app.route("/evaluations")
@login_required
@roles_required("staff", "admin")
def evaluation_list():
evaluations = get_evaluation_targets()
count = len(evaluations)
evaluation_stats = {
"target_count": count,
"attendance_average": round(
sum(float(item.get("attendance_rate") or 0)
for item in evaluations) / count, 1
) if count else 0,
"understanding_average": round(
sum(float(item.get("understanding_level") or 0)
for item in evaluations) / count, 1
) if count else 0,
"generated_count": sum(
1 for item in evaluations if item.get("ai_comment")
),
}
return render_template(
"evaluation.html",
evaluations=evaluations,
evaluation_stats=evaluation_stats,
)
AIコメント生成
POST /evaluations/<employee_id>/generate で対象社員を探し、Dify連携、評価保存、一覧へのリダイレクトを実行します。
@app.route("/evaluations/<int:employee_id>/generate", methods=["POST"])
@login_required
@roles_required("staff", "admin")
def evaluation_generate(employee_id):
target = next(
(e for e in get_evaluation_targets()
if e["employee_id"] == employee_id),
None,
)
if not target:
flash("評価対象が見つかりません", "warning")
return redirect(url_for("evaluation_list"))
comment = generate_evaluation_comment(
target["name"],
target["understanding_level"],
target["attendance_rate"],
target["report_score"],
)
save_evaluation(employee_id, target["calculated_score"], comment)
flash("AI評価コメントを生成しました", "success")
return redirect(url_for("evaluation_list"))
評価管理画面と生成中UI
テンプレートは templates/evaluation.html です。
生成・再生成ボタン
<form
method="POST"
action="{{ url_for('evaluation_generate', employee_id=ev.employee_id) }}"
class="ai-generate-form"
data-employee-name="{{ ev.name }}"
>
<button class="btn btn-small btn-generate" type="submit">
<span class="button-label">
{% if ev.ai_comment %}再生成{% else %}AI生成{% endif %}
</span>
</button>
</form>
コメント未生成なら AI生成、生成済みなら 再生成 と表示します。
生成中オーバーレイ
<div id="ai-loading-overlay"
class="ai-loading-overlay"
role="status"
aria-live="polite"
aria-hidden="true">
<div class="ai-loading-panel">
<span class="loading-spinner" aria-hidden="true"></span>
<strong>AI評価コメントを生成中...</strong>
<p id="ai-loading-message">
Difyからの回答を待っています。しばらくお待ちください。
</p>
</div>
</div>
role="status" と aria-live="polite" により、支援技術にも処理状態を伝えます。
二重送信を防止するJavaScript
<script>
document.querySelectorAll(".ai-generate-form").forEach((form) => {
form.addEventListener("submit", () => {
const button = form.querySelector("button[type='submit']");
const label = button.querySelector(".button-label");
const employeeName = form.dataset.employeeName;
const overlay = document.getElementById("ai-loading-overlay");
const message = document.getElementById("ai-loading-message");
button.disabled = true;
button.classList.add("is-loading");
label.innerHTML =
'<span class="button-spinner" aria-hidden="true"></span>生成中...';
message.textContent = employeeName
? employeeName + "さんの評価コメントを生成しています。しばらくお待ちください。"
: "Difyからの回答を待っています。しばらくお待ちください。";
overlay.classList.add("is-visible");
overlay.setAttribute("aria-hidden", "false");
document.querySelectorAll(".ai-generate-form button").forEach((item) => {
item.disabled = true;
});
});
});
</script>
フォーム送信時には次の変化が起きます。
- ボタンを
生成中...に変更する - ボタン内と画面中央にスピナーを表示する
- 対象社員名を含むメッセージを表示する
- すべての生成ボタンを無効化して二重送信を防止する
- 処理完了後に評価一覧へリダイレクトする
ローディング表示のCSS
.ai-loading-overlay {
position: fixed;
inset: 0;
z-index: 1000;
display: grid;
place-items: center;
background: rgba(15, 23, 42, 0.55);
opacity: 0;
visibility: hidden;
}
.ai-loading-overlay.is-visible {
opacity: 1;
visibility: visible;
}
.loading-spinner,
.button-spinner {
display: inline-block;
border-radius: 50%;
animation: ai-spin 0.8s linear infinite;
}
@keyframes ai-spin {
to { transform: rotate(360deg); }
}
実際の static/css/style.css では、白いパネル、大小2種類のスピナー、余白、影なども定義しています。
動作確認
1. アプリを起動する
python app.py
2. 担当者または管理者でログインする
評価管理は staff または admin ロールで利用します。社員番号 EMP001 でログインする山田太郎さんのマイページには、評価コメント生成機能は表示されません。
3. 評価管理画面を開く
/evaluations を開きます。サンプルデータの山田太郎さんは、出席率95%、理解度82%、課題88点、総合87.7点です。
4. AI生成 をクリックする
ボタンが 生成中... に変わり、画面中央に「AI評価コメントを生成中...」と表示されます。処理が完了すると一覧へ戻り、AIコメント欄に結果が表示されます。
5. 再生成 を確認する
生成済みの社員ではボタンが 再生成 になります。再生成すると evaluation テーブルへ新しい行が追加され、一覧には最新コメントが表示されます。
6. APIキー未設定時を確認する
DIFY_API_KEY を空にすると、Dify APIへ接続せずフォールバックコメントが保存されます。これは開発確認用の動作です。
現行実装の注意点
| 項目 | 現行仕様 | 本番運用での改善例 |
|---|---|---|
| API呼び出し |
blocking で最大30秒待機 |
非同期ジョブと進捗確認を導入する |
| 通信失敗 | フォールバックコメントを保存 | ログ・管理者通知・生成元フラグを追加する |
| 再試行 | 自動リトライなし | 429・一時的な5xxだけ再試行する |
| 評価履歴 | 生成のたびにINSERT | 履歴画面、生成者、モデル情報を保存する |
| CSRF対策 | 本教材のフォームには未導入 | Flask-WTFなどでトークンを付与する |
| Dify利用者ID | 固定値 | ログインユーザーごとの識別子を送る |
生成AIの出力は必ず正しいとは限りません。実際の人事評価では担当者が内容を確認・修正し、AIだけで最終判断しない運用が必要です。
おわりに
第8回では、Dify APIを使った評価コメント生成機能を実装しました。
- 社員ごとの全研修履歴を平均して評価データを作成した
- 出席率30%、理解度40%、課題30%で総合スコアを計算した
- Difyの
/chat-messagesをblockingモードで呼び出した - APIキー未設定・通信失敗・空回答時はフォールバックした
- 生成結果は履歴として
evaluationテーブルへ追加した - 一覧には社員ごとの最新評価だけを表示した
- 生成中UIとボタン無効化で待機状態を示し、二重送信を防いだ
次回は「RAG構築編」として、社内文書を自然言語で検索する機能を扱います。
次回:Python × Dify × RAGで学ぶ業務システム開発入門【第9回 RAG構築編】
本連載について
本連載について
| 回 | タイトル | 対応画面 | 主な内容 |
|---|---|---|---|
| 第1回 | 業務システム全体設計編 | ― | システム概要・技術選定・アーキテクチャ |
| 第2回 | 要求定義・要件定義編 | ― | 業務分析・機能要件・非機能要件 |
| 第3回 | ER図・画面設計編 | ― | テーブル設計・画面遷移図・ワイヤーフレーム |
| 第4回 | Flaskログイン機能編 | 🔐 ログイン | セッション・ハッシュ・ロール認可・社員番号ログイン |
| 第5回 | 社員管理CRUD編 | 👤 社員管理 | 一覧・検索・登録・更新・論理削除・入力制御 |
| 第6回 | 研修管理編 | 📚 研修管理 | 研修登録・LEFT JOIN・履歴一括入力・UPSERT |
| 第7回 | Excel業務自動化編 | 📥 取込 / 📤 出力 | pandas取込・openpyxl出力 |
| 第8回 | Dify API連携編 | 📊 評価管理 | API・フォールバック・生成中UI・二重送信防止 |
| 第9回 | RAG構築編 | 🔍 文書検索 | Dify Chat API・質問送信・回答表示・エラー処理 |
| 第10回 | FAQチャットボット編 | 💬 FAQチャット | 会話ID・Fetch API・生成中表示・二重送信防止 |
| 公開編 | Ubuntu VPS・Docker公開編 | 🌐 サービス公開 | Ubuntu VPS・Docker・Nginxを使った本番環境構築とWebアプリケーション公開 |


