OpenAI Assistants API チートシート Part 4 [Run 編]

みなさんOpenAIのCompletion APIや，Assistants APIは使用したことがありますか？？このシリーズのチートシートではOpenAIのAssistantsAPIの紹介をします．これでPart 4です．APIの紹介自体は4つのPartで完結します．(場合によっては追加で紹介するPartがあるかもしれません...)
従来のCompletion APIは1回聞いて答えが返ってきたら話していた内容を忘れてしまいます．つまり，chatgptのように会話履歴を保持してくれるわけではないです．そこでAssistants APIを使えば会話内容を保持したい場合に活用できます！Part 1ではAssistantの作成，Part 2ではThreadの作成の説明，Part 3ではMessageの作成をしました．本記事ではOpenAI Assistants API チートシート Part 4としてRunの実行方法を紹介します．

`参考`
OpenAI APIのドキュメント

https://platform.openai.com/docs/api-reference/assistants

# シリーズ OpenAI AssistantsAPI チートシート
**Part 1 Assistant編**

https://qiita.com/JavaLangRuntimeException/items/1a1abc01e8d7d05dce93

**Part 2 Thread編**

https://qiita.com/JavaLangRuntimeException/items/7ddae318202d3afcb45d

**Part 3 Message編**

https://qiita.com/JavaLangRuntimeException/items/ec1807106b51bac06de4

# 他のチートシート
git/gh コマンド(gitコマンド以外にもgitの概念も書いてあります)

https://qiita.com/JavaLangRuntimeException/items/6b46551f56e0def76eba

lazygit

https://qiita.com/JavaLangRuntimeException/items/42087d09728d5739d73d

Docker コマンド(dockerコマンド以外にもdockerの概念の記事へのリンクもあります)

https://qiita.com/JavaLangRuntimeException/items/21f7c7bf3d143f821697

ステータスコード

https://qiita.com/JavaLangRuntimeException/items/ab1bc7b976ed2dfad91c

TypeScript

https://qiita.com/JavaLangRuntimeException/items/5894391c08e0d8e28389

Go・Gorm

https://qiita.com/JavaLangRuntimeException/items/d388717fc1436bc3ec9d

C#/.NET/Unity

https://qiita.com/JavaLangRuntimeException/items/7849b32bc223d4aa0247

Ruby・Ruby on Rails

https://qiita.com/JavaLangRuntimeException/items/42d935cf92c212f1c7ec

SQL

https://qiita.com/JavaLangRuntimeException/items/f038fbaccdd92fb0308a

Vim

https://qiita.com/JavaLangRuntimeException/items/0c68ab96ea198e0a7294

プルリクエスト・マークダウン記法チートシート

https://qiita.com/JavaLangRuntimeException/items/329eb92a47a07ff4dde8

ファイル操作コマンドチートシート

https://qiita.com/JavaLangRuntimeException/items/16f244606a73f7d106e4

VSCode Github Copilot拡張機能

https://qiita.com/JavaLangRuntimeException/items/be13dc3a346cf6e5ee44

# 他のシリーズ記事
**TypeScriptで学ぶプログラミングの世界**
プログラミング言語を根本的に理解するシリーズです．

https://qiita.com/JavaLangRuntimeException/items/cadf49bb419076819963

**情報処理技術者試験合格への道[IP・SG・FE・AP]**
情報処理技術者試験の単語集です．

https://qiita.com/JavaLangRuntimeException/items/991be402099542ccb936

**IAM AWS User クラウドサービスをフル活用しよう！**
AWSのサービスを例にしてバックエンドとインフラ開発の手法を説明するシリーズです．

https://qiita.com/JavaLangRuntimeException/items/371a334f5a6e07035db5

**Project Gopher: Unlocking Go’s Secrets**
Go言語や標準ライブラリの深掘り調査レポートです．

https://qiita.com/JavaLangRuntimeException/items/dc45b412d3fbd2ccb9e8

