app:
  description: 各事業部から提出された予算明細を統合し、部門間の整合性チェック（共通コスト配賦、重複計上、合計整合）および異常値検出（前年比±20%超、予算総額乖離）を実行するワークフロー
  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: code
      id: start-to-parse1
      source: start
      target: parse1
    - data:
        sourceType: code
        targetType: code
      id: parse1-to-check1
      source: parse1
      target: check1
    - data:
        sourceType: code
        targetType: llm
      id: check1-to-llm1
      source: check1
      target: llm1
    - data:
        sourceType: llm
        targetType: end
      id: llm1-to-end
      source: llm1
      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: 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    text = inputs.get(\"input_text\", \"\
          \")\n    lines = text.splitlines()\n    departments = []\n    current =\
          \ None\n    for ln in lines:\n        s = ln.strip()\n        if not s:\n\
          \            continue\n        # 部門行の検出 (例: \"部門: 営業\" や \"Dept: Sales\"\
          )\n        if s.startswith(\"部門:\") or s.startswith(\"部門\") or s.lower().startswith(\"\
          dept\"):\n            parts = s.split(\":\", 1)\n            if len(parts)\
          \ > 1:\n                name = parts[1].strip()\n            else:\n   \
          \             name = s\n            current = dict(name=name, items=[])\n\
          \            departments.append(current)\n            continue\n       \
          \ # 項目行のパース (例: \"人件費: 100000\" または \"人件費 100000\")\n        if \":\" in\
          \ s:\n            parts = s.split(\":\", 1)\n            k = parts[0].strip()\n\
          \            v = parts[1].strip()\n        else:\n            parts = s.split()\n\
          \            if len(parts) >= 2:\n                k = \" \".join(parts[:-1])\n\
          \                v = parts[-1]\n            else:\n                k = s\n\
          \                v = \"0\"\n        # 数値のクリーンアップ\n        vn = \"\".join(ch\
          \ for ch in v if (ch.isdigit() or ch in \".,-\"))\n        vn = vn.replace(\"\
          ,\", \"\")\n        try:\n            amount = float(vn)\n        except:\n\
          \            amount = 0.0\n        item = dict(name=k, amount=amount)\n\
          \        if current is None:\n            current = dict(name=\"Unknown\"\
          , items=[])\n            departments.append(current)\n        current[\"\
          items\"].append(item)\n    parsed = dict(departments=departments)\n    return\
          \ dict(parsed=parsed)\n"
        variables:
        - value_selector:
          - start
          - query
          variable: input_text
        outputs:
          parsed:
            type: object
            children: null
      height: 90
      id: parse1
      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    parsed = inputs.get(\"parsed\", dict())\n\
          \    departments = parsed.get(\"departments\", [])\n    dept_summaries =\
          \ []\n    total = 0.0\n    item_map = dict()\n    # 部門ごとの集計とアイテム出現マップ作成\n\
          \    for dep in departments:\n        name = dep.get(\"name\", \"\")\n \
          \       items = dep.get(\"items\", [])\n        subtotal = 0.0\n       \
          \ for it in items:\n            try:\n                amt = float(it.get(\"\
          amount\", 0) or 0)\n            except:\n                amt = 0.0\n   \
          \         subtotal = subtotal + amt\n            key = str(it.get(\"name\"\
          , \"\")).lower()\n            occ = item_map.get(key)\n            if occ\
          \ is None:\n                item_map[key] = [ dict(department=name, amount=amt)\
          \ ]\n            else:\n                occ.append(dict(department=name,\
          \ amount=amt))\n        dept_summaries.append(dict(name=name, total=subtotal))\n\
          \        total = total + subtotal\n    anomalies = []\n    # 重複計上候補: 同一アイテム名が複数部門に出現する場合\n\
          \    duplicates = []\n    for k, v in item_map.items():\n        if len(v)\
          \ > 1:\n            duplicates.append(dict(item=k, occurrences=v))\n   \
          \ if len(duplicates) > 0:\n        anomalies.append(dict(type=\"duplicate_items\"\
          , details=duplicates))\n    # 前年比異常: 部門に prev_total が与えられている場合に判定\n    yoy_flags\
          \ = []\n    for dep in departments:\n        prev = dep.get(\"prev_total\"\
          )\n        if prev is not None:\n            curr = 0.0\n            for\
          \ it in dep.get(\"items\", []):\n                try:\n                \
          \    curr = curr + float(it.get(\"amount\", 0) or 0)\n                except:\n\
          \                    curr = curr + 0.0\n            try:\n             \
          \   change = (curr - float(prev)) / float(prev)\n            except:\n \
          \               change = 0.0\n            if abs(change) > 0.2:\n      \
          \          yoy_flags.append(dict(department=dep.get(\"name\"), prev=float(prev),\
          \ current=curr, change=change))\n    if len(yoy_flags) > 0:\n        anomalies.append(dict(type=\"\
          yoy\", details=yoy_flags))\n    # 合計整合チェック: 入力側で報告総額が提示されている場合にチェック (parsedに\
          \ total_reported がある想定)\n    total_reported = parsed.get(\"total_reported\"\
          )\n    total_mismatch = None\n    if total_reported is not None:\n     \
          \   try:\n            tr = float(total_reported)\n        except:\n    \
          \        tr = None\n        if tr is not None:\n            if abs(total\
          \ - tr) > max(1.0, 0.01 * tr):\n                total_mismatch = dict(reported=tr,\
          \ computed=total, diff=total - tr)\n                anomalies.append(dict(type=\"\
          total_mismatch\", details=total_mismatch))\n    report = dict(departments=dept_summaries,\
          \ total=total, anomalies=anomalies)\n    return dict(report=report)\n"
        variables:
        - value_selector:
          - parse1
          - parsed
          variable: parsed
        outputs:
          report:
            type: object
            children: null
      height: 90
      id: check1
      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.6
        prompt_template:
        - role: system
          text: 'あなたは経営企画チーム向けのアシスタントです。与えられたチェック結果を元に、要約、重要な異常点、考えられる原因、推奨アクション、および次のアクションに必要なデータや確認事項を日本語で簡潔に提示してください。出力は見出し別に整理してください。


            ---


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

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

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

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

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

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

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


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

            に変換してください。)'
        - role: user
          text: '元の提出データ（生データ）:

            {{#start.query#}}


            自動チェック結果（構造化JSON）:

            {{#check1.report#}}


            上記を参考に、管理層向けに「要約」「主要な異常」「原因の可能性」「推奨アクション」「次にとるべき具体的アクション」を出力してください。'
        vision:
          enabled: false
        memory: null
        context:
          enabled: false
      height: 90
      id: llm1
      position:
        x: 980
        y: 282
      type: custom
      width: 244
    - data:
        title: 終了
        type: end
        outputs:
        - value_selector:
          - llm1
          - text
          variable: result
      height: 90
      id: end
      position:
        x: 1280
        y: 282
      type: custom
      width: 244
  hash: null
