SnowflakeのCortex Agentsを試してみた

SnowflakeのCortex Agentsを試してみた

Clock Icon2025.03.16

はじめに

データ事業本部ビッグデータチームのkasamaです。
今回はSnowflakeのCortex Agentsをサンプルデータを用いて試したのでその内容を残しておきたいと思います。

Cortex Agentsとは

Cortex Agents は、Snowflake が提供する AI エージェントで、構造化データと非構造化データの両方を処理できる包括的なソリューションです。主に以下の 4 つの要素で構成されています:

  1. 計画

    • 複雑なクエリを解析し最適な処理計画を作成
    • 曖昧な質問を明確化するためのオプション探索
    • 複雑なタスクをサブタスクに分割
    • 適切なツールへのルーティング
  2. ツールの使用

    • Cortex Search: 文書などの非構造化データからのインサイト抽出
    • Cortex Analyst: 構造化データ処理のための SQL 生成
  3. 反映

    • 結果の評価と次のステップの決定
    • Snowflake のセキュアな環境内でのデータ処理
  4. モニタリングとイテレーション

    • TruLens によるインタラクション監視
    • 継続的な改善とガバナンス制御

Cortex Agents は REST API として提供され、あらゆるアプリケーションに統合可能です。Anthropic Claude などの LLM と連携し、高品質なデータ分析と回答生成を実現します。Snowflake のエコシステム内で動作することで、セキュリティとコンプライアンスを維持しながら AI 駆動の意思決定を支援します。詳細は以下の記事をご確認ください。

https://www.snowflake.com/ja/blog/ai-data-agents-snowflake-cortex/
https://docs.snowflake.com/en/user-guide/snowflake-cortex/cortex-agents

今回は以下のチュートリアルを参考にデータは生成AIによって生成したサンプルデータを用います。
https://quickstarts.snowflake.com/guide/getting_started_with_cortex_agents/index.html?index=..%2F..index#0

Snowflake Cortexは、LLM(大規模言語モデル)を活用したAI機能群です。Cortex Agents以外のサービスの紹介は以下が参考になりました。
https://www.snowflake.com/ja/product/features/cortex/
https://qiita.com/Ayumu-y/items/9a94d40645deb335d3e2

実装

sampleデータ作成

今回は、旅行のレビュー情報tableと予約情報tableの2種類を作成します。

sampleデータ作成
create_table.sql
-- Create database and schema
CREATE OR REPLACE DATABASE travel_intelligence;
CREATE OR REPLACE SCHEMA travel_intelligence.data;

USE DATABASE travel_intelligence;
USE SCHEMA data;

-- Create tables for travel data
CREATE TABLE travel_reviews (
    review_id VARCHAR,
    review_text TEXT,
    customer_name VARCHAR,
    destination VARCHAR,
    hotel_name VARCHAR,
    review_date TIMESTAMP,
    rating FLOAT,
    trip_type VARCHAR
);

CREATE TABLE booking_metrics (
    booking_id VARCHAR,
    customer_name VARCHAR,
    booking_value FLOAT,
    travel_date DATE,
    booking_status VARCHAR,
    is_cancelled BOOLEAN,
    travel_agent VARCHAR,
    trip_type VARCHAR
);

-- Insert data into travel_reviews with Japanese content
INSERT INTO travel_reviews 
(review_id, review_text, customer_name, destination, hotel_name, review_date, rating, trip_type)
VALUES
('REV001', '沖縄の「サンセットリゾート石垣島」での滞在は素晴らしかったです。部屋からのエメラルドグリーンの海の眺めは息をのむほど美しく、プライベートビーチへの直接アクセスが最高でした。スタッフの対応も非常に丁寧で、特にフロントの佐藤さんには地元の隠れた名所を教えていただき感謝しています。リゾート内のレストランでは新鮮な海鮮料理が楽しめ、特に夕食時の日本酒の種類の豊富さに驚きました。マリンアクティビティも充実しており、シュノーケリングでは色とりどりのサンゴと熱帯魚を間近で見ることができました。唯一の不満点は、ピーク時のプールエリアが少し混雑していたことですが、早朝や夕方は比較的空いていました。家族連れやカップルにぴったりのリゾートで、ぜひまた訪れたいと思います。', '田中 雅彦', '沖縄', 'サンセットリゾート石垣島', '2024-01-15 10:30:00', 4.8, '家族旅行'),

('REV002', '京都「古都の宿 嵐山」での3泊の滞在は、期待と少し異なる体験でした。伝統的な和室と庭園の景観は確かに美しく、朝食の京懐石料理も本格的で美味しかったです。しかし、施設の一部が古く、特に浴室は更新が必要だと感じました。また、繁忙期だったためか、接客がやや機械的で個人的な温かみに欠ける印象を受けました。立地は嵐山の観光スポットへのアクセスが良く、特に竹林への散歩道は素晴らしかったです。夕食は近くの居酒屋を探索し、地元の方と交流できたのが良い思い出です。Wi-Fiの接続が部屋によって不安定だったのも改善点です。総合的には満足できる滞在でしたが、価格を考えるともう少しサービスの質を上げてほしいと思いました。', '佐藤 恵子', '京都', '古都の宿 嵐山', '2024-01-16 14:45:00', 3.5, 'カルチャーツアー'),