**Gopher’s Journey: Exploring TCP Protocol**
Goを用いてTCPプロトコルを探求 & 作成するシリーズです．

https://qiita.com/JavaLangRuntimeException/items/38091220106d86651d2b

# Assistants APIでの実装の流れ
Assistant作成(Part 1で紹介) → Thread作成(Part 2で紹介) → メッセージ追加(Part 3で紹介) → **Run実行(Part 4で紹介)** → 応答取得

典型的な使用パターンは以下のようである．
```go
// 1. アシスタント作成
assistant, _ := client.CreateAssistant(ctx, openai.AssistantRequest{
    Name: "Math Tutor",
    Model: "gpt-4",
})

// 2. スレッド作成
thread, _ := client.CreateThread(ctx, openai.ThreadRequest{})

// 3. メッセージ追加
message, _ := client.CreateMessage(ctx, thread.ID, openai.MessageRequest{
    Content: "Help with math",
})

// 4. スレッド実行
run, _ := client.CreateRun(ctx, thread.ID, openai.RunRequest{
    AssistantID: assistant.ID,
})

// 5. 結果取得
messages, _ := client.ListMessages(ctx, thread.ID, nil)
```

# Runとは？ThreadやMessageとの関係性
一旦復習だが，AssistantとThreadの概要を述べる．
- **Assistant（アシスタント）**
    - 特定の目的や役割を持つAIアシスタントの定義
    - モデル、機能（Tools）、説明などの基本設定を保持
    - 再利用可能な設定のテンプレートのような役割
- **Thread（スレッド）**
    - 1つの会話の文脈を保持する単位
    - ユーザーとアシスタント間のメッセージの履歴を管理
    - 複数のメッセージのやり取りを1つのスレッドとして保持
- **Run（実行）**
    - ThreadとAssistantを結びつけて実際の処理を実行する単位
    - スレッド内のメッセージに対してアシスタントが応答を生成するプロセス
    - 実行状態（completed, failed等）を管理

