Snowflake の AI エージェント機能である Snowflake Intelligence で、Web から情報を検索し、その結果に基づいて MS PowerPoint のファイル(パワポ)を作成する機能を作ってみようという試みです。
以下の記事においてみのるんさん(@minorun365)が Amazon Bedrock エージェントでやられていることを Snowflake Intelligence でも実現してみようという内容です。
題材としてはこの記事の公開以降にかなり擦り切れた気もしますが、Snowflake Intelligence におけるカスタムツール利用の検証が目的なので、その点ご容赦ください。
動作結果を見たい人は [3. 実行] に飛んでください。
1. はじめに
Snowflake には Snowflake Intelligence という AI エージェントを作成する機能があります。主に、Snowflake 内の構造化データ(テーブルデータ)と非構造化データからインサイトを得ることが主目的の機能なのですが、ユーザー定義関数/ストアドプロシージャーを独自のカスタムツールとしてエージェントから実行させることもできます。
今回はこのカスタムツール実行の機能を把握する目的で、上にリンクを貼ったみのるんさんのパワポ作成の AI エージェントを Snowflake Intelligence で作ってみたいと思います。
実現したいことは、
- ユーザーの質問に必要な情報を Web から検索してくる
- 検索結果に基づいてパワーポイントファイルを作成する
- 作成したパワポファイルのリンクをユーザーにメールで通知する
ということになります。これらのタスクは LLM だけでは実行できないため、カスタムツール(ストアドプロシージャー)を用意し、AI エージェントから呼び出させるということになります。
(Snowflake Intelligence の本来の目的 = 「データからインサイトを得る」から考えると、実現したいことと道具がミスマッチな気もしますが、その点はご容赦ください)
Snowflake の AI エージェント機能としては元々 Cortex Agent と呼ばれる機能があります。Snowflake Intelligence と Cortex Agent の違いはよく分かっておらず、触った感じだと
- Cortex Agent = エージェント本体の機能
- Snowflake Intelligence = Cortex Agent を利用するユーザーインターフェース
のようにも思えるのですが、ドキュメント上では以下の機能が Snowflake Intelligence のページに記載されていることから、この記事では Snowflake Intelligence で統一します。
- エージェントの作成方法
- ユーザー定義関数/ストアドプロシージャーをカスタムツールとして登録する方法
2. エージェントの構築
全体の構成としては以下のようになります。
- まず、Web から情報を収集します。
- この機能を実現するために、ストアド内部から Tavily という Web 検索サービスの API にアクセスします。
- これを実現するためには Snowflake の外部アクセス統合とシークレットの設定が必要になります。
- Web からの検索結果を要約します。これはカスタムツールではなくエージェント自身が実行します。
- 次に、Web 検索結果の要約に基づいてパワポファイルを作成します。
- 要約した内容はフリーテキストなので、AISQL の Structured Output の機能を使い JSON に変換します。
- その JSON データに基づきパワポファイルを python-pptx を用いて作成し、Snowflake 内部ステージにアップします。
- 内部ステージ上のファイルに対して署名付き URL を発行します。
- 最後に、署名付き URL をメールで送信します。
- これを実現するにはメールアドレスの事前検証と通知統合の設定が必要になります。
2-1. 事前準備
事前準備として以下を行います。
- クロスリージョン推論の有効化
- 必要な仮想ウェアハウスや DB・スキーマ、ロールの作成
- 送信先メアドを持つ Snowflake ユーザーの作成と、メアドの検証
- Anaconda Snowflakeチャネルの利用同意(ストアド内で Python サードパーティーパッケージを利用するため)
今回の AI エージェントでは Claude 4 Sonnet を利用するので、クロスリージョン推論の有効化は必要になります。
また、Snowflake の標準機能でメールを送信する場合は、Snowflake ユーザーに対象のメアドを登録することと、そのメアドを事前に検証すること(送信確認と承認)が必要になります。
4点のうち、上3つに関しては以下のスクリプトで行いました。(4点目の方法はこちらを確認してください)
-- クロスリージョン推論の有効化
use role accountadmin;
ALTER ACCOUNT SET CORTEX_ENABLED_CROSS_REGION = 'AWS_US';
-- エージェントを動作させる仮想ウェアハウスの作成
use role sysadmin;
create or replace warehouse pptx_maker_wh
resource_constraint = STANDARD_GEN_1
auto_suspend = 60
initially_suspended = TRUE
comment = 'パワポ作成AIエージェントが利用する仮想ウェアハウス'
;
-- エージェントが利用するリソース(ストアドなど)を保存するDBの作成
create or replace database pptx_maker_db
data_retention_time_in_days = 0
max_data_extension_time_in_days = 0
comment = 'パワポ作成AIエージェントが利用するデータベース'
;
-- エージェントを格納するDBとスキーマを作成
create or replace database snowflake_intelligence;
create or replace schema snowflake_intelligence.agents;
-- エージェント実行ロールの作成
use role securityadmin;
create role pptx_maker_role;
grant all on warehouse pptx_maker_wh to role pptx_maker_role;
grant all on database pptx_maker_db to role pptx_maker_role;
grant all on schema pptx_maker_db.public to role pptx_maker_role;
grant all on database snowflake_intelligence to role pptx_maker_role;
grant all on schema snowflake_intelligence.agents to role pptx_maker_role;
grant role pptx_maker_role to role sysadmin;
-- 送信先メールアドレスを持つユーザーの作成
create user pptx_maker_user
display_name = 'パワポ作成AIエージェント'
password = '*****'
email = '*****'
must_change_password = false
default_warehouse = pptx_maker_wh
default_namespace = pptx_maker_db.public
default_role = pptx_maker_role
default_secondary_roles = ()
comment = 'パワポ作成AIエージェントの実行ユーザー'
;
grant role pptx_maker_role to user pptx_maker_user;
set current_user = (select current_user());
grant role pptx_maker_role to user identifier($current_user);
-- メールアドレスの検証
-- 以下を実行するとメールが届くので、メール上のリンクをクリックしてメール受信を承認する
use role accountadmin;
select system$start_user_email_verification('pptx_maker_user');
desc user pptx_maker_user;
show users like 'pptx_maker_user';
2-2. カスタムツール(ストアド)の作成
3つのカスタムツール(ストアド)を作成していきます。
2-2-1. Web からの情報検索
カスタムサービス Tavily を用いて Web から情報検索を行いますが、そのために必要な
- シークレット(Tavlily のアクセストークンを保持)
- 外部アクセス統合(Snowflake から Tavily へのアクセスを許可)
の作成を行います。
-- Tavilyのアクセストークンを保存するシークレットの作成
use role accountadmin;
use schema pptx_maker_db.public;
create or replace secret tavily_secret
type = generic_string
secret_string = 'tvly-dev-******'
;
-- Tavilyにアクセスするためのネットワークルールと外部アクセス統合の作成
create or replace network rule nr_tavily
type = HOST_PORT
mode = EGRESS
value_list = ('api.tavily.com')
;
create or replace external access integration eai_tavily
allowed_network_rules = (nr_tavily)
allowed_authentication_secrets = (tavily_secret)
enabled = TRUE
;
-- シークレットや外部アクセス統合の権限付与
grant usage on secret tavily_secret to role pptx_maker_role;
grant read on secret tavily_secret to role pptx_maker_role;
grant usage on integration eai_tavily to role pptx_maker_role;
一点注意が必要なのは、シークレットをストアドから利用する場合、シークレットに対して USAGE 権限だけではなく READ 権限も必要になります。
Web からの情報検索を行うストアドは以下になります。Tavily へのアクセスは tavily-python パッケージを用いるのが普通なのですが、Anaconda Snowflake チャネルには含まれていないため、REST API に直接アクセスします。
リクエストオプションは最初に示した Amazon Bedrock での記事における設定をそのまま使わせてもらっています。
use role pptx_maker_role;
use warehouse pptx_maker_wh;
use schema pptx_maker_db.public;
create or replace procedure search_info_from_web(question varchar)
returns varchar
language python
runtime_version = '3.12'
packages = ('snowflake-snowpark-python', 'requests')
handler = 'search_info_from_web'
external_access_integrations = (eai_tavily)
secrets = ('tavily_token' = tavily_secret)
as
$$
import requests
import _snowflake
def search_info_from_web(question):
# Tavilyのトークン取得と設定
tavily_token = _snowflake.get_generic_secret_string('tavily_token')
headers = {
'Authorization': f'Bearer {tavily_token}',
'Content-Type': 'application/json'
}
# 質問に有用な部分を抽出するためadvacedを指定
request_body = {
'query': question,
'search_depth': 'advanced',
'max_results': 10
}
response = requests.post(
url = 'https://api.tavily.com/search',
headers = headers,
json = request_body
)
# エージェントから呼び出す関数は文字列しか返せないため、結果(array)を文字列に変換
return str((response.json())['results'])
$$;
1点補足すると、検索結果は JSON で返ってくるのですが、こちらによると、カスタムツールとして利用するストアドは単一文字列の戻り値のみサポートするそうなので、最後の行で文字列型にキャストしています。
Snowflake Intelligence only supports custom tools that return a single string with a 16 kb size limit.
2-2-2. パワポファイル作成
次にパワポファイルを生成して内部ステージにアップ、署名付き URL を発行するストアドを作成するのですが、前準備として内部ステージを作成します。署名付き URL を発行するために内部ステージ作成で付与しているオプションは必須です。
use role pptx_maker_role;
use warehouse pptx_maker_wh;
use schema pptx_maker_db.public;
-- パワポを配置するステージを作成(署名付きURL発行のためオプション必須)
create or replace stage pptx_file_stage
encryption = (type = 'SNOWFLAKE_SSE')
DIRECTORY = (enable = true)
;
本体のストアドを作成します。少し長くなりますが、
-
format_summary_to_json関数で Structured Output (出力フォーマットの強制)を用いて要約内容を JSON に変換 -
generate_pptx_file関数で JSON に基づいてパワポファイルを生成 -
make_pptx_file関数で上2つの関数を使い要約内容からパワポファイルを生成し、内部ステージにアップロード&署名付き URL を発行
を行っています。
-- パワポファイル生成ストアド(パワポファイル作成、ステージアップロード、署名付きURL発行)
create or replace procedure make_pptx_file(
summary_text varchar
)
returns varchar
language python
runtime_version = '3.12'
packages = ('snowflake-snowpark-python', 'python-pptx')
handler = 'make_pptx_file'
as
$$
import json
from datetime import datetime
from pptx import Presentation
# パワポ生成用に要約文章を構造化(ClaudeのStructured Outputを利用)
def format_summary_to_json(session, summary_text):
prompt = f"""
以下の内容をパワーポイントに整形するための事前準備としてJSON形式に変換してください。
- 全体としてのタイトルを1つ設定する。タイトルはファイル名として使える文字のみとする。
- 各スライド用に1つのスライドタイトルと複数の文章を設定する。
```
{summary_text}
```
""";
format = """
{
'type': 'json',
'schema': {'type': 'object', 'properties': {
'entire_title': {'type': 'string'},
'pages': {'type': 'array', 'items':
{'type': 'object', 'properties':{
'page_title': {'type': 'string'},
'page_body': {'type': 'array', 'items': {'type': 'string'}}
}}}
}
}
}
""";
result = session.sql(f"""
select ai_complete(
model => 'claude-4-sonnet',
prompt => '{prompt}',
response_format => {format}
) as result_json""").collect();
return json.loads(result[0].RESULT_JSON);
# パワポファイルをローカルに作成
def generate_pptx_file(session, summary_json, file_path):
prs = Presentation()
# 1ページ目のタイトルスライド
slide_layout = prs.slide_layouts[0] # タイトルスライド
slide = prs.slides.add_slide(slide_layout)
slide.shapes.title.text = summary_json["entire_title"]
# 2ページ目以降の本文スライド
for page in summary_json["pages"]:
slide_layout = prs.slide_layouts[1] # タイトル+コンテンツ
slide = prs.slides.add_slide(slide_layout)
slide.shapes.title.text = page["page_title"]
content = slide.placeholders[1].text_frame
for i, line in enumerate(page["page_body"]):
if i == 0:
content.paragraphs[0].text = line
content.paragraphs[0].level = 0
else:
p = content.add_paragraph()
p.text = line
p.level = 0
prs.save(file_path)
def make_pptx_file(session, summary_text):
# 要約をJSONに整形
summary_json = format_summary_to_json(session, summary_text)
# パワポファイル生成(ファイル名は<タイトル>_<タイムスタンプ>.pptx
time_suffix = datetime.now().strftime('%Y%m%d_%H%M')
file_name = summary_json["entire_title"] + '_' + time_suffix + '.pptx'
file_path = '/tmp/' + file_name
generate_pptx_file(session, summary_json, '/tmp/' + file_name)
# 生成したパワポファイルをステージにアップロード
# overwrite必須(ないとなぜかエラー)
session.file.put(file_path, '@pptx_file_stage/', auto_compress=False, overwrite=True)
# 署名付きURLを生成(有効期間10分)
sign_result = session.sql(f"select get_presigned_url(@pptx_file_stage, '{file_name}', 600) as signed_url").collect()
return sign_result[0].SIGNED_URL
$$;
何点か注意ポイントがあります。
-
python-pptxパッケージをpackagesで指定したうえで Python コード内でインポートします。このパッケージは Anaconda に含まれているので、開発者がインストールなど準備する必要はありません。 - ユーザー定義関数やストアド内部では
/tmpをローカルストレージとして扱えます。今回はここにパワポファイルを一旦おきます。(このディレクトリはストアドを呼び出したクエリが終了すれば自動でクリーンアップされます) - 内部ステージにアップするコマンドは
PUTですが、ストアド内からsession.sql(...)で直接実行するとエラーになります。そのため、snowflake.snowpark.FileOperation.putを代わりに使う必要があります。- エラーになる件はこちらを参照。この資料には caller's rights と記載がありますが、owner's rights でも発生します。
- ファイルアップ時に
overwrite=Trueのオプションをつけていますが、これがないとファイル存在チェックのために内部的に Snowflake のLSコマンド(OS のコマンドではなく)が実行され内部エラーが発生します(原因は不明)。- 内部エラーはエージェントから実行した場合のみ発生し、ストアド単独で実行すると発生しません。
- このストアドは少し大きいので複数のストアドに分割したかったですが、上で触れた通りカスタムツールからエージェントに返すデータは単一文字列のみがサポートされており、JSON をうまく返す方法が分かりませんでした。そのため、1つのストアドにまとめています。
2-2-3. メール送信
メール送信は Snowflake の機能を使いますが、そのためには通知統合を事前に作成する必要があります。今回は以下で作成しました。
-- メール送信に必要な通知統合を作成し権限付与
use role accountadmin;
create or replace notification integration ni_pptx_mail
type = EMAIL
ENABLED = TRUE
allowed_recipients = ('******')
;
grant usage on integration ni_pptx_mail to role pptx_maker_role;
use role pptx_maker_role;
use warehouse pptx_maker_wh;
use schema pptx_maker_db.public;
メール送信のストアドはこちらをほぼそのまま使っています。
-- メール送信ストアド
create or replace procedure send_email(
recipient_address varchar,
subject varchar,
body varchar
)
returns varchar
language python
runtime_version = '3.12'
packages = ('snowflake-snowpark-python')
handler = 'send_email'
as
$$
def send_email(session, recipient_address, subject, body):
try:
# Escape single quotes in the body
escaped_body = body.replace("'", "''")
# Execute the system procedure call
session.sql(f"""
CALL SYSTEM$SEND_EMAIL(
'ni_pptx_mail',
'{recipient_address}',
'{subject}',
'{escaped_body}'
)
""").collect()
return "Email sent successfully"
except Exception as e:
return f"Error sending email: {str(e)}"
$$;
2-3. エージェントの作成
ここからは Snowsight 上での操作になります。具体的な手順はこちらを参照いただきたいのですが、主な設定内容は以下になります。
- エージェント作成時の設定
- Create this agent for Snowflake Intelligence のチェックボックスを有効化
- Agent object name : PPTX_MAKER_AI_AGENT (エージェントの URL に使われるため、マルチバイト不可)
- Display Name : パワポ生成AIエージェント
- エージェント編集時の設定
-
概要(ユーザーへ表示するエージェントの説明)
これはユーザーの質問に対して、Webから情報を検索し、その結果を要約し、要約結果を元にパワーポイントファイルを生成し、その署名付きURLをユーザーにメールで送信します。 送信先のメールアドレスは明示してください。 -
ツール(カスタムツールとして作成した3つのストアドを追加)
- Name : search_info_from_web (ストアド名と同じにする必要はないです)
- Resouce Type : procedure
- Database & Schema : PPTX_MAKER_DB.PUBIC
- Custom tool identifier : PPTX_MAKER_DB.PUBIC.SEARCH_INFO_FROM_WEB(VARCHAR)
- パラメータ
questionの説明 : ユーザーの質問内容 - Description : ユーザーからの質問に基づいてその回答に必要な情報をWebから検索します。
- Name : make_pptx_file
- Resouce Type : procedure
- Database & Schema : PPTX_MAKER_DB.PUBIC
- Custom tool identifier : PPTX_MAKER_DB.PUBIC.MAKE_PPTX_FILE(VARCHAR)
- パラメータ
summary_textの説明 : パワーポイントの内容の元となるWeb検索の要約内容 - Description : Web検索の要約内容に基づきパワーポイントファイルを作成し、そのファイルの署名付きURLファイルを返します。
- Name : send_mail
- Resouce Type : procedure
- Database & Schema : PPTX_MAKER_DB.PUBIC
- Custom tool identifier : PPTX_MAKER_DB.PUBIC.SEND_MAIL(VARCHAR,VARCHAR,VARCHAR)
- パラメータ
subjectの説明 : メールのサブジェクトに設定する文字列 - パラメータ
bodyの説明 : メールの本文に設定する内容 - パラメータ
recipient_addressの説明 : メールの送信先アドレス - Description : 指定されたされたサブジェクトと本文からなるメールを、指定されたメールアドレスに送信する。
- Warehouse は PPTX_MAKER_WH、Query timeoutは 60 で統一
- Name : search_info_from_web (ストアド名と同じにする必要はないです)
-
オーケストレーション
- Orchestration model : Claude 4 Sonnet
- 計画指示
- ユーザーの質問に回答するために必ず1回以上Webから検索して情報を取得してください。 - Web検索の内容を以下のルールに沿って要約してください。 - 最初にタイトルをつける。 - その後は5つ以上の見出しからなる本文を作成する。 - 各見出しの内容は複数の文章からなる。 - 要約結果に基づいてパワーポイントのファイルを生成してください。 - パワーポイントファイルの署名付きURLを指定されたメールアドレスに送信してください。 - もしユーザーが送信先メールアドレスを指定しなかった場合、上の動作は実行せず、ユーザーにメールアドレスの指定を促してください。
-
以上の設定を行うとエージェントが完成します。
3. 実行
実行は Snowsight 上のナビゲーションメニューから [AIとML] ⇒ [Snowflake Intelligence] とアクセスすることで、エージェントと対話するユーザーインターフェースが開きます。今回は Snowflake Summit 2025 で発表された新機能について調べてもらいました。
結果として、以下のようなメールが届きました。
リンクをクリックしてパワポファイルがダウンロードできます。署名付き URL なので認証なしでダウンロード可能ですが、有効期間があります。
ファイルの内容を以下に貼り付けます。問題なく動いているようですね。
(フォントサイズは手元で調整しました。また余白が少ないように見えるのは画像に変換した際の問題です。)
(オリンピックの公式パートナーになったの知らなかった…)
情報ソースとなった Web ページは以下のようです。
4. まとめ・気になった点
4-1. まとめ
今回は Snowflake Intelligence の AI エージェントにおいて、カスタムツールを利用する機能を中心にトライしてみました。
ストアド書くのは少し辛いですが、それができてしまえば AI エージェントを構築するのは容易な印象を受けました。
また、外部アクセスやメール送信、ローカルストレージアクセスなどを実現できる環境は Snowflake に一通り揃っており、カスタムツールをストアドで実現できるだろうなという印象を持ちました。
その点では、いろいろできそうで、あれこれやらせてみたいという思いが沸く結果になったかと思います。
4-2. 気になった点
最後に、気になった点を以下に記しておきます。
(1) タスク実行順番の柔軟性
今回、AI エージェントに対する [計画指示] の箇所で、タスクの実行順番を明示的に書いています。ただし、必ずこの通り実行するかというと、以下のようなケースではよい意味で指示通り動きませんでした。
- カスタムツール実行でエラーが発生した場合、その原因を分析してタスクをリトライする。
- パワポファイル作成で失敗した場合、そこでやめるのではなく、要約結果をメールで送る。
良し悪しはケースバイケースですが、ワークフローとして静的にタスク実行を指示するよりは柔軟性があるようです。(これは Snowflake Intelligence というよりエージェントそのものの特徴ですが)
(2) ライブラリ準備不要
今回は Python でストアドを作成しましたが、Anaconda のパッケージが準備不要で使えるのは良いですね。
(3) ストアドの開発環境
今回は Snowsight 上で直接ストアドを作成しましたが、コードアシスト不足やコンパイルが遅いなどを感じました。ローカルで開発して動くようになってから Snowflake 上でストアド化するなど、開発環境の整備が別途必要と感じました。
(4) デバッグのしにくさ
エラーや想定外の結果が発生した場合に調査のし辛さを感じました。
具体的には以下になります。
- エージェントからカスタムツールにどのようなパラメータを渡したか不明(ストアドではなくユーザー定義関数ならクエリ履歴から追えるのですが)
- カスタムツールがどんな結果を返したか不明
- エージェントが何かしらの理由でカスタムツール呼び出しに失敗した場合、原因が不明(エージェントの回答に含まれる場合もありますが)
対策として、以下のようなことを検討した方が良いかもしれません。
- Snowflake のロギング/トレースの機能を活用する
- カスタムツールとのデータの受け渡しはテーブル経由にする
- エージェントへの計画指示において、カスタムツールえらーが発生した場合にエラー内容をそのまま出力するよう指示する
(5) タスクの役割や単位の設計
何をエージェント側に任せて、何をカスタムツールで実行するかは慎重に考える必要があるなと感じました。
今回でいうと、以下の点を悩みました。
- Web 検索結果の要約をエージェントに任せるか、カスタムツールに任せるか
- JSON データの生成をエージェントに任せるか、カスタムツールに任せるか
- Snowflake のコマンド実行(内部ステージへのアップや署名付き URL 生成)をエージェントに任せるか(コマンドを生成させるか)、カスタムツールに任せるか
また、ある処理を1つのカスタムツールにまとめるか、複数のカスタムツールに分割するか(make_pptx_file 関数の部分)も悩みました。今回は Snowflake Intelligence の制約により1つにまとめました。
いろいろ試行錯誤して上の方法に落ち着きましたが、何かしら指針があるべきかなと思います。
(6) 事前定義済みツールの欠如
今回、Web 検索やメール送信をカスタムツールとしてストアドで用意しましたが、これぐらいは事前に Snowflake 側で提供してもらえると良いなと思います。例えば、Claude などは Web 検索の機能はサービスに包含されており、開発者がカスタムツールとして準備する必要はありませんでした。
(Snowflake Summit の発表からするとありそうなのですが、ドキュメントには見つかりませんでした。)
このあたり、Snowflake Intelligence が MCP サーバーに対応すれば、さして困らないのかもしれませんが。
(7) カスタムツールのシグニチャー変更
カスタムツールのユーザー定義関数やストアドの以下の情報をシグニチャーといいます。
- 関数/プロシージャーの名前
- 引数リストの名前とデータ型
- 戻り値のデータ型
開発中ではこれらを変更することが多々あるのですが、エージェント側の情報を更新しても反映されず、
- カスタムツールを一度エージェントから削除し、再度追加する
- 再ログインする
などの対応を要することが稀によくありました。このあたり、まだ機能として枯れてはないんですかね。