('REV003', '北海道「雪国グランドホテル」での冬季滞在は最高の体験でした!部屋から見える雪景色が絵画のように美しく、大浴場からの夜景も格別でした。スキーリゾートへの無料シャトルバスが便利で、ホテルのレンタル用品も質が良く、初心者の私たちにもスタッフが丁寧に使い方を教えてくれました。夕食バイキングでは北海道の新鮮な食材を使った料理が豊富で、特にカニとジンギスカンが絶品でした。子供向けの雪遊びプログラムもあり、息子は大喜びでした。客室は広く清潔で、床暖房が寒い季節にとても快適でした。スパ施設も充実しており、スキーの疲れを癒すのに最適でした。スタッフ全員が笑顔で対応してくれ、特にコンシェルジュの山田さんには地元ならではの体験を多く紹介していただきました。冬の北海道旅行には絶対おすすめのホテルです!', '伊藤 健太', '北海道', '雪国グランドホテル', '2024-01-17 11:20:00', 5.0, 'ウィンターリゾート'),

('REV004', '東京「メトロポリタンタワーホテル」でのビジネス滞在は効率的で快適でした。空港からのアクセスが良く、チェックインも迅速でした。ビジネスセンターの設備が充実しており、急な会議資料の印刷にも対応できました。部屋は機能的で、デスクスペースが広く、照明も作業に適していました。朝食ビュッフェは種類が豊富で、特に和食メニューのクオリティが高かったです。フィットネスセンターは24時間利用可能で、出張中の運動不足解消に役立ちました。部屋の防音性も良く、繁華街に近いにも関わらず静かに休むことができました。唯一の不満点は、ルームサービスのメニューがやや限られていたことです。総合的には、ビジネス目的の滞在に最適なホテルで、次回の東京出張でも利用したいと思います。', '鈴木 大輔', '東京', 'メトロポリタンタワーホテル', '2024-01-18 09:15:00', 4.5, 'ビジネス'),

('REV005', '福岡「博多ベイサイドホテル」での滞在は期待以上でした。最近リノベーションされたという客室は、モダンで清潔感があり、特にベッドの寝心地が素晴らしかったです。朝食で出された明太子と新鮮な魚介類を使った料理が印象的で、地元の味を堪能できました。ホテルのスタッフは皆さん親切で、特にフロントの中村さんには観光スポットや地元の美味しい飲食店を詳しく教えていただきました。立地も抜群で、博多駅から徒歩圏内なのに、部屋からは静かな海の景色が楽しめました。屋上テラスからの夕日も格別でした。館内のバーでは地元の焼酎やクラフトビールが楽しめ、夜のリラックスタイムに最適でした。価格も手頃で、コストパフォーマンスが非常に高いと感じました。福岡旅行には絶対におすすめのホテルです。', '山田 美咲', '福岡', '博多ベイサイドホテル', '2024-01-19 13:30:00', 4.7, '都市観光'),

('REV006', '長野「信州温泉旅館 森の雫」での滞在は、日常から完全に離れてリラックスするのに最適でした。100年以上の歴史を持つ建物は風情があり、所々に見られる伝統工芸品のディスプレイも魅力的でした。温泉は源泉かけ流しで、露天風呂からの山の景色が絶景でした。特に雪が降った翌朝の風景は忘れられません。夕食は個室で提供され、地元の食材を使った会席料理は見た目も味も素晴らしく、特に信州牛の石焼きと山菜の天ぷらが絶品でした。仲居さんの細やかなサービスと地元の歴史についての知識も深く、会話を楽しみました。客室は十分な広さがあり、和の雰囲気を大切にしながらも必要な現代的な設備が整っていました。朝の散歩コースも整備されており、清々しい空気の中での散策が気持ち良かったです。心身ともにリフレッシュできる、本物の日本旅館体験ができました。', '高橋 裕子', '長野', '信州温泉旅館 森の雫', '2024-01-20 15:45:00', 4.9, '温泉旅行'),

('REV007', '沖縄「コーラルリーフリゾート宮古島」での新婚旅行は、一生の思い出になりました。部屋から一歩出ればそこは白い砂浜で、透明度抜群の海が広がっていました。スイートルームは広々として清潔、バルコニーからの夕日が特に美しかったです。リゾート内のレストランでは沖縄料理と国際料理の両方が楽しめ、特に地元の食材を使ったイタリアンフュージョン料理が印象的でした。スパでのカップルマッサージは、旅の疲れを癒すのに最適でした。マリンアクティビティも充実しており、シュノーケリングツアーでは珍しいウミガメと一緒に泳ぐことができました。スタッフは皆さん笑顔で対応してくれ、特別な記念日ということで部屋にシャンパンとフルーツバスケットを用意してくれたサプライズには感動しました。ただ、レストランの予約が取りにくかったのが少し残念でした。それでも総合的には最高のリゾート体験で、記念日旅行に最適です。', '中村 健太', '沖縄', 'コーラルリーフリゾート宮古島', '2024-01-21 10:00:00', 4.8, '新婚旅行'),

