Major changes: - Add Go backend (backend/) with microservices architecture - Enhanced master-agents-svc: reranker, content-classifier, stealth-crawler, proxy-manager, media-search, fastClassifier, language detection - New web-svc widgets: KnowledgeCard, ProductCard, ProfileCard, VideoCard, UnifiedCard, CardGallery, InlineImageGallery, SourcesPanel, RelatedQuestions - Improved discover-svc with discover-db integration - Docker deployment improvements (Caddyfile, vendor.sh, BUILD.md) - Library-svc: project_id schema migration - Remove deprecated finance-svc and travel-svc - Localization improvements across services Made-with: Cursor
183 lines
3.9 KiB
Go
183 lines
3.9 KiB
Go
package llm
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
)
|
|
|
|
type AnthropicClient struct {
|
|
baseClient
|
|
apiKey string
|
|
baseURL string
|
|
client *http.Client
|
|
}
|
|
|
|
func NewAnthropicClient(cfg ProviderConfig) (*AnthropicClient, error) {
|
|
baseURL := cfg.BaseURL
|
|
if baseURL == "" {
|
|
baseURL = "https://api.anthropic.com"
|
|
}
|
|
|
|
return &AnthropicClient{
|
|
baseClient: baseClient{
|
|
providerID: cfg.ProviderID,
|
|
modelKey: cfg.ModelKey,
|
|
},
|
|
apiKey: cfg.APIKey,
|
|
baseURL: strings.TrimSuffix(baseURL, "/"),
|
|
client: &http.Client{},
|
|
}, nil
|
|
}
|
|
|
|
type anthropicRequest struct {
|
|
Model string `json:"model"`
|
|
Messages []anthropicMessage `json:"messages"`
|
|
System string `json:"system,omitempty"`
|
|
MaxTokens int `json:"max_tokens"`
|
|
Stream bool `json:"stream"`
|
|
Tools []anthropicTool `json:"tools,omitempty"`
|
|
}
|
|
|
|
type anthropicMessage struct {
|
|
Role string `json:"role"`
|
|
Content string `json:"content"`
|
|
}
|
|
|
|
type anthropicTool struct {
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
InputSchema interface{} `json:"input_schema"`
|
|
}
|
|
|
|
type anthropicStreamEvent struct {
|
|
Type string `json:"type"`
|
|
Index int `json:"index,omitempty"`
|
|
Delta struct {
|
|
Type string `json:"type,omitempty"`
|
|
Text string `json:"text,omitempty"`
|
|
} `json:"delta,omitempty"`
|
|
ContentBlock struct {
|
|
Type string `json:"type"`
|
|
Text string `json:"text,omitempty"`
|
|
} `json:"content_block,omitempty"`
|
|
}
|
|
|
|
func (c *AnthropicClient) StreamText(ctx context.Context, req StreamRequest) (<-chan StreamChunk, error) {
|
|
var systemPrompt string
|
|
messages := make([]anthropicMessage, 0)
|
|
|
|
for _, m := range req.Messages {
|
|
if m.Role == RoleSystem {
|
|
systemPrompt = m.Content
|
|
continue
|
|
}
|
|
role := string(m.Role)
|
|
if role == "tool" {
|
|
role = "user"
|
|
}
|
|
messages = append(messages, anthropicMessage{
|
|
Role: role,
|
|
Content: m.Content,
|
|
})
|
|
}
|
|
|
|
maxTokens := req.Options.MaxTokens
|
|
if maxTokens == 0 {
|
|
maxTokens = 4096
|
|
}
|
|
|
|
anthropicReq := anthropicRequest{
|
|
Model: c.modelKey,
|
|
Messages: messages,
|
|
System: systemPrompt,
|
|
MaxTokens: maxTokens,
|
|
Stream: true,
|
|
}
|
|
|
|
if len(req.Tools) > 0 {
|
|
anthropicReq.Tools = make([]anthropicTool, len(req.Tools))
|
|
for i, t := range req.Tools {
|
|
anthropicReq.Tools[i] = anthropicTool{
|
|
Name: t.Name,
|
|
Description: t.Description,
|
|
InputSchema: t.Schema,
|
|
}
|
|
}
|
|
}
|
|
|
|
body, err := json.Marshal(anthropicReq)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
httpReq, err := http.NewRequestWithContext(ctx, "POST", c.baseURL+"/v1/messages", bytes.NewReader(body))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
httpReq.Header.Set("Content-Type", "application/json")
|
|
httpReq.Header.Set("x-api-key", c.apiKey)
|
|
httpReq.Header.Set("anthropic-version", "2023-06-01")
|
|
|
|
resp, err := c.client.Do(httpReq)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
body, _ := io.ReadAll(resp.Body)
|
|
resp.Body.Close()
|
|
return nil, fmt.Errorf("anthropic API error: %d - %s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
ch := make(chan StreamChunk, 100)
|
|
go func() {
|
|
defer close(ch)
|
|
defer resp.Body.Close()
|
|
|
|
scanner := bufio.NewScanner(resp.Body)
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
if !strings.HasPrefix(line, "data: ") {
|
|
continue
|
|
}
|
|
|
|
data := strings.TrimPrefix(line, "data: ")
|
|
if data == "[DONE]" {
|
|
return
|
|
}
|
|
|
|
var event anthropicStreamEvent
|
|
if err := json.Unmarshal([]byte(data), &event); err != nil {
|
|
continue
|
|
}
|
|
|
|
switch event.Type {
|
|
case "content_block_delta":
|
|
if event.Delta.Text != "" {
|
|
ch <- StreamChunk{ContentChunk: event.Delta.Text}
|
|
}
|
|
case "message_stop":
|
|
ch <- StreamChunk{FinishReason: "stop"}
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
return ch, nil
|
|
}
|
|
|
|
func (c *AnthropicClient) GenerateText(ctx context.Context, req StreamRequest) (string, error) {
|
|
ch, err := c.StreamText(ctx, req)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return readAllChunks(ch), nil
|
|
}
|