app:
  description: 製造業向けのBOM差分検出ワークフロー。ユーザーが旧BOMと新BOMを1つのテキスト入力に含めて送信すると、構造化抽出→差分計算→影響項目のレポート化を行います。
  icon: 🧾
  icon_background: '#FFF4E6'
  mode: workflow
  name: BOM差分検出と影響レポート
  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: parameter-extractor
      id: start-to-extract1
      source: start
      target: extract1
    - data:
        sourceType: parameter-extractor
        targetType: code
      id: extract1-to-code1
      source: extract1
      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: BOM抽出（旧BOM/新BOM）
        type: parameter-extractor
        model:
          provider: openai
          name: gpt-5-mini
          mode: chat
        query:
        - start
        - query
        parameters:
        - name: old_bom
          type: string
          description: 旧BOM（元の部品リスト）。可能なら行ごとに「部品番号, 説明, 数量」の形式で記載してください。
          required: true
        - name: new_bom
          type: string
          description: 新BOM（変更後の部品リスト）。同じ形式で記載してください。
          required: true
        reasoning_mode: prompt
        instruction: '入力テキストから「旧BOM」と「新BOM」を抽出してください。ユーザーは1つのテキストフィールドに両方のBOMを含める想定です。可能な限り該当部分を切り出して、old_bom
          と new_bom にそれぞれ全文を入れて返してください。BOMは行ごとに「部品番号, 説明, 数量」の形式になっていることが多いので、そのまま抽出してください。不明な場合は空欄ではなく空文字列を返してください。

          '
      height: 90
      id: extract1
      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    # inputs: old_bom (str), new_bom (str)\n\
          \    old_text = inputs.get(\"old_bom\", \"\") or \"\"\n    new_text = inputs.get(\"\
          new_bom\", \"\") or \"\"\n    def try_parse_qty(q):\n        try:\n    \
          \        return float(q)\n        except Exception:\n            try:\n\
          \                return int(q)\n            except Exception:\n        \
          \        return 0\n    def parse_bom(text: str):\n        lines = [l.strip()\
          \ for l in text.splitlines() if l.strip()]\n        items = []\n       \
          \ for ln in lines:\n            parts = [p.strip() for p in ln.split(\"\
          ,\")]\n            pid = parts[0] if len(parts) > 0 else \"\"\n        \
          \    desc = parts[1] if len(parts) > 1 else \"\"\n            qty = 0\n\
          \            if len(parts) > 2:\n                qty = try_parse_qty(parts[2])\n\
          \            items.append(dict(part_id=pid, description=desc, quantity=qty))\n\
          \        out = dict()\n        for it in items:\n            key = it.get(\"\
          part_id\", \"\")\n            if key == \"\":\n                # skip lines\
          \ without part id\n                continue\n            out[key] = it\n\
          \        return out\n    old_map = parse_bom(old_text)\n    new_map = parse_bom(new_text)\n\
          \    added = []\n    removed = []\n    changed = []\n    # detect additions\
          \ and changes\n    for pid, new_item in new_map.items():\n        if pid\
          \ not in old_map:\n            added.append(new_item)\n        else:\n \
          \           old_item = old_map.get(pid, dict())\n            try:\n    \
          \            old_q = float(old_item.get(\"quantity\", 0))\n            except\
          \ Exception:\n                old_q = 0\n            try:\n            \
          \    new_q = float(new_item.get(\"quantity\", 0))\n            except Exception:\n\
          \                new_q = 0\n            desc_changed = (str(new_item.get(\"\
          description\", \"\")).strip() != str(old_item.get(\"description\", \"\"\
          )).strip())\n            qty_changed = (new_q != old_q)\n            if\
          \ desc_changed or qty_changed:\n                changed.append(dict(part_id=pid,\
          \ old_quantity=old_q, new_quantity=new_q, old_description=old_item.get(\"\
          description\", \"\"), new_description=new_item.get(\"description\", \"\"\
          )))\n    # detect removals\n    for pid, old_item in old_map.items():\n\
          \        if pid not in new_map:\n            removed.append(old_item)\n\
          \    # create a concise summary text\n    summary_lines = []\n    summary_lines.append(\"\
          追加: \" + str(len(added)) + \" 件\")\n    summary_lines.append(\"削除: \" +\
          \ str(len(removed)) + \" 件\")\n    summary_lines.append(\"変更: \" + str(len(changed))\
          \ + \" 件\")\n    # show up to 10 items per category\n    def preview(items):\n\
          \        out_lines = []\n        for it in items[:10]:\n            pid\
          \ = it.get(\"part_id\", \"\")\n            desc = it.get(\"description\"\
          , \"\")\n            qty = it.get(\"quantity\", it.get(\"new_quantity\"\
          , \"\"))\n            out_lines.append(str(pid) + \" | \" + str(desc) +\
          \ \" | \" + str(qty))\n        return out_lines\n    if added:\n       \
          \ summary_lines.append(\"追加一覧（最大10件）:\")\n        summary_lines.extend(preview(added))\n\
          \    if removed:\n        summary_lines.append(\"削除一覧（最大10件）:\")\n     \
          \   summary_lines.extend(preview(removed))\n    if changed:\n        summary_lines.append(\"\
          変更一覧（最大10件）:\")\n        for c in changed[:10]:\n            line = str(c.get(\"\
          part_id\", \"\")) + \" | \" + \"数量: \" + str(c.get(\"old_quantity\", \"\"\
          )) + \" → \" + str(c.get(\"new_quantity\", \"\")) + \" | 説明変更: \" + ((\"\
          あり\" if c.get(\"old_description\", \"\") != c.get(\"new_description\", \"\
          \") else \"なし\"))\n            summary_lines.append(line)\n    summary =\
          \ \"\\n\".join(summary_lines)\n    # outputs: output (string), diff (object)\n\
          \    return dict(output=summary, diff=dict(added=added, removed=removed,\
          \ changed=changed))\n"
        variables:
        - value_selector:
          - extract1
          - old_bom
          variable: old_bom
        - value_selector:
          - extract1
          - new_bom
          variable: new_bom
        outputs:
          output:
            type: string
            children: null
          diff:
            type: object
            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.6
        prompt_template:
        - role: system
          text: あなたは製造業向けのBOM差分レポート作成アシスタントです。出力は日本語で、影響範囲（調達、組立、コスト、リードタイム）を簡潔に推定し、優先対応項目を箇条書きで示してください。結論を最初に短く（1-2行）書き、その後にカテゴリ別の詳細を箇条書きで記載してください。
        - role: user
          text: 'ユーザーの入力:

            {{#start.query#}}


            差分サマリ:

            {{#code1.output#}}


            差分詳細(構造化):

            {{#code1.diff#}}


            上記を元に、影響項目（調達、組立、コスト、リードタイム、品質など）を推定し、優先度（高/中/低）と推奨アクションを短い箇条書きで出力してください。重要な変更は部品番号を明記してください。'
        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
      id: end
      position:
        x: 1280
        y: 282
      type: custom
      height: 90
      width: 244
  hash: null