('REV008', '北海道「ファームステイ十勝」での農業体験は、都会の喧騒を忘れさせてくれる素晴らしい経験でした。宿泊施設は元々農家の家屋を改装したもので、古き良き日本の田舎家の雰囲気を味わえました。オーナーの木村さん夫妻が温かく迎えてくれ、地元の歴史や農業について多くのことを学びました。朝は新鮮な卵を自分で採取し、それを使った朝食は格別でした。日中は野菜の収穫や乳牛の世話を体験し、子供たちは特に動物たちとの触れ合いを楽しんでいました。夕食は自分たちが収穫した野菜を使ったバーベキューで、星空の下での食事は忘れられない思い出です。施設は決して豪華ではありませんが、清潔で必要なものは揃っており、自然と共に過ごす本当の贅沢を感じました。Wi-Fiは弱かったですが、それも都会から離れる良い機会になりました。家族で本物の農業体験を求める方には、心からおすすめできる場所です。', '小林 家族', '北海道', 'ファームステイ十勝', '2024-01-22 14:20:00', 4.6, '体験型旅行'),

('REV009', '奈良「古都の宿 飛鳥」での滞在は、日本の歴史と文化を深く体験できる素晴らしいものでした。1300年以上の歴史を持つ寺院の敷地内に位置するこの宿は、静寂と厳かな雰囲気に包まれていました。部屋は最小限の家具で飾られた禅スタイルで、窓からは手入れの行き届いた日本庭園が見えました。朝5時からの朝のお勤めに参加し、その後の精進料理の朝食は心身ともに清められる体験でした。夕食も精進料理でしたが、野菜や豆腐を使った料理の繊細な味わいに感動しました。館内では書道や座禅の体験プログラムがあり、特に住職による仏教の教えについての講話が印象的でした。客室には現代的な設備はほとんどなく、テレビやWi-Fiもありませんが、それが逆に現代社会から離れて内省する貴重な時間となりました。静かな環境で自分自身と向き合いたい方、日本の伝統文化に興味がある方には特におすすめです。', '渡辺 哲也', '奈良', '古都の宿 飛鳥', '2024-01-23 16:30:00', 4.7, '文化体験'),

('REV010', '静岡「富士見テラス」での滞在は、景観と料理の両方で感動的な体験でした。部屋からの富士山の眺めは息をのむほど美しく、晴れた朝に目覚めて窓から見た富士山のシルエットは一生の宝物です。客室はモダンな和洋折衷スタイルで、特に檜風呂からも富士山が見えるという贅沢な設計でした。夕食は地元の食材を使った懐石料理で、特に静岡の海の幸と山の幸を融合させた創作料理は芸術品のようでした。朝食も部屋食で、地元の新鮮な野菜と卵を使った料理が美味しかったです。温泉は源泉かけ流しで、夜の星空を見ながらの入浴は格別でした。スタッフの方々も親切で、特に仲居さんの細やかな気配りが心地よかったです。館内には季節の花や伝統工芸品が飾られ、日本の美意識を感じられました。価格は高めですが、特別な記念日や贅沢な休暇にはぜひ訪れる価値があります。', '木村 直子', '静岡', '富士見テラス', '2024-01-24 11:45:00', 4.9, '高級旅行');

-- Now, let's insert corresponding data into booking_metrics
INSERT INTO booking_metrics 
(booking_id, customer_name, booking_value, travel_date, booking_status, is_cancelled, travel_agent, trip_type)
VALUES
('BOOK001', '田中 雅彦', 150000, '2024-02-15', '確定', false, '山本 旅行代理店', '家族旅行'),

('BOOK002', '佐藤 恵子', 85000, '2024-02-01', 'キャンセル', true, '日本トラベル', 'カルチャーツアー'),

('BOOK003', '伊藤 健太', 220000, '2024-01-30', '確定', false, '雪国エクスプレス', 'ウィンターリゾート'),

('BOOK004', '鈴木 大輔', 65000, '2024-02-10', '保留中', false, 'ビジネストラベル株式会社', 'ビジネス'),

('BOOK005', '山田 美咲', 95000, '2024-02-05', '確定', false, '福岡ツーリスト', '都市観光'),

('BOOK006', '高橋 裕子', 180000, '2024-02-20', '保留中', false, '温泉めぐり', '温泉旅行'),

('BOOK007', '中村 健太', 350000, '2024-01-25', '確定', false, 'ハネムーンスペシャリスト', '新婚旅行'),

