自アプリケーションのAIチャットにツールを登録し自動実行してもらうための実装メモまとめ

Go言語でAIツールインターフェースを定義する方法

9日前


// Tool はAIが使用できるツールのインターフェース type Tool interface { GetDefinition() ToolDefinition Execute(ctx context.Context, userID uint, args map[string]interface{}) (*ToolResult, error) } // ToolDefinition はツールの定義情報 type ToolDefinition struct { Name string // ツール名(例: search_memos) Description string // ツールの説明 Parameters map[string]Parameter // パラメータ定義 } // Parameter はツールのパラメータ情報 type Parameter struct { Type string // データ型(string, integer, boolean等) Description string // パラメータの説明 Required bool // 必須かどうか Default interface{} // デフォルト値(オプション) } // ToolResult はツール実行結果 type ToolResult struct { Success bool // 実行成功したか Data map[string]interface{} // 結果データ Error string // エラーメッセージ(失敗時) }

Gemini APIでツール定義を変換する実装パターン

9日前


// Gemini用の変換関数 func ConvertAIToolsToGemini(aiTools []aitool.ToolDefinition) []*genai.Tool { functionDeclarations := make([]*genai.FunctionDeclaration, 0, len(aiTools)) for _, aiTool := range aiTools { funcDecl := &genai.FunctionDeclaration{ Name: aiTool.Name, Description: aiTool.Description, Parameters: &genai.Schema{ Type: genai.TypeObject, Properties: make(map[string]*genai.Schema), Required: []string{}, }, } // パラメータを変換 for name, param := range aiTool.Parameters { var schemaType genai.Type switch param.Type { case "string": schemaType = genai.TypeString case "integer", "number": schemaType = genai.TypeNumber case "boolean": schemaType = genai.TypeBoolean } funcDecl.Parameters.Properties[name] = &genai.Schema{ Type: schemaType, Description: param.Description, } if param.Required { funcDecl.Parameters.Required = append(funcDecl.Parameters.Required, name) } } functionDeclarations = append(functionDeclarations, funcDecl) } // 全ての関数宣言を1つのToolオブジェクトにまとめる return []*genai.Tool{{FunctionDeclarations: functionDeclarations}} }

AIチャットでツールを自動実行するシステムの実装

9日前


// AIツール管理クラス type AITools struct { tools map[string]aitool.Tool } // ツールの登録 func NewAITools(memoRepo, categoryRepo, userCategoryRepo, skillSheetRepo) *AITools { tools := make(map[string]aitool.Tool) // 各ツールをインスタンス化して登録 memoTool := NewMemoTool(memoRepo) tools[memoTool.GetDefinition().Name] = memoTool categoryTool := NewCategoryTool(categoryRepo, userCategoryRepo) tools[categoryTool.GetDefinition().Name] = categoryTool skillSheetTool := NewSkillSheetTool(skillSheetRepo) tools[skillSheetTool.GetDefinition().Name] = skillSheetTool return &AITools{tools: tools} } // ツール定義を取得(AIに渡すため) func (m *AITools) GetToolDefinitions() []aitool.ToolDefinition { definitions := make([]aitool.ToolDefinition, 0, len(m.tools)) for _, tool := range m.tools { definitions = append(definitions, tool.GetDefinition()) } return definitions } // ツールを実行 func (m *AITools) CallTool(ctx context.Context, userID uint, toolName string, args map[string]interface{}) (*aitool.ToolResult, error) { tool, exists := m.tools[toolName] if !exists { return &aitool.ToolResult{ Success: false, Error: fmt.Sprintf("ツール '%s' が見つかりません", toolName), }, nil } return tool.Execute(ctx, userID, args) } // Geminiでツールを使用 func handleGeminiWithTools(ctx context.Context, userID uint, message string, aiToolDefs []aitool.ToolDefinition) { // ツールをGemini形式に変換 geminiTools := ConvertAIToolsToGemini(aiToolDefs) // ツール実行関数を定義 toolExecutor := func(ctx context.Context, fc *genai.FunctionCall) (map[string]any, error) { result, err := aiTools.CallTool(ctx, userID, fc.Name, fc.Args) if err != nil { return nil, err } if result.Success && result.Data != nil { return convertToolResultForGemini(result.Data), nil } return map[string]any{"error": result.Error}, nil } // Gemini APIを呼び出し(ツール実行も含めて自動処理) response, err := geminiService.ExecuteToolsNonStreaming( ctx, systemPrompt, message, history, geminiTools, toolExecutor, ) }

ツール実行結果のストリーミング配信の実装方法

9日前


// ストリーミングレスポンスの型定義 type AIChatStreamResponse struct { Content string SessionID uint } // ツール実行を含むメッセージ送信 func SendMessageWithTools(ctx context.Context, userID uint, sessionID uint, message string) (chan *AIChatStreamResponse, error) { // AIツール定義を取得 aiToolDefs := aiTools.GetToolDefinitions() // レスポンス用のチャンネル作成 responseChan := make(chan *AIChatStreamResponse, 100) // 非同期でツール実行とレスポンス生成 go handleGeminiWithTools(ctx, userID, sessionID, message, aiToolDefs, responseChan) return responseChan, nil } // Geminiでのツール実行とストリーミング func handleGeminiWithTools(ctx context.Context, userID uint, sessionID uint, message string, aiToolDefs []aitool.ToolDefinition, responseChan chan *AIChatStreamResponse) { defer close(responseChan) // ツール実行を含む処理(非ストリーミング) fullResponse, err := geminiService.ExecuteToolsNonStreaming( ctx, systemPrompt, message, history, geminiTools, toolExecutor, ) if err != nil { responseChan <- &AIChatStreamResponse{ Content: fmt.Sprintf("エラーが発生しました: %v", err), SessionID: sessionID, } return } // レスポンスをチャンクに分割してストリーミング送信 const chunkSize = 100 runes := []rune(fullResponse) for i := 0; i < len(runes); i += chunkSize { end := min(i + chunkSize, len(runes)) chunk := string(runes[i:end]) responseChan <- &AIChatStreamResponse{ Content: chunk, SessionID: sessionID, } } } // クライアント側での受信例 func receiveStreamingResponse(responseChan chan *AIChatStreamResponse) { for response := range responseChan { // チャンクごとに表示を更新 fmt.Print(response.Content) } }