[Claude]契約リスク自動分析&要約ツール

"""
契約リスク自動分析&要約ツール - Webアプリケーション
Gradioを使用したユーザーインターフェース
"""

import gradio as gr
from contract_analyzer import ContractAnalyzer
import os
import json


def analyze_contract_pdf(pdf_file):
    """
    契約書PDFを分析する関数
    Args:
        pdf_file: アップロードされたPDFファイル
    Returns:
        分析結果のテキスト
    """
    if pdf_file is None:
        return "PDFファイルをアップロードしてください。"

    try:
        # 分析器を初期化(初回のみ時間がかかる)
        analyzer = ContractAnalyzer()

        # PDFファイルのパスを取得
        pdf_path = pdf_file.name

        # 契約書を分析
        result = analyzer.analyze_contract(pdf_path)

        # 平易な要約を生成
        summary = analyzer.generate_plain_summary(result)

        # 詳細情報をJSON形式で追加
        detailed_json = json.dumps(result, ensure_ascii=False, indent=2)

        # 結果を組み合わせて返す
        output = f"{summary}\n\n" + "="*50 + "\n"
        output += "\n【詳細情報(JSON形式)】\n"
        output += detailed_json

        return output

    except Exception as e:
        return f"エラーが発生しました: {str(e)}\n\n詳細: {type(e).__name__}"


def create_demo():
    """Gradioデモを作成"""

    with gr.Blocks(title="契約リスク自動分析ツール") as demo:
        gr.Markdown(
            """
            # 契約リスク自動分析&要約ツール

            ## 機能
            - **PDF契約書のアップロード**: 契約書PDFを簡単にアップロード
            - **AI自動分析**: Hugging Face LLMを使用して自動分析
            - **リスク抽出**: リスク条項を自動検出
            - **義務抽出**: 義務事項を明確化
            - **期限管理**: 重要な期限・納期を抽出
            - **平易な要約**: 専門用語を避けた分かりやすい説明

            ## 使い方
            1. 下のボックスに契約書PDF(日本語または英語)をアップロード
            2. 「分析開始」ボタンをクリック
            3. 数秒~数十秒で分析結果が表示されます

            **⚠️ 注意**: 初回実行時は、AIモデルのダウンロードに数分かかる場合があります。
            """
        )

        with gr.Row():
            with gr.Column():
                pdf_input = gr.File(
                    label="契約書PDF",
                    file_types=[".pdf"],
                    type="filepath"
                )
                analyze_btn = gr.Button("分析開始", variant="primary", size="lg")

            with gr.Column():
                output_text = gr.Textbox(
                    label="分析結果",
                    lines=30,
                    max_lines=50,
                    show_copy_button=True
                )

        # ボタンクリック時の処理
        analyze_btn.click(
            fn=analyze_contract_pdf,
            inputs=[pdf_input],
            outputs=[output_text]
        )

        gr.Markdown(
            """
            ---
            ### システム情報
            - **使用モデル**:
              - 要約: facebook/bart-large-cnn
              - テキスト生成: google/flan-t5-base
            - **処理時間**: 通常5〜30秒(モデル読み込み済みの場合)
            - **対応言語**: 日本語、英語

            ### ヒント
            - PDFのテキストが抽出可能である必要があります(画像のみのPDFは非対応)
            - より正確な分析のため、契約書は明瞭な文章で記載されている必要があります
            - 大きなファイルの場合、処理に時間がかかることがあります
            """
        )

    return demo


if __name__ == "__main__":
    print("契約リスク自動分析ツールを起動中...")
    print("ブラウザが自動的に開きます...")

    demo = create_demo()

    # アプリケーション起動
    demo.launch(
        share=False,  # 公開リンクを生成しない(ローカルのみ)
        server_name="0.0.0.0",  # すべてのネットワークインターフェースでリッスン
        server_port=7860,
        show_error=True
    )
"""
契約リスク自動分析ツール - コア機能
PDFから契約書を読み込み、Hugging Face LLMを使用してリスク分析を実行
"""

import PyPDF2
from typing import Dict, List
from transformers import pipeline
import re


