fix(gemini): Implement Gemini 3 thought_signature handling for stateful reasoning
This commit is contained in:
parent
13e4028d42
commit
5ac9a46f59
4 changed files with 67 additions and 21 deletions
15
Dockerfile
15
Dockerfile
|
|
@ -3,16 +3,19 @@
|
||||||
# ============================================================
|
# ============================================================
|
||||||
FROM golang:1.26.0-alpine AS builder
|
FROM golang:1.26.0-alpine AS builder
|
||||||
|
|
||||||
RUN apk add --no-cache git make
|
# Install build dependencies
|
||||||
|
RUN apk add --no-cache git make gcc musl-dev
|
||||||
|
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
|
|
||||||
# Cache dependencies
|
# Cache dependencies for faster subsequent builds
|
||||||
COPY go.mod go.sum ./
|
COPY go.mod go.sum ./
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
|
|
||||||
# Copy source and build
|
# Copy your local source code (where you'll add the Thought Signature fix)
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
|
# Compile the binary
|
||||||
RUN make build
|
RUN make build
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
@ -20,17 +23,19 @@ RUN make build
|
||||||
# ============================================================
|
# ============================================================
|
||||||
FROM alpine:3.23
|
FROM alpine:3.23
|
||||||
|
|
||||||
|
# Install runtime essentials
|
||||||
RUN apk add --no-cache ca-certificates tzdata curl
|
RUN apk add --no-cache ca-certificates tzdata curl
|
||||||
|
|
||||||
# Health check
|
# Health check
|
||||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||||
CMD wget -q --spider http://localhost:18790/health || exit 1
|
CMD wget -q --spider http://localhost:18790/health || exit 1
|
||||||
|
|
||||||
# Copy binary
|
# Copy the compiled binary from the builder stage
|
||||||
COPY --from=builder /src/build/picoclaw /usr/local/bin/picoclaw
|
COPY --from=builder /src/build/picoclaw /usr/local/bin/picoclaw
|
||||||
|
|
||||||
# Create picoclaw home directory
|
# Create necessary directories and initialize
|
||||||
RUN /usr/local/bin/picoclaw onboard
|
RUN /usr/local/bin/picoclaw onboard
|
||||||
|
|
||||||
|
# Set the binary as the entrypoint
|
||||||
ENTRYPOINT ["picoclaw"]
|
ENTRYPOINT ["picoclaw"]
|
||||||
CMD ["gateway"]
|
CMD ["gateway"]
|
||||||
|
|
@ -621,6 +621,9 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, messages []providers.M
|
||||||
}
|
}
|
||||||
for _, tc := range response.ToolCalls {
|
for _, tc := range response.ToolCalls {
|
||||||
argumentsJSON, _ := json.Marshal(tc.Arguments)
|
argumentsJSON, _ := json.Marshal(tc.Arguments)
|
||||||
|
// Copy ExtraContent to ensure thought_signature is persisted
|
||||||
|
extraContent := tc.ExtraContent
|
||||||
|
|
||||||
assistantMsg.ToolCalls = append(assistantMsg.ToolCalls, providers.ToolCall{
|
assistantMsg.ToolCalls = append(assistantMsg.ToolCalls, providers.ToolCall{
|
||||||
ID: tc.ID,
|
ID: tc.ID,
|
||||||
Type: "function",
|
Type: "function",
|
||||||
|
|
@ -628,6 +631,9 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, messages []providers.M
|
||||||
Name: tc.Name,
|
Name: tc.Name,
|
||||||
Arguments: string(argumentsJSON),
|
Arguments: string(argumentsJSON),
|
||||||
},
|
},
|
||||||
|
ExtraContent: extraContent,
|
||||||
|
// We also set internal ThoughtSignature, but ExtraContent is what matters for serialization
|
||||||
|
ThoughtSignature: tc.ThoughtSignature,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
messages = append(messages, assistantMsg)
|
messages = append(messages, assistantMsg)
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,8 @@ func (p *HTTPProvider) Chat(ctx context.Context, messages []Message, tools []Too
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pre-process messages loop removed - relying on ExtraContent persistence in Agent Loop.
|
||||||
|
|
||||||
requestBody := map[string]interface{}{
|
requestBody := map[string]interface{}{
|
||||||
"model": model,
|
"model": model,
|
||||||
"messages": messages,
|
"messages": messages,
|
||||||
|
|
@ -135,6 +137,11 @@ func (p *HTTPProvider) parseResponse(body []byte) (*LLMResponse, error) {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Arguments string `json:"arguments"`
|
Arguments string `json:"arguments"`
|
||||||
} `json:"function"`
|
} `json:"function"`
|
||||||
|
ExtraContent *struct {
|
||||||
|
Google *struct {
|
||||||
|
ThoughtSignature string `json:"thought_signature"`
|
||||||
|
} `json:"google"`
|
||||||
|
} `json:"extra_content"`
|
||||||
} `json:"tool_calls"`
|
} `json:"tool_calls"`
|
||||||
} `json:"message"`
|
} `json:"message"`
|
||||||
FinishReason string `json:"finish_reason"`
|
FinishReason string `json:"finish_reason"`
|
||||||
|
|
@ -160,7 +167,12 @@ func (p *HTTPProvider) parseResponse(body []byte) (*LLMResponse, error) {
|
||||||
arguments := make(map[string]interface{})
|
arguments := make(map[string]interface{})
|
||||||
name := ""
|
name := ""
|
||||||
|
|
||||||
// Handle OpenAI format with nested function object
|
// Extract thought_signature from Gemini/Google-specific extra content
|
||||||
|
thoughtSignature := ""
|
||||||
|
if tc.ExtraContent != nil && tc.ExtraContent.Google != nil {
|
||||||
|
thoughtSignature = tc.ExtraContent.Google.ThoughtSignature
|
||||||
|
}
|
||||||
|
|
||||||
if tc.Type == "function" && tc.Function != nil {
|
if tc.Type == "function" && tc.Function != nil {
|
||||||
name = tc.Function.Name
|
name = tc.Function.Name
|
||||||
if tc.Function.Arguments != "" {
|
if tc.Function.Arguments != "" {
|
||||||
|
|
@ -178,11 +190,23 @@ func (p *HTTPProvider) parseResponse(body []byte) (*LLMResponse, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
toolCalls = append(toolCalls, ToolCall{
|
// Correctly map extracted ExtraContent to ToolCall struct
|
||||||
|
toolCall := ToolCall{
|
||||||
ID: tc.ID,
|
ID: tc.ID,
|
||||||
Name: name,
|
Name: name,
|
||||||
Arguments: arguments,
|
Arguments: arguments,
|
||||||
})
|
ThoughtSignature: thoughtSignature, // Populating internal field for convenience
|
||||||
|
}
|
||||||
|
|
||||||
|
if thoughtSignature != "" {
|
||||||
|
toolCall.ExtraContent = &ExtraContent{
|
||||||
|
Google: &GoogleExtra{
|
||||||
|
ThoughtSignature: thoughtSignature,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toolCalls = append(toolCalls, toolCall)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &LLMResponse{
|
return &LLMResponse{
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,22 @@ type ToolCall struct {
|
||||||
Function *FunctionCall `json:"function,omitempty"`
|
Function *FunctionCall `json:"function,omitempty"`
|
||||||
Name string `json:"name,omitempty"`
|
Name string `json:"name,omitempty"`
|
||||||
Arguments map[string]interface{} `json:"arguments,omitempty"`
|
Arguments map[string]interface{} `json:"arguments,omitempty"`
|
||||||
|
ThoughtSignature string `json:"-"` // Internal use only
|
||||||
|
ExtraContent *ExtraContent `json:"extra_content,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExtraContent struct {
|
||||||
|
Google *GoogleExtra `json:"google,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GoogleExtra struct {
|
||||||
|
ThoughtSignature string `json:"thought_signature,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FunctionCall struct {
|
type FunctionCall struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Arguments string `json:"arguments"`
|
Arguments string `json:"arguments"`
|
||||||
|
ThoughtSignature string `json:"-"` // Internal use only
|
||||||
}
|
}
|
||||||
|
|
||||||
type LLMResponse struct {
|
type LLMResponse struct {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue