mirror of
https://github.com/coaidev/coai.git
synced 2025-05-19 21:10:18 +09:00
211 lines
5.3 KiB
Go
211 lines
5.3 KiB
Go
package claude
|
|
|
|
import (
|
|
adaptercommon "chat/adapter/common"
|
|
"chat/globals"
|
|
"chat/utils"
|
|
"errors"
|
|
"fmt"
|
|
)
|
|
|
|
const defaultTokens = 2500
|
|
|
|
func (c *ChatInstance) GetChatEndpoint() string {
|
|
return fmt.Sprintf("%s/v1/messages", c.GetEndpoint())
|
|
}
|
|
|
|
func (c *ChatInstance) GetChatHeaders() map[string]string {
|
|
return map[string]string{
|
|
"content-type": "application/json",
|
|
"anthropic-version": "2023-06-01",
|
|
"x-api-key": c.GetApiKey(),
|
|
}
|
|
}
|
|
|
|
// ConvertCompletionMessage converts the completion message to anthropic complete format (deprecated)
|
|
func (c *ChatInstance) ConvertCompletionMessage(message []globals.Message) string {
|
|
mapper := map[string]string{
|
|
globals.System: "Assistant",
|
|
globals.User: "Human",
|
|
globals.Assistant: "Assistant",
|
|
}
|
|
|
|
var result string
|
|
for i, item := range message {
|
|
if item.Role == globals.Tool {
|
|
continue
|
|
}
|
|
if i == 0 && item.Role == globals.Assistant {
|
|
// skip first assistant message
|
|
continue
|
|
}
|
|
|
|
result += fmt.Sprintf("\n\n%s: %s", mapper[item.Role], item.Content)
|
|
}
|
|
return fmt.Sprintf("%s\n\nAssistant:", result)
|
|
}
|
|
|
|
func (c *ChatInstance) GetTokens(props *adaptercommon.ChatProps) int {
|
|
if props.MaxTokens == nil || *props.MaxTokens <= 0 {
|
|
return defaultTokens
|
|
}
|
|
|
|
return *props.MaxTokens
|
|
}
|
|
|
|
func (c *ChatInstance) ConvertMessages(props *adaptercommon.ChatProps) []globals.Message {
|
|
// anthropic api: top message must be user message, only `user` and `assistant` role messages are allowd
|
|
start := false
|
|
|
|
result := make([]globals.Message, 0)
|
|
|
|
for _, message := range props.Message {
|
|
if message.Role == globals.System {
|
|
continue
|
|
}
|
|
|
|
// if is first message, set it to user message
|
|
if !start {
|
|
start = true
|
|
result = append(result, globals.Message{
|
|
Role: globals.User,
|
|
Content: message.Content,
|
|
})
|
|
continue
|
|
}
|
|
|
|
// anthropic api does not allow multi-same role messages
|
|
if len(result) > 0 && result[len(result)-1].Role == message.Role {
|
|
result[len(result)-1].Content += "\n" + message.Content
|
|
continue
|
|
}
|
|
|
|
result = append(result, message)
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func (c *ChatInstance) GetMessages(props *adaptercommon.ChatProps) []Message {
|
|
converted := c.ConvertMessages(props)
|
|
return utils.Each(converted, func(message globals.Message) Message {
|
|
if !globals.IsVisionModel(props.Model) || message.Role != globals.User {
|
|
return Message{
|
|
Role: message.Role,
|
|
Content: message.Content,
|
|
}
|
|
}
|
|
|
|
content, urls := utils.ExtractImages(message.Content, true)
|
|
images := utils.EachNotNil(urls, func(url string) *MessageContent {
|
|
obj, err := utils.NewImage(url)
|
|
props.Buffer.AddImage(obj)
|
|
if err != nil {
|
|
globals.Info(fmt.Sprintf("cannot process image: %s (source: %s)", err.Error(), utils.Extract(url, 24, "...")))
|
|
}
|
|
|
|
i := utils.NewImageContent(url)
|
|
return &MessageContent{
|
|
Type: "image",
|
|
Source: &MessageImage{
|
|
Type: "base64",
|
|
MediaType: i.GetType(),
|
|
Data: i.ToRawBase64(),
|
|
},
|
|
}
|
|
})
|
|
|
|
return Message{
|
|
Role: message.Role,
|
|
Content: utils.Prepend(images, MessageContent{
|
|
Type: "text",
|
|
Text: &content,
|
|
}),
|
|
}
|
|
})
|
|
}
|
|
|
|
func (c *ChatInstance) GetSystemPrompt(props *adaptercommon.ChatProps) (prompt string) {
|
|
for _, message := range props.Message {
|
|
if message.Role == globals.System {
|
|
prompt += message.Content
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func (c *ChatInstance) GetChatBody(props *adaptercommon.ChatProps, stream bool) *ChatBody {
|
|
messages := c.GetMessages(props)
|
|
return &ChatBody{
|
|
Messages: messages,
|
|
MaxTokens: c.GetTokens(props),
|
|
Model: props.Model,
|
|
System: c.GetSystemPrompt(props),
|
|
Stream: stream,
|
|
Temperature: props.Temperature,
|
|
TopP: props.TopP,
|
|
TopK: props.TopK,
|
|
}
|
|
}
|
|
|
|
func (c *ChatInstance) ProcessLine(data string) (*globals.Chunk, error) {
|
|
if form := processChatResponse(data); form != nil {
|
|
return &globals.Chunk{
|
|
Content: form.Delta.Text,
|
|
}, nil
|
|
}
|
|
|
|
if form := processChatErrorResponse(data); form != nil {
|
|
return &globals.Chunk{Content: ""}, fmt.Errorf("anthropic error: %s (type: %s)", form.Error.Message, form.Error.Type)
|
|
}
|
|
|
|
return &globals.Chunk{Content: ""}, nil
|
|
}
|
|
|
|
func processChatErrorResponse(data string) *ChatErrorResponse {
|
|
if form := utils.UnmarshalForm[ChatErrorResponse](data); form != nil {
|
|
return form
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func processChatResponse(data string) *ChatStreamResponse {
|
|
if form := utils.UnmarshalForm[ChatStreamResponse](data); form != nil {
|
|
return form
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// CreateStreamChatRequest is the stream request for anthropic claude
|
|
func (c *ChatInstance) CreateStreamChatRequest(props *adaptercommon.ChatProps, hook globals.Hook) error {
|
|
err := utils.EventScanner(&utils.EventScannerProps{
|
|
Method: "POST",
|
|
Uri: c.GetChatEndpoint(),
|
|
Headers: c.GetChatHeaders(),
|
|
Body: c.GetChatBody(props, true),
|
|
Callback: func(data string) error {
|
|
partial, err := c.ProcessLine(data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return hook(partial)
|
|
},
|
|
},
|
|
props.Proxy,
|
|
)
|
|
|
|
if err != nil {
|
|
if form := processChatErrorResponse(err.Body); form != nil {
|
|
if form.Error.Type == "" && form.Error.Message == "" {
|
|
return errors.New(utils.ToMarkdownCode("json", err.Body))
|
|
}
|
|
|
|
return errors.New(fmt.Sprintf("%s (type: %s)", form.Error.Message, form.Error.Type))
|
|
}
|
|
return fmt.Errorf("%s\n%s", err.Error, errors.New(utils.ToMarkdownCode("json", err.Body)))
|
|
}
|
|
|
|
return nil
|
|
}
|