app:
  description: 浄水場の水質検査CSVから月次サマリを作成するワークフロー
  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-code1
      source: start
      target: code1
    - data:
        sourceType: code
        targetType: llm
      id: code1-to-llm1
      source: code1
      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    # inputs[\"csv_text\"] にCSV文字列が入る想定（ヘッダ:\
          \ 検査日,検査項目,測定値,基準値,単位）\n    csv_text = inputs.get(\"csv_text\", \"\")\n\
          \    if not csv_text:\n        return dict(output=\"エラー: 入力CSVが空です。\")\n\
          \    from io import StringIO\n    import csv\n    reader = csv.reader(StringIO(csv_text))\n\
          \    try:\n        header = next(reader)\n    except StopIteration:\n  \
          \      return dict(output=\"エラー: CSVにデータがありません。\")\n    # ヘッダ名から列インデックスを推定\n\
          \    def find_index(names):\n        for i, h in enumerate(header):\n  \
          \          if h is None:\n                continue\n            hs = h.strip()\n\
          \            for nm in names:\n                if hs == nm:\n          \
          \          return i\n        return None\n    date_idx = find_index([\"\
          検査日\", \"date\"])\n    item_idx = find_index([\"検査項目\", \"項目\", \"item\"\
          ])\n    value_idx = find_index([\"測定値\", \"値\", \"value\", \"measurement\"\
          ])\n    threshold_idx = find_index([\"基準値\", \"基準\", \"threshold\"])\n \
          \   unit_idx = find_index([\"単位\", \"unit\"])\n    # 足りない場合は標準順序を仮定\n  \
          \  if any(idx is None for idx in [date_idx, item_idx, value_idx, threshold_idx,\
          \ unit_idx]):\n        date_idx, item_idx, value_idx, threshold_idx, unit_idx\
          \ = 0, 1, 2, 3, 4\n    items = dict()\n    total_tests = 0\n    exceed_count\
          \ = 0\n    # 行を走査\n    for row in reader:\n        if not row or len(row)\
          \ <= max(date_idx, item_idx, value_idx, threshold_idx, unit_idx):\n    \
          \        # 行の列数が不足している場合はスキップ\n            continue\n        total_tests\
          \ = total_tests + 1\n        item = row[item_idx].strip()\n        raw_value\
          \ = row[value_idx].strip()\n        raw_threshold = row[threshold_idx].strip()\n\
          \        unit = row[unit_idx].strip() if len(row) > unit_idx else \"\"\n\
          \        try:\n            value = float(raw_value)\n        except:\n \
          \           # 数値変換できない場合はスキップ\n            continue\n        try:\n    \
          \        threshold = float(raw_threshold)\n        except:\n           \
          \ threshold = None\n        if item not in items:\n            items[item]\
          \ = dict(within_values=[], exceed_count=0, total_count=0, threshold=threshold,\
          \ unit=unit, warning=False)\n        items[item][\"total_count\"] = items[item][\"\
          total_count\"] + 1\n        if threshold is not None:\n            items[item][\"\
          threshold\"] = threshold\n        if unit:\n            items[item][\"unit\"\
          ] = unit\n        if threshold is not None and value > threshold:\n    \
          \        items[item][\"exceed_count\"] = items[item][\"exceed_count\"] +\
          \ 1\n            exceed_count = exceed_count + 1\n        else:\n      \
          \      items[item][\"within_values\"].append(value)\n        # 要注意フラグ: 基準値の80%超（閾値がある場合のみ）\n\
          \        if threshold is not None and value > 0.8 * threshold:\n       \
          \     items[item][\"warning\"] = True\n    # アイテム別に統計算出（基準内データを対象）\n   \
          \ lines = []\n    lines.append(\"総検査件数: \" + str(total_tests))\n    lines.append(\"\
          基準値超過件数: \" + str(exceed_count))\n    warning_items = [name for name, v\
          \ in items.items() if v.get(\"warning\")]\n    if warning_items:\n     \
          \   lines.append(\"要注意項目: \" + \", \".join(warning_items))\n    else:\n\
          \        lines.append(\"要注意項目: なし\")\n    lines.append(\"\")\n    lines.append(\"\
          項目別（基準内データ）統計:\")\n    for name, v in items.items():\n        within = v.get(\"\
          within_values\", [])\n        cnt_within = len(within)\n        if cnt_within\
          \ > 0:\n            avg = sum(within) / cnt_within\n            mx = max(within)\n\
          \            mn = min(within)\n            avg_s = str(round(avg, 2))\n\
          \            mx_s = str(round(mx, 2))\n            mn_s = str(round(mn,\
          \ 2))\n        else:\n            avg_s = \"N/A\"\n            mx_s = \"\
          N/A\"\n            mn_s = \"N/A\"\n        thr = v.get(\"threshold\")\n\
          \        thr_s = str(thr) if thr is not None else \"不明\"\n        unit_s\
          \ = v.get(\"unit\") or \"\"\n        lines.append(\"- \" + name + \" : 件数(基準内)=\"\
          \ + str(cnt_within) + \", 平均=\" + avg_s + \", 最大=\" + mx_s + \", 最小=\" +\
          \ mn_s + \", 基準=\" + thr_s + unit_s)\n    # 出力文字列を整形\n    output_text =\
          \ \"\\n\".join(lines)\n    return dict(output=output_text)\n"
        variables:
        - value_selector:
          - start
          - query
          variable: csv_text
        outputs:
          output:
            type: string
            children: null
      height: 90
      id: code1
      position:
        x: 380
        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オブジェクトのみで行ってください。JSONには次のキーを含めること:
            total_tests（検査件数、整数）、exceed_count（基準値超過数、整数）、warning_items（要注意項目の文字列配列）、summary_comment（日本語での月次総括、300〜400文字）、recommended_actions（推奨対応の文字列配列）。不要な説明や注釈は付けず、純粋なJSONのみを返してください。


            ---


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

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

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

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

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

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

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


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

            に変換してください。)'
        - role: user
          text: '以下は解析済みの集計テキストです。これを参照して、指定のJSONフォーマットで応答してください。


            {{#code1.output#}}'
        vision:
          enabled: false
        memory: null
        context:
          enabled: false
      height: 90
      id: llm1
      position:
        x: 680
        y: 282
      type: custom
      width: 244
    - data:
        title: 終了
        type: end
        outputs:
        - value_selector:
          - llm1
          - text
          variable: result
      height: 90
      id: end
      position:
        x: 980
        y: 282
      type: custom
      width: 244
  hash: null