# あるスレッドで実行する
使用するassistantと会話するthreadを選択し，threadにあるuserの会話内容に応じてassistantが回答してくれる．その流れを実装したコードは以下の通りである．
```create_run.go
package main

import (
	"context"
	"encoding/json"
	"fmt"
	"github.com/joho/godotenv"
	"log"
	"os"
	"time"

	openai "github.com/sashabaranov/go-openai"
)

func main() {
	err := godotenv.Load()
	if err != nil {
		log.Fatal("Error loading .env file")
	}

	apiKey := os.Getenv("OPENAI_API_KEY")
	if apiKey == "" {
		log.Fatal("環境変数 OPENAI_API_KEY が設定されていません")
	}

	client := openai.NewClient(apiKey)
	ctx := context.Background()

	threadID := "thread_abc123"  // 必須パラメータ：スレッドID
	assistantID := "asst_abc123" // 必須パラメータ：メッセージID

	// Runを作成
	run, err := client.CreateRun(ctx, threadID, openai.RunRequest{
		AssistantID: assistantID,
	})
	if err != nil {
		log.Fatalf("Runの作成に失敗: %v", err)
	}

	// 応答をJSONで整形
	jsonData, err := json.MarshalIndent(run, "", "  ")
	if err != nil {
		log.Fatalf("JSONエンコードに失敗: %v", err)
	}

	fmt.Println("Initial Run状態:")
	fmt.Println(string(jsonData))

	// Runの完了を待機して状態を監視
	fmt.Println("\n実行状態の監視を開始:")
	for {
		run, err = client.RetrieveRun(ctx, threadID, run.ID)
		if err != nil {
			log.Fatal(err)
		}

		fmt.Printf("現在の状態: %s\n", run.Status)

		switch run.Status {
		case "completed":
			fmt.Println("実行が完了しました")

			// 完了後のメッセージを取得（Part 3で紹介したListMessageを使用しています)
			messages, err := client.ListMessage(ctx, threadID, nil, nil, nil, nil, nil)
			if err != nil {
				log.Fatal(err)
			}

			fmt.Println("\n最新のメッセージ:")
			for _, msg := range messages.Messages {
				fmt.Printf("Role: %s\nContent: %s\n\n", msg.Role, msg.Content)
			}
			return

		case "failed":
			fmt.Printf("実行が失敗しました: %v\n", run.LastError)
			return
		case "cancelled":
			fmt.Println("実行がキャンセルされました")
			return
		case "expired":
			fmt.Println("実行が期限切れになりました")
			return
		}

		time.Sleep(1 * time.Second)
	}
}

```
```bash
❯ go run create_run.go
Initial Run状態:
{
  "id": "run_abc123",
  "object": "thread.run",
  "created_at": 1733826871,
  "thread_id": "thread_abc123",
  "assistant_id": "asst_abc123",
  "status": "queued",
  "expires_at": 1733827471,
  "model": "gpt-3.5-turbo",
  "instructions": "You are a personal math tutor. ",
  "tools": [
    {
      "type": "code_interpreter"
    }
  ],
  "file_ids": null,
  "metadata": {},
  "usage": {
    "prompt_tokens": 0,
    "completion_tokens": 0,
    "total_tokens": 0,
    "prompt_tokens_details": null,
    "completion_tokens_details": null
  },
  "temperature": 1,
  "truncation_strategy": {
    "type": "auto"
  }
}

実行状態の監視を開始:
現在の状態: in_progress
現在の状態: in_progress
現在の状態: in_progress
現在の状態: in_progress
現在の状態: in_progress
現在の状態: in_progress
現在の状態: in_progress
現在の状態: in_progress
現在の状態: completed
実行が完了しました

最新のメッセージ:
Role: assistant
Content: [{text %!s(*openai.MessageText=&{The integral of \( x^2 \) with respect to \( x \) is \( \frac{x^3}{3} + C \), where \( C \) is the constant of integration. []}) %!s(*openai.ImageFile=<nil>)}]

Role: assistant
Content: [{text %!s(*openai.MessageText=&{To integrate the function \( x^2 \), we can use the power rule of integration, which states that the integral of \( x^n \) with respect to x is \( \frac{x^{n+1}}{n+1} + C \), where C is the constant of integration.

Let's go ahead and integrate \( x^2 \) to find the result. []}) %!s(*openai.ImageFile=<nil>)}]

Role: user
Content: [{text %!s(*openai.MessageText=&{Integrate x^2 []}) %!s(*openai.ImageFile=<nil>)}]

Role: user
Content: [{text %!s(*openai.MessageText=&{Hello, I'm looking for a tutor to help me with my math homework. []}) %!s(*openai.ImageFile=<nil>)}]

```

このようにuserの
```
Hello, I'm looking for a tutor to help me with my math homework. 
Integrate x^2 
```
に応じて
```
Let's go ahead and integrate \( x^2 \) to find the result.

To integrate the function \( x^2 \), we can use the power rule of integration, which states that the integral of \( x^n \) with respect to x is \( \frac{x^{n+1}}{n+1} + C \), where C is the constant of integration.
```
と応答していることがわかりますね．
この流れ(Assistant作成→Thread作成→Message追加→Run実行)でGPTのAssistantsAPIを使用することができる．