('BOOK008', '小林 家族', 120000, '2024-02-08', '確定', false, '北海道エクスペリエンス', '体験型旅行'),

('BOOK009', '渡辺 哲也', 75000, '2024-02-12', '確定', false, '古都めぐり', '文化体験'),

('BOOK010', '木村 直子', 280000, '2024-02-18', '保留中', false, 'プレミアムジャパン', '高級旅行'),
-- 家族旅行の追加データ
('BOOK011', '吉田 康介', 165000, '2024-02-22', '確定', false, '山本 旅行代理店', '家族旅行'),
('BOOK012', '加藤 真理', 142000, '2024-02-25', '確定', false, 'ファミリートラベル', '家族旅行'),
('BOOK013', '松本 大輔', 198000, '2024-03-01', '確定', false, '日本ツーリスト', '家族旅行'),
('BOOK014', '井上 直樹', 175000, '2024-03-05', 'キャンセル', true, '山本 旅行代理店', '家族旅行'),
('BOOK015', '佐々木 美咲', 155000, '2024-03-10', '確定', false, 'ファミリートラベル', '家族旅行'),
('BOOK016', '山口 健一', 168000, '2024-03-15', 'キャンセル', true, '日本ツーリスト', '家族旅行'),
('BOOK017', '中島 裕子', 182000, '2024-03-20', '確定', false, '山本 旅行代理店', '家族旅行'),
('BOOK018', '岡田 拓也', 195000, '2024-03-25', '確定', false, 'ファミリートラベル', '家族旅行'),
('BOOK019', '後藤 明美', 159000, '2024-04-01', 'キャンセル', true, '日本ツーリスト', '家族旅行'),
('BOOK020', '村田 健太', 172000, '2024-04-05', '確定', false, '山本 旅行代理店', '家族旅行'),

-- ビジネス旅行の追加データ
('BOOK021', '田村 隆', 58000, '2024-02-22', '確定', false, 'ビジネストラベル株式会社', 'ビジネス旅行'),
('BOOK022', '西村 直子', 62000, '2024-02-25', 'キャンセル', true, 'エグゼクティブトラベル', 'ビジネス旅行'),
('BOOK023', '斎藤 健', 71000, '2024-03-01', '確定', false, 'ビジネストラベル株式会社', 'ビジネス旅行'),
('BOOK024', '上田 真由美', 59000, '2024-03-05', '確定', false, 'エグゼクティブトラベル', 'ビジネス旅行'),
('BOOK025', '原田 哲也', 68000, '2024-03-10', 'キャンセル', true, 'ビジネストラベル株式会社', 'ビジネス旅行'),
('BOOK026', '藤田 恵美', 63000, '2024-03-15', '確定', false, 'エグゼクティブトラベル', 'ビジネス旅行'),
('BOOK027', '小川 拓也', 72000, '2024-03-20', 'キャンセル', true, 'ビジネストラベル株式会社', 'ビジネス旅行'),
('BOOK028', '長谷川 美香', 67000, '2024-03-25', '確定', false, 'エグゼクティブトラベル', 'ビジネス旅行'),
('BOOK029', '石田 浩二', 61000, '2024-04-01', 'キャンセル', true, 'ビジネストラベル株式会社', 'ビジネス旅行'),
('BOOK030', '山本 由美', 69000, '2024-04-05', '確定', false, 'エグゼクティブトラベル', 'ビジネス旅行'),

-- カルチャーツアーの追加データ
('BOOK031', '近藤 隆史', 92000, '2024-02-22', '確定', false, '日本トラベル', 'カルチャーツアー'),
('BOOK032', '遠藤 香織', 88000, '2024-02-25', 'キャンセル', true, '古都めぐり', 'カルチャーツアー'),
('BOOK033', '青木 誠', 95000, '2024-03-01', 'キャンセル', true, '日本トラベル', 'カルチャーツアー'),
('BOOK034', '福田 美穂', 87000, '2024-03-05', '確定', false, '古都めぐり', 'カルチャーツアー'),
('BOOK035', '松田 健太郎', 93000, '2024-03-10', '確定', false, '日本トラベル', 'カルチャーツアー'),

-- 温泉旅行の追加データ
('BOOK036', '杉山 洋子', 175000, '2024-02-22', '確定', false, '温泉めぐり', '温泉旅行'),
('BOOK037', '野村 達也', 185000, '2024-02-25', '確定', false, '湯めぐり紀行', '温泉旅行'),
('BOOK038', '木下 真紀', 172000, '2024-03-01', 'キャンセル', true, '温泉めぐり', '温泉旅行'),
('BOOK039', '菊地 健一', 188000, '2024-03-05', '確定', false, '湯めぐり紀行', '温泉旅行'),
('BOOK040', '岩崎 恵子', 179000, '2024-03-10', '確定', false, '温泉めぐり', '温泉旅行'),

