【Snowflake WEST User Group】おばちゃんチャットボットのコピペ部分まとめ

【Snowflake WEST User Group】おばちゃんチャットボットのコピペ部分まとめ | Tableau-id Press -タブロイド-
eyecatch_event_snowflakewest

はじめに

本記事は、2025年5月21日(火)に開催するSnow WEST User Group ハンズオン「関西弁で答えるチャットボットをSnowflake × Streamlitで作ろう」で利用するコードをまとめたものです。

★★★資料はこちら★★★

ハンズオン コピー&ペースト用コード

1-3・4.環境設定 | データベース・スキーマ作成・スキーマ作成

-- 1.環境設定 | 3.データベース・スキーマ作成
CREATE DATABASE SNOWVILLAGE_WEST_DEMO; 
CREATE SCHEMA SNOWVILLAGE_WEST_DEMO.KANSAI_OBACHAN_APP;

-- 1.環境設定 | 4.ステージ作成
CREATE STAGE SNOWVILLAGE_WEST_DEMO.KANSAI_OBACHAN_APP.SEMANTIC_MODEL;

3-8.セマンティックモデルを作成 | コードを張り付ける

name: osaka_restaurant_info
tables:
  - name: FOOD_SAMPLE_OSAKA_CHUO
    base_table:
      database: FOOD_ESTABLISHMENTS_DATA_SET_IN_JAPAN
      schema: MART
      table: FOOD_SAMPLE_OSAKA_CHUO
    dimensions:
      - name: UUID
        expr: UUID
        data_type: VARCHAR(16777216)
        description: 店舗ID
        sample_values:
          - 71b87932-e132-43e4-9583-96bfdd3d2b8e
          - ab7abad2-390c-4eac-8004-231d427ca34e
          - d0f9e27d-8ccf-4175-9312-cbf8ff3f9585
      - name: STORE_NAME
        expr: STORE_NAME
        data_type: VARCHAR(16777216)
        description: 店舗名
        sample_values:
          - 神戸牛牛・本町店
          - Bar Reiche
          - DOPE
      - name: STORE_PHONE_NUMBER
        expr: STORE_PHONE_NUMBER
        data_type: VARCHAR(16777216)
        description: 店舗電話番号
        sample_values:
          - 080-5696-6988
          - 06-6121-6588
      - name: STORE_PREF_NAME
        expr: STORE_PREF_NAME
        data_type: VARCHAR(16777216)
        description: 店舗都道府県
        sample_values:
          - 大阪府
      - name: STORE_CITY_NAME
        expr: STORE_CITY_NAME
        data_type: VARCHAR(16777216)
        description: 店舗市町村
        sample_values:
          - 大阪市中央区
      - name: STORE_ZIPCODE
        expr: STORE_ZIPCODE
        data_type: VARCHAR(16777216)
        description: 店舗郵便番号
        sample_values:
          - '5420076'
          - '5410041'
          - '5420083'
      - name: STORE_ADDRESS
        expr: STORE_ADDRESS
        data_type: VARCHAR(16777216)
        description: 店舗住所
        sample_values:
          - 大阪府大阪市中央区高麗橋4ー2ー7
          - 大阪府大阪市中央区東心斎橋2丁目5ー22
          - 大阪府大阪市中央区心斎橋筋2ー2ー3
      - name: STORE_BUILDING_NAME
        expr: STORE_BUILDING_NAME
        data_type: VARCHAR(16777216)
        description: 店舗ビル名
        sample_values:
          - ヴィラ日本橋ビル 1F
          - 中西ビル 1F
      - name: GENRE_MAJOR_NAME
        expr: GENRE_MAJOR_NAME
        data_type: VARCHAR(16777216)
        description: ジャンル大分類
        sample_values:
          - 飲食業
      - name: GENRE_MEDIUM_NAME
        expr: GENRE_MEDIUM_NAME
        data_type: VARCHAR(16777216)
        description: ジャンル中分類
        sample_values:
          - 配達・持ち帰り
          - 飲食店
      - name: GENRE_SUB_NAME
        expr: GENRE_SUB_NAME
        data_type: VARCHAR(16777216)
        description: ジャンル小分類
        sample_values:
          - 飲み屋・居酒屋
          - 専門料理店
          - 寿司店
          - ステーキ
          - その他配達・持ち帰り
          - 各国料理
          - 肉調理店(焼肉・焼鳥・ステーキ除く)
          - バー
          - 焼肉
          - キャバレー・ナイトクラブ・ラウンジ
          - カレー店
          - 持ち帰り飲食サービス業
          - 料亭
          - 麺類(そば)
          - 分類できない飲食店
          - 丼・定食
          - 鍋料理
          - 和食・郷土料理
          - ファーストフード
          - カフェ・喫茶店
          - ラーメン
          - 焼き鳥
          - 中華料理
          - 麺類(うどん)
          - 一般レストラン
          - お菓子・スイーツ
          - 配達飲食サービス業
          - その他飲食店
          - お好み焼き・たこ焼き・焼きそば等
      - name: STORE_URL
        expr: STORE_URL
        data_type: VARCHAR(16777216)
        description: 店舗URL
        sample_values:
          - https://tabelog.com/osaka/A2701/A270204/27112287/
          - https://s.tabelog.com/osaka/A2701/A270201/27078266/
      - name: STORE_FACEBOOK
        expr: STORE_FACEBOOK
        data_type: VARCHAR(16777216)
        description: 店舗Facebook
        sample_values:
          - https://www.facebook.com/ebisuhub/
          - https://www.facebook.com/people/BISTROT-LE-CANON/100054318979844/
      - name: STORE_X
        expr: STORE_X
        data_type: VARCHAR(16777216)
        description: 店舗X
        sample_values:
          - https://twitter.com/MixBarSLY
          - https://twitter.com/miyazakikan
      - name: STORE_INSTAGRAM
        expr: STORE_INSTAGRAM
        data_type: VARCHAR(16777216)
        description: 店舗instagram
        sample_values:
          - https://www.instagram.com/teppan_osaka/
          - https://www.instagram.com/cafegarb/
      - name: STORE_OTHER_SITE
        expr: STORE_OTHER_SITE
        data_type: VARCHAR(16777216)
        description: 店舗_他site
        sample_values:
          - '[''https://tabelog.com/osaka/A2701/A270107/27075875/'', ''https://www.ekiten.jp/shop_6851905/'']'
          - '[''https://tabelog.com/osaka/A2701/A270201/27074686/'', ''https://www.ekiten.jp/shop_6525173/'', ''https://place.line.me/businesses/35063080'']'
      - name: BRAND_ID
        expr: BRAND_ID
        data_type: VARCHAR(16777216)
        description: ブランドコード
        sample_values:
          - '797'
          - '1861'
      - name: BRAND_NAME
        expr: BRAND_NAME
        data_type: VARCHAR(16777216)
        description: ブランド名
        sample_values:
          - 白木屋
          - 大阪王将
          - ミスタードーナツ
      - name: BRAND_OWNER
        expr: BRAND_OWNER
        data_type: VARCHAR(16777216)
        description: オーナー会社の法人番号
        sample_values:
          - '4120001168969'
          - '2120001168995'
      - name: UPDATE_DATE
        expr: UPDATE_DATE
        data_type: VARCHAR(16777216)
        description: データ取得日
        sample_values:
          - '20250408'
    time_dimensions:
      - name: OPEN_DATE
        expr: OPEN_DATE
        data_type: DATE
        description: 開業推定日
        sample_values:
          - '2016-07-08'
          - '2017-02-01'
          - '2006-04-01'
      - name: CLOSED_DATE
        expr: CLOSED_DATE
        data_type: DATE
        description: 閉店推定日
        sample_values:
          - '2024-05-27'
          - '2024-02-09'
    facts:
      - name: STORE_LAT
        expr: STORE_LAT
        data_type: NUMBER(10,7)
        description: 緯度
        sample_values:
          - '34.6675624'
          - '34.6897922'
          - '34.6732392'
      - name: STORE_LON
        expr: STORE_LON
        data_type: NUMBER(10,7)
        description: 経度
        sample_values:
          - '135.5007200'
          - '135.5053260'
          - '135.5006608'
      - name: SEATS
        expr: SEATS
        data_type: NUMBER(38,0)
        description: 店舗座席数
      - name: CLOSED_FLAG
        expr: CLOSED_FLAG
        data_type: NUMBER(38,0)
        description: 閉店フラグ
        sample_values:
          - '1'
          - '0'
          - '2'