# スレッドを作成し，実行する
Assistantの指定とthreadのroleやcontentの指定をすればスレッド作成と実行を同時に行うことができる．
```createThread_createRun.go
package main

import (
	"context"
	"encoding/json"
	"fmt"
	"github.com/joho/godotenv"
	"log"
	"os"
	"time"

	openai "github.com/sashabaranov/go-openai"
)

func main() {
	err := godotenv.Load()
	if err != nil {
		log.Fatal("Error loading .env file")
	}

	apiKey := os.Getenv("OPENAI_API_KEY")
	if apiKey == "" {
		log.Fatal("環境変数 OPENAI_API_KEY が設定されていません")
	}

	client := openai.NewClient(apiKey)
	ctx := context.Background()

	// ThreadとRunを同時に作成
	request := openai.CreateThreadAndRunRequest{
		RunRequest: openai.RunRequest{
			AssistantID: "asst_abc123",
		},
		Thread: openai.ThreadRequest{
			Messages: []openai.ThreadMessage{
				{
					Role:    "user",
					Content: "solve this problem: 2 + 2 = ?",
				},
			},
		},
	}

	run, err := client.CreateThreadAndRun(ctx, request)
	if err != nil {
		log.Fatalf("ThreadとRunの作成に失敗: %v", err)
	}

	// 応答をJSONで整形
	jsonData, err := json.MarshalIndent(run, "", "  ")
	if err != nil {
		log.Fatalf("JSONエンコードに失敗: %v", err)
	}

	fmt.Println("Initial Run状態:")
	fmt.Println(string(jsonData))

	// Runの完了を待機して状態を監視
	fmt.Println("\n実行状態の監視を開始:")
	for {
		run, err = client.RetrieveRun(ctx, run.ThreadID, run.ID)
		if err != nil {
			log.Fatal(err)
		}

		fmt.Printf("現在の状態: %s\n", run.Status)

		switch run.Status {
		case "completed":
			fmt.Println("実行が完了しました")

			// 完了後のメッセージを取得
			messages, err := client.ListMessage(ctx, run.ThreadID, nil, nil, nil, nil, nil)
			if err != nil {
				log.Fatal(err)
			}

			fmt.Println("\n最新のメッセージ:")
			for _, msg := range messages.Messages {
				fmt.Printf("Role: %s\nContent: %s\n\n", msg.Role, msg.Content)
			}
			return

		case "failed":
			fmt.Printf("実行が失敗しました: %v\n", run.LastError)
			return
		case "cancelled":
			fmt.Println("実行がキャンセルされました")
			return
		case "expired":
			fmt.Println("実行が期限切れになりました")
			return
		}

		time.Sleep(1 * time.Second)
	}
}

```
実行
```bash
go run createThread_createRun.go
```
成功すると以下のようなレスポンスが返ってくる．
```bash
❯ go run createThread_createRun.go 
Initial Run状態:
{
  "id": "run_bcd123",
  "object": "thread.run",
  "created_at": 1733827808,
  "thread_id": "thread_bcd123",
  "assistant_id": "asst_abc123",
  "status": "queued",
  "expires_at": 1733828408,
  "model": "gpt-3.5-turbo",
  "instructions": "You are a personal math tutor. ",
  "tools": [
    {
      "type": "code_interpreter"
    }
  ],
  "file_ids": null,
  "metadata": {},
  "usage": {
    "prompt_tokens": 0,
    "completion_tokens": 0,
    "total_tokens": 0,
    "prompt_tokens_details": null,
    "completion_tokens_details": null
  },
  "temperature": 1,
  "truncation_strategy": {
    "type": "auto"
  }
}

実行状態の監視を開始:
現在の状態: in_progress
現在の状態: in_progress
現在の状態: in_progress
現在の状態: in_progress
現在の状態: in_progress
現在の状態: in_progress
現在の状態: completed
実行が完了しました

最新のメッセージ:
Role: assistant
Content: [{text %!s(*openai.MessageText=&{The solution to the problem 2 + 2 is 4. []}) %!s(*openai.ImageFile=<nil>)}]

Role: user
Content: [{text %!s(*openai.MessageText=&{solve this problem: 2 + 2 = ? []}) %!s(*openai.ImageFile=<nil>)}]
```

