diff --git a/pkg/llm/gemini/gemini.go b/pkg/llm/gemini/gemini.go index 0b24a08..15677ea 100644 --- a/pkg/llm/gemini/gemini.go +++ b/pkg/llm/gemini/gemini.go @@ -100,10 +100,14 @@ func (p *Provider) GenerateStream(ctx context.Context, memory []history.Message, return agent.LLMResult{}, fmt.Errorf("gemini: %w: %s message carries %d media parts, which this API accepts only on user messages", agent.ErrUnrenderablePart, m.Role, len(m.Parts)) } + // Gemini takes a single system instruction, so every system + // message joins it as its own part, in order. Keeping only the + // last one would let a trailing hint replace the system prompt. if m.Role == "system" { - systemInstruction = &genai.Content{ - Parts: []*genai.Part{{Text: m.Content}}, + if systemInstruction == nil { + systemInstruction = &genai.Content{} } + systemInstruction.Parts = append(systemInstruction.Parts, &genai.Part{Text: m.Content}) continue } diff --git a/pkg/llm/gemini/system_instruction_test.go b/pkg/llm/gemini/system_instruction_test.go new file mode 100644 index 0000000..6ed52ca --- /dev/null +++ b/pkg/llm/gemini/system_instruction_test.go @@ -0,0 +1,67 @@ +package gemini + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/hung12ct/gopheragent/pkg/agent" + "github.com/hung12ct/gopheragent/pkg/history" + "google.golang.org/genai" +) + +// A loop hint appended as a trailing system message used to replace the +// system prompt outright, because only the last system message was kept. +func TestGenerateStream_AllSystemMessagesReachSystemInstruction(t *testing.T) { + var body []byte + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, _ = io.ReadAll(r.Body) + w.Header().Set("Content-Type", "text/event-stream") + fmt.Fprint(w, "data: {\"candidates\":[{\"content\":{\"role\":\"model\",\"parts\":[{\"text\":\"ok\"}]},\"finishReason\":\"STOP\"}]}\r\n\r\n") + })) + t.Cleanup(srv.Close) + client, err := genai.NewClient(context.Background(), &genai.ClientConfig{ + APIKey: "test-key", + Backend: genai.BackendGeminiAPI, + HTTPOptions: genai.HTTPOptions{BaseURL: srv.URL}, + }) + if err != nil { + t.Fatalf("new client: %v", err) + } + p := &Provider{client: client, model: "gemini-test"} + + memory := []history.Message{ + {Role: "system", Content: "you are a travel agent"}, + {Role: "user", Content: "hi"}, + {Role: "system", Content: "wrap up now"}, + } + if _, err := p.GenerateStream(context.Background(), memory, nil, make(chan agent.StreamEvent, 16)); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + var req struct { + SystemInstruction struct { + Parts []struct { + Text string `json:"text"` + } `json:"parts"` + } `json:"systemInstruction"` + Contents []json.RawMessage `json:"contents"` + } + if err := json.Unmarshal(body, &req); err != nil { + t.Fatalf("decode request: %v", err) + } + var got []string + for _, part := range req.SystemInstruction.Parts { + got = append(got, part.Text) + } + if want := []string{"you are a travel agent", "wrap up now"}; fmt.Sprint(got) != fmt.Sprint(want) { + t.Fatalf("systemInstruction parts = %q, want %q", got, want) + } + if len(req.Contents) != 1 { + t.Fatalf("contents = %d, want only the user message", len(req.Contents)) + } +}