Initial commit: breakpilot-compliance - Compliance SDK Platform
Services: Admin-Compliance, Backend-Compliance, AI-Compliance-SDK, Consent-SDK, Developer-Portal, PCA-Platform, DSMS Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
250
ai-compliance-sdk/internal/llm/anthropic_adapter.go
Normal file
250
ai-compliance-sdk/internal/llm/anthropic_adapter.go
Normal file
@@ -0,0 +1,250 @@
|
||||
package llm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// AnthropicAdapter implements the Provider interface for Anthropic API
|
||||
type AnthropicAdapter struct {
|
||||
apiKey string
|
||||
baseURL string
|
||||
defaultModel string
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
// NewAnthropicAdapter creates a new Anthropic adapter
|
||||
func NewAnthropicAdapter(apiKey, defaultModel string) *AnthropicAdapter {
|
||||
return &AnthropicAdapter{
|
||||
apiKey: apiKey,
|
||||
baseURL: "https://api.anthropic.com",
|
||||
defaultModel: defaultModel,
|
||||
httpClient: &http.Client{
|
||||
Timeout: 5 * time.Minute,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Name returns the provider name
|
||||
func (a *AnthropicAdapter) Name() string {
|
||||
return ProviderAnthropic
|
||||
}
|
||||
|
||||
// IsAvailable checks if Anthropic API is reachable
|
||||
func (a *AnthropicAdapter) IsAvailable(ctx context.Context) bool {
|
||||
if a.apiKey == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// Simple check - we can't really ping Anthropic without making a request
|
||||
// Just verify we have an API key
|
||||
return true
|
||||
}
|
||||
|
||||
// ListModels returns available Anthropic models
|
||||
func (a *AnthropicAdapter) ListModels(ctx context.Context) ([]Model, error) {
|
||||
// Anthropic doesn't have a models endpoint, return known models
|
||||
return []Model{
|
||||
{
|
||||
ID: "claude-3-opus-20240229",
|
||||
Name: "Claude 3 Opus",
|
||||
Provider: ProviderAnthropic,
|
||||
Description: "Most powerful model for complex tasks",
|
||||
ContextSize: 200000,
|
||||
Capabilities: []string{"chat"},
|
||||
},
|
||||
{
|
||||
ID: "claude-3-sonnet-20240229",
|
||||
Name: "Claude 3 Sonnet",
|
||||
Provider: ProviderAnthropic,
|
||||
Description: "Balanced performance and speed",
|
||||
ContextSize: 200000,
|
||||
Capabilities: []string{"chat"},
|
||||
},
|
||||
{
|
||||
ID: "claude-3-haiku-20240307",
|
||||
Name: "Claude 3 Haiku",
|
||||
Provider: ProviderAnthropic,
|
||||
Description: "Fast and efficient",
|
||||
ContextSize: 200000,
|
||||
Capabilities: []string{"chat"},
|
||||
},
|
||||
{
|
||||
ID: "claude-3-5-sonnet-20240620",
|
||||
Name: "Claude 3.5 Sonnet",
|
||||
Provider: ProviderAnthropic,
|
||||
Description: "Latest and most capable model",
|
||||
ContextSize: 200000,
|
||||
Capabilities: []string{"chat"},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Complete performs text completion (converted to chat)
|
||||
func (a *AnthropicAdapter) Complete(ctx context.Context, req *CompletionRequest) (*CompletionResponse, error) {
|
||||
// Anthropic only supports chat, so convert completion to chat
|
||||
chatReq := &ChatRequest{
|
||||
Model: req.Model,
|
||||
Messages: []Message{
|
||||
{Role: "user", Content: req.Prompt},
|
||||
},
|
||||
MaxTokens: req.MaxTokens,
|
||||
Temperature: req.Temperature,
|
||||
TopP: req.TopP,
|
||||
Stop: req.Stop,
|
||||
}
|
||||
|
||||
chatResp, err := a.Chat(ctx, chatReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &CompletionResponse{
|
||||
ID: chatResp.ID,
|
||||
Model: chatResp.Model,
|
||||
Provider: chatResp.Provider,
|
||||
Text: chatResp.Message.Content,
|
||||
FinishReason: chatResp.FinishReason,
|
||||
Usage: chatResp.Usage,
|
||||
Duration: chatResp.Duration,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Chat performs chat completion
|
||||
func (a *AnthropicAdapter) Chat(ctx context.Context, req *ChatRequest) (*ChatResponse, error) {
|
||||
if a.apiKey == "" {
|
||||
return nil, fmt.Errorf("anthropic API key not configured")
|
||||
}
|
||||
|
||||
model := req.Model
|
||||
if model == "" {
|
||||
model = a.defaultModel
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
|
||||
// Extract system message if present
|
||||
var systemMessage string
|
||||
var messages []map[string]string
|
||||
|
||||
for _, m := range req.Messages {
|
||||
if m.Role == "system" {
|
||||
systemMessage = m.Content
|
||||
} else {
|
||||
messages = append(messages, map[string]string{
|
||||
"role": m.Role,
|
||||
"content": m.Content,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
maxTokens := req.MaxTokens
|
||||
if maxTokens == 0 {
|
||||
maxTokens = 4096
|
||||
}
|
||||
|
||||
anthropicReq := map[string]any{
|
||||
"model": model,
|
||||
"messages": messages,
|
||||
"max_tokens": maxTokens,
|
||||
}
|
||||
|
||||
if systemMessage != "" {
|
||||
anthropicReq["system"] = systemMessage
|
||||
}
|
||||
|
||||
if req.Temperature > 0 {
|
||||
anthropicReq["temperature"] = req.Temperature
|
||||
}
|
||||
|
||||
if req.TopP > 0 {
|
||||
anthropicReq["top_p"] = req.TopP
|
||||
}
|
||||
|
||||
if len(req.Stop) > 0 {
|
||||
anthropicReq["stop_sequences"] = req.Stop
|
||||
}
|
||||
|
||||
body, err := json.Marshal(anthropicReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
httpReq, err := http.NewRequestWithContext(ctx, "POST", a.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", a.apiKey)
|
||||
httpReq.Header.Set("anthropic-version", "2023-06-01")
|
||||
|
||||
resp, err := a.httpClient.Do(httpReq)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("anthropic request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("anthropic error (%d): %s", resp.StatusCode, string(bodyBytes))
|
||||
}
|
||||
|
||||
var result struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Role string `json:"role"`
|
||||
Content []struct {
|
||||
Type string `json:"type"`
|
||||
Text string `json:"text"`
|
||||
} `json:"content"`
|
||||
Model string `json:"model"`
|
||||
StopReason string `json:"stop_reason"`
|
||||
StopSequence string `json:"stop_sequence,omitempty"`
|
||||
Usage struct {
|
||||
InputTokens int `json:"input_tokens"`
|
||||
OutputTokens int `json:"output_tokens"`
|
||||
} `json:"usage"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode response: %w", err)
|
||||
}
|
||||
|
||||
duration := time.Since(start)
|
||||
|
||||
// Extract text from content blocks
|
||||
var responseText string
|
||||
for _, block := range result.Content {
|
||||
if block.Type == "text" {
|
||||
responseText += block.Text
|
||||
}
|
||||
}
|
||||
|
||||
return &ChatResponse{
|
||||
ID: result.ID,
|
||||
Model: result.Model,
|
||||
Provider: ProviderAnthropic,
|
||||
Message: Message{
|
||||
Role: "assistant",
|
||||
Content: responseText,
|
||||
},
|
||||
FinishReason: result.StopReason,
|
||||
Usage: UsageStats{
|
||||
PromptTokens: result.Usage.InputTokens,
|
||||
CompletionTokens: result.Usage.OutputTokens,
|
||||
TotalTokens: result.Usage.InputTokens + result.Usage.OutputTokens,
|
||||
},
|
||||
Duration: duration,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Embed creates embeddings (Anthropic doesn't support embeddings natively)
|
||||
func (a *AnthropicAdapter) Embed(ctx context.Context, req *EmbedRequest) (*EmbedResponse, error) {
|
||||
return nil, fmt.Errorf("anthropic does not support embeddings - use Ollama or OpenAI")
|
||||
}
|
||||
Reference in New Issue
Block a user