日報をGeminiで月報にまとめてみた #ai #llm #gemini

はじめに

筆者は自主的に作業日報をMarkdown形式でテキストファイルに記載しています。ファイルは月ごとに分割しており、翌月初に『月報』にまとめています。当初は手作業でまとめていたので、かなり時間がかかっていました。

その後、ChatGPTの登場により、日報ファイルをアップロードし「これをまとめて」と指示することで、大幅な時間短縮ができました(※なお、日報ファイルに機密情報は含めていません)。

しかし、日報ファイルをアップロード、指示、結果をコピー、月報ファイルにペースト、という手順が煩雑ですし、ChatGPTのウェブUIの出力結果をコピー&ペーストしようとすると見出しや箇条書きが崩れてしまうことがあります。

そこで、日報ファイルを与えるだけで、Google Geminiでまとめて月報ファイルに出力するPython スクリプトを作成しました。なお、本稿ではAPIキーの取得などの事前準備については割愛します。

日報の例

筆者は次のような作業日報をつけています(※一部改変しています)。

## 1/29

10:00-12:00 13:00-19:00 業務効率化

* genmaicha プロジェクト
    * MCPサーバがタイムアウトする。
        * タイムアウトをデフォルトの45秒から60秒に延長。
        * openssl のセキュリティアップデート対応。
        * 本番にデプロイ。
    * MCPサーバ連携
        * ステージングはterraformを設定して変更済み。
    * イシューボード棚卸し。

12:00-13:00 昼休み

月報ファイルにする際、これから時間を抜いたり、分野ごとにまとめる手作業に手間がかかっていました。

月報まとめスクリプト

Claude Codeで作成させた、シンプルな月報まとめスクリプトです。

#!/usr/bin/env python3
import argparse
import os
from pathlib import Path

from dotenv import load_dotenv
from google import genai

def load_prompt(content: str) -> str:
    prompt_path = Path(__file__).parent.parent / "prompts" / "summary.txt"
    template = prompt_path.read_text()
    return template.format(content=content)

MIN_LINES = 5
MAX_RETRIES = 3

def summarize(file_path: Path) -> str:
    content = file_path.read_text()
    prompt = load_prompt(content)

    client = genai.Client(api_key=os.environ["GEMINI_API_KEY"])
    model_name = os.environ.get("GEMINI_MODEL", "gemini-3-flash-preview")
    response = client.models.generate_content(model=model_name, contents=prompt)
    return response.text or ""

def summarize_with_retry(file_path: Path) -> str:
    """まとめを生成し、内容が少なすぎる場合はリトライする"""
    for attempt in range(MAX_RETRIES):
        result = summarize(file_path)
        line_count = len(result.strip().split("\n"))

        if line_count >= MIN_LINES:
            return result

        print(f"生成結果が短すぎます({line_count}行)。リトライ中... ({attempt + 1}/{MAX_RETRIES})")

    print(f"警告: {MAX_RETRIES}回リトライしましたが、十分な内容が生成されませんでした")
    return result

def main():
    load_dotenv()

    parser = argparse.ArgumentParser(description="日報まとめ生成ツール")
    parser.add_argument("file", type=Path, help="日報ファイルのパス")
    args = parser.parse_args()

    result = summarize_with_retry(args.file)

    output_dir = Path(__file__).parent.parent / "monthly"
    output_dir.mkdir(exist_ok=True)
    output_path = output_dir / args.file.name
    output_path.write_text(result)
    print(f"保存しました: {output_path}")

if __name__ == "__main__":  # pragma: no cover
    main()

プロンプトは次の通りです。

以下は月間の業務日報です。作業カテゴリごとに内容をまとめてください。

# 出力形式
- Markdown形式で出力する
- カテゴリ名ごとにH2見出しで区切る
- 各カテゴリ内の作業内容を箇条書きでまとめる
- 重複する内容は統合する
- 時間情報は含めない
- 休憩や外出(昼休み、通院など)は含めない

# 日報データ
{content}

月報の例

まとめスクリプトで生成した月報です(※一部改変しています)。

## 業務効率化(genmaicha プロジェクト)

### アーキテクチャ・インフラ構築
- Dockerイメージのマルチステージビルド導入によるサイズ半減
- OpenTofu(Terraform)によるインフラ構成管理およびディレクトリ構成のリファクタリング

### モデル移行・AI機能実装
- 利用モデルをAzure OpenAIからGeminiへ完全に移行
- Gemini対応に伴う型エラー(mypy)の修正および動作確認

### セキュリティ・保守・運用
- ライブラリのセキュリティアップデート対応(openssl)
- MCPサーバにおけるタイムアウト時間の延長対応
- 不要な環境変数、ライブラリの整理
- MCP Inspectorを用いたMCPサーバの動作確認
- イシューボードの棚卸し

## 会議・ワークショップ・イベント
- チーム定例MTGへの参加
- ワークショップの運営サポート

先の 1/29 以前の分も与えており、カテゴリごとにきちんと分類されていることがわかります。

さらなる改善

