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-llm_parse
      source: start
      target: llm_parse
    - data:
        sourceType: llm
        targetType: code
      id: llm_parse-to-code_format
      source: llm_parse
      target: code_format
    - data:
        sourceType: code
        targetType: end
      id: code_format-to-end
      source: code_format
      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: 抽出と分類 (LLM)
        type: llm
        model:
          provider: openai
          name: gpt-5-mini
          mode: chat
          completion_params:
            temperature: 0.2
        prompt_template:
        - role: system
          text: 'あなたは建設現場の安全パトロール報告から個々の指摘を構造化して抽出する専門アシスタントです。出力は必ずJSON配列（top-level
            の配列）で返してください。各配列要素は必ず次のフィールドを持つこと: finding_text (入力の該当指摘の具体的テキスト), danger_level
            (High / Medium / Low のいずれか), danger_reason (危険度判定の根拠を具体的に記述), remediation_deadline
            (具体的な日付を ISO 形式 YYYY-MM-DD で指定), assigned_person (担当者名または役職を具体的に指定), priority_score
            (1から100の整数スコア), priority_reason (優先度スコアを与えた理由)。日付は必ず具体的な日付（YYYY-MM-DD）にしてください。担当者は可能な限り具体的（例:
            "現場監督: 佐藤", "設備保全部" 等）にしてください。priority_score は数値で、理由には作業量・危険度・影響範囲等を示してください。JSON以外の説明は返さないでください。'
        - role: user
          text: '以下は安全パトロール報告の本文です。報告中にあるすべての「指摘」ごとに、上記の必須フィールドを満たすJSONオブジェクトを作成し、配列として返してください。各指摘のテキストは入力の文言を反映して具体的に記述してください。


            報告:

            {{#start.query#}}'
        vision:
          enabled: false
        memory: null
        context:
          enabled: false
      height: 90
      id: llm_parse
      position:
        x: 380
        y: 282
      type: custom
      width: 244
    - data:
        title: フォーマット検証・整形 (Code)
        type: code
        code_language: python3
        code: "import json\nimport re\nfrom datetime import datetime, date, timedelta\n\
          \ndef _extract_json_array(s: str):\n    # try direct load\n    try:\n  \
          \      return json.loads(s)\n    except Exception:\n        pass\n    #\
          \ try to extract first [...] block\n    m = re.search(r'\\[.*\\]', s, re.S)\n\
          \    if m:\n        try:\n            return json.loads(m.group(0))\n  \
          \      except Exception:\n            return None\n    return None\n\ndef\
          \ _try_parse_date(s: str):\n    if not s or not isinstance(s, str):\n  \
          \      return None\n    s = s.strip()\n    # try ISO\n    try:\n       \
          \ d = datetime.fromisoformat(s).date()\n        return d\n    except Exception:\n\
          \        pass\n    patterns = ['%Y/%m/%d', '%Y.%m.%d', '%m/%d/%Y', '%d-%m-%Y',\
          \ '%d/%m/%Y', '%Y-%m-%d']\n    for p in patterns:\n        try:\n      \
          \      d = datetime.strptime(s, p).date()\n            return d\n      \
          \  except Exception:\n            continue\n    return None\n\ndef _normalize_level(s:\
          \ str):\n    if not s or not isinstance(s, str):\n        return 'Medium'\n\
          \    sl = s.lower()\n    if 'high' in sl or '高' in sl:\n        return 'High'\n\
          \    if 'low' in sl or '低' in sl:\n        return 'Low'\n    return 'Medium'\n\
          \ndef _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    raw = inputs.get('text', '') or ''\n\
          \    parsed = _extract_json_array(raw)\n    if not isinstance(parsed, list):\n\
          \        # cannot parse, return explanatory formatted fallback using entire\
          \ input as one finding\n        today = date.today()\n        default_deadline\
          \ = today + timedelta(days=7)\n        res_txt = '指摘1:\\\\n(1) 危険度: Medium\
          \ — 根拠: 自動判定。\\\\n(2) 推奨是正期限: ' + default_deadline.isoformat() + '\\\\n(3)\
          \ 担当者: 現場監督\\\\n(4) 優先順位スコア: 60 — 理由: 自動割当。\\\\n\\\\n原文:\\\\n' + raw\n \
          \       return dict(output=res_txt)\n    results = []\n    today = date.today()\n\
          \    for idx, item in enumerate(parsed):\n        # ensure keys and normalize\n\
          \        ft = item.get('finding_text') if isinstance(item.get('finding_text'),\
          \ str) else str(item.get('finding_text') or '')\n        dl_raw = item.get('remediation_deadline')\
          \ if isinstance(item.get('remediation_deadline'), str) else str(item.get('remediation_deadline')\
          \ or '')\n        dl_date = _try_parse_date(dl_raw)\n        level = _normalize_level(item.get('danger_level')\
          \ if isinstance(item.get('danger_level'), str) else '')\n        # assign\
          \ default deadlines based on level when parsing failed\n        if not dl_date:\n\
          \            if level == 'High':\n                dl_date = today + timedelta(days=3)\n\
          \            elif level == 'Low':\n                dl_date = today + timedelta(days=14)\n\
          \            else:\n                dl_date = today + timedelta(days=7)\n\
          \        assigned = item.get('assigned_person') if isinstance(item.get('assigned_person'),\
          \ str) and item.get('assigned_person').strip() else '現場監督'\n        # priority\
          \ score\n        ps_raw = item.get('priority_score')\n        score = None\n\
          \        try:\n            if isinstance(ps_raw, (int, float)):\n      \
          \          score = int(ps_raw)\n            elif isinstance(ps_raw, str)\
          \ and ps_raw.strip().isdigit():\n                score = int(ps_raw.strip())\n\
          \        except Exception:\n            score = None\n        if score is\
          \ None:\n            if level == 'High':\n                score = 90\n \
          \           elif level == 'Low':\n                score = 30\n         \
          \   else:\n                score = 60\n        # reason fields\n       \
          \ dr = item.get('danger_reason') if isinstance(item.get('danger_reason'),\
          \ str) and item.get('danger_reason').strip() else '危険性、接触/転倒のリスク、作業継続時の影響範囲に基づき判定。'\n\
          \        pr = item.get('priority_reason') if isinstance(item.get('priority_reason'),\
          \ str) and item.get('priority_reason').strip() else '危険度・作業影響度・是正にかかる工数を総合的に評価。'\n\
          \        res_item = dict(\n            finding_text = ft,\n            danger_level\
          \ = level,\n            danger_reason = dr,\n            remediation_deadline\
          \ = dl_date.isoformat(),\n            assigned_person = assigned,\n    \
          \        priority_score = int(score),\n            priority_reason = pr\n\
          \        )\n        results.append(res_item)\n    # build human-readable\
          \ output text per finding\n    parts = []\n    for i, r in enumerate(results):\n\
          \        n = i + 1\n        p = ''\n        p += '指摘' + str(n) + ':\\n'\n\
          \        p += '(1) 危険度: ' + r['danger_level'] + ' — 判定根拠: ' + r['danger_reason']\
          \ + '\\n'\n        p += '(2) 推奨是正期限: ' + r['remediation_deadline'] + '\\\
          n'\n        p += '(3) 担当者の割当: ' + r['assigned_person'] + '\\n'\n       \
          \ p += '(4) 優先順位スコア: ' + str(r['priority_score']) + ' — 理由: ' + r['priority_reason']\
          \ + '\\n'\n        p += '原文: ' + r['finding_text'] + '\\n'\n        parts.append(p)\n\
          \    final_text = '\\n'.join(parts)\n    return dict(output=final_text,\
          \ items=results)\n"
        variables:
        - value_selector:
          - llm_parse
          - text
          variable: text
        outputs:
          output:
            type: string
            children: null
          items:
            type: array[object]
            children: null
      height: 90
      id: code_format
      position:
        x: 680
        y: 282
      type: custom
      width: 244
    - data:
        title: 終了
        type: end
        outputs:
        - value_selector:
          - code_format
          - output
          variable: result
      height: 90
      id: end
      position:
        x: 980
        y: 282
      type: custom
      width: 244
  hash: null