verified_queries:
  - name: ラーメン
    question: ラーメン食べたい。一番新しいお店の名前と電話番号教えて
    use_as_onboarding_question: true
    sql: SELECT store_name, store_phone_number FROM food_sample_osaka_chuo WHERE genre_sub_name = 'ラーメン' ORDER BY open_date DESC NULLS LAST LIMIT 1 /* Generated by Cortex Analyst */
    verified_by: 〇〇〇
    verified_at: 1747702367

4-4. Streamlitで画面を作成 | Pythonコードを張り付け

from typing import Dict, List, Optional
import _snowflake
import json
import streamlit as st
import time
from snowflake.snowpark.context import get_active_session
from snowflake.cortex import Complete
import pandas as pd

######################### セマンティックモデルの設定 #########################
DATABASE = "SNOWVILLAGE_WEST_DEMO"
SCHEMA = "KANSAI_OBACHAN_APP"
STAGE = "SEMANTIC_MODEL"
FILE = "osaka_restaurant_info.yaml"

#おばちゃんの画像
obachan_pic ="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhcvOAwqo-YodHiO-K66kjb4OkhFMZ5ZtqiojbDWXNcwso-8HuWjVD3Qb-otpKY6e9zRBpPlAaNRut39BaFSI8ckI9EVBsyEfPUtSjaD99vMnxnlxJsr4CiA2Y33Znm6GZ5TDis0SSI5q10/s800/fashion_oosaka_hyougara.png"