指定日の日報を、指定のSlackチャンネルに投稿するスクリプトもClaude Codeに作らせました。

#!/usr/bin/env python3
import argparse
import os
import re
from datetime import date
from pathlib import Path

from dotenv import load_dotenv
from slack_sdk import WebClient
from slack_sdk.errors import SlackApiError

def extract_daily_section(content: str, target_date: date) -> str | None:
    """日報ファイルから指定日のセクションを抽出"""
    # ## 1/5 または ## 01/05 形式に対応
    month = target_date.month
    day = target_date.day
    pattern = rf"^## {month}/{day}\b"

    lines = content.split("\n")
    start_idx = None

    for i, line in enumerate(lines):
        if re.match(pattern, line):
            start_idx = i
            break

    if start_idx is None:
        return None

    # 次の ## が現れるまで、または末尾まで
    end_idx = len(lines)
    for i in range(start_idx + 1, len(lines)):
        if lines[i].startswith("## "):
            end_idx = i
            break

    section = "\n".join(lines[start_idx:end_idx]).rstrip()
    return section if section else None

def post_to_slack(content: str, channel: str, token: str) -> str:
    """Slackにメッセージを投稿し、タイムスタンプを返す"""
    client = WebClient(token=token)
    response = client.chat_postMessage(channel=channel, text=content)
    return response["ts"]

def post_daily_report(file_path: Path, target_date: date | None = None) -> str | None:
    """日報ファイルの指定日分をSlackに投稿"""
    token = os.environ.get("SLACK_USER_TOKEN")
    channel = os.environ.get("SLACK_CHANNEL")

    if not token or not channel:
        print("SLACK_USER_TOKEN または SLACK_CHANNEL が設定されていません")
        return None

    if target_date is None:
        target_date = date.today()

    content = file_path.read_text()
    section = extract_daily_section(content, target_date)

    if not section:
        print(f"{target_date} の日報が見つかりません")
        return None

    message = f"🤖 _この投稿は自動生成されました_\n\n{section}"

    try:
        ts = post_to_slack(message, channel, token)
        print(f"Slackに投稿しました: {target_date}")
        return ts
    except SlackApiError as e:
        print(f"Slack投稿エラー: {e.response['error']}")
        return None

def main():
    load_dotenv()

    parser = argparse.ArgumentParser(description="日報をSlackに投稿")
    parser.add_argument("file", type=Path, help="日報ファイルのパス")
    parser.add_argument(
        "--date",
        type=lambda s: date.fromisoformat(s),
        default=None,
        help="投稿する日付 (YYYY-MM-DD形式、デフォルト: 今日)",
    )
    args = parser.parse_args()

    post_daily_report(args.file, args.date)

if __name__ == "__main__":  # pragma: no cover
    main()

さらに、2つのスクリプトをそれぞれ実行するのも面倒なので、1つにまとめたシェルスクリプトもClaude Codeに作らせました。さらにGit コミット&プッシュもさせています。

#!/bin/bash
set -euo pipefail

cd "$(dirname "$0")/.."

for file in daily/*.md; do
  monthly_file="monthly/$(basename "$file")"
  if [[ ! -f "$monthly_file" ]] || [[ "$file" -nt "$monthly_file" ]]; then
    echo "Generating: $monthly_file"
    uv run python src/summarize.py "$file"

    # Slackに日報を投稿(環境変数未設定の場合はslack.py側でスキップ)
    uv run python src/slack.py "$file"
  fi
done

# daily/ の変更をコミット
git add daily/
if ! git diff --cached --quiet; then
  git commit -m "Update daily report"
fi

# monthly/ の変更をコミット
git add monthly/
if ! git diff --cached --quiet; then
  git commit -m "Generate monthly summary"
fi

# まとめてプッシュ
git push

これで業務終了時に日報を書いて、このスクリプトを実行したら、

  • その月の日報をまとめた月報をGeminiで生成。
  • その日の日報をSlackに投稿。
  • その日の日報と、生成した月報をGitコミット&プッシュ。

を行ってくれます。

まとめ

本稿では、筆者がMarkdown形式で自主的につけている作業日報をGoogle Geminiに月報にまとめ、Gitにプッシュ・Slackに投稿するシンプルなスクリプトをClaude Codeに書かせたものを紹介しました。これにより毎日の日報作成と毎月の月報作成にかかる時間が大幅に短縮されました。

シンプルな内容ですので、Google Gemini以外のサービスを利用するように書き換えるのも簡単だと思います。また、そもそも入力の日報がMarkdownの手書きファイルなので、例えばGoogleカレンダーの内容を取得して日報を生成するなど、さらなる応用・改善が考えられます。

生成AIおよびコーディング支援ツールのおかげで、日々の繰り返し作業の自動化・短縮がかなり手軽にできるようになりました。引き続き、何か任せることができないか考えていこうと思います。

Author

Chef・Docker・Mirantis製品などの技術要素に加えて、会議の進め方・文章の書き方などの業務改善にも取り組んでいます。「Chef活用ガイド」共著のほか、Debian Official Developerもやっています。

Daisuke Higuchiの記事一覧

新規CTA