クイックスタート
Package「anydoc」を使って、Agent が Word や PDF を読めるようにします。
誰かが Session に q3-review.docx をアップロードし、自分が何を決めるべきかを Agent に尋ねたとします。答えはファイルの中にあります。このページでは、その答えを取り出す方法と、それに Function が最適な理由を説明します。
課題: Agent はファイルを読めない
.docx はテキストではありません。複数の XML ファイルを ZIP でまとめたもので、段落はあるファイルに、スタイルは別のファイルに入り、表や番号付けは両方にまたがっています。バイト列をそのまま渡されても、モデルにはノイズにしか見えません。.pdf、.xlsx、.pptx も同様で、実際の仕事で扱うドキュメントはほとんどがバイナリ形式です。
すぐに思いつく回避策は、どれもモデルのコンテキストを経由します。
- ファイルを会話に貼り付ける。 2 MB のドキュメントを Base64 にしてメッセージに入れると、たいていの Context window に収まらないほどのトークンを消費します。しかも、モデルはそのバイト列をどのみち解析できません。
- MCP サーバーに渡す。 MCP サーバーは別のサービスなので、Session のファイルを見られません。ドキュメントを届けるには、結局モデルを通して Tool 呼び出しの引数として渡すしかなく、コストは変わらないうえ、扱えるサイズは Context window で頭打ちになります。
- ユーザーに先に変換してもらう。 Agent が手助けするはずだった相手に、作業を押し戻すことになります。
どれも望む形ではありません。そもそもドキュメントがモデルを通らないようにしたいのです。
発想: Tool の呼び出しをファイルへの操作にする
Session にはすでにファイルシステムがあります。Session Volume です。アップロードしたドキュメントは /volume/data/uploads/q3-review.docx に置かれています。それなら、Tool にそこで作業させればよいのです。
モデルはファイルを指定する
モデルはドキュメントそのものではなく、パスを渡して Tool を呼び出します。かかるのは数十トークンです。
Tool はファイルのそばで処理する
Tool は Volume からバイト列を読み、ZIP の展開、XML の解析、表の Markdown への整形といった、モデルにはできない処理を行います。
結果はファイルか、小さな回答になる
Tool は副作用として、次の Tool がそのまま使える q3-review.md のような出力を Volume に書き戻せます。モデルに返すのは、モデルに必要なものだけです。短いドキュメントなら Markdown そのもの、長いドキュメントならパスとサイズです。
何をするかを決めるのはモデルで、計算とバイト列はファイルシステムの側にとどまります。これが Subako Function です。Subako がホストする環境で、Session の Volume のそばで動くコードであり、自分でサーバーを用意する必要はありません。
Function としての anydoc
anydoc は、まさに必要な変換を行うオープンソースの Rust ライブラリです。ドキュメントのバイト列を渡すと、形式 (Word、PowerPoint、Excel、OpenDocument、RTF、EPUB、CSV、テキストベースの PDF) を判別し、テキストを GitHub-Flavored Markdown にして返します。ただし、単体ではただのライブラリです。ファイルのそばでそれを実行する仕組みが要ります。
anydoc のサンプル は、Subako の Rust SDK を使って、これを 1 つの関数として包んだものです。以下はその中心部分を、サンプルのソースから簡略化して示したものです。
#[derive(Deserialize, JsonSchema)]
struct ConvertInput {
/// Path of the document on the session's Volume, under `/volume/tmp` or `/volume/data`.
path: String,
/// Where the Markdown goes: back in the reply (the default), or into a Volume file.
#[serde(default)]
output: MarkdownOutput,
}
/// Convert a document on the Volume to GitHub-Flavored Markdown. Reads Word
/// (doc, docx), PowerPoint (ppt, pptx), Excel (xls, xlsx, xlsb), OpenDocument
/// (odt, ods, odp), RTF, EPUB, CSV and text-based PDF. …
#[function]
fn convert_to_markdown(input: ConvertInput) -> Result<Converted, ToolError> {
// Read the document from the session's volume…
let bytes = std::fs::read(&input.path)?;
// …let anydoc do the conversion…
let format = Format::from_bytes(&bytes).ok_or("unrecognized file")?;
let markdown = anydoc::to_markdown_bytes(&bytes, format)?;
// …and answer with the text, or write it to a file and answer with where it is.
let markdown = match input.output {
MarkdownOutput::Inline => Markdown::Inline { text: markdown },
MarkdownOutput::File { path } => {
std::fs::write(&path, &markdown)?;
Markdown::File { bytes: markdown.len(), path }
}
};
Ok(Converted { format: format_name(format).into(), markdown })
}
package! {
name: "anydoc",
description: "Converts office documents, EPUB, CSV and text-based PDFs to Markdown with anydoc.",
functions: [convert_to_markdown],
}
組み込みに必要なのはこれだけです。
#[function]は普通の Rust の関数を Tool にします。doc コメントはモデルが読む説明に、ConvertInputはモデルが呼び出しに使う入力 Schema になります。- ファイルへのアクセスは普通の
std::fsで済みます。Volume が Function のファイルシステムにマウントされているからです。 package!は crate を Function package にします。WebAssembly 向けにビルドすると 1 つのmodule.wasmになり、Subako はそれをどこでも、どの Session のためにも実行できます。デプロイするものは何もありません。
ここからは、この Package をビルドしてアップロードし、Agent に使わせるまでの手順です。
anydoc をビルドする
Package のビルドには Rust が必要です。
git clone https://github.com/subako-ai/subako-function-examples
cd subako-function-examples/rust/anydoc
./package.sh # dist/anydoc-function.tar.gz に書き出します
package.sh は Module を WASI preview 1 向けにビルドし (このターゲットは、サンプルの rust-toolchain.toml に従って rustup がインストールします)、subako-package.json と一緒に Bundle にまとめます。Bundle は、Subako が受け付ける形式の Archive です。
アップロードする
コンソールで Workspace の Function ページを開き、Package をアップロード を選びます。Archive を選んで dist/anydoc-function.tar.gz を指定し、アップロード を押します。

