app:
  description: 既契約者の属性と行動データから離脱予兆を検出し、高リスク顧客には強めのアウトバウンドリテンショントーク、その他には穏やかなリテンション提案トークを生成するワークフローです。入力は1つのテキストフィールドに顧客属性・行動ログ・補足情報をまとめて入力してください。
  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: parameter-extractor
      id: start-to-extractor1
      source: start
      target: extractor1
    - data:
        sourceType: parameter-extractor
        targetType: code
      id: extractor1-to-risk1
      source: extractor1
      target: risk1
    - data:
        sourceType: code
        targetType: if-else
      id: risk1-to-ifelse1
      source: risk1
      target: ifelse1
    - data:
        sourceType: if-else
        targetType: llm
      id: ifelse1-true-to-llm_high
      source: ifelse1
      sourceHandle: 'true'
      target: llm_high
    - data:
        sourceType: if-else
        targetType: llm
      id: ifelse1-false-to-llm_default
      source: ifelse1
      sourceHandle: 'false'
      target: llm_default
    - data:
        sourceType: llm
        targetType: code
      id: llm_high-to-assemble1
      source: llm_high
      target: assemble1
    - data:
        sourceType: llm
        targetType: code
      id: llm_default-to-assemble1
      source: llm_default
      target: assemble1
    - data:
        sourceType: code
        targetType: end
      id: assemble1-to-end
      source: assemble1
      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: parameter-extractor
        model:
          provider: openai
          name: gpt-5-mini
          mode: chat
        query:
        - start
        - query
        parameters:
        - name: age
          type: number
          description: 年齢
          required: true
        - name: tenure_years
          type: number
          description: 契約継続年数（年）
          required: true
        - name: payment_status
          type: string
          description: '支払状況（例: 正常, 延滞, 未払い）'
          required: true
        - name: missed_payments
          type: number
          description: 未払い回数
          required: false
        - name: recent_claims
          type: number
          description: 過去6か月の請求件数
          required: false
        - name: engagement_score
          type: number
          description: エンゲージメント指標（0-100）
          required: false
        - name: last_interaction_days
          type: number
          description: 最終接触からの日数
          required: false
        - name: policy_value
          type: number
          description: 保険金額（目安）
          required: false
        - name: churn_reason
          type: string
          description: 顧客が示した解約理由や懸念（あれば）
          required: false
        reasoning_mode: prompt
        instruction: ユーザーのテキストから上記パラメータを抽出してください。数値は可能なら数値型で、未指定は空にしてください。
      height: 90
      id: extractor1
      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    def to_float(v):\n        try:\n  \
          \          if v is None or v == \"\":\n                return 0.0\n    \
          \        return float(v)\n        except:\n            return 0.0\n    age\
          \ = to_float(inputs.get(\"age\"))\n    tenure = to_float(inputs.get(\"tenure_years\"\
          ))\n    missed = to_float(inputs.get(\"missed_payments\"))\n    engagement\
          \ = to_float(inputs.get(\"engagement_score\"))\n    claims = to_float(inputs.get(\"\
          recent_claims\"))\n    last_days = to_float(inputs.get(\"last_interaction_days\"\
          ))\n    score = 0.0\n    # エンゲージメントが低いほどスコア増\n    score = score + (100.0\
          \ - engagement) * 0.5\n    # 未払い・請求は離脱リスクを高める\n    score = score + missed\
          \ * 10.0\n    score = score + claims * 5.0\n    # 契約年数が短いとリスク上昇\n    if\
          \ tenure < 1.0:\n        score = score + 15.0\n    # 最終接触が長いほどリスク上昇\n  \
          \  if last_days > 90.0:\n        score = score + 10.0\n    # 年齢の若さは流動性上昇とみなす（軽く加点）\n\
          \    if age < 30.0:\n        score = score + 5.0\n    # 正規化\n    if score\
          \ < 0.0:\n        score = 0.0\n    if score > 100.0:\n        score = 100.0\n\
          \    risk_level = \"低\"\n    if score >= 70.0:\n        risk_level = \"\
          高\"\n    elif score >= 40.0:\n        risk_level = \"中\"\n    summary =\
          \ \"risk_level: \" + str(risk_level) + \"; score: \" + str(round(score,\
          \ 1))\n    return dict(risk_summary=summary)\n"
        variables:
        - value_selector:
          - extractor1
          - age
          variable: age
        - value_selector:
          - extractor1
          - tenure_years
          variable: tenure_years
        - value_selector:
          - extractor1
          - missed_payments
          variable: missed_payments
        - value_selector:
          - extractor1
          - recent_claims
          variable: recent_claims
        - value_selector:
          - extractor1
          - engagement_score
          variable: engagement_score
        - value_selector:
          - extractor1
          - last_interaction_days
          variable: last_interaction_days
        outputs:
          risk_summary:
            type: string
            children: null
      height: 90
      id: risk1
      position:
        x: 680
        y: 282
      type: custom
      width: 244
    - data:
        title: 条件分岐（高リスク判定）
        type: if-else
        conditions:
        - comparison_operator: contains
          id: cond1
          value: 高
          variable_selector:
          - risk1
          - risk_summary
        logical_operator: and
      height: 90
      id: ifelse1
      position:
        x: 980
        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: あなたは生命保険の既契約者向けアウトバウンドリテンションの専門家です。コンプライアンスを守りつつ、短時間で顧客の心配を和らげ、契約維持に繋がる説得力のあるトークを作成してください。出力はJSONで、'script'
            に全文、'sections' に各セクションの配列（order保持）、'notes' に短い要点を入れてください。
        - role: user
          text: '顧客情報: 年齢: {{#extractor1.age#}}, 契約年数: {{#extractor1.tenure_years#}},
            支払状況: {{#extractor1.payment_status#}}, 未払い回数: {{#extractor1.missed_payments#}},
            最近の請求: {{#extractor1.recent_claims#}}, エンゲージメント: {{#extractor1.engagement_score#}},
            最終接触日からの日数: {{#extractor1.last_interaction_days#}}, 保険金額: {{#extractor1.policy_value#}},
            示された解約理由: {{#extractor1.churn_reason#}}。リスク判定: {{#risk1.risk_summary#}}。元の説明:
            {{#start.query#}}。タスク: 高リスク顧客向けに、以下セクションを日本語で生成してください。1) 開始挨拶(30-40文字)、2)
            パーソナライズ(50-80文字)、3) 懸念への共感と短い要約(40-80文字)、4) 解決策提案(60-120文字)、5) 特典/オファー(30-60文字)、6)
            行動喚起(20-40文字)、7) コンプライアンス注意(30-50文字)。出力はJSONで ''script'' に各パートを改行で繋いだ全文、''sections''
            に各部の配列を含めてください。顧客が高リスクであることを前提に、早めのフォローと明確な次のアクションを示してください。'
        vision:
          enabled: false
        memory: null
        context:
          enabled: false
      height: 90
      id: llm_high
      position:
        x: 1280
        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: あなたは生命保険の既契約者向けアウトバウンドコミュニケーションの専門家です。相手に配慮した穏やかなトーンで、解約予兆を軽減するための関係深化を目的としたトークを作成してください。出力はJSONで、'script'
            に全文、'sections' に各セクションの配列を含めてください。
        - role: user
          text: '顧客情報: 年齢: {{#extractor1.age#}}, 契約年数: {{#extractor1.tenure_years#}},
            支払状況: {{#extractor1.payment_status#}}, 未払い回数: {{#extractor1.missed_payments#}},
            最近の請求: {{#extractor1.recent_claims#}}, エンゲージメント: {{#extractor1.engagement_score#}},
            最終接触日からの日数: {{#extractor1.last_interaction_days#}}, 保険金額: {{#extractor1.policy_value#}},
            示された解約理由: {{#extractor1.churn_reason#}}。リスク判定: {{#risk1.risk_summary#}}。元の説明:
            {{#start.query#}}。タスク: 中/低リスク顧客向けに、以下セクションを日本語で生成してください。1) 開始挨拶(20-40文字)、2)
            親近感のあるパーソナライズ(40-80文字)、3) 情報提供/安心材料(50-100文字)、4) 軽めの提案(40-80文字)、5) 次の誘導(20-40文字)、6)
            コンプライアンス注意(20-50文字)。出力はJSONで ''script'' と ''sections'' にしてください。'
        vision:
          enabled: false
        memory: null
        context:
          enabled: false
      height: 90
      id: llm_default
      position:
        x: 1580
        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    high = inputs.get(\"high_text\", \"\
          \")\n    default = inputs.get(\"default_text\", \"\")\n    result = \"\"\
          \n    try:\n        if high is not None and str(high).strip() != \"\":\n\
          \            result = str(high)\n        elif default is not None and str(default).strip()\
          \ != \"\":\n            result = str(default)\n        else:\n         \
          \   result = \"\"\n    except:\n        result = \"\"\n    return dict(final_script=result)\n"
        variables:
        - value_selector:
          - llm_high
          - text
          variable: high_text
        - value_selector:
          - llm_default
          - text
          variable: default_text
        outputs:
          final_script:
            type: string
            children: null
      height: 90
      id: assemble1
      position:
        x: 1880
        y: 282
      type: custom
      width: 244
    - data:
        title: 終了
        type: end
        outputs:
        - value_selector:
          - assemble1
          - final_script
          variable: result
      height: 90
      id: end
      position:
        x: 2180
        y: 282
      type: custom
      width: 244
  hash: null