-- 新婚旅行の追加データ
('BOOK041', '三浦 大輔', 320000, '2024-02-22', '確定', false, 'ハネムーンスペシャリスト', '新婚旅行'),
('BOOK042', '中野 美咲', 345000, '2024-02-25', '確定', false, 'ウェディングトラベル', '新婚旅行'),
('BOOK043', '川村 拓也', 380000, '2024-03-01', 'キャンセル', true, 'ハネムーンスペシャリスト', '新婚旅行'),
('BOOK044', '林 恵梨子', 335000, '2024-03-05', '確定', false, 'ウェディングトラベル', '新婚旅行'),
('BOOK045', '横山 裕太', 365000, '2024-03-10', '確定', false, 'ハネムーンスペシャリスト', '新婚旅行'),

-- 体験型旅行の追加データ
('BOOK046', '星野 健太', 125000, '2024-02-22', '確定', false, '北海道エクスペリエンス', '体験型旅行'),
('BOOK047', '大塚 真理子', 118000, '2024-02-25', 'キャンセル', true, 'アドベンチャーツアー', '体験型旅行'),
('BOOK048', '本田 隆史', 132000, '2024-03-01', '確定', false, '北海道エクスペリエンス', '体験型旅行'),
('BOOK049', '今井 美香', 122000, '2024-03-05', 'キャンセル', true, 'アドベンチャーツアー', '体験型旅行'),
('BOOK050', '西田 浩二', 128000, '2024-03-10', '確定', false, '北海道エクスペリエンス', '体験型旅行'),

-- 文化体験の追加データ
('BOOK051', '竹内 智子', 82000, '2024-02-22', '確定', false, '古都めぐり', '文化体験'),
('BOOK052', '内田 正人', 78000, '2024-02-25', 'キャンセル', true, '伝統文化ツアー', '文化体験'),
('BOOK053', '高木 裕美', 85000, '2024-03-01', '確定', false, '古都めぐり', '文化体験'),
('BOOK054', '安藤 健太', 79000, '2024-03-05', '確定', false, '伝統文化ツアー', '文化体験'),
('BOOK055', '坂本 恵子', 83000, '2024-03-10', 'キャンセル', true, '古都めぐり', '文化体験'),

-- 高級旅行の追加データ
('BOOK056', '丸山 隆', 295000, '2024-02-22', '確定', false, 'プレミアムジャパン', '高級旅行'),
('BOOK057', '小島 真理', 310000, '2024-02-25', '確定', false, 'ラグジュアリートラベル', '高級旅行'),
('BOOK058', '浜田 健一', 325000, '2024-03-01', 'キャンセル', true, 'プレミアムジャパン', '高級旅行'),
('BOOK059', '平野 美咲', 302000, '2024-03-05', '確定', false, 'ラグジュアリートラベル', '高級旅行'),
('BOOK060', '大西 拓也', 318000, '2024-03-10', '確定', false, 'プレミアムジャパン', '高級旅行'),

-- 都市観光の追加データ
('BOOK061', '藤井 恵子', 98000, '2024-02-22', '確定', false, '福岡ツーリスト', '都市観光'),
('BOOK062', '村上 健太', 92000, '2024-02-25', 'キャンセル', true, 'シティエクスプローラー', '都市観光'),
('BOOK063', '太田 真由美', 105000, '2024-03-01', '確定', false, '福岡ツーリスト', '都市観光'),
('BOOK064', '中山 隆史', 97000, '2024-03-05', '確定', false, 'シティエクスプローラー', '都市観光'),
('BOOK065', '石川 香織', 103000, '2024-03-10', 'キャンセル', true, '福岡ツーリスト', '都市観光'),

-- ウィンターリゾートの追加データ
('BOOK066', '前田 大輔', 215000, '2024-02-22', '確定', false, '雪国エクスプレス', 'ウィンターリゾート'),
('BOOK067', '柴田 美穂', 225000, '2024-02-25', 'キャンセル', true, 'スキーリゾートツアー', 'ウィンターリゾート'),
('BOOK068', '酒井 健太郎', 235000, '2024-03-01', '確定', false, '雪国エクスプレス', 'ウィンターリゾート'),
('BOOK069', '工藤 恵美', 218000, '2024-03-05', 'キャンセル', true, 'スキーリゾートツアー', 'ウィンターリゾート'),
('BOOK070', '横田 隆', 228000, '2024-03-10', '確定', false, '雪国エクスプレス', 'ウィンターリゾート');

cortex search作成

create_cortex_search.sql
-- Enable change tracking
ALTER TABLE travel_reviews SET CHANGE_TRACKING = TRUE;

-- Create the search service
CREATE OR REPLACE CORTEX SEARCH SERVICE travel_review_search
  ON review_text
  ATTRIBUTES customer_name, destination, hotel_name, review_date, rating, trip_type
  WAREHOUSE = compute_wh
  TARGET_LAG = '1 minute'
  AS (
    SELECT
        review_id,
        review_text,
        customer_name,
        destination,
        hotel_name,
        review_date,
        rating,
        trip_type
    FROM travel_reviews
    WHERE review_date >= '2024-01-01'
);

