Steve Dudenhoeffer
0d70ec46de
Anthropic API does not support assistants sending images directly, so the role is adjusted to "user" for such messages. This ensures compatibility and prevents errors when processing image messages.
206 lines
4.5 KiB
Go
206 lines
4.5 KiB
Go
package go_llm
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"log/slog"
|
|
"net/http"
|
|
|
|
anth "github.com/liushuangls/go-anthropic/v2"
|
|
)
|
|
|
|
type anthropic struct {
|
|
key string
|
|
model string
|
|
}
|
|
|
|
var _ LLM = anthropic{}
|
|
|
|
func (a anthropic) ModelVersion(modelVersion string) (ChatCompletion, error) {
|
|
a.model = modelVersion
|
|
|
|
// TODO: model verification?
|
|
return a, nil
|
|
}
|
|
|
|
func deferClose(c io.Closer) {
|
|
err := c.Close()
|
|
if err != nil {
|
|
slog.Error("error closing", "error", err)
|
|
}
|
|
}
|
|
|
|
func (a anthropic) requestToAnthropicRequest(req Request) anth.MessagesRequest {
|
|
res := anth.MessagesRequest{
|
|
Model: anth.Model(a.model),
|
|
MaxTokens: 1000,
|
|
}
|
|
|
|
msgs := []anth.Message{}
|
|
|
|
// we gotta convert messages into anthropic messages, however
|
|
// anthropic does not have a "system" message type, so we need to
|
|
// append it to the res.System field instead
|
|
|
|
for _, msg := range req.Messages {
|
|
if msg.Role == RoleSystem {
|
|
if len(res.System) > 0 {
|
|
res.System += "\n"
|
|
}
|
|
res.System += msg.Text
|
|
} else {
|
|
role := anth.RoleUser
|
|
|
|
if msg.Role == RoleAssistant {
|
|
role = anth.RoleAssistant
|
|
}
|
|
|
|
m := anth.Message{
|
|
Role: role,
|
|
Content: []anth.MessageContent{},
|
|
}
|
|
|
|
if msg.Text != "" {
|
|
m.Content = append(m.Content, anth.MessageContent{
|
|
Type: anth.MessagesContentTypeText,
|
|
Text: &msg.Text,
|
|
})
|
|
}
|
|
|
|
for _, img := range msg.Images {
|
|
// anthropic doesn't allow the assistant to send images, so we need to say it's from the user
|
|
if m.Role == anth.RoleAssistant {
|
|
m.Role = anth.RoleUser
|
|
}
|
|
|
|
if img.Base64 != "" {
|
|
m.Content = append(m.Content, anth.NewImageMessageContent(
|
|
anth.NewMessageContentSource(
|
|
anth.MessagesContentSourceTypeBase64,
|
|
img.ContentType,
|
|
img.Base64,
|
|
)))
|
|
} else if img.Url != "" {
|
|
|
|
// download the image
|
|
cl, err := http.NewRequest(http.MethodGet, img.Url, nil)
|
|
if err != nil {
|
|
log.Println("failed to create request", err)
|
|
continue
|
|
}
|
|
|
|
resp, err := http.DefaultClient.Do(cl)
|
|
if err != nil {
|
|
log.Println("failed to download image", err)
|
|
continue
|
|
}
|
|
|
|
defer deferClose(resp.Body)
|
|
|
|
img.ContentType = resp.Header.Get("Content-Type")
|
|
|
|
// read the image
|
|
b, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
log.Println("failed to read image", err)
|
|
continue
|
|
}
|
|
|
|
// base64 encode the image
|
|
img.Base64 = string(b)
|
|
|
|
m.Content = append(m.Content, anth.NewImageMessageContent(
|
|
anth.NewMessageContentSource(
|
|
anth.MessagesContentSourceTypeBase64,
|
|
img.ContentType,
|
|
img.Base64,
|
|
)))
|
|
}
|
|
}
|
|
|
|
// if this has the same role as the previous message, we can append it to the previous message
|
|
// as anthropic expects alternating assistant and user roles
|
|
|
|
if len(msgs) > 0 && msgs[len(msgs)-1].Role == role {
|
|
m2 := &msgs[len(msgs)-1]
|
|
|
|
m2.Content = append(m2.Content, m.Content...)
|
|
} else {
|
|
msgs = append(msgs, m)
|
|
}
|
|
}
|
|
}
|
|
|
|
if req.Toolbox != nil {
|
|
for _, tool := range req.Toolbox.funcs {
|
|
res.Tools = append(res.Tools, anth.ToolDefinition{
|
|
Name: tool.Name,
|
|
Description: tool.Description,
|
|
InputSchema: tool.Parameters,
|
|
})
|
|
}
|
|
}
|
|
|
|
res.Messages = msgs
|
|
|
|
if req.Temperature != nil {
|
|
res.Temperature = req.Temperature
|
|
}
|
|
|
|
log.Println("llm request to anthropic request", res)
|
|
|
|
return res
|
|
}
|
|
|
|
func (a anthropic) responseToLLMResponse(in anth.MessagesResponse) Response {
|
|
res := Response{}
|
|
|
|
for _, msg := range in.Content {
|
|
choice := ResponseChoice{}
|
|
|
|
switch msg.Type {
|
|
case anth.MessagesContentTypeText:
|
|
if msg.Text != nil {
|
|
choice.Content = *msg.Text
|
|
}
|
|
|
|
case anth.MessagesContentTypeToolUse:
|
|
if msg.MessageContentToolUse != nil {
|
|
b, e := json.Marshal(msg.MessageContentToolUse.Input)
|
|
if e != nil {
|
|
log.Println("failed to marshal input", e)
|
|
} else {
|
|
choice.Calls = append(choice.Calls, ToolCall{
|
|
ID: msg.MessageContentToolUse.ID,
|
|
FunctionCall: FunctionCall{
|
|
Name: msg.MessageContentToolUse.Name,
|
|
Arguments: string(b),
|
|
},
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
res.Choices = append(res.Choices, choice)
|
|
}
|
|
|
|
log.Println("anthropic response to llm response", res)
|
|
|
|
return res
|
|
}
|
|
|
|
func (a anthropic) ChatComplete(ctx context.Context, req Request) (Response, error) {
|
|
cl := anth.NewClient(a.key)
|
|
|
|
res, err := cl.CreateMessages(ctx, a.requestToAnthropicRequest(req))
|
|
|
|
if err != nil {
|
|
return Response{}, fmt.Errorf("failed to chat complete: %w", err)
|
|
}
|
|
|
|
return a.responseToLLMResponse(res), nil
|
|
}
|