Day 4〜5:議事録要約ツールでは、前回の「問い合わせ分類ツール」と同じ考え方で、今度は複数行の議事録を1件の入力として読み込み、会議タイトル・要約・決定事項・TODO・リスクなどをJSONで出力するツールを作ります。
今回の仕様はこうします。
ファイル名:
02_summarize_meeting.py
入力:
1. 標準入力
- 複数行入力OK
- 空行OK
- END だけの行が入力されたら入力終了
2. ファイル入力
- data/sample_summarize.txt
- 1ファイル全体を1つの議事録として扱う
出力:
outputs/results_summarize_meeting.jsonl
OpenAIのStructured Outputsは、モデル出力を定義したJSON Schemaに従わせるための機能で、Python SDKではPydanticモデルを使って構造化出力を扱えると説明されています。今回の議事録要約は「自然文を業務で使えるJSONに変換する」練習として非常に向いています。(OpenAI Developers)
Day 4〜5で作るもの
完成イメージはこれです。
複数行の議事録
↓
OpenAI API
↓
構造化JSONで要約
↓
画面に表示
↓
outputs/results_summarize_meeting.jsonl に保存
出力例はこうです。
{
"meeting_title": "新機能リリース前確認会",
"summary": "新機能リリースに向けて、性能試験、FAQ更新、最終判断日について確認した。",
"decisions": [
"リリース日は5月10日の予定を維持する",
"最終判断は5月8日の定例会で行う"
],
"action_items": [
{
"task": "性能試験を再実施する",
"owner": "田中",
"due_date": "5月2日",
"note": "ピーク時レスポンスの遅延を確認する"
}
],
"risks": [
"ピーク時のレスポンス遅延が解消していない可能性がある"
],
"pending_items": [
"性能試験の再実施結果を確認する"
],
"participants": [
"田中",
"佐藤"
],
"keywords": [
"新機能リリース",
"性能試験",
"FAQ",
"リリース判断"
]
}
1. フォルダ構成
前回の genai-step1 プロジェクト内で進める前提です。
genai-step1/
.env
01_classify_inquiry.py
02_summarize_meeting.py
data/
sample_summarize.txt
outputs/
results_summarize_meeting.jsonl
なければ作ります。
mkdir -p data outputs
2. サンプル議事録ファイルを作る
まず data/sample_summarize.txt を作ってください。
新機能リリース前の確認会を実施。
参加者は田中、佐藤、鈴木。
リリース日は5月10日の予定で変えない方針。
ただし、性能試験でピーク時レスポンスが遅いという指摘があり、
田中さんが5月2日までに再試験する。
佐藤さんはユーザー向けFAQを5月7日までに更新する。
鈴木さんから、ログ出力の内容が運用チームに共有されていないという指摘があった。
ログ項目の一覧を整理し、次回の定例で確認する。
最終判断は5月8日の定例で行う。
ポイントは、空行を入れておくことです。
今回の目的は、1行単位ではなく、ファイル全体を1つの議事録として読むことだからです。
3. 必要ライブラリ
前回すでに入れていれば不要です。
pip install openai python-dotenv pydantic
.env は前回と同じです。
OPENAI_API_KEY=あなたのAPIキー
4. 完成版コード
02_summarize_meeting.py を作って、以下をそのまま貼ってください。
import json
import os
from datetime import datetime
from typing import Literal
from dotenv import load_dotenv
from openai import OpenAI
from pydantic import BaseModel, Field
# =========================
# 1. 初期設定
# =========================
load_dotenv()
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
# 使えるモデル名はアカウントや時期で変わる可能性があります。
# エラーになる場合は、OpenAIのダッシュボードで利用可能なモデル名に変更してください。
MODEL = os.getenv("OPENAI_MODEL", "gpt-5.5")
OUTPUT_FILE = "outputs/results_summarize_meeting.jsonl"
DEFAULT_INPUT_FILE = "data/sample_summarize.txt"
# =========================
# 2. 出力形式の定義
# =========================
class ActionItem(BaseModel):
task: str = Field(
description="実施すべきタスク。具体的な作業内容を書く。"
)
owner: str = Field(
description="担当者。不明な場合は '不明' と書く。"
)
due_date: str = Field(
description="期限。不明な場合は '不明' と書く。"
)
note: str = Field(
description="補足情報。なければ空文字にする。"
)
class SummarizeMeeting(BaseModel):
meeting_title: str = Field(
description="会議のタイトル。明示されていない場合は内容から簡潔に推定する。"
)
summary: str = Field(
description="会議全体の要約。3文以内で簡潔に書く。"
)
decisions: list[str] = Field(
description="会議で決定した事項。決定事項がない場合は空配列にする。"
)
action_items: list[ActionItem] = Field(
description="TODO、宿題、担当者付きの作業。ない場合は空配列にする。"
)
risks: list[str] = Field(
description="リスク、懸念、注意点。ない場合は空配列にする。"
)
pending_items: list[str] = Field(
description="未決事項、次回確認事項、保留事項。ない場合は空配列にする。"
)
participants: list[str] = Field(
description="参加者名。分からない場合は空配列にする。"
)
next_meeting_or_deadline: list[str] = Field(
description="次回会議、重要な期限、判断日。ない場合は空配列にする。"
)
keywords: list[str] = Field(
description="会議内容を表す重要キーワード。3〜7個程度。"
)
confidence: float = Field(
ge=0.0,
le=1.0,
description="要約・抽出結果への自信度。0.0〜1.0で表す。"
)
# =========================
# 3. プロンプト
# =========================
SYSTEM_PROMPT = """
あなたは業務システム開発プロジェクトの議事録整理を支援するシステムエンジニアです。
以下の方針で議事録を整理してください。
目的:
- 会議内容を業務で使える形に整理する
- 決定事項、TODO、リスク、未決事項を明確に分ける
- 後で課題管理表や報告資料に転記しやすい形にする
抽出ルール:
- 決定事項は「決まったこと」だけを入れる
- action_items には「誰が」「何を」「いつまでに」が分かるものを優先して入れる
- 担当者が不明な場合は owner を '不明' にする
- 期限が不明な場合は due_date を '不明' にする
- リスクには、遅延、品質、性能、セキュリティ、運用、認識齟齬などの懸念を入れる
- 未決事項には、まだ判断されていないこと、次回確認することを入れる
- 明記されていない情報を断定しない
- ただし、会議タイトルは内容から自然に推定してよい
注意:
- 原文にない事実を作らない
- 些細な雑談は要約に含めない
- 要約は簡潔にする
- 出力は指定された構造に従う
"""
# =========================
# 4. 入力処理
# =========================
def read_meeting_from_stdin() -> str:
"""
標準入力から複数行の議事録を読み込む。
END だけの行が入力されたら終了する。
空行も議事録の一部として扱う。
"""
print("\n議事録を入力してください。")
print("複数行入力できます。")
print("入力を終了する場合は、END だけの行を入力してください。")
print("-" * 60)
lines: list[str] = []
while True:
line = input()
if line == "END":
break
lines.append(line)
return "\n".join(lines).strip()
def read_meeting_from_file(file_path: str = DEFAULT_INPUT_FILE) -> str:
"""
ファイル全体を1つの会議議事録として読み込む。
空行も含めて読み込む。
"""
with open(file_path, "r", encoding="utf-8") as f:
return f.read().strip()
# =========================
# 5. 要約処理
# =========================
def summarize_meeting(meeting_text: str) -> SummarizeMeeting:
"""
議事録テキストを構造化要約する。
"""
response = client.responses.parse(
model=MODEL,
input=[
{
"role": "system",
"content": SYSTEM_PROMPT,
},
{
"role": "user",
"content": f"以下の議事録を要約・整理してください。\n\n{meeting_text}",
},
],
text_format=SummarizeMeeting,
)
return response.output_parsed
# =========================
# 6. 結果保存
# =========================
def save_result(
meeting_text: str,
result: SummarizeMeeting,
input_type: Literal["stdin", "file"],
input_file: str | None = None,
) -> None:
"""
結果をJSONL形式で保存する。
1行が1回分の実行結果。
"""
os.makedirs("outputs", exist_ok=True)
record = {
"timestamp": datetime.now().isoformat(timespec="seconds"),
"tool_name": "summarize_meeting",
"input_type": input_type,
"input_file": input_file,
"input_chars": len(meeting_text),
"model": MODEL,
"input": meeting_text,
"result": result.model_dump(mode="json"),
}
with open(OUTPUT_FILE, "a", encoding="utf-8") as f:
f.write(json.dumps(record, ensure_ascii=False) + "\n")
# =========================
# 7. 表示処理
# =========================
def print_result(result: SummarizeMeeting) -> None:
"""
画面に見やすく表示する。
"""
print("\n" + "=" * 60)
print("議事録要約結果")
print("=" * 60)
print(f"\n会議タイトル:\n{result.meeting_title}")
print(f"\n要約:\n{result.summary}")
print("\n決定事項:")
if result.decisions:
for i, decision in enumerate(result.decisions, start=1):
print(f"{i}. {decision}")
else:
print("なし")
print("\nアクションアイテム:")
if result.action_items:
for i, item in enumerate(result.action_items, start=1):
print(f"{i}. {item.task}")
print(f" 担当者: {item.owner}")
print(f" 期限: {item.due_date}")
if item.note:
print(f" 補足: {item.note}")
else:
print("なし")
print("\nリスク・懸念:")
if result.risks:
for i, risk in enumerate(result.risks, start=1):
print(f"{i}. {risk}")
else:
print("なし")
print("\n未決事項・次回確認事項:")
if result.pending_items:
for i, item in enumerate(result.pending_items, start=1):
print(f"{i}. {item}")
else:
print("なし")
print("\n参加者:")
if result.participants:
print(", ".join(result.participants))
else:
print("不明")
print("\n次回会議・重要期限:")
if result.next_meeting_or_deadline:
for i, item in enumerate(result.next_meeting_or_deadline, start=1):
print(f"{i}. {item}")
else:
print("なし")
print("\nキーワード:")
if result.keywords:
print(", ".join(result.keywords))
else:
print("なし")
print(f"\n自信度: {result.confidence}")
# =========================
# 8. メイン処理
# =========================
def main() -> None:
print("議事録要約ツール")
print("1: 標準入力から議事録を入力する")
print(f"2: ファイルから議事録を読み込む ({DEFAULT_INPUT_FILE})")
mode = input("実行モードを選んでください [1/2]: ").strip()
try:
if mode == "1":
meeting_text = read_meeting_from_stdin()
input_type: Literal["stdin", "file"] = "stdin"
input_file = None
elif mode == "2":
meeting_text = read_meeting_from_file(DEFAULT_INPUT_FILE)
input_type = "file"
input_file = DEFAULT_INPUT_FILE
else:
print("1 または 2 を入力してください。")
return
if not meeting_text:
print("議事録が空です。処理を終了します。")
return
print("\n要約中です...")
result = summarize_meeting(meeting_text)
print_result(result)
print("\nJSON:")
print(json.dumps(result.model_dump(mode="json"), ensure_ascii=False, indent=2))
save_result(
meeting_text=meeting_text,
result=result,
input_type=input_type,
input_file=input_file,
)
print(f"\n結果を {OUTPUT_FILE} に保存しました。")
except FileNotFoundError:
print(f"\nファイルが見つかりません: {DEFAULT_INPUT_FILE}")
print("data/sample_summarize.txt を作成してください。")
except Exception as e:
print("\nエラーが発生しました。")
print(str(e))
if __name__ == "__main__":
main()
5. 実行方法
ファイルから読み込む場合
python 02_summarize_meeting.py
表示されたら 2 を選びます。
議事録要約ツール
1: 標準入力から議事録を入力する
2: ファイルから議事録を読み込む (data/sample_summarize.txt)
実行モードを選んでください [1/2]: 2
data/sample_summarize.txt の内容全体が、1つの会議の議事録として読み込まれます。
標準入力から読み込む場合
python 02_summarize_meeting.py
1 を選びます。
実行モードを選んでください [1/2]: 1
その後、複数行で入力します。
新機能リリース前の確認会を実施。
参加者は田中、佐藤、鈴木。
リリース日は5月10日の予定で変えない方針。
ただし、性能試験でピーク時レスポンスが遅いという指摘があり、
田中さんが5月2日までに再試験する。
佐藤さんはユーザー向けFAQを5月7日までに更新する。
最終判断は5月8日の定例で行う。
END
END だけの行を入力すると、そこまでの内容が1つの議事録として処理されます。
ここで大事なのは、空行もそのまま入力できることです。
空行を入力しても終了しません。終了するのは END だけの行を入れたときです。
6. 結果ファイルの確認
実行後、次のファイルに保存されます。
outputs/results_summarize_meeting.jsonl
中身はJSONL形式です。
つまり、1回の実行結果が1行です。
例:
{"timestamp":"2026-05-03T21:30:00","tool_name":"summarize_meeting","input_type":"file","input_file":"data/sample_summarize.txt","input_chars":248,"model":"gpt-5.5","input":"...","result":{"meeting_title":"新機能リリース前確認会","summary":"..."}}
JSONLにしている理由は、後でログ分析しやすいからです。
たとえば将来的に、
- 何回実行したか
- どの議事録で失敗したか
- どのモデルを使ったか
- 入力文字数がどれくらいか
- 要約品質を比較する
といった分析がしやすくなります。
7. このコードで学ぶべきポイント
ポイント1:複数行入力の扱い
今回の標準入力では、こうしています。
while True:
line = input()
if line == "END":
break
lines.append(line)
つまり、
- 空行 →
linesに追加される -
END→ 入力終了 -
END→ 終了しない -
ENDです→ 終了しない
という挙動です。
より柔軟にしたいなら、
if line.strip() == "END":
break
に変えてもよいです。
ただし今回は「ENDだけの行」という仕様なので、最初のコードのままで問題ありません。
ポイント2:ファイル全体を1議事録として扱う
ここが重要です。
with open(file_path, "r", encoding="utf-8") as f:
return f.read().strip()
readlines() で1行ずつ処理するのではなく、read() でファイル全体を読みます。
今回の仕様では、
1ファイル = 1会議の議事録
なので、これが正しいです。
前回のように「1行 = 1件」と考えると、普通の議事録では破綻します。
実務の議事録は、複数行・空行・箇条書きが当たり前だからです。
ポイント3:Pydanticで出力形式を固定する
今回の中心はここです。
class SummarizeMeeting(BaseModel):
meeting_title: str
summary: str
decisions: list[str]
action_items: list[ActionItem]
risks: list[str]
pending_items: list[str]
participants: list[str]
next_meeting_or_deadline: list[str]
keywords: list[str]
confidence: float
これにより、AIの出力が毎回バラバラな文章になるのを防ぎます。
普通に「要約してください」と頼むと、出力がこうなりがちです。
以下が要約です。
決定事項:
...
TODO:
...
一見よさそうですが、業務システムには渡しにくいです。
今回のようにJSONにしておくと、
- 課題管理表に転記
- Excel出力
- Teams投稿
- チケット起票
- DB保存
- ダッシュボード化
につなげやすくなります。
8. 出力項目の意味
meeting_title
会議タイトルです。
明示されていない議事録も多いので、内容から推定させています。
例:
"meeting_title": "新機能リリース前確認会"
summary
全体要約です。
長くしすぎると使いにくいので、プロンプトで「3文以内」と指定しています。
decisions
決定事項です。
ここには決まったことだけを入れます。
良い例:
"リリース日は5月10日の予定を維持する"
悪い例:
"性能試験について話し合った"
これは決定事項ではなく、議題です。
action_items
TODOです。
{
"task": "性能試験を再実施する",
"owner": "田中",
"due_date": "5月2日",
"note": "ピーク時レスポンスの遅延を確認する"
}
実務ではここが一番重要です。
会議後に必要なのは、きれいな要約よりも、
誰が、何を、いつまでにやるのか
です。
risks
リスクや懸念です。
例:
"ピーク時レスポンスの遅延が解消していない可能性がある"
ここはプロジェクト管理で使いやすいです。
pending_items
未決事項・次回確認事項です。
例:
"ログ項目の一覧を次回定例で確認する"
決定事項と混ぜないのが大事です。
next_meeting_or_deadline
次回会議や重要な期限です。
例:
"5月8日の定例でリリース可否を最終判断する"
9. Day 4でやること
Day 4は、まず動かすことが目的です。
Day 4の作業
-
data/sample_summarize.txtを作る -
02_summarize_meeting.pyを作る - ファイル入力モードで実行する
- 標準入力モードで実行する
-
outputs/results_summarize_meeting.jsonlに保存されることを確認する
Day 4の合格条件
- 複数行の議事録を読み込める
- 空行を含んでも壊れない
-
ENDだけの行で標準入力を終了できる - ファイル全体を1つの議事録として処理できる
- JSON形式で結果が表示される
- JSONLファイルに保存される
10. Day 5でやること
Day 5は、品質改善です。
追加サンプルを作る
data/sample_summarize.txt の内容を何パターンか差し替えて試してください。
サンプル1:障害対応会議
障害対応状況の確認会を実施。
本日9時15分頃から、注文APIで一部リクエストがタイムアウトしていた。
影響範囲は一部ユーザーで、注文完了までに時間がかかる状態だった。
原因は外部決済APIの応答遅延と推定。
山田さんが外部決済APIのログを確認する。
期限は本日18時。
暫定対応として、タイムアウト秒数を10秒から20秒に変更した。
恒久対応は次回の障害レビューで検討する。
利用者向けのお知らせは、佐藤さんが本日中に作成する。
見るポイント:
- 暫定対応が決定事項に入るか
- 山田さん・佐藤さんのTODOが抽出されるか
- 恒久対応が未決事項に入るか
- 外部決済API遅延がリスクに入るか
サンプル2:要件定義会議
在庫管理機能の要件確認。
倉庫担当者は、商品ごとの現在庫、引当数、入荷予定数を一覧で確認したい。
営業担当者は、販売可能数だけ見られればよい。
現在庫の更新タイミングについて議論した。
出荷確定時に在庫を減らす方針とする。
ただし、返品時の在庫戻しについては業務フローが未整理。
高橋さんが返品業務の流れを確認し、来週火曜日までに共有する。
画面の初期表示速度が遅くならないよう、検索条件なしでの全件表示は行わない方針。
見るポイント:
- 在庫更新タイミングが決定事項に入るか
- 返品業務フローが未決事項に入るか
- 高橋さんのTODOが抽出されるか
- 性能面の方針が決定事項またはリスクに整理されるか
サンプル3:ふわっとした会議メモ
今日は新しい管理画面について話した。
一覧画面はもう少し見やすくしたいという話があった。
検索条件が多すぎるかもしれない。
鈴木さんが画面案を見直すことになった。
期限は特に決めていない。
次回また確認する。
見るポイント:
- 期限不明を
不明にできるか - 決定事項を作りすぎていないか
- 曖昧な内容を断定していないか
- 未決事項に「次回確認」が入るか
11. 評価観点
議事録要約ツールは「きれいな文章」よりも、以下を重視してください。
| 観点 | 確認内容 |
|---|---|
| 決定事項 | 決まったことだけが入っているか |
| TODO | 誰が・何を・いつまでに、が拾えているか |
| リスク | 懸念点を拾えているか |
| 未決事項 | 決まっていないことを分けられているか |
| 期限 | 日付や次回会議を拾えているか |
| 捏造防止 | 原文にないことを作っていないか |
| 実務性 | 課題管理表に転記できる粒度か |
最初の目標はこれです。
決定事項: 80%程度拾える
TODO: 80%程度拾える
担当者: 70%程度拾える
期限: 70%程度拾える
捏造: ほぼなし
完璧でなくていいです。
重要なのは、人間が見直しやすい形に整理されていることです。
12. よくある失敗とプロンプト改善
失敗1:決定事項に「議論したこと」が入る
悪い例:
"性能試験について議論した"
これは決定事項ではありません。
この場合、プロンプトに追加します。
「議論した」「確認した」「意見が出た」だけの内容は、決定事項に入れない。
決定事項には「方針とする」「決定」「実施する」「維持する」など、会議で合意された内容だけを入れる。
失敗2:TODOの担当者が抜ける
追加プロンプト:
人名の直後に「が」「さんが」「担当」「対応」「確認」「作成」「更新」「共有」がある場合は、action_items の owner 候補として扱う。
失敗3:期限が抜ける
追加プロンプト:
「本日中」「明日まで」「来週火曜日」「5月7日まで」「次回定例まで」などは due_date に入れる。
失敗4:原文にないリスクを作る
追加プロンプト:
リスクは、原文から読み取れる懸念に限定する。
一般論として考えられるリスクを勝手に追加しない。
失敗5:要約が長すぎる
追加プロンプト:
summary は最大3文とし、詳細は decisions、action_items、risks、pending_items に分ける。
13. もう少し実務寄りにする改善案
Day 5の余力があれば、次の項目を追加するとさらに使いやすくなります。
追加案1:Teams投稿用サマリー
Pydanticモデルに追加します。
teams_post_draft: str = Field(
description="Teamsに投稿するための簡潔な共有文。"
)
用途:
【会議要約】
本日の確認会では、リリース日を5月10日に維持する方針を確認しました。
田中さんは5月2日までに性能試験を再実施、佐藤さんは5月7日までにFAQを更新します。
最終判断は5月8日の定例で行います。
追加案2:課題管理表向けの分類
ActionItemに category を追加します。
category: str = Field(
description="タスク分類。例: 開発、テスト、確認、資料作成、運用、調査"
)
追加案3:重要度
ActionItemに priority を追加します。
priority: str = Field(
description="タスクの重要度。high, medium, low のいずれか。"
)
ただし最初から項目を増やしすぎると難しくなります。
まずは今回の基本形で十分です。
14. よくあるエラー
FileNotFoundError
ファイルが見つかりません: data/sample_summarize.txt
対処:
mkdir -p data
touch data/sample_summarize.txt
その後、サンプル議事録を貼り付けます。
ModuleNotFoundError: No module named 'openai'
仮想環境を有効化してからインストールします。
source .venv/bin/activate
pip install openai python-dotenv pydantic
モデル名エラー
gpt-5.5 が使えない場合は、.env にモデル名を追加してください。
OPENAI_MODEL=あなたの環境で使えるモデル名
またはコードのここを変更します。
MODEL = os.getenv("OPENAI_MODEL", "gpt-5.5")
検証時は、あなたのAPIダッシュボードで利用可能なモデル名を使ってください。
15. Day 4〜5の完了条件
以下ができれば、このステップは合格です。
□ 02_summarize_meeting.py が動く
□ 標準入力から複数行の議事録を入力できる
□ 空行を含んでも問題なく処理できる
□ END だけの行で入力終了できる
□ data/sample_summarize.txt から読み込める
□ ファイル全体を1つの議事録として処理できる
□ 会議タイトルが出る
□ 要約が出る
□ 決定事項が出る
□ アクションアイテムが出る
□ 担当者と期限が出る
□ リスクが出る
□ 未決事項が出る
□ outputs/results_summarize_meeting.jsonl に保存される
□ JSONとして後続処理できる
16. この課題で身につくこと
この議事録要約ツールで学ぶべき本質は、単なる「要約」ではありません。
SEとして重要なのは、これです。
自然文の業務メモ
↓
構造化された業務データ
↓
課題管理・報告・チケット・Teams投稿に利用
生成AIを実務で使うときの価値は、きれいな文章を作ることだけではありません。
曖昧な会議メモから、決定事項・TODO・リスク・未決事項を分離し、業務で再利用できる形にすることです。
この 02_summarize_meeting.py ができれば、次はかなり自然に進めます。
次の発展先は、
議事録要約
↓
TODOをCSV出力
↓
課題管理表に転記
↓
Teams投稿文を生成
↓
チケット起票
です。
ここまで行くと、かなり実務で使える生成AIツールになります。