このSQLコードは、旅行レビューtableに対するCortex Search機能を実装しています。まずALTER TABLEコマンドでtravel_reviewsテーブルの変更追跡を有効化し、次にCREATE CORTEX SEARCH SERVICEで検索サービスを作成して、review_textフィールドを検索対象とし、顧客名や目的地などの属性を指定しています。検索サービスはcompute_whウェアハウスを使用し、1分間の更新ラグを設定し、2024年以降のレビューデータのみを対象としています。この設定により、ユーザーは自然言語で旅行レビューデータを検索・分析できるようになります。

例えば以下のクエリを実行します。

SELECT PARSE_JSON(
  SNOWFLAKE.CORTEX.SEARCH_PREVIEW(
      'travel_intelligence.data.travel_review_search',
      '{
        "query": "富士山の景色が見える高級旅館",
        "columns": [
            "review_id",
            "customer_name",
            "hotel_name",
            "destination",
            "rating",
            "review_text"
        ],
        "limit": 3
      }'
  )
)['results'] as results;

すると最初に最も類似性のある富士山の近くのホテルのレビューデータが表示されます。
Screenshot 2025-03-16 at 21.48.36

STAGE作成

次に以下のコマンドでSTAGEを作成します。DIRECTORY = (ENABLE = TRUE) オプションはこのステージ内でディレクトリ構造の使用を有効にしており、階層的なファイル管理が可能になります。

CREATE OR REPLACE STAGE models 
    DIRECTORY = (ENABLE = TRUE);

semantic model設定

semantic modelは、ビジネスユーザーとデータベース定義の言語の違いを橋渡しする軽量なメカニズムです。データセットに関する追加の意味的詳細(より説明的な名前や同義語など)を指定でき、これによって Cortex Analyst はデータに関する質問により正確に回答できるようになります。YAML ファイル形式で定義され、データベーススキーマだけでは不足するビジネスプロセス定義やメトリクス処理などの重要な知識を補完します。
今回は手動で作成しましたが、以下の手順で画面から作成することも可能です。

  1. In Snowsight, select AI & ML.
  2. Next to Cortex Analyst, select Try.
  3. Choose Create new.

https://docs.snowflake.com/en/user-guide/snowflake-cortex/cortex-analyst/semantic-model-spec

booking_metrics_model.yaml
booking_metrics_model.yaml
name: booking_metrics
description: 旅行予約データと分析モデル
tables:
  - name: BOOKING_METRICS
    base_table:
      database: TRAVEL_INTELLIGENCE
      schema: DATA
      table: BOOKING_METRICS
    dimensions:
      - name: BOOKING_ID
        expr: BOOKING_ID
        data_type: VARCHAR(16777216)
        sample_values:
          - BOOK001
          - BOOK002
          - BOOK003
        description: 予約の一意の識別子。個々の予約を追跡および分析するために使用されます。
        synonyms:
          - 予約番号
          - 予約コード
          - 申込ID
          - 注文ID
          - 予約識別子
      - name: CUSTOMER_NAME
        expr: CUSTOMER_NAME
        data_type: VARCHAR(16777216)
        sample_values:
          - 田中 雅彦
          - 佐藤 恵子
          - 伊藤 健太
        description: 予約を行った顧客の名前。
        synonyms:
          - 予約者名
          - 利用者
          - 顧客
          - 宿泊者名
          - お客様名
      - name: BOOKING_STATUS
        expr: BOOKING_STATUS
        data_type: VARCHAR(16777216)
        sample_values:
          - 確定
          - キャンセル
          - 保留中
        description: 予約の現在のステータス。確定済み、キャンセル済み、または保留中かを示します。
        synonyms:
          - 予約状況
          - 申込状態
          - 予約ステータス
          - 契約状況
          - 予約進捗
      - name: IS_CANCELLED
        expr: IS_CANCELLED
        data_type: BOOLEAN
        sample_values:
          - 'TRUE'
          - 'FALSE'
        description: 予約がキャンセルされたかどうかを示します(TRUE=キャンセル、FALSE=有効)。
        synonyms:
          - キャンセル済
          - 取消状態
          - 解約フラグ
          - 取り消し有無
          - キャンセル状態
      - name: TRAVEL_AGENT
        expr: TRAVEL_AGENT
        data_type: VARCHAR(16777216)
        sample_values:
          - 山本 旅行代理店
          - 日本トラベル
          - 雪国エクスプレス
        description: 予約を担当した旅行代理店または担当者。
        synonyms:
          - 代理店
          - 旅行会社
          - 予約担当
          - エージェント
          - 仲介業者
      - name: TRIP_TYPE
        expr: TRIP_TYPE
        data_type: VARCHAR(16777216)
        sample_values:
          - 家族旅行
          - カルチャーツアー
          - ウィンターリゾート
        description: 予約された旅行の種類や目的を示す分類。
        synonyms:
          - 旅行カテゴリ
          - 旅のタイプ
          - 旅行目的
          - 旅行スタイル
          - 旅行形態
    time_dimensions:
      - name: TRAVEL_DATE
        expr: TRAVEL_DATE
        data_type: DATE
        sample_values:
          - '2024-02-15'
          - '2024-02-01'
          - '2024-01-30'
        description: 旅行の予定日または出発日。
        synonyms:
          - 出発日
          - 宿泊日
          - 旅行予定日
          - 利用日
          - 予約日
    measures:
      - name: BOOKING_VALUE
        expr: BOOKING_VALUE
        data_type: FLOAT
        sample_values:
          - '150000'
          - '85000'
          - '220000'
        description: 予約の総額(日本円)。
        synonyms:
          - 予約金額
          - 旅行費用
          - 宿泊料金
          - 支払額
          - 料金