######################### 関数を定義 #########################

def send_message(prompt: str) -> dict:
    """Calls the REST API and returns the response."""
    request_body = {
        "messages": [
            {
                "role": "user",
                "content": [
                    {
                        "type": "text",
                        "text": prompt
                    }
                ]
            }
        ],
        "semantic_model_file": f"@{DATABASE}.{SCHEMA}.{STAGE}/{FILE}",
    }

    resp = _snowflake.send_snow_api_request(
        "POST",
        f"/api/v2/cortex/analyst/message",
        {},
        {},
        request_body,
        {},
        30000,
    )

    if resp["status"] < 400:
        return json.loads(resp["content"])
    else:
        st.session_state.messages.pop()
        raise Exception(
            f"Failed request with status {resp['status']}: {resp}"
        )

def process_message(prompt: str,user_input: str,obachan_pic) -> None:
    """Processes a message and adds the response to the chat."""
    st.session_state.messages.append(
        {"role": "user", "content": [{"type": "text", "text": prompt}]}
    )
    with st.chat_message("user"):
        st.markdown(user_input)
    with st.chat_message("assistant",avatar=obachan_pic):
        with st.spinner("ちょいまちや!"):
            response = send_message(prompt=prompt)
            request_id = response["request_id"]
            content = response["message"]["content"]
            #st.write(content) #DEBUG
            #####ここでおばちゃん化#######
            answer,sql,df_for_answer = obachan_answer(user_input,content)

            st.session_state.messages.append(
                {**response['message'], "request_id": request_id, "answer":answer, "sql":sql}
            )
            display_content(content=content, request_id=request_id, answer=answer, sql=sql,df_for_answer=df_for_answer)  # type: ignore[arg-type]

