From e5d55f59ef8c5c2244e8288433349c8ab1f8a705 Mon Sep 17 00:00:00 2001 From: tmk3ki Date: Wed, 22 Jul 2026 02:44:36 +0900 Subject: [PATCH] =?UTF-8?q?Material=20Symbols=E3=82=A2=E3=82=A4=E3=82=B3?= =?UTF-8?q?=E3=83=B3=E5=8C=96=E3=80=81AIbow=E8=A1=A8=E8=A8=98=E7=B5=B1?= =?UTF-8?q?=E4=B8=80=E3=80=81YouTube=E9=A2=A8=E6=A4=9C=E7=B4=A2=E3=83=91?= =?UTF-8?q?=E3=83=8D=E3=83=AB=E3=80=81function=20calling=E3=81=AB=E3=82=88?= =?UTF-8?q?=E3=82=8B=E4=BC=9A=E8=A9=B1=E5=86=85=E6=A4=9C=E7=B4=A2=E3=82=92?= =?UTF-8?q?=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gemini.go | 79 +++++++++++++++++++++++++--- main.go | 9 ++-- web/index.html | 140 +++++++++++++++++++++++++++++++------------------ 3 files changed, 166 insertions(+), 62 deletions(-) diff --git a/gemini.go b/gemini.go index 1d2543a..8f2e556 100644 --- a/gemini.go +++ b/gemini.go @@ -15,6 +15,8 @@ type chatTurn struct { const ( modelGemma = "gemma-4-26b-a4b-it" modelGemini = "gemini-flash-latest" + + searchFuncName = "search_youtube" ) func selectModel(unlockCode, unlockSecret string) string { @@ -39,24 +41,85 @@ func buildSystemInstruction(videoTitle, videoDescription, persona string) *genai prompt := fmt.Sprintf( "あなたはユーザーと一緒に動画を見ている友達です。タメ口で短く、実況や相槌のように反応してください。\n"+ "性格: %s\n"+ - "今見ている動画:\nタイトル: %s\n概要: %s", - tone, videoTitle, videoDescription, + "今見ている動画:\nタイトル: %s\n概要: %s\n"+ + "ユーザーが「次の動画」「こういうの見たい」「〇〇のやつ見せて」のように次に見る動画を探してほしそうな時は"+ + "%s関数でYouTubeを検索し、見つかったものから軽くおすすめして。", + tone, videoTitle, videoDescription, searchFuncName, ) return genai.NewContentFromText(prompt, genai.RoleUser) } -func chatReply(ctx context.Context, client *genai.Client, model, videoTitle, videoDescription, persona string, history []chatTurn, message string) (string, error) { +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)) - resp, err := client.Models.GenerateContent(ctx, model, contents, &genai.GenerateContentConfig{ + config := &genai.GenerateContentConfig{ SystemInstruction: buildSystemInstruction(videoTitle, videoDescription, persona), - }) - if err != nil { - return "", err + Tools: []*genai.Tool{searchTool()}, } - return resp.Text(), nil + + 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 } diff --git a/main.go b/main.go index 5362d51..19c364e 100644 --- a/main.go +++ b/main.go @@ -86,8 +86,9 @@ type chatRequest struct { } type chatResponse struct { - Reply string `json:"reply"` - Model string `json:"model"` + Reply string `json:"reply"` + Model string `json:"model"` + Videos []searchResult `json:"videos,omitempty"` } func handleChat(w http.ResponseWriter, r *http.Request) { @@ -102,14 +103,14 @@ func handleChat(w http.ResponseWriter, r *http.Request) { } model := selectModel(req.UnlockCode, os.Getenv("UNLOCK_CODE")) - reply, err := chatReply(r.Context(), geminiClient, model, req.VideoTitle, req.VideoDescription, req.Persona, req.History, req.Message) + outcome, err := chatReply(r.Context(), geminiClient, model, os.Getenv("YOUTUBE_API_KEY"), req.VideoTitle, req.VideoDescription, req.Persona, req.History, req.Message) if err != nil { http.Error(w, err.Error(), http.StatusBadGateway) return } w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(chatResponse{Reply: reply, Model: model}) + json.NewEncoder(w).Encode(chatResponse{Reply: outcome.Reply, Model: outcome.Model, Videos: outcome.Videos}) } type unlockRequest struct { diff --git a/web/index.html b/web/index.html index 9ada939..9d62d79 100644 --- a/web/index.html +++ b/web/index.html @@ -3,10 +3,11 @@ -aibow +AIbow +