作成したbooking_metrics_model.yamlは、StagesのMODELSを選択し、右上の➕ FilesボタンよりUploadします。
Screenshot 2025-03-16 at 22.13.29

Streamlit Apps設定

最後にナビゲーションメニューからStreamlit Appをを作成します。その際は、databaseをtravel_intelligence、schemaをdataを選択します。ソースコードを以下のものに置き換えてRunします。

streamlit.py
streamlit.py
import streamlit as st
import json
import _snowflake
from snowflake.snowpark.context import get_active_session
import pandas as pd
import altair as alt

session = get_active_session()

API_ENDPOINT = "/api/v2/cortex/agent:run"
API_TIMEOUT = 10000  # in milliseconds

CORTEX_SEARCH_SERVICES = "travel_intelligence.data.travel_review_search"
SEMANTIC_MODELS = "@travel_intelligence.data.models/booking_metrics_model.yaml"
SQL_MODEL = "llama3.1-70b"
ANSWER_MODEL = "llama3.1-70b"

st.set_page_config(page_title="旅行データアシスタント", page_icon="✈️")

def run_snowflake_query(query):
    """
    Snowflakeにクエリを実行し、結果を返す関数

    引数:
        query (str): 実行するSQLクエリ

    戻り値:
        DataFrame: クエリ結果のデータフレーム、エラー時はNone
    """
    try:
        return session.sql(query.replace(";", ""))
    except Exception as e:
        st.error(f"SQLエラー: {str(e)}")
        return None

def snowflake_api_call(query: str, model, limit: int = 5):
    """
    Cortex Agent APIを呼び出し、自然言語クエリを処理する関数

    引数:
        query (str): ユーザーからの自然言語クエリ
        limit (int): 検索結果の最大件数

    戻り値:
        dict: APIレスポンス、エラー時はNone
    """
    payload = {
        "model": model,
        "messages": [{"role": "user", "content": [{"type": "text", "text": query}]}],
        "tools": [
            # SQL生成ツール
            {"tool_spec": {"type": "cortex_analyst_text_to_sql", "name": "analyst1"}},
            # 検索ツール
            {"tool_spec": {"type": "cortex_search", "name": "search1"}},
        ],
        "tool_resources": {
            "analyst1": {"semantic_model_file": SEMANTIC_MODELS},
            "search1": {
                "name": CORTEX_SEARCH_SERVICES,
                "max_results": limit,
                "id_column": "review_id",
            },
        },
    }

    try:
        resp = _snowflake.send_snow_api_request(
            "POST", API_ENDPOINT, {}, {}, payload, None, API_TIMEOUT
        )

        if resp["status"] != 200:
            st.error(f"❌ HTTPエラー: {resp['status']}")
            st.error(f"エラー詳細: {resp.get('reason', 'なし')}")
            st.error(f"レスポンス内容: {resp.get('content', 'なし')}")
            return None

        return json.loads(resp["content"])

    except Exception as e:
        st.error(f"リクエストエラー: {str(e)}")
        return None

def process_sse_response(response):
    """
    APIからのレスポンスを処理し、テキスト、SQL、引用情報を抽出する関数

    引数:
        response (dict): APIレスポンス

    戻り値:
        tuple: (テキスト回答, 生成されたSQL, 引用情報のリスト)
    """
    text, sql, citations = "", "", []

    if not response or isinstance(response, str):
        return text, sql, citations

    try:
        for event in response:
            if event.get("event") == "message.delta":
                data = event.get("data", {})
                delta = data.get("delta", {})

                for content_item in delta.get("content", []):
                    content_type = content_item.get("type")
                    if content_type == "tool_results":
                        tool_results = content_item.get("tool_results", {})
                        if "content" in tool_results:
                            for result in tool_results["content"]:
                                if result.get("type") == "json":
                                    text += result.get("json", {}).get("text", "")
                                    search_results = result.get("json", {}).get(
                                        "searchResults", []
                                    )
                                    for search_result in search_results:
                                        citations.append(
                                            {
                                                "source_id": search_result.get(
                                                    "source_id", ""
                                                ),
                                                "doc_id": search_result.get(
                                                    "doc_id", ""
                                                ),
                                            }
                                        )
                                    sql = result.get("json", {}).get("sql", "")
                    if content_type == "text":
                        text += content_item.get("text", "")
    except Exception as e:
        st.error(f"処理エラー: {str(e)}")

    return text, sql, citations