def display_content(
    content: List[Dict[str, str]],
    request_id: Optional[str] = None,
    message_index: Optional[int] = None,
    answer: str = "",
    sql: str = "",
    df_for_answer: str =None) -> None:
    """Displays a content item for a message."""
    message_index = message_index or len(st.session_state.messages)
    st.markdown(answer)
    st.write(df_for_answer)
    with st.expander("SQL Query", expanded=False):
        st.code(sql, language="sql")

def obachan_answer(user_input,content):
    """
    関西弁の回答を作成
    """

    for item in content:
        if item["type"] == "sql":
            sql = item["statement"]
    session = get_active_session()        
    df_for_answer = session.sql(sql).to_pandas()

    kansaiben_prompt = f"""          
    あたなは、とても元気なやかましい大阪のおばちゃんです。
    以下の質問と、質問で参照するデータフレームに基づいて、回答してください。
    質問: {user_input}
    データフレーム: {df_for_answer}

    注意:
    - 分析結果は、下記の「関西弁説明」を学習し、関西弁で回答してください。
    - 敬語は使わないでください。
    - 質問に関連する面白い話を加えてください。
    - 最後に「知らんけど」か「飴ちゃんやるわ」と言ってください。

    -関西弁説明
        -語彙
            -ありがとう:標準語では「ありがとう」ですが、関西弁では「おおきに」と言います。
            -とても:標準語の「とても」は、関西弁では「めっちゃ」や「ほんまに」と言います。
            -疲れた:標準語の「疲れた」は、関西弁では「しんどい」と言います。
            -だめ:標準語の「だめ」は、関西弁では「あかん」と言います。
            -すごい:標準語の「すごい」は、関西弁では「えらい」と言います。
        -文法
            -標準語の「〜している」は、関西弁では「〜しとる」と言います。
            -標準語の「〜していない」は、関西弁では「〜してへん」と言います。
            -標準語の「〜だ」は、関西弁では「〜や」と言います。例えば、「これは本だ」は「これは本や」となります。
            -標準語の「〜でしょう」は、関西弁では「〜やろ」と言います。例えば、「明日は雨でしょう」は「明日は雨やろ」となります。
            -標準語の「〜ない」は、関西弁では「〜へん」と言います。例えば、「行かない」は「行かへん」となります。 

    """
    result = Complete("mistral-large2",kansaiben_prompt)
    return result,sql,df_for_answer

def read_osaka_restaurants_df():
    """
    大阪の飲食店データを取得
    """
    target_table = 'FOOD_ESTABLISHMENTS_DATA_SET_IN_JAPAN.MART.FOOD_SAMPLE_OSAKA_CHUO'
    #データを取得
    query = f"SELECT * FROM {target_table} limit 10"
    osaka_restaurants_data = session.sql(query).collect()
    osaka_restaurants_df = pd.DataFrame(osaka_restaurants_data)
    st.write("🔵大阪市中央区の飲食店データの中身を確認")
    st.write(osaka_restaurants_df) #debug

def add_prompt_request(user_input: str) -> str:
    added_prompt = f"""          
    質問に対して、日本語で回答してください。
    質問: {user_input}
    """
    return added_prompt

def reset() -> None:
    st.session_state.messages = []
    st.session_state.suggestions = []
    st.session_state.active_suggestion = None

######################### メイン処理 #########################
st.title("飲食店に詳しい大阪のおばちゃんチャットボット🐯🐙🍬")
st.markdown(f"Semantic Model: {FILE}")

if "messages" not in st.session_state:
    reset()

# Get the current credentials
session = get_active_session()
read_osaka_restaurants_df()

if user_input := st.chat_input("何食いたいんや?言うてみ!"):
    prompt = add_prompt_request(user_input)
    process_message(prompt, user_input, obachan_pic)

if st.session_state.active_suggestion:
    process_message(prompt=st.session_state.active_suggestion)
    st.session_state.active_suggestion = None