{
  "name": "10 Bedside Transcript - OpenAI Structured Extraction",
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "bedside-transcript-openai-demo",
        "responseMode": "responseNode",
        "options": {}
      },
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2.1,
      "position": [
        220,
        320
      ],
      "id": "79917335-58f7-4dcb-9bb1-b0ba6bde0101",
      "name": "Webhook",
      "webhookId": "bedside-transcript-openai-demo"
    },
    {
      "parameters": {
        "keepOnlySet": false,
        "values": {
          "string": [
            {
              "name": "patient_id",
              "value": "={{$json.body.patient_id || ''}}"
            },
            {
              "name": "bed_no",
              "value": "={{$json.body.bed_no || ''}}"
            },
            {
              "name": "shift",
              "value": "={{$json.body.shift || ''}}"
            },
            {
              "name": "nurse_id",
              "value": "={{$json.body.nurse_id || ''}}"
            },
            {
              "name": "recorded_at",
              "value": "={{$json.body.recorded_at || $now}}"
            },
            {
              "name": "transcript",
              "value": "={{$json.body.transcript || ''}}"
            },
            {
              "name": "created_at",
              "value": "={{$now}}"
            },
            {
              "name": "extraction_prompt",
              "value": "=你是護理床邊語音紀錄的結構化抽取器。請只根據 transcript 內容抽取欄位，不要自行推測。若沒有提到，填 null 或空字串。請回傳單一 JSON 物件，不要 Markdown，不要解釋。\\n\\n必要背景：\\npatient_id: {{$json.body.patient_id || ''}}\\nbed_no: {{$json.body.bed_no || ''}}\\nshift: {{$json.body.shift || ''}}\\nnurse_id: {{$json.body.nurse_id || ''}}\\nrecorded_at: {{$json.body.recorded_at || $now}}\\n\\n請輸出這些欄位：\\npatient_id, bed_no, shift, nurse_id, recorded_at, temperature, pulse, respiration, bp_sys, bp_dia, spo2, pain_score, consciousness, line_status, wound_status, intake_ml, urine_ml, stool_count, interventions, abnormal_notes, follow_up_needed, bedside_summary, nursing_note_draft, alert_needed, alert_reason。\\n\\n規則：\\n- 數值欄位使用 number 或 null。\\n- alert_needed 為 boolean。\\n- alert_reason 為 string array。\\n- line_status 建議使用 normal, abnormal, unknown。\\n- wound_status 建議使用 stable, worsening, unknown。\\n- nursing_note_draft 是草稿，需人工確認後才能進正式病歷。\\n\\ntranscript:\\n{{$json.body.transcript || ''}}"
            }
          ],
          "boolean": [
            {
              "name": "has_required_fields",
              "value": "={{!!($json.body.patient_id && $json.body.bed_no && $json.body.shift && $json.body.nurse_id && $json.body.transcript)}}"
            }
          ]
        },
        "options": {}
      },
      "id": "79917335-58f7-4dcb-9bb1-b0ba6bde0102",
      "name": "Normalize Transcript Input",
      "type": "n8n-nodes-base.set",
      "typeVersion": 1,
      "position": [
        500,
        320
      ]
    },
    {
      "parameters": {
        "conditions": {
          "boolean": [
            {
              "value1": "={{$json.has_required_fields}}",
              "operation": "isTrue"
            }
          ]
        }
      },
      "id": "79917335-58f7-4dcb-9bb1-b0ba6bde0103",
      "name": "Has Required Fields",
      "type": "n8n-nodes-base.if",
      "typeVersion": 1,
      "position": [
        760,
        320
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.openai.com/v1/responses",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "Bearer YOUR_OPENAI_API_KEY"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"model\": \"gpt-5.2\",\n  \"input\": [\n    {\n      \"role\": \"user\",\n      \"content\": [\n        {\n          \"type\": \"input_text\",\n          \"text\": $json.extraction_prompt\n        }\n      ]\n    }\n  ]\n}",
        "options": {}
      },
      "id": "79917335-58f7-4dcb-9bb1-b0ba6bde0104",
      "name": "OpenAI Structured Extraction",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1020,
        220
      ]
    },
    {
      "parameters": {
        "jsCode": "const response = $input.first().json;\nconst raw = response.output_text || response.text || '';\n\nlet extracted;\ntry {\n  const cleaned = raw.replace(/^```json\\s*/i, '').replace(/^```\\s*/i, '').replace(/```$/i, '').trim();\n  extracted = JSON.parse(cleaned);\n} catch (error) {\n  extracted = {\n    parse_error: true,\n    raw_output: raw,\n    alert_needed: true,\n    alert_reason: ['openai_json_parse_error'],\n    bedside_summary: 'OpenAI 結構化抽取失敗，請人工檢查 transcript。',\n    nursing_note_draft: 'OpenAI 結構化抽取失敗，請人工檢查 transcript 後再建立護理紀錄草稿。'\n  };\n}\n\nif (!Array.isArray(extracted.alert_reason)) {\n  extracted.alert_reason = extracted.alert_reason ? [String(extracted.alert_reason)] : [];\n}\n\nreturn [{\n  json: {\n    created_at: $node['Normalize Transcript Input'].json.created_at,\n    transcript: $node['Normalize Transcript Input'].json.transcript,\n    ...extracted,\n    patient_id: extracted.patient_id || $node['Normalize Transcript Input'].json.patient_id,\n    bed_no: extracted.bed_no || $node['Normalize Transcript Input'].json.bed_no,\n    shift: extracted.shift || $node['Normalize Transcript Input'].json.shift,\n    nurse_id: extracted.nurse_id || $node['Normalize Transcript Input'].json.nurse_id,\n    recorded_at: extracted.recorded_at || $node['Normalize Transcript Input'].json.recorded_at,\n    alert_reason_text: extracted.alert_reason.join(',')\n  }\n}];"
      },
      "id": "79917335-58f7-4dcb-9bb1-b0ba6bde0105",
      "name": "Parse OpenAI JSON",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1280,
        220
      ]
    },
    {
      "parameters": {
        "operation": "append",
        "documentId": {
          "__rl": true,
          "value": "https://docs.google.com/spreadsheets/d/YOUR_SPREADSHEET_ID/edit#gid=0",
          "mode": "url"
        },
        "sheetName": {
          "__rl": true,
          "value": "TranscriptDrafts",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "created_at": "={{$json.created_at}}",
            "patient_id": "={{$json.patient_id}}",
            "bed_no": "={{$json.bed_no}}",
            "shift": "={{$json.shift}}",
            "nurse_id": "={{$json.nurse_id}}",
            "recorded_at": "={{$json.recorded_at}}",
            "transcript": "={{$json.transcript}}",
            "temperature": "={{$json.temperature}}",
            "pulse": "={{$json.pulse}}",
            "respiration": "={{$json.respiration}}",
            "bp_sys": "={{$json.bp_sys}}",
            "bp_dia": "={{$json.bp_dia}}",
            "spo2": "={{$json.spo2}}",
            "pain_score": "={{$json.pain_score}}",
            "consciousness": "={{$json.consciousness}}",
            "line_status": "={{$json.line_status}}",
            "wound_status": "={{$json.wound_status}}",
            "intake_ml": "={{$json.intake_ml}}",
            "urine_ml": "={{$json.urine_ml}}",
            "stool_count": "={{$json.stool_count}}",
            "interventions": "={{$json.interventions}}",
            "abnormal_notes": "={{$json.abnormal_notes}}",
            "follow_up_needed": "={{$json.follow_up_needed}}",
            "alert_needed": "={{$json.alert_needed}}",
            "alert_reason": "={{$json.alert_reason_text}}",
            "bedside_summary": "={{$json.bedside_summary}}",
            "nursing_note_draft": "={{$json.nursing_note_draft}}"
          },
          "matchingColumns": [],
          "schema": [
            { "id": "created_at", "displayName": "created_at", "type": "string", "display": true, "required": false, "defaultMatch": false, "canBeUsedToMatch": true },
            { "id": "patient_id", "displayName": "patient_id", "type": "string", "display": true, "required": false, "defaultMatch": false, "canBeUsedToMatch": true },
            { "id": "bed_no", "displayName": "bed_no", "type": "string", "display": true, "required": false, "defaultMatch": false, "canBeUsedToMatch": true },
            { "id": "shift", "displayName": "shift", "type": "string", "display": true, "required": false, "defaultMatch": false, "canBeUsedToMatch": true },
            { "id": "nurse_id", "displayName": "nurse_id", "type": "string", "display": true, "required": false, "defaultMatch": false, "canBeUsedToMatch": true },
            { "id": "recorded_at", "displayName": "recorded_at", "type": "string", "display": true, "required": false, "defaultMatch": false, "canBeUsedToMatch": true },
            { "id": "transcript", "displayName": "transcript", "type": "string", "display": true, "required": false, "defaultMatch": false, "canBeUsedToMatch": true },
            { "id": "temperature", "displayName": "temperature", "type": "string", "display": true, "required": false, "defaultMatch": false, "canBeUsedToMatch": true },
            { "id": "pulse", "displayName": "pulse", "type": "string", "display": true, "required": false, "defaultMatch": false, "canBeUsedToMatch": true },
            { "id": "respiration", "displayName": "respiration", "type": "string", "display": true, "required": false, "defaultMatch": false, "canBeUsedToMatch": true },
            { "id": "bp_sys", "displayName": "bp_sys", "type": "string", "display": true, "required": false, "defaultMatch": false, "canBeUsedToMatch": true },
            { "id": "bp_dia", "displayName": "bp_dia", "type": "string", "display": true, "required": false, "defaultMatch": false, "canBeUsedToMatch": true },
            { "id": "spo2", "displayName": "spo2", "type": "string", "display": true, "required": false, "defaultMatch": false, "canBeUsedToMatch": true },
            { "id": "pain_score", "displayName": "pain_score", "type": "string", "display": true, "required": false, "defaultMatch": false, "canBeUsedToMatch": true },
            { "id": "consciousness", "displayName": "consciousness", "type": "string", "display": true, "required": false, "defaultMatch": false, "canBeUsedToMatch": true },
            { "id": "line_status", "displayName": "line_status", "type": "string", "display": true, "required": false, "defaultMatch": false, "canBeUsedToMatch": true },
            { "id": "wound_status", "displayName": "wound_status", "type": "string", "display": true, "required": false, "defaultMatch": false, "canBeUsedToMatch": true },
            { "id": "intake_ml", "displayName": "intake_ml", "type": "string", "display": true, "required": false, "defaultMatch": false, "canBeUsedToMatch": true },
            { "id": "urine_ml", "displayName": "urine_ml", "type": "string", "display": true, "required": false, "defaultMatch": false, "canBeUsedToMatch": true },
            { "id": "stool_count", "displayName": "stool_count", "type": "string", "display": true, "required": false, "defaultMatch": false, "canBeUsedToMatch": true },
            { "id": "interventions", "displayName": "interventions", "type": "string", "display": true, "required": false, "defaultMatch": false, "canBeUsedToMatch": true },
            { "id": "abnormal_notes", "displayName": "abnormal_notes", "type": "string", "display": true, "required": false, "defaultMatch": false, "canBeUsedToMatch": true },
            { "id": "follow_up_needed", "displayName": "follow_up_needed", "type": "string", "display": true, "required": false, "defaultMatch": false, "canBeUsedToMatch": true },
            { "id": "alert_needed", "displayName": "alert_needed", "type": "string", "display": true, "required": false, "defaultMatch": false, "canBeUsedToMatch": true },
            { "id": "alert_reason", "displayName": "alert_reason", "type": "string", "display": true, "required": false, "defaultMatch": false, "canBeUsedToMatch": true },
            { "id": "bedside_summary", "displayName": "bedside_summary", "type": "string", "display": true, "required": false, "defaultMatch": false, "canBeUsedToMatch": true },
            { "id": "nursing_note_draft", "displayName": "nursing_note_draft", "type": "string", "display": true, "required": false, "defaultMatch": false, "canBeUsedToMatch": true }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {}
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        1540,
        220
      ],
      "id": "79917335-58f7-4dcb-9bb1-b0ba6bde0106",
      "name": "Append row in sheet"
    },
    {
      "parameters": {
        "keepOnlySet": false,
        "values": {
          "boolean": [
            {
              "name": "ok",
              "value": true
            },
            {
              "name": "alert_needed",
              "value": "={{$node[\"Parse OpenAI JSON\"].json.alert_needed}}"
            }
          ],
          "string": [
            {
              "name": "message",
              "value": "Transcript processed by OpenAI structured extraction"
            },
            {
              "name": "bedside_summary",
              "value": "={{$node[\"Parse OpenAI JSON\"].json.bedside_summary}}"
            },
            {
              "name": "nursing_note_draft",
              "value": "={{$node[\"Parse OpenAI JSON\"].json.nursing_note_draft}}"
            },
            {
              "name": "alert_reason",
              "value": "={{$node[\"Parse OpenAI JSON\"].json.alert_reason_text}}"
            }
          ]
        },
        "options": {}
      },
      "id": "79917335-58f7-4dcb-9bb1-b0ba6bde0107",
      "name": "Success Response",
      "type": "n8n-nodes-base.set",
      "typeVersion": 1,
      "position": [
        1800,
        220
      ]
    },
    {
      "parameters": {
        "keepOnlySet": true,
        "values": {
          "boolean": [
            {
              "name": "ok",
              "value": false
            }
          ],
          "string": [
            {
              "name": "message",
              "value": "Missing required fields. Required: patient_id, bed_no, shift, nurse_id, transcript"
            }
          ]
        },
        "options": {}
      },
      "id": "79917335-58f7-4dcb-9bb1-b0ba6bde0108",
      "name": "Error Response",
      "type": "n8n-nodes-base.set",
      "typeVersion": 1,
      "position": [
        1020,
        420
      ]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{$json}}",
        "options": {
          "responseCode": 200
        }
      },
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.4,
      "position": [
        2060,
        320
      ],
      "id": "79917335-58f7-4dcb-9bb1-b0ba6bde0109",
      "name": "Respond to Webhook"
    }
  ],
  "connections": {
    "Webhook": {
      "main": [
        [
          {
            "node": "Normalize Transcript Input",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize Transcript Input": {
      "main": [
        [
          {
            "node": "Has Required Fields",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Has Required Fields": {
      "main": [
        [
          {
            "node": "OpenAI Structured Extraction",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Error Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI Structured Extraction": {
      "main": [
        [
          {
            "node": "Parse OpenAI JSON",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse OpenAI JSON": {
      "main": [
        [
          {
            "node": "Append row in sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Append row in sheet": {
      "main": [
        [
          {
            "node": "Success Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Success Response": {
      "main": [
        [
          {
            "node": "Respond to Webhook",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Error Response": {
      "main": [
        [
          {
            "node": "Respond to Webhook",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "pinData": {},
  "settings": {},
  "meta": {}
}