# あるスレッドにおける実行結果の一覧表示
実行結果の一覧表示は以下のように実装する
```list_runs.go
package main

import (
	"context"
	"encoding/json"
	"fmt"
	"github.com/joho/godotenv"
	"log"
	"os"

	openai "github.com/sashabaranov/go-openai"
)

func main() {
	err := godotenv.Load()
	if err != nil {
		log.Fatal("Error loading .env file")
	}

	apiKey := os.Getenv("OPENAI_API_KEY")
	if apiKey == "" {
		log.Fatal("環境変数 OPENAI_API_KEY が設定されていません")
	}

	client := openai.NewClient(apiKey)
	ctx := context.Background()

	threadID := "thread_abc123" // 対象のスレッドID

	// 必須おアラメータ以外はnilにする
	runs, err := client.ListRuns(ctx, threadID, openai.Pagination{
		Limit:  nil,
		Order:  nil,
		After:  nil,
		Before: nil,
	})
	if err != nil {
		log.Fatalf("Runの一覧取得に失敗: %v", err)
	}

	// 整形して出力
	jsonData, err := json.MarshalIndent(runs, "", "  ")
	if err != nil {
		log.Fatalf("JSONエンコードに失敗: %v", err)
	}

	fmt.Println(string(jsonData))
	
}
```
実行
```bash
go run list_runs.go
```

成功するとこのように実行した結果うの一覧が表示される．よく見ると失敗した履歴まで見られる．
```bash
{
  "data": [
    {
      "id": "run_abc123",                           # Run の一意のID
      "object": "thread.run",                       # オブジェクトタイプ
      "created_at": 1699075072,                     # Run 作成時のUnixタイムスタンプ
      "thread_id": "thread_abc123",                 # 所属するスレッドのID
      "assistant_id": "asst_abc123",                # 使用されたアシスタントのID
      "status": "completed",                        # 実行状態
      "expires_at": 0,                              # 有効期限のUnixタイムスタンプ
      "started_at": 1699075072,                     # 実行開始時のUnixタイムスタンプ
      "completed_at": 1699075073,                   # 完了時のUnixタイムスタンプ
      "model": "gpt-3.5-turbo",                     # 使用されたモデル名
      "instructions": "You are a personal math tutor. When asked a question, write and run Python code to answer the question.", # アシスタントへの指示
      "tools": [
        {
          "type": "code_interpreter"                 # 利用可能なツール
        }
      ],
      "file_ids": null,                             # 関連ファイルのID一覧
      "metadata": {},                               # カスタムメタデータ
      "usage": {
        "prompt_tokens": 348,                       # 入力で使用されたトークン数
        "completion_tokens": 54,                    # 出力で使用されたトークン数
        "total_tokens": 402,                        # 合計トークン数
        "prompt_tokens_details": null,              # 詳細なプロンプトトークン情報
        "completion_tokens_details": null           # 詳細な完了トークン情報
      },
      "temperature": 1,                             # 生成時の温度パラメータ
      "truncation_strategy": {                      # 切り詰め戦略
        "type": "auto"                             # 自動切り詰めタイプ
      }
    },
    {
      "id": "run_def456",                           # Run の一意のID
      "object": "thread.run",                       # オブジェクトタイプ
      "created_at": 1699075070,                     # Run 作成時のUnixタイムスタンプ
      "thread_id": "thread_abc123",                 # 所属するスレッドのID
      "assistant_id": "asst_abc123",                # 使用されたアシスタントのID
      "status": "failed",                           # 実行状態
      "last_error": {                               # エラー情報
        "code": "rate_limit_exceeded",              # エラーコード
        "message": "Your account is not active, please check your billing details on our website." # エラーメッセージ
      },
      "expires_at": 0,                              # 有効期限のUnixタイムスタンプ
      "started_at": 1699075070,                     # 実行開始時のUnixタイムスタンプ
      "failed_at": 1699075071,                      # 失敗時のUnixタイムスタンプ
      "model": "gpt-3.5-turbo",                     # 使用されたモデル名
      "instructions": "You are a personal math tutor. When asked a question, write and run Python code to answer the question.", # アシスタントへの指示
      "tools": [
        {
          "type": "code_interpreter"                 # 利用可能なツール
        }
      ],
      "file_ids": null,                             # 関連ファイルのID一覧
      "metadata": {},                               # カスタムメタデータ
      "usage": {
        "prompt_tokens": 0,                         # 入力で使用されたトークン数
        "completion_tokens": 0,                     # 出力で使用されたトークン数
        "total_tokens": 0,                          # 合計トークン数
        "prompt_tokens_details": null,              # 詳細なプロンプトトークン情報
        "completion_tokens_details": null           # 詳細な完了トークン情報
      },
      "temperature": 1,                             # 生成時の温度パラメータ
      "truncation_strategy": {                      # 切り詰め戦略
        "type": "auto"                             # 自動切り詰めタイプ
      }
    }
  ]
}
```

