aibow/gemini.go

125 lines
3.8 KiB
Go

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
}