1// Copyright 2025 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15// Chats client.
16
17package genai
18
19import (
20 "context"
21 "io"
22 "iter"
23 "log"
24)
25
26// Chats provides util functions for creating a new chat session.
27// You don't need to initiate this struct. Create a client instance via NewClient, and
28// then access Chats through client.Models field.
29type Chats struct {
30 apiClient *apiClient
31}
32
33// Chat represents a single chat session (multi-turn conversation) with the model.
34//
35// client, _ := genai.NewClient(ctx, &genai.ClientConfig{})
36// chat, _ := client.Chats.Create(ctx, "gemini-2.0-flash", nil, nil)
37// result, err = chat.SendMessage(ctx, genai.Part{Text: "What is 1 + 2?"})
38type Chat struct {
39 Models
40 apiClient *apiClient
41 model string
42 config *GenerateContentConfig
43 // History of the chat.
44 comprehensiveHistory []*Content
45}
46
47// Create initializes a new chat session.
48func (c *Chats) Create(ctx context.Context, model string, config *GenerateContentConfig, history []*Content) (*Chat, error) {
49 chat := &Chat{
50 apiClient: c.apiClient,
51 model: model,
52 config: config,
53 comprehensiveHistory: history,
54 }
55 chat.Models.apiClient = c.apiClient
56 return chat, nil
57}
58
59func (c *Chat) recordHistory(ctx context.Context, inputContent *Content, outputContents []*Content) {
60 c.comprehensiveHistory = append(c.comprehensiveHistory, inputContent)
61
62 for _, outputContent := range outputContents {
63 c.comprehensiveHistory = append(c.comprehensiveHistory, copySanitizedModelContent(outputContent))
64 }
65}
66
67// copySanitizedModelContent creates a (shallow) copy of modelContent with role set to
68// model and all Parts copied verbatim.
69func copySanitizedModelContent(modelContent *Content) *Content {
70 newContent := &Content{Role: RoleModel}
71 newContent.Parts = append(newContent.Parts, modelContent.Parts...)
72 return newContent
73}
74
75// History returns the chat history. Curated (valid only) history is not supported yet.
76func (c *Chat) History(curated bool) []*Content {
77 if curated {
78 log.Println("curated history is not supported yet")
79 return nil
80 }
81 return c.comprehensiveHistory
82}
83
84// SendMessage sends the conversation history with the additional user's message and returns the model's response.
85func (c *Chat) SendMessage(ctx context.Context, parts ...Part) (*GenerateContentResponse, error) {
86 // Transform Parts to single Content
87 p := make([]*Part, len(parts))
88 for i, part := range parts {
89 p[i] = &part
90 }
91 inputContent := &Content{Parts: p, Role: RoleUser}
92
93 // Combine history with input content to send to model
94 contents := append(c.comprehensiveHistory, inputContent)
95
96 // Generate Content
97 modelOutput, err := c.GenerateContent(ctx, c.model, contents, c.config)
98 if err != nil {
99 return nil, err
100 }
101
102 // Record history. By default, use the first candidate for history.
103 var outputContents []*Content
104 if len(modelOutput.Candidates) > 0 && modelOutput.Candidates[0].Content != nil {
105 outputContents = append(outputContents, modelOutput.Candidates[0].Content)
106 }
107 c.recordHistory(ctx, inputContent, outputContents)
108
109 return modelOutput, err
110}
111
112// SendMessageStream sends the conversation history with the additional user's message and returns the model's response.
113func (c *Chat) SendMessageStream(ctx context.Context, parts ...Part) iter.Seq2[*GenerateContentResponse, error] {
114 // Transform Parts to single Content
115 p := make([]*Part, len(parts))
116 for i, part := range parts {
117 p[i] = &part
118 }
119 inputContent := &Content{Parts: p, Role: "user"}
120
121 // Combine history with input content to send to model
122 contents := append(c.comprehensiveHistory, inputContent)
123
124 // Generate Content
125 response := c.GenerateContentStream(ctx, c.model, contents, c.config)
126
127 // Return a new iterator that will yield the responses and record history with merged response.
128 return func(yield func(*GenerateContentResponse, error) bool) {
129 var outputContents []*Content
130 for chunk, err := range response {
131 if err == io.EOF {
132 break
133 }
134 if err != nil {
135 yield(nil, err)
136 return
137 }
138 if len(chunk.Candidates) > 0 && chunk.Candidates[0].Content != nil {
139 outputContents = append(outputContents, chunk.Candidates[0].Content)
140 }
141 yield(chunk, nil)
142 }
143 // Record history. By default, use the first candidate for history.
144 c.recordHistory(ctx, inputContent, outputContents)
145 }
146}