# その他Run関連のAPI
他にも以下のような機能があるが，実装やレスポンスは省略する
- **Retrieve run (GET /v1/threads/{thread_id}/runs/{run_id})**
    - 特定のスレッド内の特定のRunの詳細情報を取得
    - Runの現在の状態、使用されたモデル、タイムスタンプ、トークン使用量などの情報を返す
- **Modify run (POST /v1/threads/{thread_id}/runs/{run_id})**
    - 既存のRunのメタデータを更新
    - 主にカスタムメタデータ（key-value形式）の追加・更新に使用
    - 1つのキーは最大64文字、値は最大512文字まで設定可能
- **Submit tool outputs (POST /v1/threads/{thread_id}/runs/{run_id}/submit_tool_outputs)**
    - Runが「requires_action」状態で、ツール実行が必要な場合に使用
    - 外部ツール（関数など）の実行結果をアシスタントに送信
    - すべてのツール出力を1回のリクエストでまとめて提出する必要がある
    - ストリーミングオプションでリアルタイムの経過を受信可能
- **Cancel a run (POST /v1/threads/{thread_id}/runs/{run_id}/cancel)**
    - 実行中（in_progress）のRunをキャンセル
    - キャンセルリクエスト後、ステータスは「cancelling」に変更
    - 完全にキャンセルされると「cancelled」状態になる

# Run関連の豆知識・重要ポイント

**1. 実行管理の中核機能**
- Runsは非同期処理を実現する重要なコンポーネント
- スレッド内でのアシスタントの実行状態を追跡・管理

**2. 状態管理の特徴**
- queued → in_progress → completed/failed/cancelled の遷移
- 各状態の明確な区分けにより、実行フローの制御が容易

**3. 非同期処理のメリット**
- 長時間の処理や複数のツール実行に対応
- クライアント側のタイムアウトを回避
- スケーラブルな実装が可能

**4. 実装時の注意点**
- ステータスの定期的なポーリングが必要
- エラーハンドリングの実装が重要
- メタデータを活用した実行管理の検討

**5. ツール連携の柔軟性**
- 外部ツールとの連携を統一的に管理
- 複数ツールの並列実行にも対応
- ツール出力の一括提出機能

このようにThreadとAssistantを作成し，Messageを作成し，Runを実行すればChatGPTのような会話内容を保持してOpenAIのAPIを叩くことができる．案外直感的に実装できそうだとは思わないだろうか？ぜひみなさんのプロダクトにこのOpenAI AssistantsAPIを導入してLLMを組み込んだ開発をしましょう！

それではQiitaアドカレ.24企画の今日のクリスマスツリーです．

> 詳しくは[こちら](https://qiita.com/JavaLangRuntimeException/items/1f4a6febf957f522ba45)の記事から

![Wondershare Uniconverter 15_000007.GIF](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3757442/a0ad9c70-1309-5877-c9b4-0d093b32af93.gif)