def main():
    """
    main:Streamlitアプリケーションのユーザーインターフェースと処理フローを定義
    """
    st.title("✈️ 旅行データ分析アシスタント")

    # サイドバー
    with st.sidebar:
        if st.button("新しい会話"):
            st.session_state.messages = []
            st.rerun()

        st.markdown("### 質問例")
        st.markdown("""
        - 沖縄のホテルで最も評価の高いところはどこ?
        - 富士山が見える高級旅館のレビューを教えてください。
        - 家族旅行とビジネス旅行の予約金額を比較してください。
        - キャンセル率が高い旅行タイプを比較してください。
        """)

    # セッション状態の初期化
    if "messages" not in st.session_state:
        st.session_state.messages = []

    # 過去のメッセージを表示
    for message in st.session_state.messages:
        with st.chat_message(message["role"]):
            st.markdown(message["content"])

    # ユーザー入力
    if query := st.chat_input("旅行データについて質問してください"):
        # ユーザーメッセージをチャットに追加
        with st.chat_message("user"):
            st.markdown(query)
        st.session_state.messages.append({"role": "user", "content": query})

        # APIからレスポンスを取得
        with st.spinner("処理中..."):
            response = snowflake_api_call(query, SQL_MODEL)
            text, sql, citations = process_sse_response(response)

            # アシスタントの応答をチャットに追加
            if text:
                text = text.replace("【†", "[").replace("†】", "]")
                st.session_state.messages.append({"role": "assistant", "content": text})

                with st.chat_message("assistant"):
                    st.markdown(text)

                    # 引用がある場合は表示
                    if citations:
                        with st.expander("参照レビュー"):
                            for i, citation in enumerate(citations):
                                doc_id = citation.get("doc_id", "")
                                if doc_id:
                                    query = f"""
                                    SELECT * FROM travel_intelligence.data.travel_reviews 
                                    WHERE review_id = '{doc_id}'
                                    """
                                    result = run_snowflake_query(query)
                                    if result is not None:
                                        st.dataframe(result.to_pandas())

            # SQLが存在する場合は実行
            if sql:
                with st.expander("生成されたSQL"):
                    st.code(sql, language="sql")

                results = run_snowflake_query(sql)
                if results is not None:
                    df = results.to_pandas()

                    # 数値カラムを適切に変換
                    for col in df.columns:
                        if col in ["rating", "booking_value"]:
                            df[col] = pd.to_numeric(df[col], errors="coerce")

                    st.dataframe(df)

                    # データの可視化(シンプルな棒グラフ)
                    if len(df) > 0 and len(df.columns) >= 2:
                        numeric_cols = df.select_dtypes(
                            include=["number"]
                        ).columns.tolist()
                        if numeric_cols:
                            category_cols = [
                                col for col in df.columns if col not in numeric_cols
                            ]
                            if category_cols:
                                chart = (
                                    alt.Chart(df)
                                    .mark_bar()
                                    .encode(
                                        x=category_cols[0],
                                        y=numeric_cols[0],
                                        color=category_cols[0]
                                        if len(category_cols) > 0
                                        else None,
                                    )
                                    .properties(height=400)
                                )
                                st.altair_chart(chart, use_container_width=True)

if __name__ == "__main__":
    main()

実行結果

質問例をそれぞれ実行して動作を確認します。

沖縄のホテルで最も評価の高いところはどこ?
こちらの質問にはCortex Searchの機能でレビュー情報(非構造化データ)を元に回答しています。
Screenshot 2025-03-16 at 22.34.54

富士山が見える高級旅館のレビューを教えてください。
こちらも同様にCortex Searchの機能でレビュー情報(非構造化データ)を元に回答しています。
Screenshot 2025-03-16 at 22.36.20

家族旅行とビジネス旅行の予約金額を比較してください。
こちらは、Cortex Analystの機能で予約情報(構造化データ)を元にテキストからSQLを生成し、回答してくれています。
Screenshot 2025-03-16 at 22.39.12

キャンセル率が高い旅行タイプを比較してください。
こちらも同様に、Cortex Analystの機能で予約情報(構造化データ)を元にテキストからSQLを生成し、回答してくれています。
Screenshot 2025-03-16 at 22.39.51
Screenshot 2025-03-16 at 22.39.56

最後に

Cortex Agentsの機能で、構造データ/非構造データ意識せず様々な質問を自然言語でできるので、とても便利なサービスだと思いました!
今回の私の環境では、llama3.1-70bmodelを活用したのですが、claude等を活用することでさらに精度は上がると思いました。

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.