TypeScriptで型安全なAI呼び出し

# TypeScriptで型安全なAI呼び出し

OpenAIには、tools(旧function calling)という、関数の引数などを定義したschemaを与えるとその関数を呼び出してくれる機能があって、それを使えばAIの返答を割と効率で構造化できるんですが、その関数定義や実装がものすごくめんどうなので、簡単にできないかを考えていました。

C++だとtemplateを型レベルプログラミングでどうにかできそうな気がするのですが、調べた感じtypescriptだとそれは難しいっぽく、似たようなことをやろうとするとプリプロセッサのようなものが必要になるようです（実際そういうライブラリはある）。事前として、型定義っぽいオブジェクトを作ってそれを渡すとそこから型を作ってくれるライブラリもあったので、それを使うことにしました。

今回は、io-tsを採用して、io-tsの様式で定義した型情報を使ってtoolsのスキーマを定義するライブラリを作成しました。それが"typai"です。

GitHub: [https://github.com/jonigata/typai](https://github.com/jonigata/typai)

## インストール方法

typaiは npm でインストールできます。以下のコマンドを実行してください：

```bash
npm install typai
```

typaiは以下のライブラリに依存しています。

- openai
- io-ts
- json5


## 実装の概要

typaiの核となる機能は、`queryAi`関数です。この関数は、OpenAIのクライアント、モデル名、プロンプト（または会話履歴）、そしてツールの配列を受け取り、AIの応答を型安全な形で返します。

以下は、基本的な使用例です。Toolのparmetersのところがio-tsの型です。

```typescript
import OpenAI from 'openai';
import { queryAi, Tool } from 'typai';
import * as t from 'io-ts';

// Initialize OpenAI client, and by changing the baseUrl, openrouter can also be used
const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY
});

// Define a tool
const echoTool: Tool<{ message: string }> = {
  name: 'Echo',
  description: 'Echoes back the input',
  parameters: t.type({
    message: t.string
  })
};

// Use the tool
async function main() {
  try {
    const result = await queryAi(
      openai,
      "anthropic/claude-3-sonnet:beta",
      "Please echo back: Hello, World!",
      [echoTool]
    );
    console.log("Tool called:", result.tool.name);
    console.log("Parameters:", result.parameters);
  } catch (error) {
    console.error("Error:", error);
  }
}

main();
```

## 実装で難しかったところ

### 1. ジェネリクスの活用

ツールのパラメータ型を柔軟に定義するため、ジェネリクスを多用しました。例えば、`Tool`型は以下のように定義されています：

```typescript
export type Tool<T> = {
  name: string;
  description: string;
  parameters: t.Type<T>;
}
```

これにより、各ツールが独自のパラメータ型を持つことが可能になりました。

### 2. io-tsとTypeScriptの型システムの統合

io-tsの型定義とTypeScriptの型システムを統合する過程で、いくつかの課題がありました。特に、io-tsの`Type<A, O, I>`とTypeScriptの型システムをうまく連携させるのに苦労しました。

例えば、`generateJsonSchema`関数の実装では、以下のような型チェックが必要でした：

```typescript
function generateJsonSchema(type: t.Type<any>) {
  if (type instanceof t.InterfaceType) {
    // InterfaceType specific logic
  } else if (type instanceof t.UnionType) {
    // UnionType specific logic
  }
  // ...
}
```

### 3. 非同期処理の型安全性

`queryAi`関数は非同期で動作するため、Promiseの型安全性を確保する必要がありました。TypeScriptの型推論を最大限に活用し、以下のような型定義を行いました：

```typescript
export async function queryAi<T>(
  openai: OpenAI,
  model: string,
  promptOrMessages: string | ChatCompletionMessageParam[],
  tools: Tool<T>[]
): Promise<ToolCall<T>>
```

### 4.claude

上の3つ全部ウソです。claudeがうまくやってくれたのであまり苦労しませんでした。このドキュメントの素案書いたのもclaudeです。一番大変で「◯ね」って思ったのはtsconfig.jsonとpackage.jsonです。

## 今後の展望

1. より高度なツール定義のサポート
2. スキーマ生成のさらなる最適化
3. 他のAI APIプロバイダーへの対応

typaiはまだ開発途上のライブラリですが、AI対話における型安全性の重要性は今後ますます高まると考えています。皆様のフィードバックやコントリビューションをお待ちしております。

## コントリビューション

typaiはオープンソースプロジェクトです。バグ報告、機能リクエスト、プルリクエストなど、あらゆる形での貢献を歓迎しています。詳細は[GitHubリポジトリ](https://github.com/jonigata/typai)をご覧ください。

## ライセンス

typaiはISCライセンスの下で公開されています。詳細は[LICENSE](https://github.com/jonigata/typai/blob/main/LICENSE)ファイルをご覧ください。

ISCライセンスって聞いたことなかったんですが、MITより弱いらしいです。おおよそAS ISなやつですね。