Subako は Module を一度実行して提供される Tool を問い合わせ、Package のページを開きます。ページには、Tool とその説明、Parameter が、モデルから見えるとおりに表示されます。

Agent に付与する
Agent を開き (まだなければ クイックスタート のとおりに作成し)、新しいバージョンを公開 を選びます。Tool › Function package で Package を追加 を押し、この Workspace を選んで anydoc を指定します。

anydoc は既定値のままで使えます。ネットワークは不要で、1 回の呼び出しは 60 秒よりずっと短く終わるからです。続いて Session Volume の Function に Data (/volume/data) を用意する をオンにします。これで、Session にアップロードしたファイルが、Session がある限り保持されます。

あわせて Platform package の files も付与しておくと、Agent はパスを知らされていないドキュメントを探したり、長い変換結果を 1 ページずつ読んだりできます。設定が終わったら バージョン N を公開 を押します。
ドキュメントを渡す
その Agent で Session を開始し、Session Volume タブで ファイルをアップロード を選びます。ドキュメントは Data (/volume/data) の uploads などのフォルダーにアップロードします。

Volume は、Agent の Function がファイルを読み書きする場所です。これでファイルは /volume/data/uploads/q3-review.docx に置かれました。

自分のアプリから PUT /v1/sessions/{session_id}/volume/data/{path} でファイルを置くこともできます。詳しくは API で Session を扱う を参照してください。
質問する
会話 に戻り、ドキュメントを読まなければ答えられないことを質問します。Agent はファイルを対象に convert_to_markdown を呼び出し、得られた Markdown を読んで回答します。

呼び出しを開くと、何が起きたかを正確に確認できます。モデルが送った引数と、anydoc が返した Markdown です。表もそのまま含まれています。

短いドキュメントなら、この例のように Markdown がそのまま返ってきます。300 ページのマニュアルのような長いドキュメントでは、モデルは代わりに "output": {"type": "file", "path": "/volume/data/uploads/manual.md"} を指定します。anydoc は Markdown を Volume に書き込み、そのパスとサイズだけを返します。Agent は必要な部分だけを Platform package の files で読みます。どちらの場合も、ドキュメントのバイト列がモデルを通ることはありません。
これで一連の流れは完了です。Package は一度アップロードして Agent に付与すれば、必要とするすべての Session から呼び出されます。自分で運用するサーバーは要りません。