class ContractAnalyzer:
    """契約書分析クラス"""

    def __init__(self, model_name: str = "facebook/bart-large-cnn"):
        """
        初期化
        Args:
            model_name: 使用するHugging Faceモデル名
        """
        print(f"モデルを読み込み中: {model_name}")
        # 要約用のパイプラインを初期化
        self.summarizer = pipeline("summarization", model=model_name)

        # テキスト生成用のパイプライン(リスク分析用)
        # より軽量なモデルを使用
        try:
            self.text_generator = pipeline(
                "text2text-generation",
                model="google/flan-t5-base"
            )
        except:
            # フォールバック: 要約モデルを使用
            self.text_generator = self.summarizer

    def extract_text_from_pdf(self, pdf_path: str) -> str:
        """
        PDFファイルからテキストを抽出
        Args:
            pdf_path: PDFファイルのパス
        Returns:
            抽出されたテキスト
        """
        try:
            with open(pdf_path, 'rb') as file:
                pdf_reader = PyPDF2.PdfReader(file)
                text = ""
                for page in pdf_reader.pages:
                    text += page.extract_text() + "\n"
                return text
        except Exception as e:
            raise Exception(f"PDF読み込みエラー: {str(e)}")

    def extract_risk_clauses(self, text: str) -> List[str]:
        """
        リスク条項を抽出
        Args:
            text: 契約書テキスト
        Returns:
            リスク条項のリスト
        """
        risk_keywords = [
            "損害賠償", "違約金", "解除", "終了", "責任", "義務",
            "禁止", "制限", "罰則", "ペナルティ", "補償",
            "liability", "penalty", "termination", "obligation",
            "prohibited", "restricted", "damages", "breach"
        ]

        # 文章を分割
        sentences = re.split(r'[。\.\n]', text)
        risk_clauses = []

        for sentence in sentences:
            sentence = sentence.strip()
            if not sentence:
                continue
            # リスクキーワードを含む文章を抽出
            if any(keyword in sentence.lower() for keyword in risk_keywords):
                risk_clauses.append(sentence)

        return risk_clauses

    def extract_obligations(self, text: str) -> List[str]:
        """
        義務条項を抽出
        Args:
            text: 契約書テキスト
        Returns:
            義務条項のリスト
        """
        obligation_patterns = [
            r"(甲|乙|当事者|受注者|発注者|契約者)(?:は|が).*(?:しなければならない|する義務|する責任)",
            r"shall\s+.*",
            r"must\s+.*",
            r"obligated to\s+.*"
        ]

        sentences = re.split(r'[。\.\n]', text)
        obligations = []

        for sentence in sentences:
            sentence = sentence.strip()
            if not sentence:
                continue
            for pattern in obligation_patterns:
                if re.search(pattern, sentence, re.IGNORECASE):
                    obligations.append(sentence)
                    break

        return obligations

    def extract_deadlines(self, text: str) -> List[str]:
        """
        期限・日付を抽出
        Args:
            text: 契約書テキスト
        Returns:
            期限に関する条項のリスト
        """
        deadline_patterns = [
            r"\d{4}年\d{1,2}月\d{1,2}日",
            r"\d{1,2}日以内",
            r"\d+(?:日|週間|ヶ月|年)(?:以内|前|後|まで)",
            r"期限|締切|納期|有効期限",
            r"\d{4}-\d{2}-\d{2}",
            r"within\s+\d+\s+(?:days|weeks|months|years)",
            r"deadline|due date|expiration"
        ]

        sentences = re.split(r'[。\.\n]', text)
        deadlines = []

        for sentence in sentences:
            sentence = sentence.strip()
            if not sentence:
                continue
            for pattern in deadline_patterns:
                if re.search(pattern, sentence, re.IGNORECASE):
                    deadlines.append(sentence)
                    break

        return deadlines

    def summarize_text(self, text: str, max_length: int = 150) -> str:
        """
        テキストを要約
        Args:
            text: 要約するテキスト
            max_length: 要約の最大長
        Returns:
            要約されたテキスト
        """
        if not text or len(text.strip()) == 0:
            return "(テキストが空です)"

        # テキストが短すぎる場合はそのまま返す
        if len(text) < 100:
            return text

        try:
            # 長すぎるテキストは分割
            chunk_size = 1024
            if len(text) > chunk_size:
                text = text[:chunk_size]

            summary = self.summarizer(
                text,
                max_length=max_length,
                min_length=30,
                do_sample=False
            )
            return summary[0]['summary_text']
        except Exception as e:
            # エラー時は先頭部分を返す
            return f"要約エラー: {str(e)}\n\n元のテキスト(抜粋):\n{text[:300]}..."

    def analyze_contract(self, pdf_path: str) -> Dict:
        """
        契約書を総合的に分析
        Args:
            pdf_path: PDFファイルのパス
        Returns:
            分析結果の辞書
        """
        print("PDF読み込み中...")
        text = self.extract_text_from_pdf(pdf_path)

        print("リスク条項を抽出中...")
        risk_clauses = self.extract_risk_clauses(text)

        print("義務条項を抽出中...")
        obligations = self.extract_obligations(text)

        print("期限情報を抽出中...")
        deadlines = self.extract_deadlines(text)

        print("要約を生成中...")
        # 全体の要約
        overall_summary = self.summarize_text(text[:2000], max_length=200)

        # リスク条項の要約(上位5件)
        risk_summary = []
        for clause in risk_clauses[:5]:
            try:
                summary = self.summarize_text(clause, max_length=100)
                risk_summary.append(summary)
            except:
                risk_summary.append(clause[:150] + "...")

        result = {
            "全体要約": overall_summary,
            "リスク条項": {
                "件数": len(risk_clauses),
                "主要なリスク": risk_clauses[:10],  # 上位10件
                "要約": risk_summary
            },
            "義務条項": {
                "件数": len(obligations),
                "主要な義務": obligations[:10]
            },
            "期限・納期": {
                "件数": len(deadlines),
                "主要な期限": deadlines[:10]
            },
            "リスクレベル": self._calculate_risk_level(
                len(risk_clauses),
                len(obligations),
                len(deadlines)
            )
        }

        return result

    def _calculate_risk_level(
        self,
        risk_count: int,
        obligation_count: int,
        deadline_count: int
    ) -> str:
        """
        リスクレベルを計算
        Args:
            risk_count: リスク条項数
            obligation_count: 義務条項数
            deadline_count: 期限条項数
        Returns:
            リスクレベル(高/中/低)
        """
        total_score = risk_count * 2 + obligation_count + deadline_count * 1.5

        if total_score > 50:
            return "高 - 詳細な法的レビューを推奨"
        elif total_score > 20:
            return "中 - 重要条項の確認を推奨"
        else:
            return "低 - 標準的な契約書"

    def generate_plain_summary(self, analysis_result: Dict) -> str:
        """
        平易な言葉で分析結果を要約
        Args:
            analysis_result: analyze_contractの結果
        Returns:
            平易な要約文
        """
        summary_lines = []
        summary_lines.append("=== 契約書分析結果 ===\n")

        summary_lines.append(f"【総合リスクレベル】{analysis_result['リスクレベル']}\n")

        summary_lines.append(f"\n【全体要約】\n{analysis_result['全体要約']}\n")

        summary_lines.append(f"\n【リスク条項】({analysis_result['リスク条項']['件数']}件検出)")
        if analysis_result['リスク条項']['主要なリスク']:
            summary_lines.append("主な注意点:")
            for i, risk in enumerate(analysis_result['リスク条項']['主要なリスク'][:5], 1):
                summary_lines.append(f"  {i}. {risk}")

        summary_lines.append(f"\n【義務事項】({analysis_result['義務条項']['件数']}件検出)")
        if analysis_result['義務条項']['主要な義務']:
            summary_lines.append("主な義務:")
            for i, obligation in enumerate(analysis_result['義務条項']['主要な義務'][:5], 1):
                summary_lines.append(f"  {i}. {obligation}")

        summary_lines.append(f"\n【期限・納期】({analysis_result['期限・納期']['件数']}件検出)")
        if analysis_result['期限・納期']['主要な期限']:
            summary_lines.append("重要な期限:")
            for i, deadline in enumerate(analysis_result['期限・納期']['主要な期限'][:5], 1):
                summary_lines.append(f"  {i}. {deadline}")

        return "\n".join(summary_lines)


if __name__ == "__main__":
    # テスト用
    analyzer = ContractAnalyzer()
    print("契約リスク分析ツールが初期化されました")
# 契約リスク自動分析&要約ツール - 必要パッケージ

# PDF処理
PyPDF2>=3.0.0

# Hugging Face Transformers(LLM)
transformers>=4.35.0
torch>=2.0.0
sentencepiece>=0.1.99

# Webインターフェース
gradio>=4.0.0

# データ処理
numpy>=1.24.0