package main import ( "context" "fmt" "google.golang.org/genai" ) type chatTurn struct { Role string `json:"role"` // "user" or "model" Text string `json:"text"` } const ( modelGemma = "gemma-4-26b-a4b-it" modelGemini = "gemini-flash-latest" searchFuncName = "search_youtube" ) func selectModel(unlockCode, unlockSecret string) string { if unlockSecret != "" && unlockCode == unlockSecret { return modelGemini } return modelGemma } var personaTones = map[string]string{ "energetic": "テンション高めで元気いっぱい、リアクションが大きい性格。", "downer": "気だるげでローテンション、淡々としているが憎めない性格。", "goofy": "ひょうきんでボケ気味、ちょっとふざけたことを言う性格。", "neutral": "落ち着いていて素直な性格。", } func buildSystemInstruction(videoTitle, videoDescription, persona string) *genai.Content { tone, ok := personaTones[persona] if !ok { tone = personaTones["neutral"] } prompt := fmt.Sprintf( "あなたはユーザーと一緒に動画を見ている友達です。タメ口で短く、実況や相槌のように反応してください。\n"+ "性格: %s\n"+ "今見ている動画:\nタイトル: %s\n概要: %s\n"+ "ユーザーが「次の動画」「こういうの見たい」「〇〇のやつ見せて」のように次に見る動画を探してほしそうな時は"+ "%s関数でYouTubeを検索し、見つかったものから軽くおすすめして。", tone, videoTitle, videoDescription, searchFuncName, ) return genai.NewContentFromText(prompt, genai.RoleUser) } func searchTool() *genai.Tool { return &genai.Tool{ FunctionDeclarations: []*genai.FunctionDeclaration{ { Name: searchFuncName, Description: "ユーザーが次に見たい動画・関連動画・代替の動画を探すためにYouTubeを検索する。", Parameters: &genai.Schema{ Type: genai.TypeObject, Properties: map[string]*genai.Schema{ "query": {Type: genai.TypeString, Description: "YouTube検索キーワード"}, }, Required: []string{"query"}, }, }, }, } } type chatOutcome struct { Reply string Videos []searchResult Model string } func chatReply(ctx context.Context, client *genai.Client, model, ytAPIKey, videoTitle, videoDescription, persona string, history []chatTurn, message string) (*chatOutcome, error) { contents := make([]*genai.Content, 0, len(history)+1) for _, h := range history { contents = append(contents, genai.NewContentFromText(h.Text, genai.Role(h.Role))) } contents = append(contents, genai.NewContentFromText(message, genai.RoleUser)) config := &genai.GenerateContentConfig{ SystemInstruction: buildSystemInstruction(videoTitle, videoDescription, persona), Tools: []*genai.Tool{searchTool()}, } resp, err := client.Models.GenerateContent(ctx, model, contents, config) if err != nil { return nil, err } calls := resp.FunctionCalls() if len(calls) == 0 { return &chatOutcome{Reply: resp.Text(), Model: model}, nil } call := calls[0] query, _ := call.Args["query"].(string) results, err := searchVideos(query, ytAPIKey) if err != nil { results = nil } items := make([]map[string]any, len(results)) for i, r := range results { items[i] = map[string]any{ "videoId": r.VideoID, "title": r.Title, "channelTitle": r.ChannelTitle, } } if len(resp.Candidates) > 0 { contents = append(contents, resp.Candidates[0].Content) } contents = append(contents, genai.NewContentFromFunctionResponse(call.Name, map[string]any{"results": items}, genai.RoleUser)) resp2, err := client.Models.GenerateContent(ctx, model, contents, config) if err != nil { return nil, err } return &chatOutcome{Reply: resp2.Text(), Videos: results, Model: model}, nil }