mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-08-19 01:17:16 +02:00
Add files via upload
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
package llm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/cloudwego/eino/components/model"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
)
|
||||
|
||||
// AgenticChatModelAdapter exposes a text-oriented AgenticModel as a classic
|
||||
// BaseChatModel for Eino components that have not adopted AgenticMessage yet.
|
||||
// It adapts only Eino's in-memory message shape; no HTTP protocol is translated.
|
||||
type AgenticChatModelAdapter struct {
|
||||
model model.AgenticModel
|
||||
tools []*schema.ToolInfo
|
||||
}
|
||||
|
||||
func NewAgenticChatModelAdapter(agenticModel model.AgenticModel) model.ChatModel {
|
||||
return &AgenticChatModelAdapter{model: agenticModel}
|
||||
}
|
||||
|
||||
func (a *AgenticChatModelAdapter) BindTools(tools []*schema.ToolInfo) error {
|
||||
if a == nil || a.model == nil {
|
||||
return fmt.Errorf("agentic chat adapter: model is nil")
|
||||
}
|
||||
a.tools = append([]*schema.ToolInfo(nil), tools...)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *AgenticChatModelAdapter) Generate(
|
||||
ctx context.Context,
|
||||
input []*schema.Message,
|
||||
opts ...model.Option,
|
||||
) (*schema.Message, error) {
|
||||
if a == nil || a.model == nil {
|
||||
return nil, fmt.Errorf("agentic chat adapter: model is nil")
|
||||
}
|
||||
out, err := a.model.Generate(ctx, classicMessagesToAgentic(input), commonAgenticOptions(a.tools, opts...)...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return agenticMessageToClassic(out), nil
|
||||
}
|
||||
|
||||
func (a *AgenticChatModelAdapter) Stream(
|
||||
ctx context.Context,
|
||||
input []*schema.Message,
|
||||
opts ...model.Option,
|
||||
) (*schema.StreamReader[*schema.Message], error) {
|
||||
if a == nil || a.model == nil {
|
||||
return nil, fmt.Errorf("agentic chat adapter: model is nil")
|
||||
}
|
||||
stream, err := a.model.Stream(ctx, classicMessagesToAgentic(input), commonAgenticOptions(a.tools, opts...)...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return schema.StreamReaderWithConvert(stream, func(msg *schema.AgenticMessage) (*schema.Message, error) {
|
||||
return agenticMessageToClassic(msg), nil
|
||||
}), nil
|
||||
}
|
||||
|
||||
func classicMessagesToAgentic(input []*schema.Message) []*schema.AgenticMessage {
|
||||
out := make([]*schema.AgenticMessage, 0, len(input))
|
||||
for _, msg := range input {
|
||||
if msg == nil {
|
||||
continue
|
||||
}
|
||||
role := schema.AgenticRoleTypeUser
|
||||
switch msg.Role {
|
||||
case schema.System:
|
||||
role = schema.AgenticRoleTypeSystem
|
||||
case schema.Assistant:
|
||||
role = schema.AgenticRoleTypeAssistant
|
||||
}
|
||||
agentic := &schema.AgenticMessage{Role: role}
|
||||
if msg.Role == schema.Assistant {
|
||||
if msg.Content != "" {
|
||||
agentic.ContentBlocks = append(agentic.ContentBlocks, schema.NewContentBlock(&schema.AssistantGenText{Text: msg.Content}))
|
||||
}
|
||||
} else if msg.Content != "" {
|
||||
agentic.ContentBlocks = append(agentic.ContentBlocks, schema.NewContentBlock(&schema.UserInputText{Text: msg.Content}))
|
||||
}
|
||||
out = append(out, agentic)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func agenticMessageToClassic(msg *schema.AgenticMessage) *schema.Message {
|
||||
if msg == nil {
|
||||
return nil
|
||||
}
|
||||
content, reasoning := AgenticText(msg)
|
||||
return &schema.Message{
|
||||
Role: schema.Assistant,
|
||||
Content: content,
|
||||
ReasoningContent: reasoning,
|
||||
}
|
||||
}
|
||||
|
||||
func commonAgenticOptions(boundTools []*schema.ToolInfo, opts ...model.Option) []model.Option {
|
||||
common := model.GetCommonOptions(&model.Options{
|
||||
Tools: append([]*schema.ToolInfo(nil), boundTools...),
|
||||
}, opts...)
|
||||
out := make([]model.Option, 0, 6)
|
||||
if common.Temperature != nil {
|
||||
out = append(out, model.WithTemperature(*common.Temperature))
|
||||
}
|
||||
if common.Model != nil {
|
||||
out = append(out, model.WithModel(*common.Model))
|
||||
}
|
||||
if common.TopP != nil {
|
||||
out = append(out, model.WithTopP(*common.TopP))
|
||||
}
|
||||
if common.MaxTokens != nil {
|
||||
out = append(out, model.WithMaxTokens(*common.MaxTokens))
|
||||
}
|
||||
if len(common.Stop) > 0 {
|
||||
out = append(out, model.WithStop(common.Stop))
|
||||
}
|
||||
if common.Tools != nil {
|
||||
out = append(out, model.WithTools(common.Tools))
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package llm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"cyberstrike-ai/internal/config"
|
||||
|
||||
agenticclaude "github.com/cloudwego/eino-ext/components/model/agenticclaude"
|
||||
"github.com/cloudwego/eino/components/model"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
)
|
||||
|
||||
func IsClaudeProvider(provider string) bool {
|
||||
provider = strings.ToLower(strings.TrimSpace(provider))
|
||||
return provider == "claude" || provider == "anthropic"
|
||||
}
|
||||
|
||||
func NewClaudeAgenticModel(
|
||||
ctx context.Context,
|
||||
cfg config.OpenAIConfig,
|
||||
httpClient *http.Client,
|
||||
maxTokens int,
|
||||
extraFields map[string]any,
|
||||
) (model.AgenticModel, error) {
|
||||
if maxTokens <= 0 {
|
||||
maxTokens = cfg.MaxCompletionTokensEffective()
|
||||
}
|
||||
if cfg.IsDeepSeekEndpointOrModel() {
|
||||
httpClient = newDeepSeekAnthropicCompatibleClient(httpClient)
|
||||
}
|
||||
return agenticclaude.New(ctx, &agenticclaude.Config{
|
||||
APIKey: strings.TrimSpace(cfg.APIKey),
|
||||
BaseURL: strings.TrimSuffix(strings.TrimSpace(cfg.BaseURL), "/"),
|
||||
Model: strings.TrimSpace(cfg.Model),
|
||||
MaxTokens: maxTokens,
|
||||
HTTPClient: httpClient,
|
||||
ExtraFields: extraFields,
|
||||
})
|
||||
}
|
||||
|
||||
func AgenticText(msg *schema.AgenticMessage) (content, reasoning string) {
|
||||
if msg == nil {
|
||||
return "", ""
|
||||
}
|
||||
var contentParts, reasoningParts []string
|
||||
for _, block := range msg.ContentBlocks {
|
||||
if block == nil {
|
||||
continue
|
||||
}
|
||||
switch {
|
||||
case block.AssistantGenText != nil:
|
||||
contentParts = append(contentParts, block.AssistantGenText.Text)
|
||||
case block.Reasoning != nil:
|
||||
reasoningParts = append(reasoningParts, block.Reasoning.Text)
|
||||
}
|
||||
}
|
||||
return strings.Join(contentParts, ""), strings.Join(reasoningParts, "")
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package llm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// newDeepSeekAnthropicCompatibleClient compensates for DeepSeek's Anthropic
|
||||
// endpoint lagging behind the current Anthropic SDK. The SDK emits
|
||||
// {"type":"custom"} for function tools, while DeepSeek expects the older
|
||||
// name/input_schema/description shape without that discriminator.
|
||||
//
|
||||
// This is a field-level compatibility fix; requests still originate from
|
||||
// Eino's native agenticclaude model and remain Anthropic Messages API requests.
|
||||
func newDeepSeekAnthropicCompatibleClient(base *http.Client) *http.Client {
|
||||
if base == nil {
|
||||
base = http.DefaultClient
|
||||
}
|
||||
cloned := *base
|
||||
transport := base.Transport
|
||||
if transport == nil {
|
||||
transport = http.DefaultTransport
|
||||
}
|
||||
cloned.Transport = &deepSeekAnthropicCompatRoundTripper{base: transport}
|
||||
return &cloned
|
||||
}
|
||||
|
||||
type deepSeekAnthropicCompatRoundTripper struct {
|
||||
base http.RoundTripper
|
||||
}
|
||||
|
||||
func (rt *deepSeekAnthropicCompatRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
if req == nil || req.Body == nil || req.Method != http.MethodPost {
|
||||
return rt.base.RoundTrip(req)
|
||||
}
|
||||
body, err := io.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read DeepSeek Anthropic request: %w", err)
|
||||
}
|
||||
_ = req.Body.Close()
|
||||
|
||||
var payload map[string]any
|
||||
if err := json.Unmarshal(body, &payload); err != nil {
|
||||
req.Body = io.NopCloser(bytes.NewReader(body))
|
||||
return rt.base.RoundTrip(req)
|
||||
}
|
||||
tools, ok := payload["tools"].([]any)
|
||||
if !ok {
|
||||
req.Body = io.NopCloser(bytes.NewReader(body))
|
||||
return rt.base.RoundTrip(req)
|
||||
}
|
||||
changed := false
|
||||
for _, rawTool := range tools {
|
||||
tool, ok := rawTool.(map[string]any)
|
||||
if !ok || tool["type"] != "custom" {
|
||||
continue
|
||||
}
|
||||
delete(tool, "type")
|
||||
changed = true
|
||||
}
|
||||
if changed {
|
||||
body, err = json.Marshal(payload)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshal DeepSeek Anthropic request: %w", err)
|
||||
}
|
||||
}
|
||||
req.Body = io.NopCloser(bytes.NewReader(body))
|
||||
req.ContentLength = int64(len(body))
|
||||
req.GetBody = func() (io.ReadCloser, error) {
|
||||
return io.NopCloser(bytes.NewReader(body)), nil
|
||||
}
|
||||
return rt.base.RoundTrip(req)
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package llm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type captureRoundTripper struct {
|
||||
body string
|
||||
}
|
||||
|
||||
func (rt *captureRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
body, err := io.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rt.body = string(body)
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Header: make(http.Header),
|
||||
Body: io.NopCloser(bytes.NewReader(nil)),
|
||||
Request: req,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func TestDeepSeekAnthropicCompatStripsOnlyCustomToolType(t *testing.T) {
|
||||
t.Parallel()
|
||||
capture := &captureRoundTripper{}
|
||||
client := newDeepSeekAnthropicCompatibleClient(&http.Client{Transport: capture})
|
||||
req, err := http.NewRequest(
|
||||
http.MethodPost,
|
||||
"https://api.deepseek.com/anthropic/v1/messages",
|
||||
strings.NewReader(`{"tools":[{"type":"custom","name":"mcp_tool","input_schema":{"type":"object"}},{"type":"web_search_20260209","name":"web_search"}]}`),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("NewRequest: %v", err)
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("Do: %v", err)
|
||||
}
|
||||
_ = resp.Body.Close()
|
||||
if strings.Contains(capture.body, `"type":"custom"`) {
|
||||
t.Fatalf("custom discriminator was not removed: %s", capture.body)
|
||||
}
|
||||
if !strings.Contains(capture.body, `"type":"web_search_20260209"`) {
|
||||
t.Fatalf("server tool discriminator was removed: %s", capture.body)
|
||||
}
|
||||
if !strings.Contains(capture.body, `"name":"mcp_tool"`) {
|
||||
t.Fatalf("custom tool definition was removed: %s", capture.body)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user