app:
  description: 総務向け：文書保管期限の到来アラート、廃棄推奨リスト、延長判断が必要な文書の抽出とコスト/コンプライアンスのバランス提案を行うワークフロー
  icon: 📁
  icon_background: '#FFEAD5'
  mode: workflow
  name: 文書保管期限アラート・分類ワークフロー
  use_icon_as_answer_icon: false
kind: app
version: 0.6.0
workflow:
  conversation_variables: []
  environment_variables: []
  features:
    file_upload:
      image:
        enabled: false
    opening_statement: ''
    suggested_questions: []
    suggested_questions_after_answer:
      enabled: false
    text_to_speech:
      enabled: false
  graph:
    edges:
    - data:
        sourceType: start
        targetType: llm
      id: start-to-llm1
      source: start
      target: llm1
    - data:
        sourceType: llm
        targetType: code
      id: llm1-to-code1
      source: llm1
      target: code1
    - data:
        sourceType: code
        targetType: llm
      id: code1-to-llm2
      source: code1
      target: llm2
    - data:
        sourceType: llm
        targetType: end
      id: llm2-to-end
      source: llm2
      target: end
    nodes:
    - data:
        title: 開始
        type: start
        variables:
        - label: query
          max_length: 2048
          options: []
          required: true
          type: paragraph
          variable: query
      height: 90
      id: start
      position:
        x: 80
        y: 282
      type: custom
      width: 244
    - data:
        title: '入力解析: 文書リストと保管ルール抽出'
        type: llm
        model:
          provider: openai
          name: gpt-5-mini
          mode: chat
          completion_params:
            temperature: 0.2
        prompt_template:
        - role: system
          text: 'あなたは総務向けのデータ抽出アシスタントです。ユーザーが1つのテキスト入力（文書リストと保存年限ルール）を与えます。入力から「documents」と「retention_rules」を含む厳格なJSONを出力してください。出力フォーマット（厳守）:
            {"documents": [{"id": "一意の識別子(任意)", "title": "文書名", "type": "契約書/経理/議事録/個人情報関連/その他",
            "issue_year": 2020, "retention_years": 7, "notes": "任意の備考"}], "retention_rules":
            [{"type": "契約書", "default_retention_years": 7}, {"type": "個人情報", "default_retention_years":
            5}], "assumptions": "入力から判断した前提"}'
        - role: user
          text: '{{#start.query#}}'
        vision:
          enabled: false
        memory: null
        context:
          enabled: false
      height: 90
      id: llm1
      position:
        x: 380
        y: 282
      type: custom
      width: 244
    - data:
        title: 判定ロジック実行
        type: code
        code_language: python3
        code: "def _safe_str(v):\n    \"\"\"Convert any value to string safely.\"\"\
          \"\n    if v is None:\n        return \"\"\n    if isinstance(v, list):\n\
          \        return \"\\n\".join(str(i) for i in v)\n    return str(v)\n\ndef\
          \ _safe_list(v):\n    \"\"\"Ensure value is a list.\"\"\"\n    if v is None:\n\
          \        return []\n    if isinstance(v, str):\n        return [v]\n   \
          \ if isinstance(v, list):\n        return v\n    return [v]\n\ndef main(**kwargs)\
          \ -> dict:\n    inputs = kwargs\n    import json\n    from datetime import\
          \ date\n    text = inputs.get(\"text\", \"\")\n    current_year = date.today().year\n\
          \    # 初期化\n    documents = []\n    retention_rules = []\n    assumptions\
          \ = \"\"\n    # Try parse JSON first\n    parsed = None\n    try:\n    \
          \    parsed = json.loads(text)\n    except Exception:\n        parsed =\
          \ None\n    if parsed and isinstance(parsed, dict):\n        documents =\
          \ parsed.get(\"documents\", []) or []\n        retention_rules = parsed.get(\"\
          retention_rules\", []) or []\n        assumptions = parsed.get(\"assumptions\"\
          , \"\")\n    else:\n        # フォールバック: テキストを行単位で解析（簡易）\n        lines =\
          \ [l.strip() for l in text.splitlines() if l.strip()]\n        for i, l\
          \ in enumerate(lines):\n            # 期待形式: タイトル | 種類 | 発行年 | 保管年限\n   \
          \         parts = [p.strip() for p in l.split(\"|\")]\n            if len(parts)\
          \ >= 4:\n                try:\n                    issue_year = int(parts[2])\n\
          \                except Exception:\n                    issue_year = None\n\
          \                try:\n                    retention = int(parts[3])\n \
          \               except Exception:\n                    retention = None\n\
          \                documents.append(dict(id=\"doc{}\".format(i+1), title=parts[0],\
          \ type=parts[1], issue_year=issue_year, retention_years=retention, notes=\"\
          parsed from line\"))\n            else:\n                # 最低限タイトルだけでも登録\n\
          \                documents.append(dict(id=\"doc{}\".format(i+1), title=l,\
          \ type=\"unknown\", issue_year=None, retention_years=None, notes=\"parsed\
          \ fallback\"))\n        assumptions = \"Parsed as lines when JSON parse\
          \ failed.\"\n    # Build quick lookup for default retention by type\n  \
          \  default_retention = {}\n    for r in retention_rules:\n        try:\n\
          \            t = r.get(\"type\") if isinstance(r, dict) else None\n    \
          \        y = r.get(\"default_retention_years\") if isinstance(r, dict) else\
          \ None\n            if t and isinstance(y, (int, float)):\n            \
          \    default_retention[t] = int(y)\n        except Exception:\n        \
          \    continue\n    # 判定結果リスト\n    alerts = []\n    recommend_dispose = []\n\
          \    needs_extension = []\n    details = []\n    for doc in documents:\n\
          \        title = doc.get(\"title\") if isinstance(doc, dict) else None\n\
          \        dtype = doc.get(\"type\") if isinstance(doc, dict) else None\n\
          \        issue_year = doc.get(\"issue_year\") if isinstance(doc, dict) else\
          \ None\n        retention_years = doc.get(\"retention_years\") if isinstance(doc,\
          \ dict) else None\n        notes = doc.get(\"notes\") if isinstance(doc,\
          \ dict) else \"\"\n        # 補完: typeに対するデフォルト年限が設定されていれば使用\n        if\
          \ (retention_years is None or retention_years == \"\") and dtype in default_retention:\n\
          \            retention_years = default_retention.get(dtype)\n        # 判定用計算\n\
          \        if isinstance(issue_year, (int, float)) and isinstance(retention_years,\
          \ (int, float)):\n            end_year = int(issue_year) + int(retention_years)\n\
          \            delta = end_year - current_year\n            entry = dict(title=title,\
          \ type=dtype, issue_year=issue_year, retention_years=int(retention_years),\
          \ retention_end_year=end_year, notes=notes)\n            details.append(entry)\n\
          \            if end_year < current_year:\n                recommend_dispose.append(entry)\n\
          \            elif delta <= 1:\n                alerts.append(entry)\n  \
          \          # 延長判断の簡易基準: 個人情報関連は慎重に延長審査を推奨、または保管終了が遠すぎるケースも検討対象\n       \
          \     if isinstance(dtype, str) and (\"個人\" in dtype or \"個人情報\" in dtype\
          \ or \"personal\" in dtype.lower()):\n                needs_extension.append(dict(**entry,\
          \ reason=\"個人情報に該当するため延長/再審査を推奨\"))\n            elif int(retention_years)\
          \ >= 20:\n                needs_extension.append(dict(**entry, reason=\"\
          非常に長い保管年限。法務/コストの再評価を推奨\"))\n        else:\n            # 情報不足: 要確認\n  \
          \          entry = dict(title=title, type=dtype, issue_year=issue_year if\
          \ issue_year is not None else None, retention_years=retention_years if retention_years\
          \ is not None else None, notes=notes)\n            details.append(entry)\n\
          \            needs_extension.append(dict(**entry, reason=\"発行年または保管年限が不明なため確認必要\"\
          ))\n    # 簡易コスト推定（例: 年間1文書あたり10単位）\n    try:\n        total_retention_cost\
          \ = 0\n        for d in details:\n            ry = d.get(\"retention_years\"\
          )\n            if isinstance(ry, int):\n                total_retention_cost\
          \ += ry * 10\n    except Exception:\n        total_retention_cost = None\n\
          \    result = dict(\n        current_year=current_year,\n        counts=dict(total=len(details),\
          \ alerts=len(alerts), recommend_dispose=len(recommend_dispose), needs_extension=len(needs_extension)),\n\
          \        alerts=alerts,\n        recommend_dispose=recommend_dispose,\n\
          \        needs_extension=needs_extension,\n        details=details,\n  \
          \      total_retention_cost=total_retention_cost,\n        assumptions=assumptions\n\
          \    )\n    return dict(output=json.dumps(result, ensure_ascii=False))\n"
        variables:
        - value_selector:
          - llm1
          - text
          variable: text
        outputs:
          output:
            type: string
            children: null
      height: 90
      id: code1
      position:
        x: 680
        y: 282
      type: custom
      width: 244
    - data:
        title: 結果整形と提言生成
        type: llm
        model:
          provider: openai
          name: gpt-5-mini
          mode: chat
          completion_params:
            temperature: 0.3
        prompt_template:
        - role: system
          text: 'あなたは総務の文書管理専門家です。以下のJSON出力を読み取り、実務向けの『保管期限到来アラート』『廃棄推奨リスト』『延長判断が必要な文書』『コストと法令遵守の観点からの提言』を日本語で分かりやすく箇条書きと表形式でまとめてください。出力は人間がそのまま運用に使える形式で、重要な件数・具体的な文書名・推奨アクション（即時廃棄/保管延長レビュー/追加確認項目）とその理由を含めてください。


            ---


            【最終出力フォーマット (この指示が最優先)】

            最終出力は **読みやすい日本語の Markdown** で返してください。JSON

            配列・オブジェクトをそのまま出力してはいけません。

            - 見出し (## / ###) でセクションを区切る

            - リスト (- / 1.) や表 (| ... |) で構造化

            - 各項目の意図や根拠が分かるよう短い説明文を添える

            - 営業デモで顧客に直接見せられる体裁にする


            (構造化されたデータ項目を内部で扱うのは構いませんが、出力時に Markdown

            に変換してください。)'
        - role: user
          text: '{{#code1.output#}}


            元入力（参照）:

            {{#start.query#}}'
        vision:
          enabled: false
        memory: null
        context:
          enabled: false
      height: 90
      id: llm2
      position:
        x: 980
        y: 282
      type: custom
      width: 244
    - data:
        title: 終了
        type: end
        outputs:
        - value_selector:
          - llm2
          - text
          variable: result
      height: 90
      id: end
      position:
        x: 1280
        y: 282
      type: custom
      width: 244
  hash: null
