mirror of
https://github.com/mark3labs/kit.git
synced 2026-06-13 19:20:06 +00:00
fix: eliminate escape sequence leak from spinner tea.Program instances
Each spinner created a new tea.NewProgram which sent DECRQM queries for synchronized output mode 2026. When the program exited and restored cooked terminal mode, the terminal's DECRPM response leaked as visible ^[[?2026;2$y characters. Replace Bubble Tea spinner with a simple goroutine animation loop writing directly to stderr via lipgloss.
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
version: "2"
|
||||
|
||||
linters:
|
||||
enable:
|
||||
- modernize
|
||||
|
||||
formatters:
|
||||
enable:
|
||||
- gofmt
|
||||
@@ -23,6 +23,18 @@
|
||||
"type": "git",
|
||||
"name": "glamour",
|
||||
"url": "https://github.com/charmbracelet/glamour",
|
||||
"branch": "v2-exp"
|
||||
},
|
||||
{
|
||||
"type": "git",
|
||||
"name": "fantasy",
|
||||
"url": "https://github.com/charmbracelet/fantasy",
|
||||
"branch": "main"
|
||||
},
|
||||
{
|
||||
"type": "git",
|
||||
"name": "catwalk",
|
||||
"url": "https://github.com/charmbracelet/catwalk",
|
||||
"branch": "main"
|
||||
}
|
||||
],
|
||||
|
||||
+38
-30
@@ -10,7 +10,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cloudwego/eino/schema"
|
||||
"charm.land/fantasy"
|
||||
"github.com/mark3labs/mcphost/internal/agent"
|
||||
"github.com/mark3labs/mcphost/internal/config"
|
||||
"github.com/mark3labs/mcphost/internal/hooks"
|
||||
@@ -608,13 +608,12 @@ func runNormalMode(ctx context.Context) error {
|
||||
tools := mcpAgent.GetTools()
|
||||
var toolNames []string
|
||||
for _, tool := range tools {
|
||||
if info, err := tool.Info(ctx); err == nil {
|
||||
info := tool.Info()
|
||||
toolNames = append(toolNames, info.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// Main interaction logic
|
||||
var messages []*schema.Message
|
||||
var messages []fantasy.Message
|
||||
var sessionManager *session.Manager
|
||||
if sessionPath != "" {
|
||||
_, err := os.Stat(sessionPath)
|
||||
@@ -637,7 +636,8 @@ func runNormalMode(ctx context.Context) error {
|
||||
|
||||
// Convert session messages to schema messages
|
||||
for _, msg := range loadedSession.Messages {
|
||||
messages = append(messages, msg.ConvertToSchemaMessage())
|
||||
fantasyMsg := msg.ConvertToFantasyMessage()
|
||||
messages = append(messages, fantasyMsg)
|
||||
}
|
||||
|
||||
// If we're also saving, use the loaded session with the session manager
|
||||
@@ -658,9 +658,10 @@ func runNormalMode(ctx context.Context) error {
|
||||
|
||||
// Display all previous messages as they would have appeared
|
||||
for _, sessionMsg := range loadedSession.Messages {
|
||||
if sessionMsg.Role == "user" {
|
||||
switch sessionMsg.Role {
|
||||
case "user":
|
||||
cli.DisplayUserMessage(sessionMsg.Content)
|
||||
} else if sessionMsg.Role == "assistant" {
|
||||
case "assistant":
|
||||
// Display tool calls if present
|
||||
if len(sessionMsg.ToolCalls) > 0 {
|
||||
for _, tc := range sessionMsg.ToolCalls {
|
||||
@@ -679,7 +680,7 @@ func runNormalMode(ctx context.Context) error {
|
||||
if sessionMsg.Content != "" {
|
||||
cli.DisplayAssistantMessage(sessionMsg.Content)
|
||||
}
|
||||
} else if sessionMsg.Role == "tool" {
|
||||
case "tool":
|
||||
// Display tool result
|
||||
if sessionMsg.ToolCallID != "" {
|
||||
if toolCall, exists := toolCallMap[sessionMsg.ToolCallID]; exists {
|
||||
@@ -773,7 +774,7 @@ type AgenticLoopConfig struct {
|
||||
}
|
||||
|
||||
// addMessagesToHistory adds messages to the conversation history and saves to session if available
|
||||
func addMessagesToHistory(messages *[]*schema.Message, sessionManager *session.Manager, cli *ui.CLI, newMessages ...*schema.Message) {
|
||||
func addMessagesToHistory(messages *[]fantasy.Message, sessionManager *session.Manager, cli *ui.CLI, newMessages ...fantasy.Message) {
|
||||
// Add to local history
|
||||
*messages = append(*messages, newMessages...)
|
||||
|
||||
@@ -790,7 +791,7 @@ func addMessagesToHistory(messages *[]*schema.Message, sessionManager *session.M
|
||||
}
|
||||
|
||||
// replaceMessagesHistory replaces the conversation history and saves to session if available
|
||||
func replaceMessagesHistory(messages *[]*schema.Message, sessionManager *session.Manager, cli *ui.CLI, newMessages []*schema.Message) {
|
||||
func replaceMessagesHistory(messages *[]fantasy.Message, sessionManager *session.Manager, cli *ui.CLI, newMessages []fantasy.Message) {
|
||||
// Replace local history
|
||||
*messages = newMessages
|
||||
|
||||
@@ -807,7 +808,7 @@ func replaceMessagesHistory(messages *[]*schema.Message, sessionManager *session
|
||||
}
|
||||
|
||||
// runAgenticLoop handles all execution modes with a single unified loop
|
||||
func runAgenticLoop(ctx context.Context, mcpAgent *agent.Agent, cli *ui.CLI, messages []*schema.Message, config AgenticLoopConfig, hookExecutor *hooks.Executor) error {
|
||||
func runAgenticLoop(ctx context.Context, mcpAgent *agent.Agent, cli *ui.CLI, messages []fantasy.Message, config AgenticLoopConfig, hookExecutor *hooks.Executor) error {
|
||||
// Handle initial prompt for non-interactive modes
|
||||
if !config.IsInteractive && config.InitialPrompt != "" {
|
||||
// Execute UserPromptSubmit hooks for non-interactive mode
|
||||
@@ -837,7 +838,7 @@ func runAgenticLoop(ctx context.Context, mcpAgent *agent.Agent, cli *ui.CLI, mes
|
||||
}
|
||||
|
||||
// Create temporary messages with user input for processing (don't add to history yet)
|
||||
tempMessages := append(messages, schema.UserMessage(config.InitialPrompt))
|
||||
tempMessages := append(messages, fantasy.NewUserMessage(config.InitialPrompt))
|
||||
|
||||
// Process the initial prompt with tool calls
|
||||
_, conversationMessages, err := runAgenticStep(ctx, mcpAgent, cli, tempMessages, config, hookExecutor)
|
||||
@@ -875,7 +876,7 @@ func runAgenticLoop(ctx context.Context, mcpAgent *agent.Agent, cli *ui.CLI, mes
|
||||
}
|
||||
|
||||
// runAgenticStep processes a single step of the agentic loop (handles tool calls)
|
||||
func runAgenticStep(ctx context.Context, mcpAgent *agent.Agent, cli *ui.CLI, messages []*schema.Message, config AgenticLoopConfig, hookExecutor *hooks.Executor) (*schema.Message, []*schema.Message, error) {
|
||||
func runAgenticStep(ctx context.Context, mcpAgent *agent.Agent, cli *ui.CLI, messages []fantasy.Message, config AgenticLoopConfig, hookExecutor *hooks.Executor) (*fantasy.Response, []fantasy.Message, error) {
|
||||
var currentSpinner *ui.Spinner
|
||||
|
||||
// Start initial spinner (skip if quiet)
|
||||
@@ -1153,13 +1154,22 @@ func runAgenticStep(ctx context.Context, mcpAgent *agent.Agent, cli *ui.CLI, mes
|
||||
if len(messages) > 0 {
|
||||
// Find the last user message
|
||||
for i := len(messages) - 1; i >= 0; i-- {
|
||||
if messages[i].Role == schema.User {
|
||||
lastUserMessage = messages[i].Content
|
||||
if messages[i].Role == fantasy.MessageRoleUser {
|
||||
// Extract text from message parts
|
||||
for _, part := range messages[i].Content {
|
||||
if tp, ok := part.(fantasy.TextPart); ok {
|
||||
lastUserMessage = tp.Text
|
||||
break
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get text content from response
|
||||
responseText := response.Content.Text()
|
||||
|
||||
// Update usage tracking for ALL responses (streaming and non-streaming)
|
||||
if !config.Quiet && cli != nil {
|
||||
cli.UpdateUsageFromResponse(response, lastUserMessage)
|
||||
@@ -1167,15 +1177,15 @@ func runAgenticStep(ctx context.Context, mcpAgent *agent.Agent, cli *ui.CLI, mes
|
||||
|
||||
// Display assistant response with model name
|
||||
// Skip if: quiet mode, same content already displayed, or if streaming completed the full response
|
||||
streamedFullResponse := responseWasStreamed && streamingContent.String() == response.Content
|
||||
if !config.Quiet && cli != nil && response.Content != lastDisplayedContent && response.Content != "" && !streamedFullResponse {
|
||||
if err := cli.DisplayAssistantMessageWithModel(response.Content, config.ModelName); err != nil {
|
||||
streamedFullResponse := responseWasStreamed && streamingContent.String() == responseText
|
||||
if !config.Quiet && cli != nil && responseText != lastDisplayedContent && responseText != "" && !streamedFullResponse {
|
||||
if err := cli.DisplayAssistantMessageWithModel(responseText, config.ModelName); err != nil {
|
||||
cli.DisplayError(fmt.Errorf("display error: %v", err))
|
||||
return nil, nil, err
|
||||
}
|
||||
} else if config.Quiet {
|
||||
// In quiet mode, only output the final response content to stdout
|
||||
fmt.Print(response.Content)
|
||||
fmt.Print(responseText)
|
||||
}
|
||||
|
||||
// Display usage information immediately after the response (for both streaming and non-streaming)
|
||||
@@ -1191,15 +1201,14 @@ func runAgenticStep(ctx context.Context, mcpAgent *agent.Agent, cli *ui.CLI, mes
|
||||
}
|
||||
|
||||
// executeStopHook executes the Stop hook if a hook executor is available
|
||||
func executeStopHook(hookExecutor *hooks.Executor, response *schema.Message, stopReason string, modelName string) {
|
||||
func executeStopHook(hookExecutor *hooks.Executor, response *fantasy.Response, stopReason string, modelName string) {
|
||||
if hookExecutor != nil {
|
||||
// Prepare metadata
|
||||
var meta json.RawMessage
|
||||
if response != nil {
|
||||
metaData := map[string]interface{}{
|
||||
metaData := map[string]any{
|
||||
"model": modelName,
|
||||
"role": string(response.Role),
|
||||
"has_tool_calls": len(response.ToolCalls) > 0,
|
||||
"has_tool_calls": len(response.Content.ToolCalls()) > 0,
|
||||
}
|
||||
if metaBytes, err := json.Marshal(metaData); err == nil {
|
||||
meta = json.RawMessage(metaBytes)
|
||||
@@ -1208,7 +1217,7 @@ func executeStopHook(hookExecutor *hooks.Executor, response *schema.Message, sto
|
||||
|
||||
responseContent := ""
|
||||
if response != nil {
|
||||
responseContent = response.Content
|
||||
responseContent = response.Content.Text()
|
||||
}
|
||||
|
||||
input := &hooks.StopInput{
|
||||
@@ -1225,7 +1234,7 @@ func executeStopHook(hookExecutor *hooks.Executor, response *schema.Message, sto
|
||||
}
|
||||
|
||||
// runInteractiveLoop handles the interactive portion of the agentic loop
|
||||
func runInteractiveLoop(ctx context.Context, mcpAgent *agent.Agent, cli *ui.CLI, messages []*schema.Message, config AgenticLoopConfig, hookExecutor *hooks.Executor) error {
|
||||
func runInteractiveLoop(ctx context.Context, mcpAgent *agent.Agent, cli *ui.CLI, messages []fantasy.Message, config AgenticLoopConfig, hookExecutor *hooks.Executor) error {
|
||||
for {
|
||||
// Get user input
|
||||
prompt, err := cli.GetPrompt()
|
||||
@@ -1292,7 +1301,7 @@ func runInteractiveLoop(ctx context.Context, mcpAgent *agent.Agent, cli *ui.CLI,
|
||||
cli.DisplayUserMessage(prompt)
|
||||
|
||||
// Create temporary messages with user input for processing
|
||||
tempMessages := append(messages, schema.UserMessage(prompt))
|
||||
tempMessages := append(messages, fantasy.NewUserMessage(prompt))
|
||||
// Process the user input with tool calls
|
||||
_, conversationMessages, err := runAgenticStep(ctx, mcpAgent, cli, tempMessages, config, hookExecutor)
|
||||
if err != nil {
|
||||
@@ -1312,7 +1321,7 @@ func runInteractiveLoop(ctx context.Context, mcpAgent *agent.Agent, cli *ui.CLI,
|
||||
}
|
||||
|
||||
// runNonInteractiveMode handles the non-interactive mode execution
|
||||
func runNonInteractiveMode(ctx context.Context, mcpAgent *agent.Agent, cli *ui.CLI, prompt, modelName string, messages []*schema.Message, quiet, noExit bool, mcpConfig *config.Config, sessionManager *session.Manager, hookExecutor *hooks.Executor) error {
|
||||
func runNonInteractiveMode(ctx context.Context, mcpAgent *agent.Agent, cli *ui.CLI, prompt, modelName string, messages []fantasy.Message, quiet, noExit bool, mcpConfig *config.Config, sessionManager *session.Manager, hookExecutor *hooks.Executor) error {
|
||||
// Prepare data for slash commands (needed if continuing to interactive mode)
|
||||
var serverNames []string
|
||||
for name := range mcpConfig.MCPServers {
|
||||
@@ -1322,10 +1331,9 @@ func runNonInteractiveMode(ctx context.Context, mcpAgent *agent.Agent, cli *ui.C
|
||||
tools := mcpAgent.GetTools()
|
||||
var toolNames []string
|
||||
for _, tool := range tools {
|
||||
if info, err := tool.Info(ctx); err == nil {
|
||||
info := tool.Info()
|
||||
toolNames = append(toolNames, info.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// Configure and run unified agentic loop
|
||||
config := AgenticLoopConfig{
|
||||
@@ -1345,7 +1353,7 @@ func runNonInteractiveMode(ctx context.Context, mcpAgent *agent.Agent, cli *ui.C
|
||||
}
|
||||
|
||||
// runInteractiveMode handles the interactive mode execution
|
||||
func runInteractiveMode(ctx context.Context, mcpAgent *agent.Agent, cli *ui.CLI, serverNames, toolNames []string, modelName string, messages []*schema.Message, sessionManager *session.Manager, hookExecutor *hooks.Executor, approveToolRun bool) error {
|
||||
func runInteractiveMode(ctx context.Context, mcpAgent *agent.Agent, cli *ui.CLI, serverNames, toolNames []string, modelName string, messages []fantasy.Message, sessionManager *session.Manager, hookExecutor *hooks.Executor, approveToolRun bool) error {
|
||||
// Configure and run unified agentic loop
|
||||
config := AgenticLoopConfig{
|
||||
IsInteractive: true,
|
||||
|
||||
+6
-7
@@ -10,7 +10,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cloudwego/eino/schema"
|
||||
"charm.land/fantasy"
|
||||
"github.com/mark3labs/mcphost/internal/agent"
|
||||
"github.com/mark3labs/mcphost/internal/config"
|
||||
"github.com/mark3labs/mcphost/internal/hooks"
|
||||
@@ -177,8 +177,8 @@ func parseCustomVariables(_ *cobra.Command) map[string]string {
|
||||
}
|
||||
|
||||
// Parse custom variables with --args: prefix
|
||||
if strings.HasPrefix(arg, "--args:") {
|
||||
varName := strings.TrimPrefix(arg, "--args:")
|
||||
if after, ok := strings.CutPrefix(arg, "--args:"); ok {
|
||||
varName := after
|
||||
if varName == "" {
|
||||
continue // Skip malformed --args: without name
|
||||
}
|
||||
@@ -312,7 +312,7 @@ func parseScriptContent(content string, variables map[string]string) (*config.Co
|
||||
var promptLines []string
|
||||
var inFrontmatter bool
|
||||
var foundFrontmatter bool
|
||||
var frontmatterEnd int = -1
|
||||
var frontmatterEnd = -1
|
||||
|
||||
for i, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
@@ -699,13 +699,12 @@ func runScriptMode(ctx context.Context, mcpConfig *config.Config, prompt string,
|
||||
tools := mcpAgent.GetTools()
|
||||
var toolNames []string
|
||||
for _, tool := range tools {
|
||||
if info, err := tool.Info(ctx); err == nil {
|
||||
info := tool.Info()
|
||||
toolNames = append(toolNames, info.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// Configure and run unified agentic loop
|
||||
var messages []*schema.Message
|
||||
var messages []fantasy.Message
|
||||
config := AgenticLoopConfig{
|
||||
IsInteractive: prompt == "", // If no prompt, start in interactive mode
|
||||
InitialPrompt: prompt,
|
||||
|
||||
@@ -82,7 +82,7 @@ Working directory is ${env://WORK_DIR:-/tmp}.
|
||||
t.Fatal("Filesystem server not found in script config")
|
||||
}
|
||||
|
||||
allowedDirs, ok := fsServer.Options["allowed_directories"].([]interface{})
|
||||
allowedDirs, ok := fsServer.Options["allowed_directories"].([]any)
|
||||
if !ok {
|
||||
t.Fatal("allowed_directories should be an array")
|
||||
}
|
||||
|
||||
@@ -1,150 +1,129 @@
|
||||
module github.com/mark3labs/mcphost
|
||||
|
||||
go 1.24.2
|
||||
|
||||
toolchain go1.24.5
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
charm.land/bubbles/v2 v2.0.0
|
||||
charm.land/bubbletea/v2 v2.0.0
|
||||
charm.land/catwalk v0.22.1
|
||||
charm.land/fantasy v0.10.0
|
||||
charm.land/lipgloss/v2 v2.0.0
|
||||
github.com/JohannesKaufmann/html-to-markdown v1.6.0
|
||||
github.com/PuerkitoBio/goquery v1.10.3
|
||||
github.com/bytedance/sonic v1.15.0
|
||||
github.com/charmbracelet/fang v0.4.4
|
||||
github.com/cloudwego/eino v0.7.13
|
||||
github.com/cloudwego/eino-ext/components/model/claude v0.1.12
|
||||
github.com/cloudwego/eino-ext/components/model/ollama v0.1.8
|
||||
github.com/cloudwego/eino-ext/components/model/openai v0.0.0-20250903035842-96774a3ec845
|
||||
github.com/eino-contrib/jsonschema v1.0.3
|
||||
github.com/getkin/kin-openapi v0.131.0
|
||||
github.com/mark3labs/mcp-filesystem-server v0.11.1
|
||||
github.com/mark3labs/mcp-go v0.44.0-beta.2
|
||||
github.com/spf13/cobra v1.10.1
|
||||
github.com/spf13/viper v1.20.1
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
golang.org/x/term v0.37.0
|
||||
google.golang.org/genai v1.22.0
|
||||
golang.org/x/term v0.40.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.121.6 // indirect
|
||||
cloud.google.com/go/auth v0.16.5 // indirect
|
||||
cloud.google.com/go v0.123.0 // indirect
|
||||
cloud.google.com/go/auth v0.18.2 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.8.0 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
|
||||
github.com/alecthomas/chroma/v2 v2.20.0 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.3 // indirect
|
||||
github.com/anthropics/anthropic-sdk-go v1.10.0 // indirect
|
||||
github.com/atotto/clipboard v0.1.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.38.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.29.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.38.2 // indirect
|
||||
github.com/aws/smithy-go v1.23.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.9 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.9 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.14 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 // indirect
|
||||
github.com/aws/smithy-go v1.24.1 // indirect
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||
github.com/buger/jsonparser v1.1.1 // indirect
|
||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||
github.com/bytedance/sonic/loader v0.5.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/charmbracelet/anthropic-sdk-go v0.0.0-20260223140439-63879b0b8dab // indirect
|
||||
github.com/charmbracelet/colorprofile v0.4.2 // indirect
|
||||
github.com/charmbracelet/harmonica v0.2.0 // indirect
|
||||
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 // indirect
|
||||
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 // indirect
|
||||
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
|
||||
github.com/charmbracelet/x/etag v0.2.0 // indirect
|
||||
github.com/charmbracelet/x/exp/charmtone v0.0.0-20250902204034-1cdc10c66d5b // indirect
|
||||
github.com/charmbracelet/x/exp/color v0.0.0-20250902204034-1cdc10c66d5b // indirect
|
||||
github.com/charmbracelet/x/exp/slice v0.0.0-20250902204034-1cdc10c66d5b // indirect
|
||||
github.com/charmbracelet/x/exp/slice v0.0.0-20250904123553-b4e2667e5ad5 // indirect
|
||||
github.com/charmbracelet/x/json v0.2.0 // indirect
|
||||
github.com/charmbracelet/x/termios v0.1.1 // indirect
|
||||
github.com/charmbracelet/x/windows v0.2.2 // indirect
|
||||
github.com/clipperhouse/displaywidth v0.11.0 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/cloudwego/eino-ext/libs/acl/openai v0.0.0-20250826113018-8c6f6358d4bb // indirect
|
||||
github.com/djherbis/times v1.6.0 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/eino-contrib/ollama v0.1.0 // indirect
|
||||
github.com/evanphx/json-patch v0.5.2 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
|
||||
github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.22.0 // indirect
|
||||
github.com/go-openapi/swag/jsonname v0.24.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||
github.com/gobwas/glob v0.2.3 // indirect
|
||||
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
|
||||
github.com/goph/emperror v0.17.2 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.12 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.17.0 // indirect
|
||||
github.com/gorilla/css v1.0.1 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/invopop/jsonschema v0.13.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/kaptinlin/go-i18n v0.2.11 // indirect
|
||||
github.com/kaptinlin/jsonpointer v0.4.16 // indirect
|
||||
github.com/kaptinlin/jsonschema v0.7.3 // indirect
|
||||
github.com/kaptinlin/messageformat-go v0.4.18 // indirect
|
||||
github.com/mailru/easyjson v0.9.0 // indirect
|
||||
github.com/meguminnnnnnnnn/go-openai v0.0.0-20250821095446-07791bea23a0 // indirect
|
||||
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
|
||||
github.com/muesli/mango v0.2.0 // indirect
|
||||
github.com/muesli/mango-cobra v1.2.0 // indirect
|
||||
github.com/muesli/mango-pflag v0.1.0 // indirect
|
||||
github.com/muesli/reflow v0.3.0 // indirect
|
||||
github.com/muesli/roff v0.1.0 // indirect
|
||||
github.com/nikolalohinski/gonja v1.5.3 // indirect
|
||||
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect
|
||||
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect
|
||||
github.com/openai/openai-go/v2 v2.7.1 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/perimeterx/marshmallow v1.1.5 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/sagikazarmark/locafero v0.10.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/slongfield/pyfmt v0.0.0-20220222012616-ea85ff4c361f // indirect
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
||||
github.com/spf13/afero v1.14.0 // indirect
|
||||
github.com/spf13/afero v1.15.0 // indirect
|
||||
github.com/spf13/cast v1.9.2 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
github.com/yargevad/filepathx v1.0.0 // indirect
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
|
||||
github.com/yuin/goldmark v1.7.13 // indirect
|
||||
github.com/yuin/goldmark-emoji v1.0.6 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
|
||||
go.opentelemetry.io/otel v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
||||
golang.org/x/arch v0.23.0 // indirect
|
||||
golang.org/x/crypto v0.45.0 // indirect
|
||||
golang.org/x/net v0.47.0 // indirect
|
||||
golang.org/x/oauth2 v0.30.0 // indirect
|
||||
golang.org/x/time v0.12.0 // indirect
|
||||
google.golang.org/api v0.246.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1 // indirect
|
||||
google.golang.org/grpc v1.75.0 // indirect
|
||||
google.golang.org/protobuf v1.36.8 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect
|
||||
go.opentelemetry.io/otel v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.40.0 // indirect
|
||||
golang.org/x/crypto v0.48.0 // indirect
|
||||
golang.org/x/net v0.50.0 // indirect
|
||||
golang.org/x/oauth2 v0.35.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
google.golang.org/api v0.267.0 // indirect
|
||||
google.golang.org/genai v1.47.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d // indirect
|
||||
google.golang.org/grpc v1.79.1 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -160,8 +139,7 @@ require (
|
||||
github.com/muesli/termenv v0.16.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/text v0.31.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
)
|
||||
|
||||
@@ -2,16 +2,28 @@ charm.land/bubbles/v2 v2.0.0 h1:tE3eK/pHjmtrDiRdoC9uGNLgpopOd8fjhEe31B/ai5s=
|
||||
charm.land/bubbles/v2 v2.0.0/go.mod h1:rCHoleP2XhU8um45NTuOWBPNVHxnkXKTiZqcclL/qOI=
|
||||
charm.land/bubbletea/v2 v2.0.0 h1:p0d6CtWyJXJ9GfzMpUUqbP/XUUhhlk06+vCKWmox1wQ=
|
||||
charm.land/bubbletea/v2 v2.0.0/go.mod h1:3LRff2U4WIYXy7MTxfbAQ+AdfM3D8Xuvz2wbsOD9OHQ=
|
||||
charm.land/catwalk v0.22.1 h1:i7nxxYyEzgWqDD3ifAZ8SQR/cEaPbviiBbxq+ZGhk6M=
|
||||
charm.land/catwalk v0.22.1/go.mod h1:rFC/V96rIHX7VES215c/qzI1EW/Moo1ggs1Q6seTy5s=
|
||||
charm.land/fantasy v0.10.0 h1:6PD+1rrsCgLIG1n+PAZp/gHiC0dltU0cvb7c8zUKyu8=
|
||||
charm.land/fantasy v0.10.0/go.mod h1:KIeNQUpJTswwpY0P6HJsr3LBFgfTDb8FDpOdVQMsKqY=
|
||||
charm.land/lipgloss/v2 v2.0.0 h1:sd8N/B3x892oiOjFfBQdXBQp3cAkvjGaU5TvVZC3ivo=
|
||||
charm.land/lipgloss/v2 v2.0.0/go.mod h1:w6SnmsBFBmEFBodiEDurGS/sdUY/u1+v72DqUzc6J14=
|
||||
cloud.google.com/go v0.121.6 h1:waZiuajrI28iAf40cWgycWNgaXPO06dupuS+sgibK6c=
|
||||
cloud.google.com/go v0.121.6/go.mod h1:coChdst4Ea5vUpiALcYKXEpR1S9ZgXbhEzzMcMR66vI=
|
||||
cloud.google.com/go/auth v0.16.5 h1:mFWNQ2FEVWAliEQWpAdH80omXFokmrnbDhUS9cBywsI=
|
||||
cloud.google.com/go/auth v0.16.5/go.mod h1:utzRfHMP+Vv0mpOkTRQoWD2q3BatTOoWbA7gCc2dUhQ=
|
||||
cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=
|
||||
cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
|
||||
cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM=
|
||||
cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
||||
cloud.google.com/go/compute/metadata v0.8.0 h1:HxMRIbao8w17ZX6wBnjhcDkW6lTFpgcaobyVfZWqRLA=
|
||||
cloud.google.com/go/compute/metadata v0.8.0/go.mod h1:sYOGTp851OV9bOFJ9CH7elVvyzopvWQFNNghtDQ/Biw=
|
||||
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
|
||||
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 h1:g0EZJwz7xkXQiZAI5xi9f3WWFYBlX1CPTrR+NDToRkQ=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0/go.mod h1:XCW7KnZet0Opnr7HccfUw1PLc4CjHqpcaxW8DHklNkQ=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 h1:tfLQ34V6F7tVSwoTf/4lH5sE0o6eCJuNDTmH09nDpbc=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
|
||||
github.com/JohannesKaufmann/html-to-markdown v1.6.0 h1:04VXMiE50YYfCfLboJCLcgqF5x+rHJnb1ssNmqpLH/k=
|
||||
github.com/JohannesKaufmann/html-to-markdown v1.6.0/go.mod h1:NUI78lGg/a7vpEJTz/0uOcYMaibytE4BUOQS8k78yPQ=
|
||||
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
|
||||
@@ -19,7 +31,6 @@ github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6
|
||||
github.com/PuerkitoBio/goquery v1.9.2/go.mod h1:GHPCaP0ODyyxqcNoFGYlAprUFH81NuRPd0GX3Zu2Mvk=
|
||||
github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo=
|
||||
github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=
|
||||
github.com/airbrake/gobrake v3.6.1+incompatible/go.mod h1:wM4gu3Cn0W0K7GUuVWnlXZU11AGBXMILnrdOU8Kn00o=
|
||||
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
||||
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||
github.com/alecthomas/chroma/v2 v2.20.0 h1:sfIHpxPyR07/Oylvmcai3X/exDlE8+FA820NTz+9sGw=
|
||||
@@ -29,38 +40,38 @@ github.com/alecthomas/repr v0.5.1/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW5
|
||||
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
|
||||
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
|
||||
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
|
||||
github.com/anthropics/anthropic-sdk-go v1.10.0 h1:jDKQTfC0miIEj21eMmPrNSLKTNdNa3nHZOhd4wZz1cI=
|
||||
github.com/anthropics/anthropic-sdk-go v1.10.0/go.mod h1:WTz31rIUHUHqai2UslPpw5CwXrQP3geYBioRV4WOLvE=
|
||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||
github.com/aws/aws-sdk-go-v2 v1.38.3 h1:B6cV4oxnMs45fql4yRH+/Po/YU+597zgWqvDpYMturk=
|
||||
github.com/aws/aws-sdk-go-v2 v1.38.3/go.mod h1:sDioUELIUO9Znk23YVmIk86/9DOpkbyyVb1i/gUNFXY=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1 h1:i8p8P4diljCr60PpJp6qZXNlgX4m2yQFpYk+9ZT+J4E=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1/go.mod h1:ddqbooRZYNoJ2dsTwOty16rM+/Aqmk/GOXrK8cg7V00=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.6 h1:a1t8fXY4GT4xjyJExz4knbuoxSCacB5hT/WgtfPyLjo=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.6/go.mod h1:5ByscNi7R+ztvOGzeUaIu49vkMk2soq5NaH5PYe33MQ=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.10 h1:xdJnXCouCx8Y0NncgoptztUocIYLKeQxrCgN6x9sdhg=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.10/go.mod h1:7tQk08ntj914F/5i9jC4+2HQTAuJirq7m1vZVIhEkWs=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.6 h1:wbjnrrMnKew78/juW7I2BtKQwa1qlf6EjQgS69uYY14=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.6/go.mod h1:AtiqqNrDioJXuUgz3+3T0mBWN7Hro2n9wll2zRUc0ww=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.6 h1:uF68eJA6+S9iVr9WgX1NaRGyQ/6MdIyc4JNUo6TN1FA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.6/go.mod h1:qlPeVZCGPiobx8wb1ft0GHT5l+dc6ldnwInDFaMvC7Y=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.6 h1:pa1DEC6JoI0zduhZePp3zmhWvk/xxm4NB8Hy/Tlsgos=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.6/go.mod h1:gxEjPebnhWGJoaDdtDkA0JX46VRg1wcTHYe63OfX5pE=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 h1:oegbebPEMA/1Jny7kvwejowCaHz1FWZAQ94WXFNCyTM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1/go.mod h1:kemo5Myr9ac0U9JfSjMo9yHLtw+pECEHsFtJ9tqCEI8=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.6 h1:LHS1YAIJXJ4K9zS+1d/xa9JAA9sL2QyXIQCQFQW/X08=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.6/go.mod h1:c9PCiTEuh0wQID5/KqA32J+HAgZxN9tOGXKCiYJjTZI=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.29.1 h1:8OLZnVJPvjnrxEwHFg9hVUof/P4sibH+Ea4KKuqAGSg=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.29.1/go.mod h1:27M3BpVi0C02UiQh1w9nsBEit6pLhlaH3NHna6WUbDE=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.2 h1:gKWSTnqudpo8dAxqBqZnDoDWCiEh/40FziUjr/mo6uA=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.2/go.mod h1:x7+rkNmRoEN1U13A6JE2fXne9EWyJy54o3n6d4mGaXQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.38.2 h1:YZPjhyaGzhDQEvsffDEcpycq49nl7fiGcfJTIo8BszI=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.38.2/go.mod h1:2dIN8qhQfv37BdUYGgEC8Q3tteM3zFxTI1MLO2O3J3c=
|
||||
github.com/aws/smithy-go v1.23.0 h1:8n6I3gXzWJB2DxBDnfxgBaSX6oe0d/t10qGz7OKqMCE=
|
||||
github.com/aws/smithy-go v1.23.0/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.1 h1:ABlyEARCDLN034NhxlRUSZr4l71mh+T5KAeGh6cerhU=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4/go.mod h1:IOAPF6oT9KCsceNTvvYMNHy0+kMF8akOjeDvPENWxp4=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.9 h1:ktda/mtAydeObvJXlHzyGpK1xcsLaP16zfUPDGoW90A=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.9/go.mod h1:U+fCQ+9QKsLW786BCfEjYRj34VVTbPdsLP3CHSYXMOI=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.9 h1:sWvTKsyrMlJGEuj/WgrwilpoJ6Xa1+KhIpGdzw7mMU8=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.9/go.mod h1:+J44MBhmfVY/lETFiKI+klz0Vym2aCmIjqgClMmW82w=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 h1:I0GyV8wiYrP8XpA70g1HBcQO1JlQxCMTW9npl5UbDHY=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17/go.mod h1:tyw7BOl5bBe/oqvoIeECFJjMdzXoa/dfVz3QQ5lgHGA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 h1:xOLELNKGp2vsiteLsvLPwxC+mYmO6OZ8PYgiuPJzF8U=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17/go.mod h1:5M5CI3D12dNOtH3/mk6minaRwI2/37ifCURZISxA/IQ=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 h1:WWLqlh79iO48yLkj1v3ISRNiv+3KdQoZ6JWyfcsyQik=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17/go.mod h1:EhG22vHRrvF8oXSTYStZhJc1aUgKtnJe+aOiFEV90cM=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 h1:RuNSMoozM8oXlgLG/n6WLaFGoea7/CddrCfIiSA+xdY=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17/go.mod h1:F2xxQ9TZz5gDWsclCtPQscGpP0VUOc8RqgFM3vDENmU=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 h1:VrhDvQib/i0lxvr3zqlUwLwJP4fpmpyD9wYG1vfSu+Y=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.5/go.mod h1:k029+U8SY30/3/ras4G/Fnv/b88N4mAfliNn08Dem4M=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.10 h1:+VTRawC4iVY58pS/lzpo0lnoa/SYNGF4/B/3/U5ro8Y=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.10/go.mod h1:yifAsgBxgJWn3ggx70A3urX2AN49Y5sJTD1UQFlfqBw=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.14 h1:0jbJeuEHlwKJ9PfXtpSFc4MF+WIWORdhN1n30ITZGFM=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.14/go.mod h1:sTGThjphYE4Ohw8vJiRStAcu3rbjtXRsdNB0TvZ5wwo=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 h1:5fFjR/ToSOzB2OQ/XqWpZBmNvmP/pJ1jOWYlFDJTjRQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6/go.mod h1:qgFDZQSD/Kys7nJnVqYlWKnh0SSdMjAi0uSwON4wgYQ=
|
||||
github.com/aws/smithy-go v1.24.1 h1:VbyeNfmYkWoxMVpGUAbQumkODcYmfMRfZ8yQiH30SK0=
|
||||
github.com/aws/smithy-go v1.24.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
github.com/aymanbagabas/go-udiff v0.4.0 h1:TKnLPh7IbnizJIBKFWa9mKayRUBQ9Kh1BPCk6w2PnYM=
|
||||
@@ -69,21 +80,12 @@ github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuP
|
||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
|
||||
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
|
||||
github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=
|
||||
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
|
||||
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
|
||||
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
||||
github.com/bugsnag/bugsnag-go v1.4.0/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8=
|
||||
github.com/bugsnag/panicwrap v1.2.0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE=
|
||||
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
||||
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
||||
github.com/bytedance/mockey v1.2.14 h1:KZaFgPdiUwW+jOWFieo3Lr7INM1P+6adO3hxZhDswY8=
|
||||
github.com/bytedance/mockey v1.2.14/go.mod h1:1BPHF9sol5R1ud/+0VEHGQq/+i2lN+GTsr3O2Q9IENY=
|
||||
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
|
||||
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
|
||||
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
|
||||
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||
github.com/certifi/gocertifi v0.0.0-20190105021004-abcd57078448/go.mod h1:GJKEexRPVJrBSOjoqN5VNOIKJ5Q3RViH6eu3puDRwx4=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/charmbracelet/anthropic-sdk-go v0.0.0-20260223140439-63879b0b8dab h1:J7XQLgl9sefgTnTGrmX3xqvp5o6MCiBzEjGv5igAlc4=
|
||||
github.com/charmbracelet/anthropic-sdk-go v0.0.0-20260223140439-63879b0b8dab/go.mod h1:hqlYqR7uPKOKfnNeicUbZp0Ps0GeYFlKYtwh5HGDCx8=
|
||||
github.com/charmbracelet/colorprofile v0.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY=
|
||||
github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8=
|
||||
github.com/charmbracelet/fang v0.4.4 h1:G4qKxF6or/eTPgmAolwPuRNyuci3hTUGGX1rj1YkHJY=
|
||||
@@ -100,14 +102,18 @@ github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF
|
||||
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
|
||||
github.com/charmbracelet/x/etag v0.2.0 h1:Euj1VkheoHfTYA9y+TCwkeXF/hN8Fb9l4LqZl79pt04=
|
||||
github.com/charmbracelet/x/etag v0.2.0/go.mod h1:C1B7/bsgvzzxpfu0Rabbd+rTHJa5TmC/qgTseCf6DF0=
|
||||
github.com/charmbracelet/x/exp/charmtone v0.0.0-20250902204034-1cdc10c66d5b h1:U9SnQTnrxy8y3gEpxhpBS3ztHAR7IvL0CjvFHOR4sbE=
|
||||
github.com/charmbracelet/x/exp/charmtone v0.0.0-20250902204034-1cdc10c66d5b/go.mod h1:T9jr8CzFpjhFVHjNjKwbAD7KwBNyFnj2pntAO7F2zw0=
|
||||
github.com/charmbracelet/x/exp/color v0.0.0-20250902204034-1cdc10c66d5b h1:x4wRlDV7e7qM6yYS06W6wMKh6z1NeD1+DTjvOm2grzo=
|
||||
github.com/charmbracelet/x/exp/color v0.0.0-20250902204034-1cdc10c66d5b/go.mod h1:hk/GyTELmEgX54pBAOHcFvH8Xed53JWo/g8kJXFo/PI=
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA=
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I=
|
||||
github.com/charmbracelet/x/exp/slice v0.0.0-20250902204034-1cdc10c66d5b h1:DZ2Li1O0j+wWw6AgEUDrODB7PAIKpmOy65yu1UBPYc4=
|
||||
github.com/charmbracelet/x/exp/slice v0.0.0-20250902204034-1cdc10c66d5b/go.mod h1:vI5nDVMWi6veaYH+0Fmvpbe/+cv/iJfMntdh+N0+Tms=
|
||||
github.com/charmbracelet/x/exp/slice v0.0.0-20250904123553-b4e2667e5ad5 h1:DTSZxdV9qQagD4iGcAt9RgaRBZtJl01bfKgdLzUzUPI=
|
||||
github.com/charmbracelet/x/exp/slice v0.0.0-20250904123553-b4e2667e5ad5/go.mod h1:vI5nDVMWi6veaYH+0Fmvpbe/+cv/iJfMntdh+N0+Tms=
|
||||
github.com/charmbracelet/x/json v0.2.0 h1:DqB+ZGx2h+Z+1s98HOuOyli+i97wsFQIxP2ZQANTPrQ=
|
||||
github.com/charmbracelet/x/json v0.2.0/go.mod h1:opFIflx2YgXgi49xVUu8gEQ21teFAxyMwvOiZhIvWNM=
|
||||
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
||||
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
||||
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
|
||||
@@ -118,105 +124,78 @@ github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSE
|
||||
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||
github.com/cloudwego/eino v0.7.13 h1:Ku7hY+83gGJJjf4On3UgqjC57UcA+DXe0tqAZiNDDew=
|
||||
github.com/cloudwego/eino v0.7.13/go.mod h1:nA8Vacmuqv3pqKBQbTWENBLQ8MmGmPt/WqiyLeB8ohQ=
|
||||
github.com/cloudwego/eino-ext/components/model/claude v0.1.12 h1:c66gFH9J5Ku2/v1f7jPwI9R4CYw5TiAlIVzsfzjsF1g=
|
||||
github.com/cloudwego/eino-ext/components/model/claude v0.1.12/go.mod h1:a9oQkf4Ib+/VqjsLRdRETytt2m/C4fbcvfjPNu6nVAg=
|
||||
github.com/cloudwego/eino-ext/components/model/ollama v0.1.8 h1:+BStnQlkRxWMV9jsPopLmmut2ARG88e9hDSMaDNAI/w=
|
||||
github.com/cloudwego/eino-ext/components/model/ollama v0.1.8/go.mod h1:C3rf3yy2nEoXFP/CQJne4gbiu1pREKplHKmFlhuOzPE=
|
||||
github.com/cloudwego/eino-ext/components/model/openai v0.0.0-20250903035842-96774a3ec845 h1:nxflfiBwWNPoKS9X4SMhmT+si7rtYv+lQzIyPJik4DM=
|
||||
github.com/cloudwego/eino-ext/components/model/openai v0.0.0-20250903035842-96774a3ec845/go.mod h1:QQhCuQxuBAVWvu/YAZBhs/RsR76mUigw59Tl0kh04C8=
|
||||
github.com/cloudwego/eino-ext/libs/acl/openai v0.0.0-20250826113018-8c6f6358d4bb h1:RMslzyijc3bi9EkqCulpS0hZupTl1y/wayR3+fVRN/c=
|
||||
github.com/cloudwego/eino-ext/libs/acl/openai v0.0.0-20250826113018-8c6f6358d4bb/go.mod h1:fHn/6OqPPY1iLLx9wzz+MEVT5Dl9gwuZte1oLEnCoYw=
|
||||
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik=
|
||||
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
|
||||
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
|
||||
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
||||
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/eino-contrib/jsonschema v1.0.3 h1:2Kfsm1xlMV0ssY2nuxshS4AwbLFuqmPmzIjLVJ1Fsp0=
|
||||
github.com/eino-contrib/jsonschema v1.0.3/go.mod h1:cpnX4SyKjWjGC7iN2EbhxaTdLqGjCi0e9DxpLYxddD4=
|
||||
github.com/eino-contrib/ollama v0.1.0 h1:z1NaMdKW6X1ftP8g5xGGR5zDRPUtuTKFq35vBQgxsN4=
|
||||
github.com/eino-contrib/ollama v0.1.0/go.mod h1:mYsQ7b3DeqY8bHPuD3MZJYTqkgyL6LoemxoP/B7ZNhA=
|
||||
github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k=
|
||||
github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ=
|
||||
github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI=
|
||||
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
|
||||
github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA=
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNfdSbEPe9Yyl09/B6wBrQ=
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/getkin/kin-openapi v0.131.0 h1:NO2UeHnFKRYhZ8wg6Nyh5Cq7dHk4suQQr72a4pMrDxE=
|
||||
github.com/getkin/kin-openapi v0.131.0/go.mod h1:3OlG51PCYNsPByuiMB0t4fjnNlIDnaEDsjiKUV8nL58=
|
||||
github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
|
||||
github.com/go-check/check v0.0.0-20180628173108-788fd7840127 h1:0gkP6mzaMqkmpcJYCFOLkIBwI7xFExG03bbkOkCvUPI=
|
||||
github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98=
|
||||
github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e h1:Lf/gRkoycfOBPa42vU2bbgPurFong6zXeFtPoxholzU=
|
||||
github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e/go.mod h1:uNVvRXArCGbZ508SxYYTC5v1JWoz2voff5pm25jU1Ok=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-openapi/jsonpointer v0.22.0 h1:TmMhghgNef9YXxTu1tOopo+0BGEytxA+okbry0HjZsM=
|
||||
github.com/go-openapi/jsonpointer v0.22.0/go.mod h1:xt3jV88UtExdIkkL7NloURjRQjbeUgcxFblMjq2iaiU=
|
||||
github.com/go-openapi/swag/jsonname v0.24.0 h1:2wKS9bgRV/xB8c62Qg16w4AUiIrqqiniJFtZGi3dg5k=
|
||||
github.com/go-openapi/swag/jsonname v0.24.0/go.mod h1:GXqrPzGJe611P7LG4QB9JKPtUZ7flE4DOVechNaDd7Q=
|
||||
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
|
||||
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
||||
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
|
||||
github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=
|
||||
github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc=
|
||||
github.com/goph/emperror v0.17.2 h1:yLapQcmEsO0ipe9p5TaN22djm3OFV/TfM/fcYP0/J18=
|
||||
github.com/goph/emperror v0.17.2/go.mod h1:+ZbQ+fUNO/6FNiUo0ujtMjhgad9Xa6fQL9KhH4LNHic=
|
||||
github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g=
|
||||
github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.12 h1:Fg+zsqzYEs1ZnvmcztTYxhgCBsx3eEhEwQ1W/lHq/sQ=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.12/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=
|
||||
github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc=
|
||||
github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY=
|
||||
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
||||
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
|
||||
github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kaptinlin/go-i18n v0.2.11 h1:OayNt8mWt8nDaqAOp09/C1VG9Y5u8LpQnnxbyGARDV4=
|
||||
github.com/kaptinlin/go-i18n v0.2.11/go.mod h1:pVcu9qsW5pOIOoZFJXesRYmLos1vMQrby70JPAoWmJU=
|
||||
github.com/kaptinlin/jsonpointer v0.4.16 h1:Ux4w4FY+uLv+K+TxaCJtM/TpPv+1+eS6gH4Z9/uhOuA=
|
||||
github.com/kaptinlin/jsonpointer v0.4.16/go.mod h1:SsfsjqnHG5zuKo1DTBzk1VknaHlL4osHw+X9kZKukpU=
|
||||
github.com/kaptinlin/jsonschema v0.7.3 h1:kyIydij76ORiSxmfy0xFYy0cOx8MwG6pyyaSoQshsK4=
|
||||
github.com/kaptinlin/jsonschema v0.7.3/go.mod h1:Ys6zr+W6/1330FzZEouFrAYImK+AmYt5HQVTHQQXQo8=
|
||||
github.com/kaptinlin/messageformat-go v0.4.18 h1:RBlHVWgZyoxTcUgGWBsl2AcyScq/urqbLZvzgryTmSI=
|
||||
github.com/kaptinlin/messageformat-go v0.4.18/go.mod h1:ntI3154RnqJgr7GaC+vZBnIExl2V3sv9selvRNNEM24=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
@@ -224,6 +203,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
|
||||
@@ -232,26 +213,13 @@ github.com/mark3labs/mcp-filesystem-server v0.11.1 h1:7uKIZRMaKWfgvtDj/uLAvo0+7M
|
||||
github.com/mark3labs/mcp-filesystem-server v0.11.1/go.mod h1:xDqJizVYWZ5a31Mt4xuYbVku2AR/kT56H3O0SbpANoQ=
|
||||
github.com/mark3labs/mcp-go v0.44.0-beta.2 h1:gfUT0m77E4odfgiHkqV/E+MQVaQ06rbutW7Ln0JRkBA=
|
||||
github.com/mark3labs/mcp-go v0.44.0-beta.2/go.mod h1:YnJfOL382MIWDx1kMY+2zsRHU/q78dBg9aFb8W6Thdw=
|
||||
github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
|
||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ=
|
||||
github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||
github.com/meguminnnnnnnnn/go-openai v0.0.0-20250821095446-07791bea23a0 h1:nIohpHs1ViKR0SVgW/cbBstHjmnqFZDM9RqgX9m9Xu8=
|
||||
github.com/meguminnnnnnnnn/go-openai v0.0.0-20250821095446-07791bea23a0/go.mod h1:qs96ysDmxhE4BZoU45I43zcyfnaYxU3X+aRzLko/htY=
|
||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
|
||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
|
||||
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
|
||||
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||
github.com/muesli/mango v0.2.0 h1:iNNc0c5VLQ6fsMgAqGQofByNUBH2Q2nEbD6TaI+5yyQ=
|
||||
@@ -266,32 +234,24 @@ github.com/muesli/roff v0.1.0 h1:YD0lalCotmYuF5HhZliKWlIx7IEhiXeSfq7hNjFqGF8=
|
||||
github.com/muesli/roff v0.1.0/go.mod h1:pjAHQM9hdUUwm/krAfrLGgJkXJ+YuhtsfZ42kieB2Ig=
|
||||
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||
github.com/nikolalohinski/gonja v1.5.3 h1:GsA+EEaZDZPGJ8JtpeGN78jidhOlxeJROpqMT9fTj9c=
|
||||
github.com/nikolalohinski/gonja v1.5.3/go.mod h1:RmjwxNiXAEqcq1HeK5SSMmqFJvKOfTfXhkJv6YBtPa4=
|
||||
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY=
|
||||
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw=
|
||||
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c=
|
||||
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/openai/openai-go/v2 v2.7.1 h1:/tfvTJhfv7hTSL8mWwc5VL4WLLSDL5yn9VqVykdu9r8=
|
||||
github.com/openai/openai-go/v2 v2.7.1/go.mod h1:jrJs23apqJKKbT+pqtFgNKpRju/KP9zpUTZhz3GElQE=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s=
|
||||
github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/rollbar/rollbar-go v1.0.2/go.mod h1:AcFs5f0I+c71bpHlXNNDbOWJiKwjFDtISeXco0L5PKQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sagikazarmark/locafero v0.10.0 h1:FM8Cv6j2KqIhM2ZK7HZjm4mpj9NBktLgowT1aN9q5Cc=
|
||||
github.com/sagikazarmark/locafero v0.10.0/go.mod h1:Ieo3EUsjifvQu4NZwV5sPd4dwvu0OCgEQV7vjc9yDjw=
|
||||
@@ -300,19 +260,10 @@ github.com/sebdah/goldie/v2 v2.5.3/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvK
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
|
||||
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/slongfield/pyfmt v0.0.0-20220222012616-ea85ff4c361f h1:Z2cODYsUxQPofhpYRMQVwWz4yUVpHF+vPi+eUdruUYI=
|
||||
github.com/slongfield/pyfmt v0.0.0-20220222012616-ea85ff4c361f/go.mod h1:JqzWyvTuI2X4+9wOHmKSQCYxybB/8j6Ko43qVmXDuZg=
|
||||
github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGByCY=
|
||||
github.com/smarty/assertions v1.15.0/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec=
|
||||
github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY=
|
||||
github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60=
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
|
||||
github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=
|
||||
github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo=
|
||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||
github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE=
|
||||
github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
||||
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
|
||||
@@ -323,18 +274,8 @@ github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3A
|
||||
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
|
||||
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
@@ -349,18 +290,10 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
|
||||
github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg=
|
||||
github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
github.com/yargevad/filepathx v1.0.0 h1:SYcT+N3tYGi+NvazubCNlvgIPbzAk7i7y2dwg3I5FYc=
|
||||
github.com/yargevad/filepathx v1.0.0/go.mod h1:BprfX/gpYNJHJfc35GjRRpVcwWXS89gGulUIU5tK3tA=
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
@@ -369,27 +302,22 @@ github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA=
|
||||
github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs=
|
||||
github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
|
||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
||||
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
||||
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
||||
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
|
||||
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
||||
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
|
||||
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
|
||||
golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg=
|
||||
golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0 h1:XmiuHzgJt067+a6kwyAzkhXooYVv3/TOw9cM2VfJgUM=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0/go.mod h1:KDgtbWKTQs4bM+VPUr6WlL9m/WXcmkCcBlIzqxPGzmI=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0=
|
||||
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
|
||||
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
|
||||
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
|
||||
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
|
||||
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
|
||||
go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
|
||||
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
|
||||
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
@@ -397,16 +325,15 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf
|
||||
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b h1:DXr+pvt3nC887026GRP39Ej11UATqWDmWuS99x26cD0=
|
||||
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
|
||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0=
|
||||
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
@@ -418,11 +345,10 @@ golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
||||
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
||||
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
|
||||
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -432,14 +358,11 @@ golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -463,8 +386,8 @@ golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
||||
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
||||
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
|
||||
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
@@ -474,10 +397,10 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
@@ -487,26 +410,22 @@ golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxb
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/api v0.246.0 h1:H0ODDs5PnMZVZAEtdLMn2Ul2eQi7QNjqM2DIFp8TlTM=
|
||||
google.golang.org/api v0.246.0/go.mod h1:dMVhVcylamkirHdzEBAIQWUCgqY885ivNeZYd7VAVr8=
|
||||
google.golang.org/genai v1.22.0 h1:5hrEhXXWJQZa3tdPocl4vQ/0w6myEAxdNns2Kmx0f4Y=
|
||||
google.golang.org/genai v1.22.0/go.mod h1:QPj5NGJw+3wEOHg+PrsWwJKvG6UC84ex5FR7qAYsN/M=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1 h1:pmJpJEvT846VzausCQ5d7KreSROcDqmO388w5YbnltA=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1/go.mod h1:GmFNa4BdJZ2a8G+wCe9Bg3wwThLrJun751XstdJt5Og=
|
||||
google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4=
|
||||
google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
|
||||
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
|
||||
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
google.golang.org/api v0.267.0 h1:w+vfWPMPYeRs8qH1aYYsFX68jMls5acWl/jocfLomwE=
|
||||
google.golang.org/api v0.267.0/go.mod h1:Jzc0+ZfLnyvXma3UtaTl023TdhZu6OMBP9tJ+0EmFD0=
|
||||
google.golang.org/genai v1.47.0 h1:iWCS7gEdO6rctOqfCYLOrZGKu2D+N42aTnCEcBvB1jo=
|
||||
google.golang.org/genai v1.47.0/go.mod h1:A3kkl0nyBjyFlNjgxIwKq70julKbIxpSxqKO5gw/gmk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d h1:t/LOSXPJ9R0B6fnZNyALBRfZBH0Uy0gT+uR+SJ6syqQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
|
||||
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
+219
-329
@@ -8,102 +8,114 @@ import (
|
||||
"time"
|
||||
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"github.com/cloudwego/eino/components/model"
|
||||
"github.com/cloudwego/eino/components/tool"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
"charm.land/fantasy"
|
||||
|
||||
"github.com/mark3labs/mcphost/internal/config"
|
||||
"github.com/mark3labs/mcphost/internal/models"
|
||||
"github.com/mark3labs/mcphost/internal/tools"
|
||||
)
|
||||
|
||||
// AgentConfig holds configuration options for creating a new Agent.
|
||||
// It includes model configuration, MCP settings, and various behavioral options.
|
||||
type AgentConfig struct {
|
||||
// ModelConfig specifies the LLM provider and model to use
|
||||
ModelConfig *models.ProviderConfig
|
||||
// MCPConfig contains MCP server configurations
|
||||
MCPConfig *config.Config
|
||||
// SystemPrompt is the initial system message for the agent
|
||||
SystemPrompt string
|
||||
// MaxSteps limits the number of tool calls (0 for unlimited)
|
||||
MaxSteps int
|
||||
// StreamingEnabled controls whether responses are streamed
|
||||
StreamingEnabled bool
|
||||
// DebugLogger is an optional logger for debugging MCP communications
|
||||
DebugLogger tools.DebugLogger // Optional debug logger
|
||||
DebugLogger tools.DebugLogger
|
||||
}
|
||||
|
||||
// ToolCallHandler is a function type for handling tool calls as they happen.
|
||||
// It receives the tool name and its arguments when a tool is about to be invoked.
|
||||
type ToolCallHandler func(toolName, toolArgs string)
|
||||
|
||||
// ToolExecutionHandler is a function type for handling tool execution start/end events.
|
||||
// The isStarting parameter indicates whether the tool is starting (true) or finished (false).
|
||||
type ToolExecutionHandler func(toolName string, isStarting bool)
|
||||
|
||||
// ToolResultHandler is a function type for handling tool results.
|
||||
// It receives the tool name, arguments, result, and whether the result is an error.
|
||||
type ToolResultHandler func(toolName, toolArgs, result string, isError bool)
|
||||
|
||||
// ResponseHandler is a function type for handling LLM responses.
|
||||
// It receives the complete response content from the model.
|
||||
type ResponseHandler func(content string)
|
||||
|
||||
// StreamingResponseHandler is a function type for handling streaming LLM responses.
|
||||
// It receives content chunks as they are streamed from the model.
|
||||
type StreamingResponseHandler func(content string)
|
||||
|
||||
// ToolCallContentHandler is a function type for handling content that accompanies tool calls.
|
||||
// It receives any text content that the model generates alongside tool calls.
|
||||
type ToolCallContentHandler func(content string)
|
||||
|
||||
// ToolApprovalHandler is a function type for handling user approval of tool calls.
|
||||
// It receives the tool name and arguments, and returns true if the user approves.
|
||||
type ToolApprovalHandler func(toolName, toolArgs string) (bool, error)
|
||||
|
||||
// Agent represents an AI agent with MCP tool integration and real-time tool call display.
|
||||
// Agent represents an AI agent with MCP tool integration using the fantasy library.
|
||||
// It manages the interaction between an LLM and various tools through the MCP protocol.
|
||||
type Agent struct {
|
||||
toolManager *tools.MCPToolManager
|
||||
model model.ToolCallingChatModel
|
||||
fantasyAgent fantasy.Agent
|
||||
model fantasy.LanguageModel
|
||||
maxSteps int
|
||||
systemPrompt string
|
||||
loadingMessage string // Message from provider loading (e.g., GPU fallback info)
|
||||
providerType string // Provider type for streaming behavior
|
||||
streamingEnabled bool // Whether streaming is enabled
|
||||
loadingMessage string
|
||||
providerType string
|
||||
streamingEnabled bool
|
||||
}
|
||||
|
||||
// GenerateWithLoopResult contains the result and conversation history from an agent interaction.
|
||||
type GenerateWithLoopResult struct {
|
||||
// FinalResponse is the last message generated by the model
|
||||
FinalResponse *fantasy.Response
|
||||
// ConversationMessages contains all messages in the conversation including tool calls and results
|
||||
ConversationMessages []fantasy.Message
|
||||
// TotalUsage contains aggregate token usage across all steps
|
||||
TotalUsage fantasy.Usage
|
||||
}
|
||||
|
||||
// NewAgent creates a new Agent with MCP tool integration and streaming support.
|
||||
// It initializes the LLM provider, loads MCP tools, and configures the agent
|
||||
// based on the provided configuration. Returns an error if provider creation
|
||||
// or tool loading fails.
|
||||
func NewAgent(ctx context.Context, config *AgentConfig) (*Agent, error) {
|
||||
// Create the LLM provider
|
||||
providerResult, err := models.CreateProvider(ctx, config.ModelConfig)
|
||||
func NewAgent(ctx context.Context, agentConfig *AgentConfig) (*Agent, error) {
|
||||
// Create the LLM provider via fantasy
|
||||
providerResult, err := models.CreateProvider(ctx, agentConfig.ModelConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create model provider: %v", err)
|
||||
}
|
||||
|
||||
// Create and load MCP tools
|
||||
toolManager := tools.NewMCPToolManager()
|
||||
|
||||
// Set the model for sampling support
|
||||
toolManager.SetModel(providerResult.Model)
|
||||
|
||||
// Set the debug logger if provided
|
||||
if config.DebugLogger != nil {
|
||||
toolManager.SetDebugLogger(config.DebugLogger)
|
||||
if agentConfig.DebugLogger != nil {
|
||||
toolManager.SetDebugLogger(agentConfig.DebugLogger)
|
||||
}
|
||||
|
||||
if err := toolManager.LoadTools(ctx, config.MCPConfig); err != nil {
|
||||
if err := toolManager.LoadTools(ctx, agentConfig.MCPConfig); err != nil {
|
||||
return nil, fmt.Errorf("failed to load MCP tools: %v", err)
|
||||
}
|
||||
|
||||
// Build fantasy agent options
|
||||
var agentOpts []fantasy.AgentOption
|
||||
|
||||
if agentConfig.SystemPrompt != "" {
|
||||
agentOpts = append(agentOpts, fantasy.WithSystemPrompt(agentConfig.SystemPrompt))
|
||||
}
|
||||
|
||||
// Register all MCP tools with the fantasy agent
|
||||
mcpTools := toolManager.GetTools()
|
||||
if len(mcpTools) > 0 {
|
||||
agentOpts = append(agentOpts, fantasy.WithTools(mcpTools...))
|
||||
}
|
||||
|
||||
// Set max steps as stop condition
|
||||
if agentConfig.MaxSteps > 0 {
|
||||
agentOpts = append(agentOpts, fantasy.WithStopConditions(
|
||||
fantasy.StepCountIs(agentConfig.MaxSteps),
|
||||
))
|
||||
}
|
||||
|
||||
// Create the fantasy agent
|
||||
fantasyAgent := fantasy.NewAgent(providerResult.Model, agentOpts...)
|
||||
|
||||
// Determine provider type from model string
|
||||
providerType := "default"
|
||||
if config.ModelConfig != nil && config.ModelConfig.ModelString != "" {
|
||||
parts := strings.SplitN(config.ModelConfig.ModelString, ":", 2)
|
||||
if agentConfig.ModelConfig != nil && agentConfig.ModelConfig.ModelString != "" {
|
||||
parts := strings.SplitN(agentConfig.ModelConfig.ModelString, ":", 2)
|
||||
if len(parts) >= 1 {
|
||||
providerType = parts[0]
|
||||
}
|
||||
@@ -111,345 +123,239 @@ func NewAgent(ctx context.Context, config *AgentConfig) (*Agent, error) {
|
||||
|
||||
return &Agent{
|
||||
toolManager: toolManager,
|
||||
fantasyAgent: fantasyAgent,
|
||||
model: providerResult.Model,
|
||||
maxSteps: config.MaxSteps, // Keep 0 for infinite, handle in loop
|
||||
systemPrompt: config.SystemPrompt,
|
||||
maxSteps: agentConfig.MaxSteps,
|
||||
systemPrompt: agentConfig.SystemPrompt,
|
||||
loadingMessage: providerResult.Message,
|
||||
providerType: providerType,
|
||||
streamingEnabled: config.StreamingEnabled,
|
||||
streamingEnabled: agentConfig.StreamingEnabled,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GenerateWithLoopResult contains the result and conversation history from an agent interaction.
|
||||
// It includes both the final response and the complete message history with tool interactions.
|
||||
type GenerateWithLoopResult struct {
|
||||
// FinalResponse is the last message generated by the model
|
||||
FinalResponse *schema.Message
|
||||
// ConversationMessages contains all messages in the conversation including tool calls and results
|
||||
ConversationMessages []*schema.Message // All messages in the conversation (including tool calls and results)
|
||||
}
|
||||
|
||||
// GenerateWithLoop processes messages with a custom loop that displays tool calls in real-time.
|
||||
// It handles the conversation flow, executing tools as needed and invoking callbacks for various events.
|
||||
// This method does not support streaming responses; use GenerateWithLoopAndStreaming for streaming support.
|
||||
func (a *Agent) GenerateWithLoop(ctx context.Context, messages []*schema.Message,
|
||||
onToolCall ToolCallHandler, onToolExecution ToolExecutionHandler, onToolResult ToolResultHandler, onResponse ResponseHandler, onToolCallContent ToolCallContentHandler, onToolApproval ToolApprovalHandler,
|
||||
func (a *Agent) GenerateWithLoop(ctx context.Context, messages []fantasy.Message,
|
||||
onToolCall ToolCallHandler, onToolExecution ToolExecutionHandler, onToolResult ToolResultHandler,
|
||||
onResponse ResponseHandler, onToolCallContent ToolCallContentHandler, onToolApproval ToolApprovalHandler,
|
||||
) (*GenerateWithLoopResult, error) {
|
||||
return a.GenerateWithLoopAndStreaming(ctx, messages, onToolCall, onToolExecution, onToolResult, onResponse, onToolCallContent, nil, onToolApproval)
|
||||
return a.GenerateWithLoopAndStreaming(ctx, messages, onToolCall, onToolExecution, onToolResult,
|
||||
onResponse, onToolCallContent, nil, onToolApproval)
|
||||
}
|
||||
|
||||
// GenerateWithLoopAndStreaming processes messages with a custom loop that displays tool calls in real-time and supports streaming callbacks.
|
||||
// It handles the conversation flow, executing tools as needed and invoking callbacks for various events including streaming chunks.
|
||||
// The onStreamingResponse callback is invoked for each content chunk during streaming if streaming is enabled.
|
||||
func (a *Agent) GenerateWithLoopAndStreaming(ctx context.Context, messages []*schema.Message,
|
||||
onToolCall ToolCallHandler, onToolExecution ToolExecutionHandler, onToolResult ToolResultHandler, onResponse ResponseHandler, onToolCallContent ToolCallContentHandler, onStreamingResponse StreamingResponseHandler, onToolApproval ToolApprovalHandler,
|
||||
// GenerateWithLoopAndStreaming processes messages using the fantasy agent with streaming and callbacks.
|
||||
// Fantasy handles the tool call loop internally. We map fantasy's rich callback system
|
||||
// to mcphost's existing callback interface for UI integration.
|
||||
func (a *Agent) GenerateWithLoopAndStreaming(ctx context.Context, messages []fantasy.Message,
|
||||
onToolCall ToolCallHandler, onToolExecution ToolExecutionHandler, onToolResult ToolResultHandler,
|
||||
onResponse ResponseHandler, onToolCallContent ToolCallContentHandler,
|
||||
onStreamingResponse StreamingResponseHandler, onToolApproval ToolApprovalHandler,
|
||||
) (*GenerateWithLoopResult, error) {
|
||||
// Create a copy of messages to avoid modifying the original
|
||||
workingMessages := make([]*schema.Message, len(messages))
|
||||
copy(workingMessages, messages)
|
||||
|
||||
// Add system prompt if provided
|
||||
if a.systemPrompt != "" {
|
||||
hasSystemMessage := false
|
||||
if len(workingMessages) > 0 && workingMessages[0].Role == schema.System {
|
||||
hasSystemMessage = true
|
||||
}
|
||||
// Fantasy requires the current user input as Prompt, with prior messages as history.
|
||||
// Extract the last user message text as the prompt, and pass everything before it as Messages.
|
||||
prompt, history := splitPromptAndHistory(messages)
|
||||
|
||||
if !hasSystemMessage {
|
||||
systemMsg := schema.SystemMessage(a.systemPrompt)
|
||||
workingMessages = append([]*schema.Message{systemMsg}, workingMessages...)
|
||||
}
|
||||
}
|
||||
// Track current tool call info for callbacks
|
||||
var currentToolName string
|
||||
var currentToolArgs string
|
||||
|
||||
// Get available tools
|
||||
availableTools := a.toolManager.GetTools()
|
||||
var toolInfos []*schema.ToolInfo
|
||||
toolMap := make(map[string]tool.BaseTool)
|
||||
if a.streamingEnabled {
|
||||
// Use fantasy's streaming agent
|
||||
result, err := a.fantasyAgent.Stream(ctx, fantasy.AgentStreamCall{
|
||||
Prompt: prompt,
|
||||
Messages: history,
|
||||
|
||||
for _, t := range availableTools {
|
||||
info, err := t.Info(ctx)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if info == nil {
|
||||
continue
|
||||
}
|
||||
toolInfos = append(toolInfos, info)
|
||||
toolMap[info.Name] = t
|
||||
// Text streaming callback
|
||||
OnTextDelta: func(id, text string) error {
|
||||
if onStreamingResponse != nil {
|
||||
onStreamingResponse(text)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
|
||||
// Main loop
|
||||
for step := 0; a.maxSteps == 0 || step < a.maxSteps; step++ {
|
||||
// Check if context was cancelled before making LLM call
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
default:
|
||||
}
|
||||
// Tool call complete - the tool has been parsed and is about to execute
|
||||
OnToolCall: func(tc fantasy.ToolCallContent) error {
|
||||
currentToolName = tc.ToolName
|
||||
currentToolArgs = tc.Input
|
||||
|
||||
// Call the LLM with cancellation support
|
||||
response, err := a.generateWithCancellationAndStreaming(ctx, workingMessages, toolInfos, onStreamingResponse)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Add response to working messages
|
||||
workingMessages = append(workingMessages, response)
|
||||
|
||||
// Check if this is a tool call or final response
|
||||
if len(response.ToolCalls) > 0 {
|
||||
// Display any content that accompanies the tool calls
|
||||
if response.Content != "" && onToolCallContent != nil {
|
||||
onToolCallContent(response.Content)
|
||||
}
|
||||
|
||||
// Handle tool calls
|
||||
for _, toolCall := range response.ToolCalls {
|
||||
// Check approval if handler is set
|
||||
if onToolApproval != nil {
|
||||
approved, err := onToolApproval(toolCall.Function.Name, toolCall.Function.Arguments)
|
||||
approved, err := onToolApproval(tc.ToolName, tc.Input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
if !approved {
|
||||
rejectedMsg := fmt.Sprintf("The user did not allow tool call %s. Reason: User cancelled.", toolCall.Function.Name)
|
||||
toolMessage := schema.ToolMessage(rejectedMsg, toolCall.ID)
|
||||
workingMessages = append(workingMessages, toolMessage)
|
||||
continue
|
||||
return fmt.Errorf("tool call %s rejected by user", tc.ToolName)
|
||||
}
|
||||
}
|
||||
|
||||
// Notify about tool call
|
||||
// Notify about the tool call
|
||||
if onToolCall != nil {
|
||||
onToolCall(toolCall.Function.Name, toolCall.Function.Arguments)
|
||||
onToolCall(tc.ToolName, tc.Input)
|
||||
}
|
||||
|
||||
// Execute the tool
|
||||
if selectedTool, exists := toolMap[toolCall.Function.Name]; exists {
|
||||
// Notify tool execution start
|
||||
// Notify tool execution starting
|
||||
if onToolExecution != nil {
|
||||
onToolExecution(toolCall.Function.Name, true)
|
||||
onToolExecution(tc.ToolName, true)
|
||||
}
|
||||
|
||||
// Sanitize arguments for common LLM junk like "}{"
|
||||
arguments := toolCall.Function.Arguments
|
||||
if len(arguments) > 0 && strings.Trim(arguments, " \t\n\r{}") == "" {
|
||||
arguments = "{}"
|
||||
}
|
||||
return nil
|
||||
},
|
||||
|
||||
output, err := selectedTool.(tool.InvokableTool).InvokableRun(ctx, arguments)
|
||||
|
||||
// Notify tool execution end
|
||||
// Tool result - tool execution completed
|
||||
OnToolResult: func(tr fantasy.ToolResultContent) error {
|
||||
// Notify tool execution finished
|
||||
if onToolExecution != nil {
|
||||
onToolExecution(toolCall.Function.Name, false)
|
||||
onToolExecution(tr.ToolName, false)
|
||||
}
|
||||
|
||||
if onToolResult != nil {
|
||||
// Extract result text and error status
|
||||
resultText, isError := extractToolResultText(tr)
|
||||
onToolResult(tr.ToolName, currentToolArgs, resultText, isError)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
|
||||
// Step callbacks for content that accompanies tool calls
|
||||
OnStepFinish: func(step fantasy.StepResult) error {
|
||||
// Check if step has text content alongside tool calls
|
||||
text := step.Content.Text()
|
||||
toolCalls := step.Content.ToolCalls()
|
||||
if text != "" && len(toolCalls) > 0 && onToolCallContent != nil {
|
||||
onToolCallContent(text)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
errorMsg := fmt.Sprintf("Tool execution error: %v", err)
|
||||
toolMessage := schema.ToolMessage(errorMsg, toolCall.ID)
|
||||
workingMessages = append(workingMessages, toolMessage)
|
||||
|
||||
if onToolResult != nil {
|
||||
onToolResult(toolCall.Function.Name, toolCall.Function.Arguments, errorMsg, true)
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
// Check if this is an MCP tool response with an error
|
||||
isError := false
|
||||
if output != "" {
|
||||
var mcpResult mcp.CallToolResult
|
||||
if err := json.Unmarshal([]byte(output), &mcpResult); err == nil && mcpResult.IsError {
|
||||
isError = true
|
||||
|
||||
return convertAgentResult(result, messages), nil
|
||||
}
|
||||
|
||||
// Non-streaming path
|
||||
result, err := a.fantasyAgent.Generate(ctx, fantasy.AgentCall{
|
||||
Prompt: prompt,
|
||||
Messages: history,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// For non-streaming, fire the response callback with the final text
|
||||
if onResponse != nil && result.Response.Content.Text() != "" {
|
||||
onResponse(result.Response.Content.Text())
|
||||
}
|
||||
|
||||
_ = currentToolName // satisfy compiler for non-streaming path
|
||||
|
||||
return convertAgentResult(result, messages), nil
|
||||
}
|
||||
|
||||
// splitPromptAndHistory extracts the last user message as the prompt string,
|
||||
// and returns everything before it as conversation history. Fantasy's agent
|
||||
// requires the current turn's input as Prompt (string), with prior messages
|
||||
// passed separately as Messages (history).
|
||||
func splitPromptAndHistory(messages []fantasy.Message) (string, []fantasy.Message) {
|
||||
if len(messages) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// Walk backwards to find the last user message
|
||||
for i := len(messages) - 1; i >= 0; i-- {
|
||||
if messages[i].Role == fantasy.MessageRoleUser {
|
||||
// Extract text from the user message parts
|
||||
var prompt string
|
||||
for _, part := range messages[i].Content {
|
||||
if tp, ok := part.(fantasy.TextPart); ok {
|
||||
prompt = tp.Text
|
||||
break
|
||||
}
|
||||
}
|
||||
// History is everything except this last user message
|
||||
history := make([]fantasy.Message, 0, len(messages)-1)
|
||||
history = append(history, messages[:i]...)
|
||||
history = append(history, messages[i+1:]...)
|
||||
return prompt, history
|
||||
}
|
||||
}
|
||||
|
||||
toolMessage := schema.ToolMessage(output, toolCall.ID)
|
||||
workingMessages = append(workingMessages, toolMessage)
|
||||
// No user message found — use the last message's text as prompt
|
||||
last := messages[len(messages)-1]
|
||||
for _, part := range last.Content {
|
||||
if tp, ok := part.(fantasy.TextPart); ok {
|
||||
return tp.Text, messages[:len(messages)-1]
|
||||
}
|
||||
}
|
||||
|
||||
if onToolResult != nil {
|
||||
onToolResult(toolCall.Function.Name, toolCall.Function.Arguments, output, isError)
|
||||
return "", messages
|
||||
}
|
||||
}
|
||||
} else {
|
||||
errorMsg := fmt.Sprintf("Tool not found: %s", toolCall.Function.Name)
|
||||
toolMessage := schema.ToolMessage(errorMsg, toolCall.ID)
|
||||
workingMessages = append(workingMessages, toolMessage)
|
||||
|
||||
if onToolResult != nil {
|
||||
onToolResult(toolCall.Function.Name, toolCall.Function.Arguments, errorMsg, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// This is a final response
|
||||
if onResponse != nil && response.Content != "" {
|
||||
onResponse(response.Content)
|
||||
// convertAgentResult converts a fantasy AgentResult to our GenerateWithLoopResult.
|
||||
func convertAgentResult(result *fantasy.AgentResult, originalMessages []fantasy.Message) *GenerateWithLoopResult {
|
||||
// Collect all conversation messages: original + all step messages
|
||||
var allMessages []fantasy.Message
|
||||
allMessages = append(allMessages, originalMessages...)
|
||||
|
||||
for _, step := range result.Steps {
|
||||
allMessages = append(allMessages, step.Messages...)
|
||||
}
|
||||
|
||||
return &GenerateWithLoopResult{
|
||||
FinalResponse: response,
|
||||
ConversationMessages: workingMessages,
|
||||
}, nil
|
||||
FinalResponse: &result.Response,
|
||||
ConversationMessages: allMessages,
|
||||
TotalUsage: result.TotalUsage,
|
||||
}
|
||||
}
|
||||
|
||||
// If we reach here, we've exceeded max steps
|
||||
finalResponse := schema.AssistantMessage("Maximum number of steps reached.", nil)
|
||||
return &GenerateWithLoopResult{
|
||||
FinalResponse: finalResponse,
|
||||
ConversationMessages: workingMessages,
|
||||
}, nil
|
||||
// extractToolResultText extracts the text and error status from a fantasy ToolResultContent.
|
||||
func extractToolResultText(tr fantasy.ToolResultContent) (string, bool) {
|
||||
if tr.Result == nil {
|
||||
return "", false
|
||||
}
|
||||
|
||||
// Marshal the result to JSON for display
|
||||
resultBytes, err := json.Marshal(tr.Result)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("%v", tr.Result), false
|
||||
}
|
||||
|
||||
resultText := string(resultBytes)
|
||||
|
||||
// Check if this is an error result by examining the type
|
||||
if errResult, ok := tr.Result.(fantasy.ToolResultOutputContentError); ok {
|
||||
return errResult.Error.Error(), true
|
||||
}
|
||||
|
||||
return resultText, false
|
||||
}
|
||||
|
||||
// GetTools returns the list of available tools loaded in the agent.
|
||||
// These tools are available for the model to use during interactions.
|
||||
func (a *Agent) GetTools() []tool.BaseTool {
|
||||
func (a *Agent) GetTools() []fantasy.AgentTool {
|
||||
return a.toolManager.GetTools()
|
||||
}
|
||||
|
||||
// GetLoadingMessage returns the loading message from provider creation.
|
||||
// This may contain information about GPU fallback or other provider-specific initialization details.
|
||||
func (a *Agent) GetLoadingMessage() string {
|
||||
return a.loadingMessage
|
||||
}
|
||||
|
||||
// GetLoadedServerNames returns the names of successfully loaded MCP servers.
|
||||
// This includes both builtin servers and external MCP server configurations.
|
||||
func (a *Agent) GetLoadedServerNames() []string {
|
||||
return a.toolManager.GetLoadedServerNames()
|
||||
}
|
||||
|
||||
// generateWithCancellationAndStreaming calls the LLM with ESC key cancellation support and streaming callbacks
|
||||
func (a *Agent) generateWithCancellationAndStreaming(ctx context.Context, messages []*schema.Message, toolInfos []*schema.ToolInfo, streamingCallback StreamingResponseHandler) (*schema.Message, error) {
|
||||
// Check if streaming is enabled
|
||||
if !a.streamingEnabled {
|
||||
// Use traditional non-streaming approach
|
||||
return a.generateWithoutStreaming(ctx, messages, toolInfos)
|
||||
// GetModel returns the underlying fantasy LanguageModel.
|
||||
func (a *Agent) GetModel() fantasy.LanguageModel {
|
||||
return a.model
|
||||
}
|
||||
|
||||
// Try streaming first if no tools are expected or if we can detect tool calls early
|
||||
if len(toolInfos) == 0 {
|
||||
// No tools available, use streaming directly
|
||||
return a.generateWithStreamingAndCallback(ctx, messages, toolInfos, streamingCallback)
|
||||
}
|
||||
|
||||
// Try streaming with tool call detection
|
||||
return a.generateWithStreamingFirstAndCallback(ctx, messages, toolInfos, streamingCallback)
|
||||
}
|
||||
|
||||
// generateWithStreamingAndCallback uses streaming for responses without tool calls with real-time callbacks
|
||||
func (a *Agent) generateWithStreamingAndCallback(ctx context.Context, messages []*schema.Message, toolInfos []*schema.ToolInfo, callback StreamingResponseHandler) (*schema.Message, error) {
|
||||
// Try streaming first
|
||||
reader, err := a.model.Stream(ctx, messages, model.WithTools(toolInfos))
|
||||
if err != nil {
|
||||
// Fallback to non-streaming if streaming fails
|
||||
return a.model.Generate(ctx, messages, model.WithTools(toolInfos))
|
||||
}
|
||||
|
||||
// Use streaming with callback for real-time display
|
||||
response, err := StreamWithCallback(ctx, reader, func(chunk string) {
|
||||
if callback != nil {
|
||||
callback(chunk)
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
// Fallback to non-streaming on error
|
||||
return a.model.Generate(ctx, messages, model.WithTools(toolInfos))
|
||||
}
|
||||
|
||||
// Return the complete streamed response (with tool calls if any)
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// generateWithStreamingFirstAndCallback attempts streaming first with provider-aware tool call detection and callbacks
|
||||
func (a *Agent) generateWithStreamingFirstAndCallback(ctx context.Context, messages []*schema.Message, toolInfos []*schema.ToolInfo, callback StreamingResponseHandler) (*schema.Message, error) {
|
||||
// Try streaming first
|
||||
reader, err := a.model.Stream(ctx, messages, model.WithTools(toolInfos))
|
||||
if err != nil {
|
||||
// Fallback to non-streaming if streaming fails
|
||||
return a.model.Generate(ctx, messages, model.WithTools(toolInfos))
|
||||
}
|
||||
|
||||
// Use streaming with callback for real-time display
|
||||
response, err := StreamWithCallback(ctx, reader, func(chunk string) {
|
||||
if callback != nil {
|
||||
callback(chunk)
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
// Fallback to non-streaming on error
|
||||
return a.model.Generate(ctx, messages, model.WithTools(toolInfos))
|
||||
}
|
||||
|
||||
// Return the complete streamed response (with tool calls if any)
|
||||
// No need to restart - we have everything we need!
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// generateWithoutStreaming uses the traditional non-streaming approach
|
||||
func (a *Agent) generateWithoutStreaming(ctx context.Context, messages []*schema.Message, toolInfos []*schema.ToolInfo) (*schema.Message, error) {
|
||||
// Create a cancellable context for just this LLM call
|
||||
llmCtx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
// Channel to receive the LLM result
|
||||
resultChan := make(chan struct {
|
||||
message *schema.Message
|
||||
err error
|
||||
}, 1)
|
||||
|
||||
// Start ESC key listener first and wait for it to be ready
|
||||
escChan := make(chan bool, 1)
|
||||
stopListening := make(chan bool, 1)
|
||||
escReady := make(chan bool, 1)
|
||||
|
||||
go func() {
|
||||
if a.listenForESC(stopListening, escReady) {
|
||||
escChan <- true
|
||||
} else {
|
||||
escChan <- false
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for ESC listener to be ready before starting LLM
|
||||
select {
|
||||
case <-escReady:
|
||||
// ESC listener is ready, proceed
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
// Timeout waiting for ESC listener, proceed anyway
|
||||
case <-ctx.Done():
|
||||
close(stopListening)
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
|
||||
// Now start the LLM generation
|
||||
go func() {
|
||||
message, err := a.model.Generate(llmCtx, messages, model.WithTools(toolInfos))
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to generate response: %v", err)
|
||||
}
|
||||
resultChan <- struct {
|
||||
message *schema.Message
|
||||
err error
|
||||
}{message, err}
|
||||
}()
|
||||
|
||||
// Wait for either LLM completion or ESC key
|
||||
select {
|
||||
case result := <-resultChan:
|
||||
// Stop the ESC listener
|
||||
close(stopListening)
|
||||
return result.message, result.err
|
||||
case escPressed := <-escChan:
|
||||
if escPressed {
|
||||
cancel() // Cancel the LLM context
|
||||
return nil, fmt.Errorf("generation cancelled by user")
|
||||
}
|
||||
// ESC listener stopped normally, wait for LLM result
|
||||
result := <-resultChan
|
||||
return result.message, result.err
|
||||
case <-ctx.Done():
|
||||
// Stop the ESC listener
|
||||
close(stopListening)
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
// Close closes the agent and cleans up resources.
|
||||
func (a *Agent) Close() error {
|
||||
return a.toolManager.Close()
|
||||
}
|
||||
|
||||
// escListenerModel is a simple Bubble Tea model for ESC key detection
|
||||
@@ -462,10 +368,8 @@ func (m escListenerModel) Init() tea.Cmd {
|
||||
}
|
||||
|
||||
func (m escListenerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyPressMsg:
|
||||
if msg, ok := msg.(tea.KeyPressMsg); ok {
|
||||
if msg.String() == "esc" {
|
||||
// Signal ESC was pressed
|
||||
select {
|
||||
case m.escPressed <- true:
|
||||
default:
|
||||
@@ -477,7 +381,7 @@ func (m escListenerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
|
||||
func (m escListenerModel) View() tea.View {
|
||||
return tea.NewView("") // No visual output needed
|
||||
return tea.NewView("")
|
||||
}
|
||||
|
||||
// listenForESC listens for ESC key press using Bubble Tea and returns true if detected
|
||||
@@ -488,13 +392,10 @@ func (a *Agent) listenForESC(stopChan chan bool, readyChan chan bool) bool {
|
||||
escPressed: escPressed,
|
||||
}
|
||||
|
||||
// Create a Bubble Tea program
|
||||
p := tea.NewProgram(model, tea.WithoutRenderer())
|
||||
|
||||
// Start the program in a goroutine
|
||||
go func() {
|
||||
if _, err := p.Run(); err != nil {
|
||||
// Program failed, try to signal completion
|
||||
select {
|
||||
case escPressed <- false:
|
||||
default:
|
||||
@@ -502,7 +403,6 @@ func (a *Agent) listenForESC(stopChan chan bool, readyChan chan bool) bool {
|
||||
}
|
||||
}()
|
||||
|
||||
// Give the program a moment to initialize, then signal ready
|
||||
go func() {
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
select {
|
||||
@@ -511,28 +411,18 @@ func (a *Agent) listenForESC(stopChan chan bool, readyChan chan bool) bool {
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for either ESC key or stop signal
|
||||
select {
|
||||
case <-stopChan:
|
||||
p.Kill()
|
||||
// Give the program time to fully terminate
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
return false
|
||||
case pressed := <-escPressed:
|
||||
p.Kill()
|
||||
// Give the program time to fully terminate
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
return pressed
|
||||
case <-time.After(30 * time.Second):
|
||||
// Timeout after 30 seconds to prevent hanging
|
||||
p.Kill()
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Close closes the agent and cleans up resources.
|
||||
// It ensures all MCP connections are properly closed and resources are released.
|
||||
func (a *Agent) Close() error {
|
||||
return a.toolManager.Close()
|
||||
}
|
||||
|
||||
@@ -1,147 +0,0 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/cloudwego/eino/schema"
|
||||
)
|
||||
|
||||
// StreamWithCallback streams content with real-time callbacks and returns the complete response.
|
||||
// It accumulates content and tool calls from the stream, invoking the callback for each content chunk.
|
||||
// IMPORTANT: Tool calls are only processed after EOF is reached to ensure we have the complete
|
||||
// and final tool call information. This prevents premature tool execution on partial data.
|
||||
// Handles different provider streaming patterns:
|
||||
// - Anthropic: Text content first, then tool calls streamed incrementally
|
||||
// - OpenAI/Others: Tool calls first or alone
|
||||
// - Mixed: Tool calls and content interleaved
|
||||
func StreamWithCallback(ctx context.Context, reader *schema.StreamReader[*schema.Message], callback func(string)) (*schema.Message, error) {
|
||||
defer reader.Close()
|
||||
|
||||
var content strings.Builder
|
||||
var accumulatedToolCalls map[string]*schema.ToolCall // Track tool calls by ID to handle incremental updates
|
||||
var streamComplete bool
|
||||
var finalResponseMeta *schema.ResponseMeta // Accumulate response metadata from all chunks
|
||||
|
||||
accumulatedToolCalls = make(map[string]*schema.ToolCall)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
default:
|
||||
}
|
||||
|
||||
msg, err := reader.Recv()
|
||||
if err == io.EOF {
|
||||
// Stream is complete - now we can safely process tool calls
|
||||
streamComplete = true
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Call callback for each chunk if provided (for real-time display)
|
||||
if callback != nil && msg.Content != "" {
|
||||
callback(msg.Content)
|
||||
}
|
||||
|
||||
// Accumulate content from all chunks
|
||||
content.WriteString(msg.Content)
|
||||
|
||||
// Accumulate response metadata - merge from multiple chunks for accuracy
|
||||
if msg.ResponseMeta != nil {
|
||||
if finalResponseMeta == nil {
|
||||
// First metadata we've seen - use as base
|
||||
finalResponseMeta = &schema.ResponseMeta{}
|
||||
if msg.ResponseMeta.Usage != nil {
|
||||
finalResponseMeta.Usage = &schema.TokenUsage{}
|
||||
}
|
||||
}
|
||||
|
||||
// Merge metadata intelligently to handle Anthropic's streaming behavior
|
||||
if msg.ResponseMeta.Usage != nil && finalResponseMeta.Usage != nil {
|
||||
usage := msg.ResponseMeta.Usage
|
||||
|
||||
// Take PromptTokens from first chunk that has them (usually non-zero)
|
||||
if finalResponseMeta.Usage.PromptTokens == 0 && usage.PromptTokens > 0 {
|
||||
finalResponseMeta.Usage.PromptTokens = usage.PromptTokens
|
||||
}
|
||||
|
||||
// Always take the latest CompletionTokens (accumulates over chunks)
|
||||
if usage.CompletionTokens > 0 {
|
||||
finalResponseMeta.Usage.CompletionTokens = usage.CompletionTokens
|
||||
}
|
||||
|
||||
// Calculate TotalTokens from the components
|
||||
finalResponseMeta.Usage.TotalTokens = finalResponseMeta.Usage.PromptTokens + finalResponseMeta.Usage.CompletionTokens
|
||||
}
|
||||
|
||||
// Preserve other metadata fields from the latest chunk
|
||||
if msg.ResponseMeta.FinishReason != "" {
|
||||
finalResponseMeta.FinishReason = msg.ResponseMeta.FinishReason
|
||||
}
|
||||
}
|
||||
|
||||
// Accumulate tool calls incrementally - Anthropic streams them piece by piece
|
||||
// NOTE: We don't process these tool calls until EOF is reached
|
||||
if len(msg.ToolCalls) > 0 {
|
||||
for _, toolCall := range msg.ToolCalls {
|
||||
// Use tool call ID as key, but handle cases where ID might be empty in partial chunks
|
||||
key := toolCall.ID
|
||||
if key == "" {
|
||||
// For chunks without ID, try to find existing tool call or create a temporary key
|
||||
if len(accumulatedToolCalls) == 1 {
|
||||
// If we have exactly one tool call being built, assume this chunk belongs to it
|
||||
for existingKey := range accumulatedToolCalls {
|
||||
key = existingKey
|
||||
break
|
||||
}
|
||||
} else {
|
||||
// Create a temporary key for this tool call
|
||||
key = "temp_" + toolCall.Function.Name
|
||||
}
|
||||
}
|
||||
|
||||
existing := accumulatedToolCalls[key]
|
||||
if existing == nil {
|
||||
// First time seeing this tool call
|
||||
accumulatedToolCalls[key] = &schema.ToolCall{
|
||||
ID: toolCall.ID,
|
||||
Function: toolCall.Function,
|
||||
}
|
||||
} else {
|
||||
// Update existing tool call with new information
|
||||
// Preserve non-empty values, accumulate arguments
|
||||
if toolCall.ID != "" {
|
||||
existing.ID = toolCall.ID
|
||||
}
|
||||
if toolCall.Function.Name != "" {
|
||||
existing.Function.Name = toolCall.Function.Name
|
||||
}
|
||||
// Accumulate arguments (they come in pieces)
|
||||
existing.Function.Arguments += toolCall.Function.Arguments
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Only process tool calls after EOF - ensures we have complete information
|
||||
var finalToolCalls []schema.ToolCall
|
||||
if streamComplete && len(accumulatedToolCalls) > 0 {
|
||||
finalToolCalls = make([]schema.ToolCall, 0, len(accumulatedToolCalls))
|
||||
for _, toolCall := range accumulatedToolCalls {
|
||||
finalToolCalls = append(finalToolCalls, *toolCall)
|
||||
}
|
||||
}
|
||||
|
||||
// Return complete message with all content, final tool calls, and preserved metadata
|
||||
return &schema.Message{
|
||||
Role: schema.Assistant,
|
||||
Content: content.String(),
|
||||
ToolCalls: finalToolCalls,
|
||||
ResponseMeta: finalResponseMeta, // Preserve usage and other metadata from streaming
|
||||
}, nil
|
||||
}
|
||||
@@ -109,7 +109,7 @@ func (c *OAuthClient) ExchangeCode(code, verifier string) (*AnthropicCredentials
|
||||
parsedCode, parsedState := c.parseCodeAndState(code)
|
||||
|
||||
// Build request body
|
||||
reqBody := map[string]interface{}{
|
||||
reqBody := map[string]any{
|
||||
"code": parsedCode,
|
||||
"grant_type": "authorization_code",
|
||||
"client_id": c.ClientID,
|
||||
@@ -131,7 +131,7 @@ func (c *OAuthClient) ExchangeCode(code, verifier string) (*AnthropicCredentials
|
||||
// rotated), and new expiration timestamp. Returns an error if the refresh fails or
|
||||
// the refresh token is invalid.
|
||||
func (c *OAuthClient) RefreshToken(refreshToken string) (*AnthropicCredentials, error) {
|
||||
reqBody := map[string]interface{}{
|
||||
reqBody := map[string]any{
|
||||
"grant_type": "refresh_token",
|
||||
"refresh_token": refreshToken,
|
||||
"client_id": c.ClientID,
|
||||
@@ -141,7 +141,7 @@ func (c *OAuthClient) RefreshToken(refreshToken string) (*AnthropicCredentials,
|
||||
}
|
||||
|
||||
// makeTokenRequest makes a token request to the OAuth server
|
||||
func (c *OAuthClient) makeTokenRequest(body map[string]interface{}) (*AnthropicCredentials, error) {
|
||||
func (c *OAuthClient) makeTokenRequest(body map[string]any) (*AnthropicCredentials, error) {
|
||||
jsonBody, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal request body: %w", err)
|
||||
@@ -162,7 +162,7 @@ func (c *OAuthClient) makeTokenRequest(body map[string]interface{}) (*AnthropicC
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
var errorResp map[string]interface{}
|
||||
var errorResp map[string]any
|
||||
if err := json.NewDecoder(resp.Body).Decode(&errorResp); err == nil {
|
||||
return nil, fmt.Errorf("token request failed: %v", errorResp)
|
||||
}
|
||||
|
||||
@@ -84,11 +84,7 @@ func executeBash(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToo
|
||||
timeout := defaultTimeout
|
||||
if timeoutMs := request.GetFloat("timeout", 0); timeoutMs > 0 {
|
||||
timeoutDuration := time.Duration(timeoutMs) * time.Millisecond
|
||||
if timeoutDuration > maxTimeout {
|
||||
timeout = maxTimeout
|
||||
} else {
|
||||
timeout = timeoutDuration
|
||||
}
|
||||
timeout = min(timeoutDuration, maxTimeout)
|
||||
}
|
||||
|
||||
// Check for banned commands
|
||||
|
||||
@@ -2,6 +2,7 @@ package builtin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
@@ -23,13 +24,7 @@ func TestBashServerRegistry(t *testing.T) {
|
||||
|
||||
// Test that bash server is registered
|
||||
servers := registry.ListServers()
|
||||
found := false
|
||||
for _, name := range servers {
|
||||
if name == "bash" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
found := slices.Contains(servers, "bash")
|
||||
|
||||
if !found {
|
||||
t.Error("bash server not found in registry")
|
||||
|
||||
@@ -73,11 +73,7 @@ func executeFetch(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallTo
|
||||
timeout := defaultFetchTimeout
|
||||
if timeoutSec := request.GetFloat("timeout", 0); timeoutSec > 0 {
|
||||
timeoutDuration := time.Duration(timeoutSec) * time.Second
|
||||
if timeoutDuration > maxFetchTimeout {
|
||||
timeout = maxFetchTimeout
|
||||
} else {
|
||||
timeout = timeoutDuration
|
||||
}
|
||||
timeout = min(timeoutDuration, maxFetchTimeout)
|
||||
}
|
||||
|
||||
// Validate URL
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -26,13 +27,7 @@ func TestFetchServerRegistry(t *testing.T) {
|
||||
|
||||
// Test that fetch server is registered
|
||||
servers := registry.ListServers()
|
||||
found := false
|
||||
for _, name := range servers {
|
||||
if name == "fetch" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
found := slices.Contains(servers, "fetch")
|
||||
|
||||
if !found {
|
||||
t.Error("fetch server not found in registry")
|
||||
|
||||
+22
-99
@@ -10,13 +10,13 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/JohannesKaufmann/html-to-markdown"
|
||||
md "github.com/JohannesKaufmann/html-to-markdown"
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/cloudwego/eino/components/model"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
"github.com/tidwall/gjson"
|
||||
|
||||
"charm.land/fantasy"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -26,15 +26,14 @@ const (
|
||||
)
|
||||
|
||||
// httpServerModel holds the model for the HTTP server
|
||||
var httpServerModel model.ToolCallingChatModel
|
||||
var httpServerModel fantasy.LanguageModel
|
||||
|
||||
// NewHTTPServer creates a new MCP server providing advanced HTTP fetching capabilities.
|
||||
// The server includes tools for fetching web content, summarizing pages, extracting
|
||||
// specific information, and filtering JSON responses. If an LLM model is provided,
|
||||
// AI-powered summarization and extraction tools are enabled. Returns an error if
|
||||
// server initialization fails.
|
||||
func NewHTTPServer(llmModel model.ToolCallingChatModel) (*server.MCPServer, error) {
|
||||
// Store the model globally for use in tool handlers
|
||||
func NewHTTPServer(llmModel fantasy.LanguageModel) (*server.MCPServer, error) {
|
||||
httpServerModel = llmModel
|
||||
|
||||
s := server.NewMCPServer("http-server", "1.0.0", server.WithToolCapabilities(true))
|
||||
@@ -63,7 +62,7 @@ func NewHTTPServer(llmModel model.ToolCallingChatModel) (*server.MCPServer, erro
|
||||
|
||||
s.AddTool(fetchTool, executeHTTPFetch)
|
||||
|
||||
// Only add the summarize tool if we have a model
|
||||
// Only add AI-powered tools if we have a model
|
||||
if llmModel != nil {
|
||||
summarizeTool := mcp.NewTool("fetch_summarize",
|
||||
mcp.WithDescription(httpSummarizeDescription),
|
||||
@@ -114,7 +113,6 @@ func NewHTTPServer(llmModel model.ToolCallingChatModel) (*server.MCPServer, erro
|
||||
|
||||
// executeHTTPFetch handles the fetch tool execution
|
||||
func executeHTTPFetch(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
// Extract parameters
|
||||
urlStr, err := request.RequireString("url")
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError("url parameter is required and must be a string"), nil
|
||||
@@ -125,32 +123,23 @@ func executeHTTPFetch(ctx context.Context, request mcp.CallToolRequest) (*mcp.Ca
|
||||
return mcp.NewToolResultError("format parameter is required and must be a string"), nil
|
||||
}
|
||||
|
||||
// Validate format
|
||||
if format != "html" && format != "markdown" {
|
||||
return mcp.NewToolResultError("format must be 'html' or 'markdown'"), nil
|
||||
}
|
||||
|
||||
// Get bodyOnly parameter (optional, defaults to false)
|
||||
bodyOnly := request.GetBool("bodyOnly", false)
|
||||
|
||||
// Parse timeout (optional)
|
||||
timeout := httpDefaultFetchTimeout
|
||||
if timeoutSec := request.GetFloat("timeout", 0); timeoutSec > 0 {
|
||||
timeoutDuration := time.Duration(timeoutSec) * time.Second
|
||||
if timeoutDuration > httpMaxFetchTimeout {
|
||||
timeout = httpMaxFetchTimeout
|
||||
} else {
|
||||
timeout = timeoutDuration
|
||||
}
|
||||
timeout = min(timeoutDuration, httpMaxFetchTimeout)
|
||||
}
|
||||
|
||||
// Validate URL
|
||||
parsedURL, err := url.Parse(urlStr)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("invalid URL: %v", err)), nil
|
||||
}
|
||||
|
||||
// Ensure URL has a scheme
|
||||
if parsedURL.Scheme == "" {
|
||||
urlStr = "https://" + urlStr
|
||||
parsedURL, err = url.Parse(urlStr)
|
||||
@@ -159,52 +148,43 @@ func executeHTTPFetch(ctx context.Context, request mcp.CallToolRequest) (*mcp.Ca
|
||||
}
|
||||
}
|
||||
|
||||
// Only allow HTTP and HTTPS
|
||||
if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" {
|
||||
return mcp.NewToolResultError("URL must use http:// or https://"), nil
|
||||
}
|
||||
|
||||
// Create HTTP client with timeout
|
||||
client := &http.Client{
|
||||
Timeout: timeout,
|
||||
}
|
||||
|
||||
// Create request with context
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", urlStr, nil)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("failed to create request: %v", err)), nil
|
||||
}
|
||||
|
||||
// Set headers to mimic a real browser
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
|
||||
req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8")
|
||||
req.Header.Set("Accept-Language", "en-US,en;q=0.9")
|
||||
|
||||
// Make the request
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("request failed: %v", err)), nil
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Check status code
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("request failed with status code: %d", resp.StatusCode)), nil
|
||||
}
|
||||
|
||||
// Check content length
|
||||
if resp.ContentLength > httpMaxResponseSize {
|
||||
return mcp.NewToolResultError("response too large (exceeds 5MB limit)"), nil
|
||||
}
|
||||
|
||||
// Read response body with size limit
|
||||
limitedReader := io.LimitReader(resp.Body, httpMaxResponseSize+1)
|
||||
bodyBytes, err := io.ReadAll(limitedReader)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("failed to read response: %v", err)), nil
|
||||
}
|
||||
|
||||
// Check if we exceeded the size limit
|
||||
if len(bodyBytes) > httpMaxResponseSize {
|
||||
return mcp.NewToolResultError("response too large (exceeds 5MB limit)"), nil
|
||||
}
|
||||
@@ -215,7 +195,6 @@ func executeHTTPFetch(ctx context.Context, request mcp.CallToolRequest) (*mcp.Ca
|
||||
contentType = "unknown"
|
||||
}
|
||||
|
||||
// Extract body content if requested
|
||||
if bodyOnly && strings.Contains(contentType, "text/html") {
|
||||
content, err = extractBodyContent(content)
|
||||
if err != nil {
|
||||
@@ -223,12 +202,10 @@ func executeHTTPFetch(ctx context.Context, request mcp.CallToolRequest) (*mcp.Ca
|
||||
}
|
||||
}
|
||||
|
||||
// Process content based on format
|
||||
var output string
|
||||
switch format {
|
||||
case "html":
|
||||
output = content
|
||||
|
||||
case "markdown":
|
||||
if strings.Contains(contentType, "text/html") {
|
||||
output, err = httpConvertHTMLToMarkdown(content)
|
||||
@@ -236,12 +213,10 @@ func executeHTTPFetch(ctx context.Context, request mcp.CallToolRequest) (*mcp.Ca
|
||||
return mcp.NewToolResultError(fmt.Sprintf("failed to convert HTML to markdown: %v", err)), nil
|
||||
}
|
||||
} else {
|
||||
// Non-HTML content, wrap in code block
|
||||
output = "```\n" + content + "\n```"
|
||||
}
|
||||
}
|
||||
|
||||
// Create result with metadata
|
||||
title := fmt.Sprintf("%s (%s)", urlStr, contentType)
|
||||
result := mcp.NewToolResultText(output)
|
||||
result.Meta = &mcp.Meta{
|
||||
@@ -263,14 +238,11 @@ func extractBodyContent(htmlContent string) (string, error) {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Find the body tag
|
||||
bodySelection := doc.Find("body")
|
||||
if bodySelection.Length() == 0 {
|
||||
// No body tag found, return the original content
|
||||
return htmlContent, nil
|
||||
}
|
||||
|
||||
// Get the inner HTML of the body tag
|
||||
bodyHTML, err := bodySelection.Html()
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -283,7 +255,6 @@ func extractBodyContent(htmlContent string) (string, error) {
|
||||
func httpConvertHTMLToMarkdown(htmlContent string) (string, error) {
|
||||
converter := md.NewConverter("", true, nil)
|
||||
|
||||
// Remove unwanted elements
|
||||
converter.Remove("script")
|
||||
converter.Remove("style")
|
||||
converter.Remove("meta")
|
||||
@@ -300,43 +271,39 @@ func httpConvertHTMLToMarkdown(htmlContent string) (string, error) {
|
||||
|
||||
// executeHTTPFetchSummarize handles the fetch_summarize tool execution
|
||||
func executeHTTPFetchSummarize(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
// Get URL
|
||||
urlStr, err := request.RequireString("url")
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError("url parameter is required and must be a string"), nil
|
||||
}
|
||||
|
||||
// Get optional instructions
|
||||
instructions := request.GetString("instructions", "Provide a concise summary of this content.")
|
||||
|
||||
// Fetch content as text (reuse existing logic)
|
||||
content, err := httpFetchAndExtractText(ctx, urlStr)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("Failed to fetch content: %v", err)), nil
|
||||
}
|
||||
|
||||
// Check if we have a model available
|
||||
if httpServerModel == nil {
|
||||
return mcp.NewToolResultError("LLM model not available for summarization"), nil
|
||||
}
|
||||
|
||||
// Create messages for the LLM
|
||||
messages := []*schema.Message{
|
||||
schema.UserMessage(fmt.Sprintf("%s\n\nContent to summarize:\n%s", instructions, content)),
|
||||
// Use fantasy model for summarization
|
||||
call := fantasy.Call{
|
||||
Prompt: fantasy.Prompt{
|
||||
fantasy.NewUserMessage(fmt.Sprintf("%s\n\nContent to summarize:\n%s", instructions, content)),
|
||||
},
|
||||
}
|
||||
|
||||
// Generate summary using the model directly
|
||||
response, err := httpServerModel.Generate(ctx, messages)
|
||||
response, err := httpServerModel.Generate(ctx, call)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("Summarization failed: %v", err)), nil
|
||||
}
|
||||
|
||||
// Return summary
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: response.Content,
|
||||
Text: response.Content.Text(),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
@@ -344,30 +311,25 @@ func executeHTTPFetchSummarize(ctx context.Context, request mcp.CallToolRequest)
|
||||
|
||||
// executeHTTPFetchExtract handles the fetch_extract tool execution
|
||||
func executeHTTPFetchExtract(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
// Get URL
|
||||
urlStr, err := request.RequireString("url")
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError("url parameter is required and must be a string"), nil
|
||||
}
|
||||
|
||||
// Get extraction instructions
|
||||
instructions, err := request.RequireString("instructions")
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError("instructions parameter is required and must be a string"), nil
|
||||
}
|
||||
|
||||
// Fetch content as text (reuse existing logic)
|
||||
content, err := httpFetchAndExtractText(ctx, urlStr)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("Failed to fetch content: %v", err)), nil
|
||||
}
|
||||
|
||||
// Check if we have a model available
|
||||
if httpServerModel == nil {
|
||||
return mcp.NewToolResultError("LLM model not available for extraction"), nil
|
||||
}
|
||||
|
||||
// Create extraction prompt
|
||||
extractionPrompt := fmt.Sprintf(`Extract the requested information from the following web content.
|
||||
|
||||
Extraction Instructions: %s
|
||||
@@ -377,23 +339,22 @@ Web Content:
|
||||
|
||||
Please extract only the requested information. If the requested information is not found, respond with "Information not found" and explain what was searched for.`, instructions, content)
|
||||
|
||||
// Create messages for the LLM
|
||||
messages := []*schema.Message{
|
||||
schema.UserMessage(extractionPrompt),
|
||||
call := fantasy.Call{
|
||||
Prompt: fantasy.Prompt{
|
||||
fantasy.NewUserMessage(extractionPrompt),
|
||||
},
|
||||
}
|
||||
|
||||
// Generate extraction using the model directly
|
||||
response, err := httpServerModel.Generate(ctx, messages)
|
||||
response, err := httpServerModel.Generate(ctx, call)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("Extraction failed: %v", err)), nil
|
||||
}
|
||||
|
||||
// Return extracted data
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: response.Content,
|
||||
Text: response.Content.Text(),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
@@ -401,16 +362,13 @@ Please extract only the requested information. If the requested information is n
|
||||
|
||||
// httpFetchAndExtractText fetches content from URL and extracts as text
|
||||
func httpFetchAndExtractText(ctx context.Context, urlStr string) (string, error) {
|
||||
// Parse timeout (use default)
|
||||
timeout := httpDefaultFetchTimeout
|
||||
|
||||
// Validate URL
|
||||
parsedURL, err := url.Parse(urlStr)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("invalid URL: %v", err)
|
||||
}
|
||||
|
||||
// Ensure URL has a scheme
|
||||
if parsedURL.Scheme == "" {
|
||||
urlStr = "https://" + urlStr
|
||||
parsedURL, err = url.Parse(urlStr)
|
||||
@@ -419,52 +377,43 @@ func httpFetchAndExtractText(ctx context.Context, urlStr string) (string, error)
|
||||
}
|
||||
}
|
||||
|
||||
// Only allow HTTP and HTTPS
|
||||
if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" {
|
||||
return "", fmt.Errorf("URL must use http:// or https://")
|
||||
}
|
||||
|
||||
// Create HTTP client with timeout
|
||||
client := &http.Client{
|
||||
Timeout: timeout,
|
||||
}
|
||||
|
||||
// Create request with context
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", urlStr, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create request: %v", err)
|
||||
}
|
||||
|
||||
// Set headers to mimic a real browser
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
|
||||
req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8")
|
||||
req.Header.Set("Accept-Language", "en-US,en;q=0.9")
|
||||
|
||||
// Make the request
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Check status code
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return "", fmt.Errorf("request failed with status code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// Check content length
|
||||
if resp.ContentLength > httpMaxResponseSize {
|
||||
return "", fmt.Errorf("response too large (exceeds 5MB limit)")
|
||||
}
|
||||
|
||||
// Read response body with size limit
|
||||
limitedReader := io.LimitReader(resp.Body, httpMaxResponseSize+1)
|
||||
bodyBytes, err := io.ReadAll(limitedReader)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read response: %v", err)
|
||||
}
|
||||
|
||||
// Check if we exceeded the size limit
|
||||
if len(bodyBytes) > httpMaxResponseSize {
|
||||
return "", fmt.Errorf("response too large (exceeds 5MB limit)")
|
||||
}
|
||||
@@ -472,7 +421,6 @@ func httpFetchAndExtractText(ctx context.Context, urlStr string) (string, error)
|
||||
content := string(bodyBytes)
|
||||
contentType := resp.Header.Get("Content-Type")
|
||||
|
||||
// Extract text content
|
||||
if strings.Contains(contentType, "text/html") {
|
||||
return httpExtractTextFromHTML(content)
|
||||
}
|
||||
@@ -486,13 +434,10 @@ func httpExtractTextFromHTML(htmlContent string) (string, error) {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Remove script, style, and other non-content elements
|
||||
doc.Find("script, style, noscript, iframe, object, embed").Remove()
|
||||
|
||||
// Extract text content
|
||||
text := doc.Text()
|
||||
|
||||
// Clean up whitespace
|
||||
lines := strings.Split(text, "\n")
|
||||
var cleanLines []string
|
||||
for _, line := range lines {
|
||||
@@ -507,7 +452,6 @@ func httpExtractTextFromHTML(htmlContent string) (string, error) {
|
||||
|
||||
// executeHTTPFetchFilteredJSON handles the fetch_filtered_json tool execution
|
||||
func executeHTTPFetchFilteredJSON(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
// Extract parameters
|
||||
urlStr, err := request.RequireString("url")
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError("url parameter is required and must be a string"), nil
|
||||
@@ -518,24 +462,17 @@ func executeHTTPFetchFilteredJSON(ctx context.Context, request mcp.CallToolReque
|
||||
return mcp.NewToolResultError("path parameter is required and must be a string"), nil
|
||||
}
|
||||
|
||||
// Parse timeout (optional)
|
||||
timeout := httpDefaultFetchTimeout
|
||||
if timeoutSec := request.GetFloat("timeout", 0); timeoutSec > 0 {
|
||||
timeoutDuration := time.Duration(timeoutSec) * time.Second
|
||||
if timeoutDuration > httpMaxFetchTimeout {
|
||||
timeout = httpMaxFetchTimeout
|
||||
} else {
|
||||
timeout = timeoutDuration
|
||||
}
|
||||
timeout = min(timeoutDuration, httpMaxFetchTimeout)
|
||||
}
|
||||
|
||||
// Validate URL
|
||||
parsedURL, err := url.Parse(urlStr)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("invalid URL: %v", err)), nil
|
||||
}
|
||||
|
||||
// Ensure URL has a scheme
|
||||
if parsedURL.Scheme == "" {
|
||||
urlStr = "https://" + urlStr
|
||||
parsedURL, err = url.Parse(urlStr)
|
||||
@@ -544,75 +481,62 @@ func executeHTTPFetchFilteredJSON(ctx context.Context, request mcp.CallToolReque
|
||||
}
|
||||
}
|
||||
|
||||
// Only allow HTTP and HTTPS
|
||||
if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" {
|
||||
return mcp.NewToolResultError("URL must use http:// or https://"), nil
|
||||
}
|
||||
|
||||
// Create HTTP client with timeout
|
||||
client := &http.Client{
|
||||
Timeout: timeout,
|
||||
}
|
||||
|
||||
// Create request with context
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", urlStr, nil)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("failed to create request: %v", err)), nil
|
||||
}
|
||||
|
||||
// Set headers to mimic a real browser and accept JSON
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
|
||||
req.Header.Set("Accept", "application/json, text/plain, */*")
|
||||
req.Header.Set("Accept-Language", "en-US,en;q=0.9")
|
||||
|
||||
// Make the request
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("request failed: %v", err)), nil
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Check status code
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("request failed with status code: %d", resp.StatusCode)), nil
|
||||
}
|
||||
|
||||
// Check content length
|
||||
if resp.ContentLength > httpMaxResponseSize {
|
||||
return mcp.NewToolResultError("response too large (exceeds 5MB limit)"), nil
|
||||
}
|
||||
|
||||
// Read response body with size limit
|
||||
limitedReader := io.LimitReader(resp.Body, httpMaxResponseSize+1)
|
||||
bodyBytes, err := io.ReadAll(limitedReader)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("failed to read response: %v", err)), nil
|
||||
}
|
||||
|
||||
// Check if we exceeded the size limit
|
||||
if len(bodyBytes) > httpMaxResponseSize {
|
||||
return mcp.NewToolResultError("response too large (exceeds 5MB limit)"), nil
|
||||
}
|
||||
|
||||
content := string(bodyBytes)
|
||||
|
||||
// Validate that the content is valid JSON
|
||||
if !json.Valid(bodyBytes) {
|
||||
return mcp.NewToolResultError("response is not valid JSON"), nil
|
||||
}
|
||||
|
||||
// Apply gjson path to filter the JSON
|
||||
result := gjson.Get(content, path)
|
||||
if !result.Exists() {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("gjson path '%s' did not match any data", path)), nil
|
||||
}
|
||||
|
||||
// Get the filtered JSON as a string
|
||||
var filteredJSON string
|
||||
if result.IsArray() || result.IsObject() {
|
||||
filteredJSON = result.Raw
|
||||
} else {
|
||||
// For primitive values, wrap in quotes if it's a string
|
||||
if result.Type == gjson.String {
|
||||
filteredJSON = fmt.Sprintf(`"%s"`, result.Str)
|
||||
} else {
|
||||
@@ -620,7 +544,6 @@ func executeHTTPFetchFilteredJSON(ctx context.Context, request mcp.CallToolReque
|
||||
}
|
||||
}
|
||||
|
||||
// Create result with metadata
|
||||
contentType := resp.Header.Get("Content-Type")
|
||||
if contentType == "" {
|
||||
contentType = "application/json"
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -26,13 +27,7 @@ func TestHTTPServerRegistry(t *testing.T) {
|
||||
|
||||
// Test that HTTP server is registered
|
||||
servers := registry.ListServers()
|
||||
found := false
|
||||
for _, name := range servers {
|
||||
if name == "http" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
found := slices.Contains(servers, "http")
|
||||
|
||||
if !found {
|
||||
t.Error("http server not found in registry")
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/cloudwego/eino/components/model"
|
||||
"charm.land/fantasy"
|
||||
"github.com/mark3labs/mcp-filesystem-server/filesystemserver"
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
)
|
||||
@@ -19,7 +19,6 @@ type BuiltinServerWrapper struct {
|
||||
// a no-op as the server is initialized during creation. Returns an error if
|
||||
// initialization fails.
|
||||
func (w *BuiltinServerWrapper) Initialize() error {
|
||||
// The server is already initialized when created
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -33,7 +32,7 @@ func (w *BuiltinServerWrapper) GetServer() *server.MCPServer {
|
||||
// It provides a centralized registry for creating instances of builtin MCP servers
|
||||
// with their respective configurations.
|
||||
type Registry struct {
|
||||
servers map[string]func(options map[string]any, model model.ToolCallingChatModel) (*BuiltinServerWrapper, error)
|
||||
servers map[string]func(options map[string]any, model fantasy.LanguageModel) (*BuiltinServerWrapper, error)
|
||||
}
|
||||
|
||||
// NewRegistry creates a new builtin server registry with all available builtin
|
||||
@@ -41,10 +40,9 @@ type Registry struct {
|
||||
// and HTTP servers.
|
||||
func NewRegistry() *Registry {
|
||||
r := &Registry{
|
||||
servers: make(map[string]func(options map[string]any, model model.ToolCallingChatModel) (*BuiltinServerWrapper, error)),
|
||||
servers: make(map[string]func(options map[string]any, model fantasy.LanguageModel) (*BuiltinServerWrapper, error)),
|
||||
}
|
||||
|
||||
// Register builtin servers
|
||||
r.registerFilesystemServer()
|
||||
r.registerBashServer()
|
||||
r.registerTodoServer()
|
||||
@@ -58,7 +56,7 @@ func NewRegistry() *Registry {
|
||||
// parameter provides server-specific configuration, and the model parameter provides
|
||||
// an optional LLM for AI-powered features. Returns an error if the server name
|
||||
// is unknown or if creation fails.
|
||||
func (r *Registry) CreateServer(name string, options map[string]any, model model.ToolCallingChatModel) (*BuiltinServerWrapper, error) {
|
||||
func (r *Registry) CreateServer(name string, options map[string]any, model fantasy.LanguageModel) (*BuiltinServerWrapper, error) {
|
||||
factory, exists := r.servers[name]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("unknown builtin server: %s", name)
|
||||
@@ -67,8 +65,7 @@ func (r *Registry) CreateServer(name string, options map[string]any, model model
|
||||
return factory(options, model)
|
||||
}
|
||||
|
||||
// ListServers returns a list of all available builtin server names that can be
|
||||
// created using CreateServer. The order of names is not guaranteed.
|
||||
// ListServers returns a list of all available builtin server names.
|
||||
func (r *Registry) ListServers() []string {
|
||||
names := make([]string, 0, len(r.servers))
|
||||
for name := range r.servers {
|
||||
@@ -79,8 +76,7 @@ func (r *Registry) ListServers() []string {
|
||||
|
||||
// registerFilesystemServer registers the filesystem server
|
||||
func (r *Registry) registerFilesystemServer() {
|
||||
r.servers["fs"] = func(options map[string]any, model model.ToolCallingChatModel) (*BuiltinServerWrapper, error) {
|
||||
// Extract allowed directories from options
|
||||
r.servers["fs"] = func(options map[string]any, model fantasy.LanguageModel) (*BuiltinServerWrapper, error) {
|
||||
var allowedDirs []string
|
||||
if dirs, ok := options["allowed_directories"]; ok {
|
||||
switch v := dirs.(type) {
|
||||
@@ -101,7 +97,6 @@ func (r *Registry) registerFilesystemServer() {
|
||||
return nil, fmt.Errorf("allowed_directories must be a string or array of strings")
|
||||
}
|
||||
} else {
|
||||
// Default to current working directory if no directories specified
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get current working directory: %v", err)
|
||||
@@ -109,7 +104,6 @@ func (r *Registry) registerFilesystemServer() {
|
||||
allowedDirs = []string{cwd}
|
||||
}
|
||||
|
||||
// Create the filesystem server
|
||||
server, err := filesystemserver.NewFilesystemServer(allowedDirs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create filesystem server: %v", err)
|
||||
@@ -121,8 +115,7 @@ func (r *Registry) registerFilesystemServer() {
|
||||
|
||||
// registerBashServer registers the bash server
|
||||
func (r *Registry) registerBashServer() {
|
||||
r.servers["bash"] = func(options map[string]any, model model.ToolCallingChatModel) (*BuiltinServerWrapper, error) {
|
||||
// Create the bash server
|
||||
r.servers["bash"] = func(options map[string]any, model fantasy.LanguageModel) (*BuiltinServerWrapper, error) {
|
||||
server, err := NewBashServer()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create bash server: %v", err)
|
||||
@@ -134,8 +127,7 @@ func (r *Registry) registerBashServer() {
|
||||
|
||||
// registerTodoServer registers the todo server
|
||||
func (r *Registry) registerTodoServer() {
|
||||
r.servers["todo"] = func(options map[string]any, model model.ToolCallingChatModel) (*BuiltinServerWrapper, error) {
|
||||
// Create the todo server
|
||||
r.servers["todo"] = func(options map[string]any, model fantasy.LanguageModel) (*BuiltinServerWrapper, error) {
|
||||
server, err := NewTodoServer()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create todo server: %v", err)
|
||||
@@ -147,8 +139,7 @@ func (r *Registry) registerTodoServer() {
|
||||
|
||||
// registerFetchServer registers the fetch server
|
||||
func (r *Registry) registerFetchServer() {
|
||||
r.servers["fetch"] = func(options map[string]any, model model.ToolCallingChatModel) (*BuiltinServerWrapper, error) {
|
||||
// Create the fetch server
|
||||
r.servers["fetch"] = func(options map[string]any, model fantasy.LanguageModel) (*BuiltinServerWrapper, error) {
|
||||
server, err := NewFetchServer()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create fetch server: %v", err)
|
||||
@@ -160,8 +151,7 @@ func (r *Registry) registerFetchServer() {
|
||||
|
||||
// registerHTTPServer registers the HTTP server
|
||||
func (r *Registry) registerHTTPServer() {
|
||||
r.servers["http"] = func(options map[string]any, model model.ToolCallingChatModel) (*BuiltinServerWrapper, error) {
|
||||
// Create the HTTP server
|
||||
r.servers["http"] = func(options map[string]any, model fantasy.LanguageModel) (*BuiltinServerWrapper, error) {
|
||||
server, err := NewHTTPServer(model)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create HTTP server: %v", err)
|
||||
|
||||
@@ -2,6 +2,7 @@ package builtin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
@@ -23,13 +24,7 @@ func TestTodoServerRegistry(t *testing.T) {
|
||||
|
||||
// Test that todo server is registered
|
||||
servers := registry.ListServers()
|
||||
found := false
|
||||
for _, name := range servers {
|
||||
if name == "todo" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
found := slices.Contains(servers, "todo")
|
||||
|
||||
if !found {
|
||||
t.Error("todo server not found in registry")
|
||||
|
||||
@@ -3,6 +3,7 @@ package config
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -77,13 +78,7 @@ mcpServers:
|
||||
|
||||
// Check command has substituted value
|
||||
expectedInCommand := "GITHUB_PERSONAL_ACCESS_TOKEN=ghp_test_token_123"
|
||||
found := false
|
||||
for _, arg := range githubServer.Command {
|
||||
if arg == expectedInCommand {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
found := slices.Contains(githubServer.Command, expectedInCommand)
|
||||
if !found {
|
||||
t.Errorf("Expected '%s' in command, got: %v", expectedInCommand, githubServer.Command)
|
||||
}
|
||||
|
||||
@@ -51,10 +51,10 @@ func fixEnvironmentCase(config *Config) {
|
||||
|
||||
// Check if we have mcpServers in the raw config
|
||||
if mcpServersRaw, ok := rawConfig["mcpservers"]; ok {
|
||||
if mcpServersMap, ok := mcpServersRaw.(map[string]interface{}); ok {
|
||||
if mcpServersMap, ok := mcpServersRaw.(map[string]any); ok {
|
||||
// Iterate through each server
|
||||
for serverName, serverDataRaw := range mcpServersMap {
|
||||
if serverData, ok := serverDataRaw.(map[string]interface{}); ok {
|
||||
if serverData, ok := serverDataRaw.(map[string]any); ok {
|
||||
// Check if this server has an environment field
|
||||
if _, hasEnv := serverData["environment"]; hasEnv {
|
||||
// Get the server config from our parsed config
|
||||
|
||||
@@ -24,15 +24,15 @@ mcpServers:
|
||||
|
||||
// Test 1: Direct YAML parsing
|
||||
t.Run("DirectYAMLParsing", func(t *testing.T) {
|
||||
var yamlData map[string]interface{}
|
||||
var yamlData map[string]any
|
||||
err := yaml.Unmarshal([]byte(yamlContent), &yamlData)
|
||||
if err != nil {
|
||||
t.Fatalf("YAML unmarshal error: %v", err)
|
||||
}
|
||||
|
||||
servers := yamlData["mcpServers"].(map[string]interface{})
|
||||
testServer := servers["test"].(map[string]interface{})
|
||||
env := testServer["environment"].(map[string]interface{})
|
||||
servers := yamlData["mcpServers"].(map[string]any)
|
||||
testServer := servers["test"].(map[string]any)
|
||||
env := testServer["environment"].(map[string]any)
|
||||
|
||||
if env["KEY1"] != "value1" {
|
||||
t.Errorf("Expected KEY1=value1, got %v", env["KEY1"])
|
||||
|
||||
@@ -74,7 +74,7 @@ func (e *Executor) PopulateCommonFields(event HookEvent) CommonInput {
|
||||
// it matches hooks based on tool name patterns. Hooks are executed in parallel
|
||||
// with configurable timeouts. Returns a combined HookOutput from all executed
|
||||
// hooks, with blocking decisions taking precedence.
|
||||
func (e *Executor) ExecuteHooks(ctx context.Context, event HookEvent, input interface{}) (*HookOutput, error) {
|
||||
func (e *Executor) ExecuteHooks(ctx context.Context, event HookEvent, input any) (*HookOutput, error) {
|
||||
matchers, ok := e.config.Hooks[event]
|
||||
if !ok || len(matchers) == 0 {
|
||||
return nil, nil
|
||||
@@ -119,7 +119,7 @@ func (e *Executor) ExecuteHooks(ctx context.Context, event HookEvent, input inte
|
||||
}
|
||||
|
||||
// executeHook runs a single hook command
|
||||
func (e *Executor) executeHook(ctx context.Context, hook HookEntry, input interface{}) *hookResult {
|
||||
func (e *Executor) executeHook(ctx context.Context, hook HookEntry, input any) *hookResult {
|
||||
// Prepare input JSON
|
||||
inputJSON, err := json.Marshal(input)
|
||||
if err != nil {
|
||||
@@ -187,7 +187,7 @@ func matchesPattern(pattern, toolName string) bool {
|
||||
}
|
||||
|
||||
// extractToolName gets the tool name from various input types
|
||||
func extractToolName(input interface{}) string {
|
||||
func extractToolName(input any) string {
|
||||
switch v := input.(type) {
|
||||
case *PreToolUseInput:
|
||||
return v.ToolName
|
||||
|
||||
@@ -42,7 +42,7 @@ echo '{"decision": "approve", "reason": "Approved by test"}'
|
||||
name string
|
||||
config *HookConfig
|
||||
event HookEvent
|
||||
input interface{}
|
||||
input any
|
||||
expected *HookOutput
|
||||
wantErr bool
|
||||
}{
|
||||
@@ -87,7 +87,7 @@ echo '{"decision": "approve", "reason": "Approved by test"}'
|
||||
expected: &HookOutput{
|
||||
Decision: "block",
|
||||
Reason: "Blocked by policy\n",
|
||||
Continue: boolPtr(false),
|
||||
Continue: new(false),
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -166,8 +166,9 @@ echo '{"decision": "approve", "reason": "Approved by test"}'
|
||||
}
|
||||
}
|
||||
|
||||
//go:fix inline
|
||||
func boolPtr(b bool) *bool {
|
||||
return &b
|
||||
return new(b)
|
||||
}
|
||||
|
||||
func compareHookOutputs(a, b *HookOutput) bool {
|
||||
|
||||
@@ -1,283 +0,0 @@
|
||||
package anthropic
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
einoclaude "github.com/cloudwego/eino-ext/components/model/claude"
|
||||
"github.com/cloudwego/eino/components/model"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
)
|
||||
|
||||
// CustomChatModel wraps the eino-ext Claude model with custom tool schema handling.
|
||||
// It provides a compatibility layer that fixes malformed JSON in tool calls and
|
||||
// ensures proper schema validation for Anthropic's API requirements.
|
||||
// This wrapper is necessary to handle edge cases where the underlying library
|
||||
// may generate invalid JSON for empty tool inputs or missing properties.
|
||||
type CustomChatModel struct {
|
||||
// wrapped is the underlying eino-ext Claude model instance
|
||||
wrapped *einoclaude.ChatModel
|
||||
}
|
||||
|
||||
// CustomRoundTripper intercepts HTTP requests to fix Anthropic function schemas.
|
||||
// It acts as a middleware that modifies requests before they reach the Anthropic API,
|
||||
// ensuring that tool schemas and function calls are properly formatted.
|
||||
// This is particularly important for handling edge cases like empty tool inputs
|
||||
// or missing schema properties that would otherwise cause API errors.
|
||||
type CustomRoundTripper struct {
|
||||
// wrapped is the underlying HTTP transport to use for actual requests
|
||||
wrapped http.RoundTripper
|
||||
}
|
||||
|
||||
// NewCustomChatModel creates a new custom Anthropic chat model.
|
||||
// It wraps the standard eino-ext Claude model with additional request
|
||||
// preprocessing to ensure compatibility with Anthropic's API requirements.
|
||||
//
|
||||
// Parameters:
|
||||
// - ctx: Context for the operation
|
||||
// - config: Configuration for the Claude model including API key, model name, and parameters
|
||||
//
|
||||
// Returns:
|
||||
// - *CustomChatModel: A wrapped Claude model with enhanced compatibility
|
||||
// - error: Returns an error if model creation fails
|
||||
//
|
||||
// The custom model automatically:
|
||||
// - Fixes malformed JSON in tool calls
|
||||
// - Ensures tool schemas have required properties
|
||||
// - Handles empty or missing input fields in function calls
|
||||
func NewCustomChatModel(ctx context.Context, config *einoclaude.Config) (*CustomChatModel, error) {
|
||||
// Create a custom HTTP client that intercepts requests
|
||||
if config.HTTPClient == nil {
|
||||
config.HTTPClient = &http.Client{}
|
||||
}
|
||||
|
||||
// Wrap the transport with our custom round tripper
|
||||
if config.HTTPClient.Transport == nil {
|
||||
config.HTTPClient.Transport = http.DefaultTransport
|
||||
}
|
||||
config.HTTPClient.Transport = &CustomRoundTripper{
|
||||
wrapped: config.HTTPClient.Transport,
|
||||
}
|
||||
|
||||
// Create the wrapped model
|
||||
wrapped, err := einoclaude.NewChatModel(ctx, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &CustomChatModel{
|
||||
wrapped: wrapped,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// RoundTrip implements http.RoundTripper to intercept and fix requests.
|
||||
// It preprocesses outgoing requests to the Anthropic API to ensure
|
||||
// they meet the API's requirements for tool schemas and function calls.
|
||||
//
|
||||
// Parameters:
|
||||
// - req: The HTTP request to be sent to the Anthropic API
|
||||
//
|
||||
// Returns:
|
||||
// - *http.Response: The response from the Anthropic API
|
||||
// - error: Any error that occurred during the request
|
||||
//
|
||||
// The method performs the following fixes:
|
||||
// - Ensures tool input_schema properties are not null
|
||||
// - Fixes malformed JSON patterns in tool_use content
|
||||
// - Validates and corrects empty or invalid function call inputs
|
||||
func (rt *CustomRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
// Only process Anthropic API requests
|
||||
if !strings.Contains(req.URL.Host, "anthropic.com") {
|
||||
return rt.wrapped.RoundTrip(req)
|
||||
}
|
||||
|
||||
// Read the request body
|
||||
body, err := io.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Body = io.NopCloser(bytes.NewReader(body))
|
||||
|
||||
// Apply string-based fixes BEFORE JSON parsing for malformed patterns
|
||||
bodyStr := string(body)
|
||||
|
||||
// Replace common malformed patterns - be more specific about context
|
||||
replacements := []struct {
|
||||
old string
|
||||
new string
|
||||
}{
|
||||
// Handle input field in tool_use objects
|
||||
{`"input":,"name"`, `"input":{},"name"`},
|
||||
{`"input":,"type"`, `"input":{},"type"`},
|
||||
{`"input":}`, `"input":{}}`},
|
||||
// Handle arguments field in function calls
|
||||
{`"arguments":,"name"`, `"arguments":"{}","name"`},
|
||||
{`"arguments":,"type"`, `"arguments":"{}","type"`},
|
||||
{`"arguments":}`, `"arguments":"{}"`},
|
||||
// Fallback patterns (less specific)
|
||||
{`"input":,`, `"input":{}`},
|
||||
{`"arguments":,`, `"arguments":"{}"`},
|
||||
}
|
||||
|
||||
for _, r := range replacements {
|
||||
if strings.Contains(bodyStr, r.old) {
|
||||
bodyStr = strings.ReplaceAll(bodyStr, r.old, r.new)
|
||||
}
|
||||
}
|
||||
|
||||
// Parse the JSON request (after string fixes)
|
||||
var requestData map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(bodyStr), &requestData); err != nil {
|
||||
// Return the original request to avoid panic
|
||||
req.Body = io.NopCloser(bytes.NewReader(body))
|
||||
req.ContentLength = int64(len(body))
|
||||
return rt.wrapped.RoundTrip(req)
|
||||
}
|
||||
|
||||
// Fix tool schemas if present
|
||||
if tools, ok := requestData["tools"].([]interface{}); ok {
|
||||
for _, tool := range tools {
|
||||
if toolMap, ok := tool.(map[string]interface{}); ok {
|
||||
if inputSchema, ok := toolMap["input_schema"].(map[string]interface{}); ok {
|
||||
// Ensure properties exists and is not null
|
||||
if properties, exists := inputSchema["properties"]; !exists || properties == nil {
|
||||
inputSchema["properties"] = map[string]interface{}{}
|
||||
} else if propertiesMap, ok := properties.(map[string]interface{}); ok {
|
||||
// Ensure each property has a type
|
||||
for _, propValue := range propertiesMap {
|
||||
if propMap, ok := propValue.(map[string]interface{}); ok {
|
||||
if _, hasType := propMap["type"]; !hasType {
|
||||
propMap["type"] = "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fix tool_use content in messages if present
|
||||
if messages, ok := requestData["messages"].([]interface{}); ok {
|
||||
for _, message := range messages {
|
||||
if msgMap, ok := message.(map[string]interface{}); ok {
|
||||
if content, ok := msgMap["content"].([]interface{}); ok {
|
||||
for _, contentItem := range content {
|
||||
if contentMap, ok := contentItem.(map[string]interface{}); ok {
|
||||
if contentType, ok := contentMap["type"].(string); ok && contentType == "tool_use" {
|
||||
// Ensure tool_use input is valid JSON
|
||||
if input, exists := contentMap["input"]; exists {
|
||||
// If input is nil or empty, set it to an empty object
|
||||
if input == nil {
|
||||
contentMap["input"] = map[string]interface{}{}
|
||||
} else if inputBytes, ok := input.(json.RawMessage); ok {
|
||||
if len(inputBytes) == 0 {
|
||||
contentMap["input"] = map[string]interface{}{}
|
||||
} else {
|
||||
// Validate that it's valid JSON
|
||||
var temp interface{}
|
||||
if err := json.Unmarshal(inputBytes, &temp); err != nil {
|
||||
contentMap["input"] = map[string]interface{}{}
|
||||
}
|
||||
}
|
||||
} else if inputStr, ok := input.(string); ok {
|
||||
// Handle string inputs that might be empty or invalid JSON
|
||||
if inputStr == "" || inputStr == "{}" {
|
||||
contentMap["input"] = map[string]interface{}{}
|
||||
} else {
|
||||
// Try to parse as JSON
|
||||
var temp interface{}
|
||||
if err := json.Unmarshal([]byte(inputStr), &temp); err != nil {
|
||||
contentMap["input"] = map[string]interface{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If input field doesn't exist, add it as empty object
|
||||
contentMap["input"] = map[string]interface{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Marshal the fixed request back to JSON
|
||||
fixedBody, err := json.Marshal(requestData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Use the fixed body from JSON marshaling
|
||||
finalBodyStr := string(fixedBody)
|
||||
|
||||
// Validate the final JSON
|
||||
var finalCheck interface{}
|
||||
if err := json.Unmarshal([]byte(finalBodyStr), &finalCheck); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create new request with fixed body
|
||||
req.Body = io.NopCloser(strings.NewReader(finalBodyStr))
|
||||
req.ContentLength = int64(len(finalBodyStr))
|
||||
// Make the actual request
|
||||
return rt.wrapped.RoundTrip(req)
|
||||
}
|
||||
|
||||
// Generate implements the model.BaseChatModel interface.
|
||||
// It generates a single response from the model based on the input messages.
|
||||
//
|
||||
// Parameters:
|
||||
// - ctx: Context for the operation, supporting cancellation and deadlines
|
||||
// - input: The conversation history as a slice of messages
|
||||
// - opts: Optional configuration options for the generation
|
||||
//
|
||||
// Returns:
|
||||
// - *schema.Message: The generated response message
|
||||
// - error: Any error that occurred during generation
|
||||
func (m *CustomChatModel) Generate(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.Message, error) {
|
||||
return m.wrapped.Generate(ctx, input, opts...)
|
||||
}
|
||||
|
||||
// Stream implements the model.BaseChatModel interface.
|
||||
// It generates a streaming response from the model, allowing incremental
|
||||
// processing of the model's output as it's generated.
|
||||
//
|
||||
// Parameters:
|
||||
// - ctx: Context for the operation, supporting cancellation and deadlines
|
||||
// - input: The conversation history as a slice of messages
|
||||
// - opts: Optional configuration options for the generation
|
||||
//
|
||||
// Returns:
|
||||
// - *schema.StreamReader[*schema.Message]: A reader for the streaming response
|
||||
// - error: Any error that occurred during stream setup
|
||||
func (m *CustomChatModel) Stream(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.StreamReader[*schema.Message], error) {
|
||||
return m.wrapped.Stream(ctx, input, opts...)
|
||||
}
|
||||
|
||||
// WithTools implements the model.ToolCallingChatModel interface.
|
||||
// It creates a new model instance with the specified tools available for function calling.
|
||||
// The original model instance remains unchanged.
|
||||
//
|
||||
// Parameters:
|
||||
// - tools: A slice of tool definitions that the model can use
|
||||
//
|
||||
// Returns:
|
||||
// - model.ToolCallingChatModel: A new model instance with tools enabled
|
||||
// - error: Returns an error if tool binding fails
|
||||
func (m *CustomChatModel) WithTools(tools []*schema.ToolInfo) (model.ToolCallingChatModel, error) {
|
||||
wrappedWithTools, err := m.wrapped.WithTools(tools)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &CustomChatModel{
|
||||
wrapped: wrappedWithTools.(*einoclaude.ChatModel),
|
||||
}, nil
|
||||
}
|
||||
@@ -1,791 +0,0 @@
|
||||
package gemini
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"runtime/debug"
|
||||
|
||||
"github.com/cloudwego/eino/callbacks"
|
||||
"github.com/cloudwego/eino/components"
|
||||
"github.com/cloudwego/eino/components/model"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
"github.com/eino-contrib/jsonschema"
|
||||
"github.com/getkin/kin-openapi/openapi3"
|
||||
"google.golang.org/genai"
|
||||
)
|
||||
|
||||
var _ model.ToolCallingChatModel = (*ChatModel)(nil)
|
||||
|
||||
// NewChatModel creates a new Gemini chat model instance.
|
||||
// It initializes a Google Gemini model with the specified configuration,
|
||||
// supporting both text generation and tool calling capabilities.
|
||||
//
|
||||
// Parameters:
|
||||
// - ctx: The context for the operation (currently unused but kept for interface consistency)
|
||||
// - cfg: Configuration for the Gemini model including client, model name, and parameters
|
||||
//
|
||||
// Returns:
|
||||
// - *ChatModel: A Gemini chat model instance implementing ToolCallingChatModel
|
||||
// - error: Any error that occurred during creation
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// client, _ := genai.NewClient(ctx, &genai.ClientConfig{
|
||||
// APIKey: "your-api-key",
|
||||
// })
|
||||
// model, err := gemini.NewChatModel(ctx, &gemini.Config{
|
||||
// Client: client,
|
||||
// Model: "gemini-pro",
|
||||
// MaxTokens: &maxTokens,
|
||||
// })
|
||||
func NewChatModel(_ context.Context, cfg *Config) (*ChatModel, error) {
|
||||
return &ChatModel{
|
||||
cli: cfg.Client,
|
||||
model: cfg.Model,
|
||||
maxTokens: cfg.MaxTokens,
|
||||
temperature: cfg.Temperature,
|
||||
topP: cfg.TopP,
|
||||
topK: cfg.TopK,
|
||||
responseSchema: cfg.ResponseSchema,
|
||||
enableCodeExecution: cfg.EnableCodeExecution,
|
||||
safetySettings: cfg.SafetySettings,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Config contains the configuration options for the Gemini model
|
||||
type Config struct {
|
||||
// Client is the Gemini API client instance
|
||||
// Required for making API calls to Gemini
|
||||
Client *genai.Client
|
||||
|
||||
// Model specifies which Gemini model to use
|
||||
// Examples: "gemini-pro", "gemini-pro-vision", "gemini-1.5-flash"
|
||||
Model string
|
||||
|
||||
// MaxTokens limits the maximum number of tokens in the response
|
||||
// Optional. Example: maxTokens := 100
|
||||
MaxTokens *int
|
||||
|
||||
// Temperature controls randomness in responses
|
||||
// Range: [0.0, 1.0], where 0.0 is more focused and 1.0 is more creative
|
||||
// Optional. Example: temperature := float32(0.7)
|
||||
Temperature *float32
|
||||
|
||||
// TopP controls diversity via nucleus sampling
|
||||
// Range: [0.0, 1.0], where 1.0 disables nucleus sampling
|
||||
// Optional. Example: topP := float32(0.95)
|
||||
TopP *float32
|
||||
|
||||
// TopK controls diversity by limiting the top K tokens to sample from
|
||||
// Optional. Example: topK := int32(40)
|
||||
TopK *int32
|
||||
|
||||
// ResponseSchema defines the structure for JSON responses
|
||||
// Optional. Used when you want structured output in JSON format
|
||||
ResponseSchema *jsonschema.Schema
|
||||
|
||||
// EnableCodeExecution allows the model to execute code
|
||||
// Warning: Be cautious with code execution in production
|
||||
// Optional. Default: false
|
||||
EnableCodeExecution bool
|
||||
|
||||
// SafetySettings configures content filtering for different harm categories
|
||||
// Controls the model's filtering behavior for potentially harmful content
|
||||
// Optional.
|
||||
SafetySettings []*genai.SafetySetting
|
||||
}
|
||||
|
||||
// options contains Gemini-specific options for model configuration.
|
||||
// These are options that are specific to the Gemini API and not part
|
||||
// of the common model options interface.
|
||||
type options struct {
|
||||
// TopK limits the number of tokens to sample from
|
||||
TopK *int32
|
||||
// ResponseSchema defines the expected JSON structure for responses
|
||||
ResponseSchema *jsonschema.Schema
|
||||
}
|
||||
|
||||
// ChatModel implements the Gemini chat model for the eino framework.
|
||||
// It provides integration with Google's Gemini API, supporting both
|
||||
// text generation and tool calling capabilities.
|
||||
type ChatModel struct {
|
||||
// cli is the Gemini API client instance
|
||||
cli *genai.Client
|
||||
|
||||
// model specifies which Gemini model to use
|
||||
model string
|
||||
// maxTokens limits the response length
|
||||
maxTokens *int
|
||||
// topP controls nucleus sampling
|
||||
topP *float32
|
||||
// temperature controls randomness
|
||||
temperature *float32
|
||||
// topK limits token sampling
|
||||
topK *int32
|
||||
// responseSchema for structured JSON output
|
||||
responseSchema *jsonschema.Schema
|
||||
// tools converted to Gemini format
|
||||
tools []*genai.Tool
|
||||
// origTools stores the original tool definitions
|
||||
origTools []*schema.ToolInfo
|
||||
// toolChoice controls how tools are used
|
||||
toolChoice *schema.ToolChoice
|
||||
// enableCodeExecution allows code execution (use with caution)
|
||||
enableCodeExecution bool
|
||||
// safetySettings for content filtering
|
||||
safetySettings []*genai.SafetySetting
|
||||
}
|
||||
|
||||
// Generate generates a single response from the Gemini model.
|
||||
// It processes the input messages and returns a complete response.
|
||||
//
|
||||
// Parameters:
|
||||
// - ctx: Context for the operation, supporting cancellation and callbacks
|
||||
// - input: The conversation history as a slice of messages
|
||||
// - opts: Optional configuration options for the generation
|
||||
//
|
||||
// Returns:
|
||||
// - *schema.Message: The generated response message with content and metadata
|
||||
// - error: Any error that occurred during generation
|
||||
func (cm *ChatModel) Generate(ctx context.Context, input []*schema.Message, opts ...model.Option) (message *schema.Message, err error) {
|
||||
ctx = callbacks.EnsureRunInfo(ctx, cm.GetType(), components.ComponentOfChatModel)
|
||||
|
||||
config, conf, err := cm.buildGenerateConfig(opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx = callbacks.OnStart(ctx, &model.CallbackInput{
|
||||
Messages: input,
|
||||
Tools: model.GetCommonOptions(&model.Options{Tools: cm.origTools}, opts...).Tools,
|
||||
Config: conf,
|
||||
})
|
||||
defer func() {
|
||||
if err != nil {
|
||||
callbacks.OnError(ctx, err)
|
||||
}
|
||||
}()
|
||||
|
||||
if len(input) == 0 {
|
||||
return nil, fmt.Errorf("gemini input is empty")
|
||||
}
|
||||
|
||||
contents, err := cm.convertSchemaMessages(input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result, err := cm.cli.Models.GenerateContent(ctx, cm.model, contents, config)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("generate content failed: %w", err)
|
||||
}
|
||||
|
||||
message, err = cm.convertResponse(result)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("convert response failed: %w", err)
|
||||
}
|
||||
|
||||
callbacks.OnEnd(ctx, cm.convertCallbackOutput(message, conf))
|
||||
return message, nil
|
||||
}
|
||||
|
||||
// Stream generates a streaming response from the Gemini model.
|
||||
// It allows incremental processing of the model's output as it's generated.
|
||||
//
|
||||
// Parameters:
|
||||
// - ctx: Context for the operation, supporting cancellation and callbacks
|
||||
// - input: The conversation history as a slice of messages
|
||||
// - opts: Optional configuration options for the generation
|
||||
//
|
||||
// Returns:
|
||||
// - *schema.StreamReader[*schema.Message]: A reader for the streaming response
|
||||
// - error: Any error that occurred during stream setup
|
||||
func (cm *ChatModel) Stream(ctx context.Context, input []*schema.Message, opts ...model.Option) (result *schema.StreamReader[*schema.Message], err error) {
|
||||
ctx = callbacks.EnsureRunInfo(ctx, cm.GetType(), components.ComponentOfChatModel)
|
||||
|
||||
config, conf, err := cm.buildGenerateConfig(opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx = callbacks.OnStart(ctx, &model.CallbackInput{
|
||||
Messages: input,
|
||||
Tools: model.GetCommonOptions(&model.Options{Tools: cm.origTools}, opts...).Tools,
|
||||
Config: conf,
|
||||
})
|
||||
defer func() {
|
||||
if err != nil {
|
||||
callbacks.OnError(ctx, err)
|
||||
}
|
||||
}()
|
||||
|
||||
if len(input) == 0 {
|
||||
return nil, fmt.Errorf("gemini input is empty")
|
||||
}
|
||||
|
||||
contents, err := cm.convertSchemaMessages(input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sr, sw := schema.Pipe[*model.CallbackOutput](1)
|
||||
go func() {
|
||||
defer func() {
|
||||
panicErr := recover()
|
||||
if panicErr != nil {
|
||||
_ = sw.Send(nil, newPanicErr(panicErr, debug.Stack()))
|
||||
}
|
||||
sw.Close()
|
||||
}()
|
||||
|
||||
for resp, err := range cm.cli.Models.GenerateContentStream(ctx, cm.model, contents, config) {
|
||||
if err != nil {
|
||||
sw.Send(nil, err)
|
||||
return
|
||||
}
|
||||
|
||||
message, err := cm.convertResponse(resp)
|
||||
if err != nil {
|
||||
sw.Send(nil, err)
|
||||
return
|
||||
}
|
||||
|
||||
closed := sw.Send(cm.convertCallbackOutput(message, conf), nil)
|
||||
if closed {
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
srList := sr.Copy(2)
|
||||
callbacks.OnEndWithStreamOutput(ctx, srList[0])
|
||||
return schema.StreamReaderWithConvert(srList[1], func(t *model.CallbackOutput) (*schema.Message, error) {
|
||||
return t.Message, nil
|
||||
}), nil
|
||||
}
|
||||
|
||||
// WithTools creates a new model instance with the specified tools available.
|
||||
// It returns a new ChatModel with tools configured for function calling.
|
||||
// The original model instance remains unchanged.
|
||||
//
|
||||
// Parameters:
|
||||
// - tools: A slice of tool definitions that the model can use
|
||||
//
|
||||
// Returns:
|
||||
// - model.ToolCallingChatModel: A new model instance with tools enabled
|
||||
// - error: Returns an error if no tools provided or conversion fails
|
||||
func (cm *ChatModel) WithTools(tools []*schema.ToolInfo) (model.ToolCallingChatModel, error) {
|
||||
if len(tools) == 0 {
|
||||
return nil, errors.New("no tools to bind")
|
||||
}
|
||||
gTools, err := cm.convertToGeminiTools(tools)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("convert to gemini tools failed: %w", err)
|
||||
}
|
||||
|
||||
tc := schema.ToolChoiceAllowed
|
||||
ncm := *cm
|
||||
ncm.toolChoice = &tc
|
||||
ncm.tools = gTools
|
||||
ncm.origTools = tools
|
||||
return &ncm, nil
|
||||
}
|
||||
|
||||
// BindTools binds tools to the current model instance.
|
||||
// Unlike WithTools, this modifies the current instance rather than
|
||||
// creating a new one. Tools are set to "allowed" mode by default.
|
||||
//
|
||||
// Parameters:
|
||||
// - tools: A slice of tool definitions to bind to the model
|
||||
//
|
||||
// Returns:
|
||||
// - error: Returns an error if no tools provided or conversion fails
|
||||
func (cm *ChatModel) BindTools(tools []*schema.ToolInfo) error {
|
||||
if len(tools) == 0 {
|
||||
return errors.New("no tools to bind")
|
||||
}
|
||||
gTools, err := cm.convertToGeminiTools(tools)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cm.tools = gTools
|
||||
cm.origTools = tools
|
||||
tc := schema.ToolChoiceAllowed
|
||||
cm.toolChoice = &tc
|
||||
return nil
|
||||
}
|
||||
|
||||
// BindForcedTools binds tools to the current model instance in forced mode.
|
||||
// This ensures the model will always use one of the provided tools
|
||||
// rather than generating a text response.
|
||||
//
|
||||
// Parameters:
|
||||
// - tools: A slice of tool definitions to bind to the model
|
||||
//
|
||||
// Returns:
|
||||
// - error: Returns an error if no tools provided or conversion fails
|
||||
func (cm *ChatModel) BindForcedTools(tools []*schema.ToolInfo) error {
|
||||
if len(tools) == 0 {
|
||||
return errors.New("no tools to bind")
|
||||
}
|
||||
gTools, err := cm.convertToGeminiTools(tools)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cm.tools = gTools
|
||||
cm.origTools = tools
|
||||
tc := schema.ToolChoiceForced
|
||||
cm.toolChoice = &tc
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cm *ChatModel) buildGenerateConfig(opts ...model.Option) (*genai.GenerateContentConfig, *model.Config, error) {
|
||||
commonOptions := model.GetCommonOptions(&model.Options{
|
||||
Temperature: cm.temperature,
|
||||
MaxTokens: cm.maxTokens,
|
||||
TopP: cm.topP,
|
||||
Tools: nil,
|
||||
ToolChoice: cm.toolChoice,
|
||||
}, opts...)
|
||||
geminiOptions := model.GetImplSpecificOptions(&options{
|
||||
TopK: cm.topK,
|
||||
ResponseSchema: cm.responseSchema,
|
||||
}, opts...)
|
||||
|
||||
conf := &model.Config{}
|
||||
config := &genai.GenerateContentConfig{}
|
||||
|
||||
// Set model
|
||||
if commonOptions.Model != nil {
|
||||
conf.Model = *commonOptions.Model
|
||||
} else {
|
||||
conf.Model = cm.model
|
||||
}
|
||||
|
||||
// Set temperature
|
||||
if commonOptions.Temperature != nil {
|
||||
conf.Temperature = *commonOptions.Temperature
|
||||
config.Temperature = commonOptions.Temperature
|
||||
} else if cm.temperature != nil {
|
||||
conf.Temperature = *cm.temperature
|
||||
config.Temperature = cm.temperature
|
||||
}
|
||||
|
||||
// Set max tokens
|
||||
if commonOptions.MaxTokens != nil {
|
||||
conf.MaxTokens = *commonOptions.MaxTokens
|
||||
config.MaxOutputTokens = int32(*commonOptions.MaxTokens)
|
||||
} else if cm.maxTokens != nil {
|
||||
conf.MaxTokens = *cm.maxTokens
|
||||
config.MaxOutputTokens = int32(*cm.maxTokens)
|
||||
}
|
||||
|
||||
// Set top P
|
||||
if commonOptions.TopP != nil {
|
||||
conf.TopP = *commonOptions.TopP
|
||||
config.TopP = commonOptions.TopP
|
||||
} else if cm.topP != nil {
|
||||
conf.TopP = *cm.topP
|
||||
config.TopP = cm.topP
|
||||
}
|
||||
|
||||
// Set top K
|
||||
if geminiOptions.TopK != nil {
|
||||
config.TopK = genai.Ptr(float32(*geminiOptions.TopK))
|
||||
} else if cm.topK != nil {
|
||||
config.TopK = genai.Ptr(float32(*cm.topK))
|
||||
}
|
||||
|
||||
// Set tools
|
||||
tools := cm.tools
|
||||
if commonOptions.Tools != nil {
|
||||
var err error
|
||||
tools, err = cm.convertToGeminiTools(commonOptions.Tools)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
if len(tools) > 0 {
|
||||
config.Tools = tools
|
||||
}
|
||||
|
||||
// Set tool choice
|
||||
if commonOptions.ToolChoice != nil {
|
||||
switch *commonOptions.ToolChoice {
|
||||
case schema.ToolChoiceForbidden:
|
||||
config.ToolConfig = &genai.ToolConfig{
|
||||
FunctionCallingConfig: &genai.FunctionCallingConfig{
|
||||
Mode: genai.FunctionCallingConfigModeNone,
|
||||
},
|
||||
}
|
||||
case schema.ToolChoiceAllowed:
|
||||
config.ToolConfig = &genai.ToolConfig{
|
||||
FunctionCallingConfig: &genai.FunctionCallingConfig{
|
||||
Mode: genai.FunctionCallingConfigModeAuto,
|
||||
},
|
||||
}
|
||||
case schema.ToolChoiceForced:
|
||||
if len(tools) == 0 {
|
||||
return nil, nil, fmt.Errorf("tool choice is forced but no tools provided")
|
||||
}
|
||||
config.ToolConfig = &genai.ToolConfig{
|
||||
FunctionCallingConfig: &genai.FunctionCallingConfig{
|
||||
Mode: genai.FunctionCallingConfigModeAny,
|
||||
},
|
||||
}
|
||||
default:
|
||||
return nil, nil, fmt.Errorf("tool choice=%s not supported", *commonOptions.ToolChoice)
|
||||
}
|
||||
}
|
||||
|
||||
// Set safety settings
|
||||
if len(cm.safetySettings) > 0 {
|
||||
config.SafetySettings = cm.safetySettings
|
||||
}
|
||||
|
||||
// Set response schema for JSON mode
|
||||
if geminiOptions.ResponseSchema != nil {
|
||||
gSchema, err := cm.convertJSONSchema(geminiOptions.ResponseSchema)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("convert response schema failed: %w", err)
|
||||
}
|
||||
config.ResponseMIMEType = "application/json"
|
||||
config.ResponseSchema = gSchema
|
||||
}
|
||||
|
||||
return config, conf, nil
|
||||
}
|
||||
|
||||
func (cm *ChatModel) convertToGeminiTools(tools []*schema.ToolInfo) ([]*genai.Tool, error) {
|
||||
if len(tools) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var functionDeclarations []*genai.FunctionDeclaration
|
||||
for _, tool := range tools {
|
||||
openSchema, err := tool.ToJSONSchema()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get open schema failed: %w", err)
|
||||
}
|
||||
|
||||
gSchema, err := cm.convertJSONSchema(openSchema)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("convert open schema failed: %w", err)
|
||||
}
|
||||
|
||||
funcDecl := &genai.FunctionDeclaration{
|
||||
Name: tool.Name,
|
||||
Description: tool.Desc,
|
||||
Parameters: gSchema,
|
||||
}
|
||||
functionDeclarations = append(functionDeclarations, funcDecl)
|
||||
}
|
||||
|
||||
return []*genai.Tool{{FunctionDeclarations: functionDeclarations}}, nil
|
||||
}
|
||||
|
||||
func (cm *ChatModel) convertJSONSchema(schema *jsonschema.Schema) (*genai.Schema, error) {
|
||||
if schema == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
result := &genai.Schema{
|
||||
Description: schema.Description,
|
||||
}
|
||||
|
||||
switch schema.Type {
|
||||
case openapi3.TypeObject:
|
||||
result.Type = genai.TypeObject
|
||||
if schema.Properties != nil {
|
||||
properties := make(map[string]*genai.Schema)
|
||||
for pair := schema.Properties.Oldest(); pair != nil; pair = pair.Next() {
|
||||
propSchema, err := cm.convertJSONSchema(pair.Value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
properties[pair.Key] = propSchema
|
||||
}
|
||||
result.Properties = properties
|
||||
}
|
||||
if schema.Required != nil {
|
||||
result.Required = schema.Required
|
||||
}
|
||||
case openapi3.TypeArray:
|
||||
result.Type = genai.TypeArray
|
||||
if schema.Items != nil {
|
||||
itemSchema, err := cm.convertJSONSchema(schema.Items)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.Items = itemSchema
|
||||
}
|
||||
case openapi3.TypeString:
|
||||
result.Type = genai.TypeString
|
||||
if schema.Enum != nil {
|
||||
enums := make([]string, 0, len(schema.Enum))
|
||||
for _, e := range schema.Enum {
|
||||
if str, ok := e.(string); ok {
|
||||
enums = append(enums, str)
|
||||
} else {
|
||||
return nil, fmt.Errorf("enum value must be a string, schema: %+v", schema)
|
||||
}
|
||||
}
|
||||
result.Enum = enums
|
||||
}
|
||||
case openapi3.TypeNumber:
|
||||
result.Type = genai.TypeNumber
|
||||
case openapi3.TypeInteger:
|
||||
result.Type = genai.TypeInteger
|
||||
case openapi3.TypeBoolean:
|
||||
result.Type = genai.TypeBoolean
|
||||
default:
|
||||
result.Type = genai.TypeUnspecified
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (cm *ChatModel) convertSchemaMessages(messages []*schema.Message) ([]*genai.Content, error) {
|
||||
var contents []*genai.Content
|
||||
for _, message := range messages {
|
||||
content, err := cm.convertSchemaMessage(message)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("convert schema message failed: %w", err)
|
||||
}
|
||||
if content != nil {
|
||||
contents = append(contents, content)
|
||||
}
|
||||
}
|
||||
return contents, nil
|
||||
}
|
||||
|
||||
func (cm *ChatModel) convertSchemaMessage(message *schema.Message) (*genai.Content, error) {
|
||||
if message == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var parts []*genai.Part
|
||||
|
||||
// Handle tool calls
|
||||
if message.ToolCalls != nil {
|
||||
for _, call := range message.ToolCalls {
|
||||
var args map[string]any
|
||||
if err := json.Unmarshal([]byte(call.Function.Arguments), &args); err != nil {
|
||||
return nil, fmt.Errorf("unmarshal tool call arguments failed: %w", err)
|
||||
}
|
||||
parts = append(parts, &genai.Part{
|
||||
FunctionCall: &genai.FunctionCall{
|
||||
Name: call.Function.Name,
|
||||
Args: args,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Handle tool responses
|
||||
if message.Role == schema.Tool {
|
||||
var response map[string]any
|
||||
if err := json.Unmarshal([]byte(message.Content), &response); err != nil {
|
||||
// If the content is not valid JSON, treat it as a plain text error response
|
||||
response = map[string]any{
|
||||
"error": message.Content,
|
||||
}
|
||||
}
|
||||
parts = append(parts, &genai.Part{
|
||||
FunctionResponse: &genai.FunctionResponse{
|
||||
Name: message.ToolCallID,
|
||||
Response: response,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
// Handle text content
|
||||
if message.Content != "" {
|
||||
parts = append(parts, &genai.Part{Text: message.Content})
|
||||
}
|
||||
|
||||
// Handle multi-content (images, audio, etc.)
|
||||
for _, content := range message.MultiContent {
|
||||
switch content.Type {
|
||||
case schema.ChatMessagePartTypeText:
|
||||
parts = append(parts, &genai.Part{Text: content.Text})
|
||||
case schema.ChatMessagePartTypeImageURL:
|
||||
if content.ImageURL != nil {
|
||||
parts = append(parts, &genai.Part{
|
||||
FileData: &genai.FileData{
|
||||
MIMEType: content.ImageURL.MIMEType,
|
||||
FileURI: content.ImageURL.URI,
|
||||
},
|
||||
})
|
||||
}
|
||||
case schema.ChatMessagePartTypeAudioURL:
|
||||
if content.AudioURL != nil {
|
||||
parts = append(parts, &genai.Part{
|
||||
FileData: &genai.FileData{
|
||||
MIMEType: content.AudioURL.MIMEType,
|
||||
FileURI: content.AudioURL.URI,
|
||||
},
|
||||
})
|
||||
}
|
||||
case schema.ChatMessagePartTypeVideoURL:
|
||||
if content.VideoURL != nil {
|
||||
parts = append(parts, &genai.Part{
|
||||
FileData: &genai.FileData{
|
||||
MIMEType: content.VideoURL.MIMEType,
|
||||
FileURI: content.VideoURL.URI,
|
||||
},
|
||||
})
|
||||
}
|
||||
case schema.ChatMessagePartTypeFileURL:
|
||||
if content.FileURL != nil {
|
||||
parts = append(parts, &genai.Part{
|
||||
FileData: &genai.FileData{
|
||||
MIMEType: content.FileURL.MIMEType,
|
||||
FileURI: content.FileURL.URI,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(parts) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return &genai.Content{
|
||||
Role: string(cm.convertRole(message.Role)),
|
||||
Parts: parts,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (cm *ChatModel) convertRole(role schema.RoleType) genai.Role {
|
||||
switch role {
|
||||
case schema.Assistant:
|
||||
return genai.RoleModel
|
||||
case schema.User:
|
||||
return genai.RoleUser
|
||||
case schema.Tool:
|
||||
return genai.RoleUser // Tool responses are treated as user messages in the new API
|
||||
default:
|
||||
return genai.RoleUser
|
||||
}
|
||||
}
|
||||
|
||||
func (cm *ChatModel) convertResponse(resp *genai.GenerateContentResponse) (*schema.Message, error) {
|
||||
if len(resp.Candidates) == 0 {
|
||||
return nil, fmt.Errorf("gemini result is empty")
|
||||
}
|
||||
|
||||
candidate := resp.Candidates[0]
|
||||
message := &schema.Message{
|
||||
Role: schema.Assistant,
|
||||
ResponseMeta: &schema.ResponseMeta{
|
||||
FinishReason: string(candidate.FinishReason),
|
||||
},
|
||||
}
|
||||
|
||||
// Handle usage metadata
|
||||
if resp.UsageMetadata != nil {
|
||||
message.ResponseMeta.Usage = &schema.TokenUsage{
|
||||
PromptTokens: int(resp.UsageMetadata.PromptTokenCount),
|
||||
CompletionTokens: int(resp.UsageMetadata.CandidatesTokenCount),
|
||||
TotalTokens: int(resp.UsageMetadata.TotalTokenCount),
|
||||
}
|
||||
}
|
||||
|
||||
// Process content parts
|
||||
var textParts []string
|
||||
for _, part := range candidate.Content.Parts {
|
||||
switch {
|
||||
case part.Text != "":
|
||||
textParts = append(textParts, part.Text)
|
||||
case part.FunctionCall != nil:
|
||||
args, err := json.Marshal(part.FunctionCall.Args)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshal function call arguments failed: %w", err)
|
||||
}
|
||||
message.ToolCalls = append(message.ToolCalls, schema.ToolCall{
|
||||
ID: part.FunctionCall.Name,
|
||||
Function: schema.FunctionCall{
|
||||
Name: part.FunctionCall.Name,
|
||||
Arguments: string(args),
|
||||
},
|
||||
})
|
||||
case part.ExecutableCode != nil:
|
||||
textParts = append(textParts, part.ExecutableCode.Code)
|
||||
case part.CodeExecutionResult != nil:
|
||||
textParts = append(textParts, part.CodeExecutionResult.Output)
|
||||
}
|
||||
}
|
||||
|
||||
// Set content
|
||||
if len(textParts) == 1 {
|
||||
message.Content = textParts[0]
|
||||
} else if len(textParts) > 1 {
|
||||
for _, text := range textParts {
|
||||
message.MultiContent = append(message.MultiContent, schema.ChatMessagePart{
|
||||
Type: schema.ChatMessagePartTypeText,
|
||||
Text: text,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return message, nil
|
||||
}
|
||||
|
||||
func (cm *ChatModel) convertCallbackOutput(message *schema.Message, conf *model.Config) *model.CallbackOutput {
|
||||
callbackOutput := &model.CallbackOutput{
|
||||
Message: message,
|
||||
Config: conf,
|
||||
}
|
||||
if message.ResponseMeta != nil && message.ResponseMeta.Usage != nil {
|
||||
callbackOutput.TokenUsage = &model.TokenUsage{
|
||||
PromptTokens: message.ResponseMeta.Usage.PromptTokens,
|
||||
CompletionTokens: message.ResponseMeta.Usage.CompletionTokens,
|
||||
TotalTokens: message.ResponseMeta.Usage.TotalTokens,
|
||||
}
|
||||
}
|
||||
return callbackOutput
|
||||
}
|
||||
|
||||
// IsCallbacksEnabled indicates whether this model supports callbacks.
|
||||
// For the Gemini model, callbacks are always enabled to support
|
||||
// token usage tracking and other monitoring features.
|
||||
//
|
||||
// Returns:
|
||||
// - bool: Always returns true for Gemini models
|
||||
func (cm *ChatModel) IsCallbacksEnabled() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
const typ = "Gemini"
|
||||
|
||||
// GetType returns the type identifier for this model.
|
||||
// This is used for logging and debugging purposes to identify
|
||||
// which model implementation is being used.
|
||||
//
|
||||
// Returns:
|
||||
// - string: Returns "Gemini" as the model type
|
||||
func (cm *ChatModel) GetType() string {
|
||||
return typ
|
||||
}
|
||||
|
||||
type panicErr struct {
|
||||
info any
|
||||
stack []byte
|
||||
}
|
||||
|
||||
func (p *panicErr) Error() string {
|
||||
return fmt.Sprintf("panic error: %v, \nstack: %s", p.info, string(p.stack))
|
||||
}
|
||||
|
||||
func newPanicErr(info any, stack []byte) error {
|
||||
return &panicErr{
|
||||
info: info,
|
||||
stack: stack,
|
||||
}
|
||||
}
|
||||
@@ -1,226 +0,0 @@
|
||||
//go:build ignore
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"text/template"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ModelInfo represents information about a specific model.
|
||||
// This struct is used during code generation to parse model data
|
||||
// from the models.dev API and generate the static Go code.
|
||||
type ModelInfo struct {
|
||||
// ID is the unique identifier for the model
|
||||
ID string `json:"id"`
|
||||
// Name is the human-readable name of the model
|
||||
Name string `json:"name"`
|
||||
// Attachment indicates whether the model supports file attachments
|
||||
Attachment bool `json:"attachment"`
|
||||
// Reasoning indicates whether this is a reasoning/chain-of-thought model
|
||||
Reasoning bool `json:"reasoning"`
|
||||
// Temperature indicates whether the model supports temperature parameter
|
||||
Temperature bool `json:"temperature"`
|
||||
// Cost contains the pricing information for the model
|
||||
Cost Cost `json:"cost"`
|
||||
// Limit contains the context and output token limits
|
||||
Limit Limit `json:"limit"`
|
||||
}
|
||||
|
||||
// Cost represents the pricing information for a model.
|
||||
// Used during code generation to parse pricing data from models.dev.
|
||||
type Cost struct {
|
||||
// Input is the cost per million input tokens
|
||||
Input float64 `json:"input"`
|
||||
// Output is the cost per million output tokens
|
||||
Output float64 `json:"output"`
|
||||
// CacheRead is the cost per million cached read tokens (optional)
|
||||
CacheRead *float64 `json:"cache_read,omitempty"`
|
||||
// CacheWrite is the cost per million cached write tokens (optional)
|
||||
CacheWrite *float64 `json:"cache_write,omitempty"`
|
||||
}
|
||||
|
||||
// Limit represents the context and output limits for a model.
|
||||
// Used during code generation to parse token limit data from models.dev.
|
||||
type Limit struct {
|
||||
// Context is the maximum number of input tokens
|
||||
Context int `json:"context"`
|
||||
// Output is the maximum number of output tokens
|
||||
Output int `json:"output"`
|
||||
}
|
||||
|
||||
// ProviderInfo represents information about a model provider.
|
||||
// Used during code generation to parse provider data from models.dev
|
||||
// and generate the static provider registry.
|
||||
type ProviderInfo struct {
|
||||
// ID is the unique identifier for the provider
|
||||
ID string `json:"id"`
|
||||
// Env lists the environment variables for API credentials
|
||||
Env []string `json:"env"`
|
||||
// NPM is the NPM package name (for reference)
|
||||
NPM string `json:"npm"`
|
||||
// Name is the human-readable provider name
|
||||
Name string `json:"name"`
|
||||
// Models maps model IDs to their information
|
||||
Models map[string]ModelInfo `json:"models"`
|
||||
}
|
||||
|
||||
const codeTemplate = `// Code generated by go generate; DO NOT EDIT.
|
||||
// Generated at: {{.Timestamp}}
|
||||
|
||||
package models
|
||||
|
||||
// ModelInfo represents information about a specific model
|
||||
type ModelInfo struct {
|
||||
ID string
|
||||
Name string
|
||||
Attachment bool
|
||||
Reasoning bool
|
||||
Temperature bool
|
||||
Cost Cost
|
||||
Limit Limit
|
||||
}
|
||||
|
||||
// Cost represents the pricing information for a model
|
||||
type Cost struct {
|
||||
Input float64
|
||||
Output float64
|
||||
CacheRead *float64
|
||||
CacheWrite *float64
|
||||
}
|
||||
|
||||
// Limit represents the context and output limits for a model
|
||||
type Limit struct {
|
||||
Context int
|
||||
Output int
|
||||
}
|
||||
|
||||
// ProviderInfo represents information about a model provider
|
||||
type ProviderInfo struct {
|
||||
ID string
|
||||
Env []string
|
||||
NPM string
|
||||
Name string
|
||||
Models map[string]ModelInfo
|
||||
}
|
||||
|
||||
// GetModelsData returns the static models data from models.dev
|
||||
func GetModelsData() map[string]ProviderInfo {
|
||||
return map[string]ProviderInfo{
|
||||
{{- range $providerID, $provider := .Providers}}
|
||||
"{{$providerID}}": {
|
||||
ID: "{{$provider.ID}}",
|
||||
Env: []string{ {{- range $i, $env := $provider.Env}}{{if $i}}, {{end}}"{{$env}}"{{end}} },
|
||||
NPM: "{{$provider.NPM}}",
|
||||
Name: "{{$provider.Name}}",
|
||||
Models: map[string]ModelInfo{
|
||||
{{- range $modelID, $model := $provider.Models}}
|
||||
"{{$modelID}}": {
|
||||
ID: "{{$model.ID}}",
|
||||
Name: "{{$model.Name}}",
|
||||
Attachment: {{$model.Attachment}},
|
||||
Reasoning: {{$model.Reasoning}},
|
||||
Temperature: {{$model.Temperature}},
|
||||
Cost: Cost{
|
||||
Input: {{$model.Cost.Input}},
|
||||
Output: {{$model.Cost.Output}},
|
||||
{{- if $model.Cost.CacheRead}}
|
||||
CacheRead: &[]float64{{"{"}}{{$model.Cost.CacheRead}}{{"}"}}[0],
|
||||
{{- else}}
|
||||
CacheRead: nil,
|
||||
{{- end}}
|
||||
{{- if $model.Cost.CacheWrite}}
|
||||
CacheWrite: &[]float64{{"{"}}{{$model.Cost.CacheWrite}}{{"}"}}[0],
|
||||
{{- else}}
|
||||
CacheWrite: nil,
|
||||
{{- end}}
|
||||
},
|
||||
Limit: Limit{
|
||||
Context: {{$model.Limit.Context}},
|
||||
Output: {{$model.Limit.Output}},
|
||||
},
|
||||
},
|
||||
{{- end}}
|
||||
},
|
||||
},
|
||||
{{- end}}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
func main() {
|
||||
fmt.Println("Fetching models data from models.dev...")
|
||||
|
||||
// Fetch data from API
|
||||
resp, err := http.Get("https://models.dev/api.json")
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error fetching data: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
fmt.Fprintf(os.Stderr, "API returned status %d\n", resp.StatusCode)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error reading response: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Parse JSON
|
||||
var providers map[string]ProviderInfo
|
||||
if err := json.Unmarshal(body, &providers); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error parsing JSON: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Fix Google provider environment variables to match our implementation
|
||||
if googleProvider, exists := providers["google"]; exists {
|
||||
googleProvider.Env = []string{"GOOGLE_API_KEY", "GEMINI_API_KEY", "GOOGLE_GENERATIVE_AI_API_KEY"}
|
||||
providers["google"] = googleProvider
|
||||
}
|
||||
|
||||
// Generate Go code
|
||||
tmpl, err := template.New("models").Parse(codeTemplate)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error parsing template: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Create output file
|
||||
file, err := os.Create("models_data.go")
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error creating output file: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Execute template
|
||||
data := struct {
|
||||
Providers map[string]ProviderInfo
|
||||
Timestamp string
|
||||
}{
|
||||
Providers: providers,
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
}
|
||||
|
||||
if err := tmpl.Execute(file, data); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error executing template: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Printf("Generated models_data.go with %d providers\n", len(providers))
|
||||
|
||||
// Print summary
|
||||
for providerID, provider := range providers {
|
||||
fmt.Printf(" %s: %d models\n", providerID, len(provider.Models))
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,249 +0,0 @@
|
||||
package openai
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
einoopenai "github.com/cloudwego/eino-ext/components/model/openai"
|
||||
"github.com/cloudwego/eino/components/model"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
)
|
||||
|
||||
// CustomChatModel wraps the eino-ext OpenAI model with custom tool schema handling.
|
||||
// It provides a compatibility layer that ensures proper JSON schema formatting
|
||||
// for OpenAI's function calling feature. This wrapper addresses cases where
|
||||
// tool schemas might have missing or empty properties that would cause API errors.
|
||||
type CustomChatModel struct {
|
||||
// wrapped is the underlying eino-ext OpenAI model instance
|
||||
wrapped *einoopenai.ChatModel
|
||||
}
|
||||
|
||||
// CustomRoundTripper intercepts HTTP requests to fix OpenAI function schemas.
|
||||
// It acts as middleware that modifies outgoing requests to ensure that
|
||||
// function/tool schemas are properly formatted according to OpenAI's requirements.
|
||||
// This is particularly important for handling edge cases where tool schemas
|
||||
// might have missing or empty properties fields.
|
||||
type CustomRoundTripper struct {
|
||||
// wrapped is the underlying HTTP transport to use for actual requests
|
||||
wrapped http.RoundTripper
|
||||
}
|
||||
|
||||
// NewCustomChatModel creates a new custom OpenAI chat model.
|
||||
// It wraps the standard eino-ext OpenAI model with additional request
|
||||
// preprocessing to ensure compatibility with OpenAI's API requirements,
|
||||
// particularly for function calling and tool schemas.
|
||||
//
|
||||
// Parameters:
|
||||
// - ctx: Context for the operation
|
||||
// - config: Configuration for the OpenAI model including API key, model name, and parameters
|
||||
//
|
||||
// Returns:
|
||||
// - *CustomChatModel: A wrapped OpenAI model with enhanced compatibility
|
||||
// - error: Returns an error if model creation fails
|
||||
//
|
||||
// The custom model automatically:
|
||||
// - Ensures function parameter schemas have properties fields
|
||||
// - Fixes missing or empty properties in tool schemas
|
||||
// - Maintains compatibility with OpenAI's function calling requirements
|
||||
func NewCustomChatModel(ctx context.Context, config *einoopenai.ChatModelConfig) (*CustomChatModel, error) {
|
||||
// Create a custom HTTP client that intercepts requests
|
||||
if config.HTTPClient == nil {
|
||||
config.HTTPClient = &http.Client{}
|
||||
}
|
||||
|
||||
// Wrap the transport to intercept requests
|
||||
if config.HTTPClient.Transport == nil {
|
||||
config.HTTPClient.Transport = http.DefaultTransport
|
||||
}
|
||||
config.HTTPClient.Transport = &CustomRoundTripper{
|
||||
wrapped: config.HTTPClient.Transport,
|
||||
}
|
||||
|
||||
wrapped, err := einoopenai.NewChatModel(ctx, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &CustomChatModel{
|
||||
wrapped: wrapped,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// RoundTrip implements http.RoundTripper to intercept and fix OpenAI requests.
|
||||
// It preprocesses outgoing requests to the OpenAI API to ensure tool/function
|
||||
// schemas meet the API's requirements.
|
||||
//
|
||||
// Parameters:
|
||||
// - req: The HTTP request to be sent to the OpenAI API
|
||||
//
|
||||
// Returns:
|
||||
// - *http.Response: The response from the OpenAI API
|
||||
// - error: Any error that occurred during the request
|
||||
//
|
||||
// The method performs the following fixes:
|
||||
// - Ensures function parameter schemas of type "object" have a properties field
|
||||
// - Adds empty properties object if missing to prevent API validation errors
|
||||
func (c *CustomRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
// Only intercept OpenAI chat completions requests
|
||||
if !strings.Contains(req.URL.Path, "/chat/completions") {
|
||||
return c.wrapped.RoundTrip(req)
|
||||
}
|
||||
|
||||
// Read the request body
|
||||
if req.Body == nil {
|
||||
return c.wrapped.RoundTrip(req)
|
||||
}
|
||||
|
||||
bodyBytes, err := io.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
return c.wrapped.RoundTrip(req)
|
||||
}
|
||||
req.Body.Close()
|
||||
|
||||
// Parse the JSON request
|
||||
var requestData map[string]interface{}
|
||||
if err := json.Unmarshal(bodyBytes, &requestData); err != nil {
|
||||
// If we can't parse it, just pass it through
|
||||
req.Body = io.NopCloser(bytes.NewReader(bodyBytes))
|
||||
return c.wrapped.RoundTrip(req)
|
||||
}
|
||||
|
||||
// Fix function schemas if present
|
||||
if tools, ok := requestData["tools"].([]interface{}); ok {
|
||||
for _, tool := range tools {
|
||||
if toolMap, ok := tool.(map[string]interface{}); ok {
|
||||
if function, ok := toolMap["function"].(map[string]interface{}); ok {
|
||||
if parameters, ok := function["parameters"].(map[string]interface{}); ok {
|
||||
if typeVal, ok := parameters["type"].(string); ok && typeVal == "object" {
|
||||
// Check if properties is missing or empty
|
||||
if properties, exists := parameters["properties"]; !exists || properties == nil {
|
||||
parameters["properties"] = map[string]interface{}{}
|
||||
} else if propMap, ok := properties.(map[string]interface{}); ok && len(propMap) == 0 {
|
||||
parameters["properties"] = map[string]interface{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Marshal the fixed request back to JSON
|
||||
fixedBodyBytes, err := json.Marshal(requestData)
|
||||
if err != nil {
|
||||
// If we can't marshal it, use the original
|
||||
req.Body = io.NopCloser(bytes.NewReader(bodyBytes))
|
||||
return c.wrapped.RoundTrip(req)
|
||||
}
|
||||
|
||||
// Create new request body with fixed data
|
||||
req.Body = io.NopCloser(bytes.NewReader(fixedBodyBytes))
|
||||
req.ContentLength = int64(len(fixedBodyBytes))
|
||||
|
||||
return c.wrapped.RoundTrip(req)
|
||||
}
|
||||
|
||||
// Generate implements model.ChatModel interface.
|
||||
// It generates a single response from the OpenAI model based on the input messages.
|
||||
//
|
||||
// Parameters:
|
||||
// - ctx: Context for the operation, supporting cancellation and deadlines
|
||||
// - in: The conversation history as a slice of messages
|
||||
// - opts: Optional configuration options for the generation
|
||||
//
|
||||
// Returns:
|
||||
// - *schema.Message: The generated response message
|
||||
// - error: Any error that occurred during generation
|
||||
func (c *CustomChatModel) Generate(ctx context.Context, in []*schema.Message, opts ...model.Option) (*schema.Message, error) {
|
||||
return c.wrapped.Generate(ctx, in, opts...)
|
||||
}
|
||||
|
||||
// Stream implements model.ChatModel interface.
|
||||
// It generates a streaming response from the OpenAI model, allowing
|
||||
// incremental processing of the model's output as it's generated.
|
||||
//
|
||||
// Parameters:
|
||||
// - ctx: Context for the operation, supporting cancellation and deadlines
|
||||
// - in: The conversation history as a slice of messages
|
||||
// - opts: Optional configuration options for the generation
|
||||
//
|
||||
// Returns:
|
||||
// - *schema.StreamReader[*schema.Message]: A reader for the streaming response
|
||||
// - error: Any error that occurred during stream setup
|
||||
func (c *CustomChatModel) Stream(ctx context.Context, in []*schema.Message, opts ...model.Option) (*schema.StreamReader[*schema.Message], error) {
|
||||
return c.wrapped.Stream(ctx, in, opts...)
|
||||
}
|
||||
|
||||
// WithTools implements model.ToolCallingChatModel interface.
|
||||
// It creates a new model instance with the specified tools available for function calling.
|
||||
// The original model instance remains unchanged.
|
||||
//
|
||||
// Parameters:
|
||||
// - tools: A slice of tool definitions that the model can use
|
||||
//
|
||||
// Returns:
|
||||
// - model.ToolCallingChatModel: A new model instance with tools enabled
|
||||
// - error: Returns an error if tool binding fails
|
||||
func (c *CustomChatModel) WithTools(tools []*schema.ToolInfo) (model.ToolCallingChatModel, error) {
|
||||
wrappedWithTools, err := c.wrapped.WithTools(tools)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Type assert back to *einoopenai.ChatModel
|
||||
wrappedChatModel, ok := wrappedWithTools.(*einoopenai.ChatModel)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unexpected type returned from WithTools")
|
||||
}
|
||||
|
||||
return &CustomChatModel{wrapped: wrappedChatModel}, nil
|
||||
}
|
||||
|
||||
// BindTools implements model.ToolCallingChatModel interface.
|
||||
// It binds tools to the current model instance, modifying it in place
|
||||
// rather than creating a new instance.
|
||||
//
|
||||
// Parameters:
|
||||
// - tools: A slice of tool definitions to bind to the model
|
||||
//
|
||||
// Returns:
|
||||
// - error: Returns an error if tool binding fails
|
||||
func (c *CustomChatModel) BindTools(tools []*schema.ToolInfo) error {
|
||||
return c.wrapped.BindTools(tools)
|
||||
}
|
||||
|
||||
// BindForcedTools implements model.ToolCallingChatModel interface.
|
||||
// It binds tools to the current model instance in forced mode,
|
||||
// ensuring the model will always use one of the provided tools.
|
||||
//
|
||||
// Parameters:
|
||||
// - tools: A slice of tool definitions to bind to the model
|
||||
//
|
||||
// Returns:
|
||||
// - error: Returns an error if tool binding fails
|
||||
func (c *CustomChatModel) BindForcedTools(tools []*schema.ToolInfo) error {
|
||||
return c.wrapped.BindForcedTools(tools)
|
||||
}
|
||||
|
||||
// GetType implements model.ChatModel interface.
|
||||
// It returns the type identifier for this model implementation.
|
||||
//
|
||||
// Returns:
|
||||
// - string: Returns "CustomOpenAI" as the model type identifier
|
||||
func (c *CustomChatModel) GetType() string {
|
||||
return "CustomOpenAI"
|
||||
}
|
||||
|
||||
// IsCallbacksEnabled implements model.ChatModel interface.
|
||||
// It indicates whether this model supports callbacks for monitoring
|
||||
// and tracking purposes.
|
||||
//
|
||||
// Returns:
|
||||
// - bool: Returns the callback enabled status from the wrapped model
|
||||
func (c *CustomChatModel) IsCallbacksEnabled() bool {
|
||||
return c.wrapped.IsCallbacksEnabled()
|
||||
}
|
||||
+279
-496
File diff suppressed because it is too large
Load Diff
+140
-80
@@ -1,46 +1,164 @@
|
||||
//go:generate go run generate_models.go
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"charm.land/catwalk/pkg/embedded"
|
||||
)
|
||||
|
||||
// ModelInfo represents information about a specific model.
|
||||
type ModelInfo struct {
|
||||
ID string
|
||||
Name string
|
||||
Attachment bool
|
||||
Reasoning bool
|
||||
Temperature bool
|
||||
Cost Cost
|
||||
Limit Limit
|
||||
}
|
||||
|
||||
// Cost represents the pricing information for a model.
|
||||
type Cost struct {
|
||||
Input float64
|
||||
Output float64
|
||||
CacheRead *float64
|
||||
CacheWrite *float64
|
||||
}
|
||||
|
||||
// Limit represents the context and output limits for a model.
|
||||
type Limit struct {
|
||||
Context int
|
||||
Output int
|
||||
}
|
||||
|
||||
// ProviderInfo represents information about a model provider.
|
||||
type ProviderInfo struct {
|
||||
ID string
|
||||
Env []string
|
||||
Name string
|
||||
Models map[string]ModelInfo
|
||||
}
|
||||
|
||||
// providerEnvVars maps provider IDs to their required environment variables.
|
||||
// Catwalk provides APIKey field names but we need the actual env var names.
|
||||
var providerEnvVars = map[string][]string{
|
||||
"anthropic": {"ANTHROPIC_API_KEY"},
|
||||
"openai": {"OPENAI_API_KEY"},
|
||||
"google": {"GOOGLE_API_KEY", "GEMINI_API_KEY", "GOOGLE_GENERATIVE_AI_API_KEY"},
|
||||
"azure": {"AZURE_OPENAI_API_KEY"},
|
||||
"openrouter": {"OPENROUTER_API_KEY"},
|
||||
"bedrock": {"AWS_ACCESS_KEY_ID"},
|
||||
"google-vertex-anthropic": {"GOOGLE_APPLICATION_CREDENTIALS"},
|
||||
"ollama": {},
|
||||
"mistral": {"MISTRAL_API_KEY"},
|
||||
"groq": {"GROQ_API_KEY"},
|
||||
"deepseek": {"DEEPSEEK_API_KEY"},
|
||||
"xai": {"XAI_API_KEY"},
|
||||
"fireworks": {"FIREWORKS_API_KEY"},
|
||||
"together": {"TOGETHER_API_KEY"},
|
||||
"perplexity": {"PERPLEXITY_API_KEY"},
|
||||
"alibaba": {"DASHSCOPE_API_KEY"},
|
||||
"cohere": {"COHERE_API_KEY"},
|
||||
}
|
||||
|
||||
// ModelsRegistry provides validation and information about models.
|
||||
// It maintains a registry of all supported LLM providers and their models,
|
||||
// including capabilities, pricing, and configuration requirements.
|
||||
// The registry data is generated from models.dev and provides a single
|
||||
// source of truth for model validation and discovery.
|
||||
// The registry data comes from the catwalk embedded database.
|
||||
type ModelsRegistry struct {
|
||||
// providers maps provider IDs to their information and available models
|
||||
providers map[string]ProviderInfo
|
||||
}
|
||||
|
||||
// NewModelsRegistry creates a new models registry with static data.
|
||||
// The registry is populated with model information generated from models.dev,
|
||||
// providing comprehensive metadata about available models across all supported providers.
|
||||
//
|
||||
// Returns:
|
||||
// - *ModelsRegistry: A new registry instance populated with current model data
|
||||
// NewModelsRegistry creates a new models registry populated from the catwalk embedded database.
|
||||
func NewModelsRegistry() *ModelsRegistry {
|
||||
return &ModelsRegistry{
|
||||
providers: GetModelsData(),
|
||||
providers: buildFromCatwalk(),
|
||||
}
|
||||
}
|
||||
|
||||
// buildFromCatwalk converts catwalk embedded data into our internal format.
|
||||
func buildFromCatwalk() map[string]ProviderInfo {
|
||||
providers := make(map[string]ProviderInfo)
|
||||
|
||||
for _, cp := range embedded.GetAll() {
|
||||
providerID := string(cp.ID)
|
||||
|
||||
modelsMap := make(map[string]ModelInfo, len(cp.Models))
|
||||
for _, cm := range cp.Models {
|
||||
var cacheRead, cacheWrite *float64
|
||||
if cm.CostPer1MInCached > 0 {
|
||||
v := cm.CostPer1MInCached
|
||||
cacheRead = &v
|
||||
}
|
||||
if cm.CostPer1MOutCached > 0 {
|
||||
v := cm.CostPer1MOutCached
|
||||
cacheWrite = &v
|
||||
}
|
||||
|
||||
hasTemperature := true // most models support temperature
|
||||
if cm.Options.Temperature != nil && *cm.Options.Temperature == 0 {
|
||||
hasTemperature = false
|
||||
}
|
||||
|
||||
modelsMap[cm.ID] = ModelInfo{
|
||||
ID: cm.ID,
|
||||
Name: cm.Name,
|
||||
Attachment: cm.SupportsImages,
|
||||
Reasoning: cm.CanReason,
|
||||
Temperature: hasTemperature,
|
||||
Cost: Cost{
|
||||
Input: cm.CostPer1MIn,
|
||||
Output: cm.CostPer1MOut,
|
||||
CacheRead: cacheRead,
|
||||
CacheWrite: cacheWrite,
|
||||
},
|
||||
Limit: Limit{
|
||||
Context: int(cm.ContextWindow),
|
||||
Output: int(cm.DefaultMaxTokens),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
envVars := providerEnvVars[providerID]
|
||||
if envVars == nil {
|
||||
// Derive from the catwalk APIKey field if available
|
||||
if cp.APIKey != "" {
|
||||
envVars = []string{cp.APIKey}
|
||||
}
|
||||
}
|
||||
|
||||
providers[providerID] = ProviderInfo{
|
||||
ID: providerID,
|
||||
Env: envVars,
|
||||
Name: cp.Name,
|
||||
Models: modelsMap,
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure providers that mcphost explicitly supports are always present
|
||||
// even if catwalk doesn't list them (e.g. ollama, google-vertex-anthropic)
|
||||
ensureProvider(providers, "ollama", "Ollama", nil)
|
||||
ensureProvider(providers, "google-vertex-anthropic", "Google Vertex (Anthropic)",
|
||||
providerEnvVars["google-vertex-anthropic"])
|
||||
|
||||
return providers
|
||||
}
|
||||
|
||||
// ensureProvider ensures a provider entry exists in the map.
|
||||
func ensureProvider(providers map[string]ProviderInfo, id, name string, env []string) {
|
||||
if _, exists := providers[id]; !exists {
|
||||
providers[id] = ProviderInfo{
|
||||
ID: id,
|
||||
Env: env,
|
||||
Name: name,
|
||||
Models: make(map[string]ModelInfo),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateModel validates if a model exists and returns detailed information.
|
||||
// It checks whether a specific model is available for a given provider and
|
||||
// returns comprehensive information about the model's capabilities and limits.
|
||||
//
|
||||
// Parameters:
|
||||
// - provider: The provider ID (e.g., "anthropic", "openai", "google")
|
||||
// - modelID: The specific model ID (e.g., "claude-3-sonnet-20240620", "gpt-4")
|
||||
//
|
||||
// Returns:
|
||||
// - *ModelInfo: Detailed information about the model including pricing, limits, and capabilities
|
||||
// - error: Returns an error if the provider is unsupported or model is not found
|
||||
func (r *ModelsRegistry) ValidateModel(provider, modelID string) (*ModelInfo, error) {
|
||||
providerInfo, exists := r.providers[provider]
|
||||
if !exists {
|
||||
@@ -56,20 +174,6 @@ func (r *ModelsRegistry) ValidateModel(provider, modelID string) (*ModelInfo, er
|
||||
}
|
||||
|
||||
// GetRequiredEnvVars returns the required environment variables for a provider.
|
||||
// These are the environment variable names that should contain API keys or
|
||||
// other authentication credentials for the specified provider.
|
||||
//
|
||||
// Parameters:
|
||||
// - provider: The provider ID (e.g., "anthropic", "openai", "google")
|
||||
//
|
||||
// Returns:
|
||||
// - []string: List of environment variable names the provider checks for credentials
|
||||
// - error: Returns an error if the provider is unsupported
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// For "anthropic", returns ["ANTHROPIC_API_KEY"]
|
||||
// For "google", returns ["GOOGLE_API_KEY", "GEMINI_API_KEY", "GOOGLE_GENERATIVE_AI_API_KEY"]
|
||||
func (r *ModelsRegistry) GetRequiredEnvVars(provider string) ([]string, error) {
|
||||
providerInfo, exists := r.providers[provider]
|
||||
if !exists {
|
||||
@@ -80,28 +184,17 @@ func (r *ModelsRegistry) GetRequiredEnvVars(provider string) ([]string, error) {
|
||||
}
|
||||
|
||||
// ValidateEnvironment checks if required environment variables are set.
|
||||
// It verifies that at least one of the provider's required environment variables
|
||||
// contains an API key, unless an API key is explicitly provided via configuration.
|
||||
//
|
||||
// Parameters:
|
||||
// - provider: The provider ID to validate environment for
|
||||
// - apiKey: An API key provided via configuration (if empty, checks environment variables)
|
||||
//
|
||||
// Returns:
|
||||
// - error: Returns nil if validation passes, or an error describing missing credentials
|
||||
func (r *ModelsRegistry) ValidateEnvironment(provider string, apiKey string) error {
|
||||
envVars, err := r.GetRequiredEnvVars(provider)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If API key is provided via config, we don't need to check env vars
|
||||
if apiKey != "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Add alternative environment variable names for google-vertex-anthropic
|
||||
// These match the env vars checked by eino-claude and other tools
|
||||
if provider == "google-vertex-anthropic" {
|
||||
envVars = append(envVars,
|
||||
"ANTHROPIC_VERTEX_PROJECT_ID",
|
||||
@@ -113,7 +206,6 @@ func (r *ModelsRegistry) ValidateEnvironment(provider string, apiKey string) err
|
||||
)
|
||||
}
|
||||
|
||||
// Check if at least one environment variable is set
|
||||
var foundVar bool
|
||||
for _, envVar := range envVars {
|
||||
if os.Getenv(envVar) != "" {
|
||||
@@ -131,15 +223,6 @@ func (r *ModelsRegistry) ValidateEnvironment(provider string, apiKey string) err
|
||||
}
|
||||
|
||||
// SuggestModels returns similar model names when an invalid model is provided.
|
||||
// It helps users discover the correct model ID by finding models that partially
|
||||
// match the provided input, useful for correcting typos or finding alternatives.
|
||||
//
|
||||
// Parameters:
|
||||
// - provider: The provider ID to search within
|
||||
// - invalidModel: The invalid or misspelled model name to find suggestions for
|
||||
//
|
||||
// Returns:
|
||||
// - []string: A list of up to 5 suggested model IDs that partially match the input
|
||||
func (r *ModelsRegistry) SuggestModels(provider, invalidModel string) []string {
|
||||
providerInfo, exists := r.providers[provider]
|
||||
if !exists {
|
||||
@@ -149,12 +232,10 @@ func (r *ModelsRegistry) SuggestModels(provider, invalidModel string) []string {
|
||||
var suggestions []string
|
||||
invalidLower := strings.ToLower(invalidModel)
|
||||
|
||||
// Look for models that contain parts of the invalid model name
|
||||
for modelID, modelInfo := range providerInfo.Models {
|
||||
modelIDLower := strings.ToLower(modelID)
|
||||
modelNameLower := strings.ToLower(modelInfo.Name)
|
||||
|
||||
// Check if the invalid model is a substring of existing models
|
||||
if strings.Contains(modelIDLower, invalidLower) ||
|
||||
strings.Contains(modelNameLower, invalidLower) ||
|
||||
strings.Contains(invalidLower, strings.ToLower(strings.Split(modelID, "-")[0])) {
|
||||
@@ -162,7 +243,6 @@ func (r *ModelsRegistry) SuggestModels(provider, invalidModel string) []string {
|
||||
}
|
||||
}
|
||||
|
||||
// Limit suggestions to avoid overwhelming output
|
||||
if len(suggestions) > 5 {
|
||||
suggestions = suggestions[:5]
|
||||
}
|
||||
@@ -171,11 +251,6 @@ func (r *ModelsRegistry) SuggestModels(provider, invalidModel string) []string {
|
||||
}
|
||||
|
||||
// GetSupportedProviders returns a list of all supported providers.
|
||||
// This includes all providers that have models registered in the system,
|
||||
// such as "anthropic", "openai", "google", "alibaba", etc.
|
||||
//
|
||||
// Returns:
|
||||
// - []string: A list of all provider IDs available in the registry
|
||||
func (r *ModelsRegistry) GetSupportedProviders() []string {
|
||||
var providers []string
|
||||
for providerID := range r.providers {
|
||||
@@ -185,15 +260,6 @@ func (r *ModelsRegistry) GetSupportedProviders() []string {
|
||||
}
|
||||
|
||||
// GetModelsForProvider returns all models for a specific provider.
|
||||
// This is useful for listing available models when a user wants to see
|
||||
// all options for a particular provider.
|
||||
//
|
||||
// Parameters:
|
||||
// - provider: The provider ID to get models for
|
||||
//
|
||||
// Returns:
|
||||
// - map[string]ModelInfo: A map of model IDs to their detailed information
|
||||
// - error: Returns an error if the provider is unsupported
|
||||
func (r *ModelsRegistry) GetModelsForProvider(provider string) (map[string]ModelInfo, error) {
|
||||
providerInfo, exists := r.providers[provider]
|
||||
if !exists {
|
||||
@@ -207,12 +273,6 @@ func (r *ModelsRegistry) GetModelsForProvider(provider string) (map[string]Model
|
||||
var globalRegistry = NewModelsRegistry()
|
||||
|
||||
// GetGlobalRegistry returns the global models registry instance.
|
||||
// This provides a singleton registry that can be accessed throughout
|
||||
// the application for model validation and information retrieval.
|
||||
// The registry is initialized once with data from models.dev.
|
||||
//
|
||||
// Returns:
|
||||
// - *ModelsRegistry: The global registry instance
|
||||
func GetGlobalRegistry() *ModelsRegistry {
|
||||
return globalRegistry
|
||||
}
|
||||
|
||||
+13
-57
@@ -4,14 +4,12 @@ import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/cloudwego/eino/schema"
|
||||
"charm.land/fantasy"
|
||||
)
|
||||
|
||||
// Manager manages session state and auto-saving functionality.
|
||||
// It provides thread-safe operations for managing a conversation session,
|
||||
// including automatic persistence to disk after each modification.
|
||||
// The Manager ensures that all session operations are synchronized and
|
||||
// that the session file is kept up-to-date with any changes.
|
||||
type Manager struct {
|
||||
session *Session
|
||||
filePath string
|
||||
@@ -19,9 +17,6 @@ type Manager struct {
|
||||
}
|
||||
|
||||
// NewManager creates a new session manager with a fresh session.
|
||||
// The filePath parameter specifies where the session will be auto-saved.
|
||||
// If filePath is empty, the session will not be automatically saved to disk.
|
||||
// Returns a Manager instance ready to track conversation messages.
|
||||
func NewManager(filePath string) *Manager {
|
||||
return &Manager{
|
||||
session: NewSession(),
|
||||
@@ -30,10 +25,6 @@ func NewManager(filePath string) *Manager {
|
||||
}
|
||||
|
||||
// NewManagerWithSession creates a new session manager with an existing session.
|
||||
// This is useful when loading a session from a file and wanting to continue
|
||||
// managing it with auto-save functionality.
|
||||
// The session parameter is the existing session to manage.
|
||||
// The filePath parameter specifies where the session will be auto-saved.
|
||||
func NewManagerWithSession(session *Session, filePath string) *Manager {
|
||||
return &Manager{
|
||||
session: session,
|
||||
@@ -41,17 +32,12 @@ func NewManagerWithSession(session *Session, filePath string) *Manager {
|
||||
}
|
||||
}
|
||||
|
||||
// AddMessage adds a message to the session and auto-saves.
|
||||
// The message is converted from schema.Message format to the internal
|
||||
// session Message format before being added. If a filePath was specified
|
||||
// when creating the Manager, the session is automatically saved to disk.
|
||||
// This operation is thread-safe.
|
||||
// Returns an error if auto-saving fails, nil otherwise.
|
||||
func (m *Manager) AddMessage(msg *schema.Message) error {
|
||||
// AddMessage adds a fantasy message to the session and auto-saves.
|
||||
func (m *Manager) AddMessage(msg fantasy.Message) error {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
sessionMsg := ConvertFromSchemaMessage(msg)
|
||||
sessionMsg := ConvertFromFantasyMessage(msg)
|
||||
m.session.AddMessage(sessionMsg)
|
||||
|
||||
if m.filePath != "" {
|
||||
@@ -61,17 +47,13 @@ func (m *Manager) AddMessage(msg *schema.Message) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddMessages adds multiple messages to the session and auto-saves.
|
||||
// All messages are added in order and then the session is saved once.
|
||||
// This is more efficient than calling AddMessage multiple times when
|
||||
// adding several messages at once. The operation is thread-safe.
|
||||
// Returns an error if auto-saving fails, nil otherwise.
|
||||
func (m *Manager) AddMessages(msgs []*schema.Message) error {
|
||||
// AddMessages adds multiple fantasy messages to the session and auto-saves.
|
||||
func (m *Manager) AddMessages(msgs []fantasy.Message) error {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
for _, msg := range msgs {
|
||||
sessionMsg := ConvertFromSchemaMessage(msg)
|
||||
sessionMsg := ConvertFromFantasyMessage(msg)
|
||||
m.session.AddMessage(sessionMsg)
|
||||
}
|
||||
|
||||
@@ -83,12 +65,7 @@ func (m *Manager) AddMessages(msgs []*schema.Message) error {
|
||||
}
|
||||
|
||||
// ReplaceAllMessages replaces all messages in the session with the provided messages.
|
||||
// This method completely clears the existing message history and replaces it with
|
||||
// the new set of messages. Useful for resetting a conversation or loading a
|
||||
// different conversation context. The operation is thread-safe and triggers
|
||||
// an auto-save if a filePath is configured.
|
||||
// Returns an error if auto-saving fails, nil otherwise.
|
||||
func (m *Manager) ReplaceAllMessages(msgs []*schema.Message) error {
|
||||
func (m *Manager) ReplaceAllMessages(msgs []fantasy.Message) error {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
@@ -97,7 +74,7 @@ func (m *Manager) ReplaceAllMessages(msgs []*schema.Message) error {
|
||||
|
||||
// Add all new messages
|
||||
for _, msg := range msgs {
|
||||
sessionMsg := ConvertFromSchemaMessage(msg)
|
||||
sessionMsg := ConvertFromFantasyMessage(msg)
|
||||
m.session.AddMessage(sessionMsg)
|
||||
}
|
||||
|
||||
@@ -109,10 +86,6 @@ func (m *Manager) ReplaceAllMessages(msgs []*schema.Message) error {
|
||||
}
|
||||
|
||||
// SetMetadata sets the session metadata.
|
||||
// This updates the session's metadata with information about the provider,
|
||||
// model, and MCPHost version. The operation is thread-safe and triggers
|
||||
// an auto-save if a filePath is configured.
|
||||
// Returns an error if auto-saving fails, nil otherwise.
|
||||
func (m *Manager) SetMetadata(metadata Metadata) error {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
@@ -126,33 +99,24 @@ func (m *Manager) SetMetadata(metadata Metadata) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetMessages returns all messages as a schema.Message slice.
|
||||
// This method converts all stored session messages to the schema format
|
||||
// used by LLM providers. The returned slice is a new allocation, so
|
||||
// modifications to it won't affect the stored session. This operation
|
||||
// is thread-safe for concurrent reads.
|
||||
func (m *Manager) GetMessages() []*schema.Message {
|
||||
// GetMessages returns all messages as fantasy.Message slice.
|
||||
func (m *Manager) GetMessages() []fantasy.Message {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
|
||||
messages := make([]*schema.Message, len(m.session.Messages))
|
||||
messages := make([]fantasy.Message, len(m.session.Messages))
|
||||
for i, msg := range m.session.Messages {
|
||||
messages[i] = msg.ConvertToSchemaMessage()
|
||||
messages[i] = msg.ConvertToFantasyMessage()
|
||||
}
|
||||
|
||||
return messages
|
||||
}
|
||||
|
||||
// GetSession returns a copy of the current session.
|
||||
// The returned session is a deep copy, including all messages, so
|
||||
// modifications to it won't affect the managed session. This is useful
|
||||
// for safely inspecting the session state without risk of concurrent
|
||||
// modification. This operation is thread-safe for concurrent reads.
|
||||
func (m *Manager) GetSession() *Session {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
|
||||
// Return a copy to prevent external modification
|
||||
sessionCopy := *m.session
|
||||
sessionCopy.Messages = make([]Message, len(m.session.Messages))
|
||||
copy(sessionCopy.Messages, m.session.Messages)
|
||||
@@ -161,10 +125,6 @@ func (m *Manager) GetSession() *Session {
|
||||
}
|
||||
|
||||
// Save manually saves the session to file.
|
||||
// This forces a save operation even if no changes have been made.
|
||||
// Useful for ensuring the session is persisted at specific points.
|
||||
// Returns an error if no filePath was specified when creating the
|
||||
// Manager, or if the save operation fails.
|
||||
func (m *Manager) Save() error {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
@@ -177,15 +137,11 @@ func (m *Manager) Save() error {
|
||||
}
|
||||
|
||||
// GetFilePath returns the file path for this session.
|
||||
// Returns the path where the session is being auto-saved, or an
|
||||
// empty string if no auto-save path was configured.
|
||||
func (m *Manager) GetFilePath() string {
|
||||
return m.filePath
|
||||
}
|
||||
|
||||
// MessageCount returns the number of messages in the session.
|
||||
// This provides a quick way to check the conversation length without
|
||||
// retrieving all messages. This operation is thread-safe for concurrent reads.
|
||||
func (m *Manager) MessageCount() int {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
|
||||
+71
-77
@@ -8,7 +8,7 @@ import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/cloudwego/eino/schema"
|
||||
"charm.land/fantasy"
|
||||
)
|
||||
|
||||
// Session represents a complete conversation session with metadata.
|
||||
@@ -30,9 +30,7 @@ type Session struct {
|
||||
}
|
||||
|
||||
// Metadata contains session metadata that provides context about the
|
||||
// environment and configuration used during the conversation. This helps
|
||||
// with debugging and understanding the session's context when reviewing
|
||||
// conversation history.
|
||||
// environment and configuration used during the conversation.
|
||||
type Metadata struct {
|
||||
// MCPHostVersion is the version of MCPHost used for this session
|
||||
MCPHostVersion string `json:"mcphost_version"`
|
||||
@@ -61,8 +59,6 @@ type Message struct {
|
||||
}
|
||||
|
||||
// ToolCall represents a tool invocation within an assistant message.
|
||||
// When the assistant decides to use a tool, it creates a ToolCall with
|
||||
// the necessary information to execute that tool.
|
||||
type ToolCall struct {
|
||||
// ID is a unique identifier for this tool call, used to link results
|
||||
ID string `json:"id"`
|
||||
@@ -73,9 +69,6 @@ type ToolCall struct {
|
||||
}
|
||||
|
||||
// NewSession creates a new session with default values.
|
||||
// It initializes a session with version 1.0, current timestamps,
|
||||
// empty message list, and empty metadata. The returned session
|
||||
// is ready to receive messages and can be saved to a file.
|
||||
func NewSession() *Session {
|
||||
return &Session{
|
||||
Version: "1.0",
|
||||
@@ -87,9 +80,6 @@ func NewSession() *Session {
|
||||
}
|
||||
|
||||
// AddMessage adds a message to the session.
|
||||
// If the message doesn't have an ID, one will be auto-generated.
|
||||
// If the message doesn't have a timestamp, the current time will be used.
|
||||
// The session's UpdatedAt timestamp is automatically updated.
|
||||
func (s *Session) AddMessage(msg Message) {
|
||||
if msg.ID == "" {
|
||||
msg.ID = generateMessageID()
|
||||
@@ -103,20 +93,12 @@ func (s *Session) AddMessage(msg Message) {
|
||||
}
|
||||
|
||||
// SetMetadata sets the session metadata.
|
||||
// This replaces the existing metadata with the provided metadata
|
||||
// and updates the session's UpdatedAt timestamp. Use this to record
|
||||
// information about the provider, model, and MCPHost version.
|
||||
func (s *Session) SetMetadata(metadata Metadata) {
|
||||
s.Metadata = metadata
|
||||
s.UpdatedAt = time.Now()
|
||||
}
|
||||
|
||||
// SaveToFile saves the session to a JSON file.
|
||||
// The session is serialized as indented JSON for readability.
|
||||
// The UpdatedAt timestamp is automatically updated before saving.
|
||||
// The file is created with 0644 permissions if it doesn't exist,
|
||||
// or overwritten if it does exist.
|
||||
// Returns an error if marshaling fails or file writing fails.
|
||||
func (s *Session) SaveToFile(filePath string) error {
|
||||
s.UpdatedAt = time.Now()
|
||||
|
||||
@@ -129,11 +111,6 @@ func (s *Session) SaveToFile(filePath string) error {
|
||||
}
|
||||
|
||||
// LoadFromFile loads a session from a JSON file.
|
||||
// It reads the file at the specified path and deserializes it into
|
||||
// a Session struct. This is useful for resuming previous conversations
|
||||
// or reviewing session history.
|
||||
// Returns the loaded session on success, or an error if the file
|
||||
// cannot be read or the JSON is invalid.
|
||||
func LoadFromFile(filePath string) (*Session, error) {
|
||||
data, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
@@ -148,85 +125,102 @@ func LoadFromFile(filePath string) (*Session, error) {
|
||||
return &session, nil
|
||||
}
|
||||
|
||||
// ConvertFromSchemaMessage converts a schema.Message to a session Message.
|
||||
// This function bridges between the eino schema message format and the
|
||||
// session's internal message format. It preserves role, content, and
|
||||
// tool-related information while adding a timestamp.
|
||||
// Tool calls from assistant messages and tool call IDs from tool messages
|
||||
// are properly converted and preserved.
|
||||
func ConvertFromSchemaMessage(msg *schema.Message) Message {
|
||||
// ConvertFromFantasyMessage converts a fantasy.Message to a session Message.
|
||||
// This function bridges between the fantasy message format and the
|
||||
// session's internal message format for JSON persistence.
|
||||
func ConvertFromFantasyMessage(msg fantasy.Message) Message {
|
||||
sessionMsg := Message{
|
||||
Role: string(msg.Role),
|
||||
Content: msg.Content,
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
|
||||
// Convert tool calls if present (for assistant messages)
|
||||
if len(msg.ToolCalls) > 0 {
|
||||
sessionMsg.ToolCalls = make([]ToolCall, len(msg.ToolCalls))
|
||||
for i, tc := range msg.ToolCalls {
|
||||
sessionMsg.ToolCalls[i] = ToolCall{
|
||||
ID: tc.ID,
|
||||
Name: tc.Function.Name,
|
||||
Arguments: tc.Function.Arguments,
|
||||
// Extract text content and tool calls from message parts
|
||||
var textParts []string
|
||||
for _, part := range msg.Content {
|
||||
switch p := part.(type) {
|
||||
case fantasy.TextPart:
|
||||
textParts = append(textParts, p.Text)
|
||||
case fantasy.ToolCallPart:
|
||||
sessionMsg.ToolCalls = append(sessionMsg.ToolCalls, ToolCall{
|
||||
ID: p.ToolCallID,
|
||||
Name: p.ToolName,
|
||||
Arguments: p.Input,
|
||||
})
|
||||
case fantasy.ToolResultPart:
|
||||
// Tool result messages — store the tool call ID
|
||||
sessionMsg.ToolCallID = p.ToolCallID
|
||||
// Marshal result for storage
|
||||
if p.Output != nil {
|
||||
if resultBytes, err := json.Marshal(p.Output); err == nil {
|
||||
textParts = append(textParts, string(resultBytes))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle tool result messages - extract tool call ID from ToolCallID field
|
||||
if msg.Role == schema.Tool && msg.ToolCallID != "" {
|
||||
sessionMsg.ToolCallID = msg.ToolCallID
|
||||
// Join all text parts
|
||||
for i, t := range textParts {
|
||||
if i > 0 {
|
||||
sessionMsg.Content += "\n"
|
||||
}
|
||||
sessionMsg.Content += t
|
||||
}
|
||||
|
||||
return sessionMsg
|
||||
}
|
||||
|
||||
// ConvertToSchemaMessage converts a session Message to a schema.Message.
|
||||
// ConvertToFantasyMessage converts a session Message to a fantasy.Message.
|
||||
// This method bridges between the session's internal message format and
|
||||
// the eino schema message format used by the LLM providers.
|
||||
// It properly handles tool calls for assistant messages and tool call IDs
|
||||
// for tool result messages. Arguments are converted to string format as
|
||||
// required by the schema.
|
||||
func (m *Message) ConvertToSchemaMessage() *schema.Message {
|
||||
msg := &schema.Message{
|
||||
Role: schema.RoleType(m.Role),
|
||||
Content: m.Content,
|
||||
// the fantasy message format used by the LLM providers.
|
||||
func (m *Message) ConvertToFantasyMessage() fantasy.Message {
|
||||
msg := fantasy.Message{
|
||||
Role: fantasy.MessageRole(m.Role),
|
||||
}
|
||||
|
||||
// Convert tool calls if present (for assistant messages)
|
||||
if len(m.ToolCalls) > 0 {
|
||||
msg.ToolCalls = make([]schema.ToolCall, len(m.ToolCalls))
|
||||
for i, tc := range m.ToolCalls {
|
||||
// Arguments are already stored as a string, use them directly
|
||||
var argsStr string
|
||||
// Build content parts based on role
|
||||
switch m.Role {
|
||||
case "assistant":
|
||||
// Add text content if present
|
||||
if m.Content != "" {
|
||||
msg.Content = append(msg.Content, fantasy.TextPart{Text: m.Content})
|
||||
}
|
||||
// Add tool calls if present
|
||||
for _, tc := range m.ToolCalls {
|
||||
var inputStr string
|
||||
if str, ok := tc.Arguments.(string); ok {
|
||||
argsStr = str
|
||||
} else {
|
||||
// Fallback: marshal to JSON if not a string
|
||||
if argBytes, err := json.Marshal(tc.Arguments); err == nil {
|
||||
argsStr = string(argBytes)
|
||||
}
|
||||
inputStr = str
|
||||
} else if argBytes, err := json.Marshal(tc.Arguments); err == nil {
|
||||
inputStr = string(argBytes)
|
||||
}
|
||||
|
||||
msg.ToolCalls[i] = schema.ToolCall{
|
||||
ID: tc.ID,
|
||||
Function: schema.FunctionCall{
|
||||
Name: tc.Name,
|
||||
Arguments: argsStr,
|
||||
},
|
||||
}
|
||||
}
|
||||
msg.Content = append(msg.Content, fantasy.ToolCallPart{
|
||||
ToolCallID: tc.ID,
|
||||
ToolName: tc.Name,
|
||||
Input: inputStr,
|
||||
})
|
||||
}
|
||||
case "tool":
|
||||
// Tool result message
|
||||
msg.Role = fantasy.MessageRoleTool
|
||||
var resultContent fantasy.ToolResultOutputContent
|
||||
resultContent = fantasy.ToolResultOutputContentText{Text: m.Content}
|
||||
|
||||
// Handle tool result messages - set the tool call ID
|
||||
if m.Role == "tool" && m.ToolCallID != "" {
|
||||
msg.ToolCallID = m.ToolCallID
|
||||
msg.Content = append(msg.Content, fantasy.ToolResultPart{
|
||||
ToolCallID: m.ToolCallID,
|
||||
Output: resultContent,
|
||||
})
|
||||
case "user":
|
||||
msg.Content = append(msg.Content, fantasy.TextPart{Text: m.Content})
|
||||
case "system":
|
||||
msg.Content = append(msg.Content, fantasy.TextPart{Text: m.Content})
|
||||
default:
|
||||
msg.Content = append(msg.Content, fantasy.TextPart{Text: m.Content})
|
||||
}
|
||||
|
||||
return msg
|
||||
}
|
||||
|
||||
// generateMessageID generates a unique message ID
|
||||
// generateMessageID generates a unique message ID.
|
||||
func generateMessageID() string {
|
||||
bytes := make([]byte, 8)
|
||||
rand.Read(bytes)
|
||||
|
||||
@@ -3,11 +3,12 @@ package tools
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"maps"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/cloudwego/eino/components/model"
|
||||
"charm.land/fantasy"
|
||||
"github.com/mark3labs/mcp-go/client"
|
||||
"github.com/mark3labs/mcp-go/client/transport"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
@@ -63,7 +64,7 @@ type MCPConnectionPool struct {
|
||||
connections map[string]*MCPConnection
|
||||
config *ConnectionPoolConfig
|
||||
mu sync.RWMutex
|
||||
model model.ToolCallingChatModel
|
||||
model fantasy.LanguageModel
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
debug bool
|
||||
@@ -75,7 +76,7 @@ type MCPConnectionPool struct {
|
||||
// goroutine for periodic health checks that runs until Close is called.
|
||||
// The model parameter is used for MCP servers that require sampling support.
|
||||
// Thread-safe for concurrent use immediately after creation.
|
||||
func NewMCPConnectionPool(config *ConnectionPoolConfig, model model.ToolCallingChatModel, debug bool) *MCPConnectionPool {
|
||||
func NewMCPConnectionPool(config *ConnectionPoolConfig, model fantasy.LanguageModel, debug bool) *MCPConnectionPool {
|
||||
if config == nil {
|
||||
config = DefaultConnectionPoolConfig()
|
||||
}
|
||||
@@ -406,7 +407,7 @@ func (p *MCPConnectionPool) initializeClient(ctx context.Context, client client.
|
||||
}
|
||||
|
||||
if p.debugLogger != nil && p.debugLogger.IsDebugEnabled() {
|
||||
p.debugLogger.LogDebug(fmt.Sprintf("[POOL] Initialized MCP client"))
|
||||
p.debugLogger.LogDebug("[POOL] Initialized MCP client")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -430,9 +431,7 @@ func (p *MCPConnectionPool) startHealthCheck() {
|
||||
func (p *MCPConnectionPool) checkConnectionsHealth() {
|
||||
p.mu.RLock()
|
||||
connections := make(map[string]*MCPConnection)
|
||||
for k, v := range p.connections {
|
||||
connections[k] = v
|
||||
}
|
||||
maps.Copy(connections, p.connections)
|
||||
p.mu.RUnlock()
|
||||
|
||||
for serverName, conn := range connections {
|
||||
@@ -494,14 +493,14 @@ func (p *MCPConnectionPool) HandleConnectionError(serverName string, err error)
|
||||
// The returned map includes health status, last usage time, error counts, and
|
||||
// last error for each connection. Useful for monitoring and debugging connection
|
||||
// pool behavior. The returned data is a snapshot and safe for concurrent access.
|
||||
func (p *MCPConnectionPool) GetConnectionStats() map[string]interface{} {
|
||||
func (p *MCPConnectionPool) GetConnectionStats() map[string]any {
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
|
||||
stats := make(map[string]interface{})
|
||||
stats := make(map[string]any)
|
||||
for serverName, conn := range p.connections {
|
||||
conn.mu.RLock()
|
||||
stats[serverName] = map[string]interface{}{
|
||||
stats[serverName] = map[string]any{
|
||||
"is_healthy": conn.isHealthy,
|
||||
"last_used": conn.lastUsed,
|
||||
"last_error": conn.lastError,
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"charm.land/fantasy"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
)
|
||||
|
||||
// mcpFantasyTool adapts an MCP tool to the fantasy.AgentTool interface.
|
||||
// It bridges the MCP tool protocol with fantasy's agent tool system, handling
|
||||
// name prefixing, schema conversion, connection pooling, and result marshaling.
|
||||
type mcpFantasyTool struct {
|
||||
toolInfo fantasy.ToolInfo
|
||||
mapping *toolMapping
|
||||
providerOptions fantasy.ProviderOptions
|
||||
}
|
||||
|
||||
// Info returns the fantasy tool info including name, description, and parameter schema.
|
||||
func (t *mcpFantasyTool) Info() fantasy.ToolInfo {
|
||||
return t.toolInfo
|
||||
}
|
||||
|
||||
// Run executes the MCP tool by routing through the connection pool.
|
||||
// It maps the prefixed tool name back to the original name, retrieves a healthy
|
||||
// connection, invokes the tool, and converts the MCP result to a fantasy ToolResponse.
|
||||
func (t *mcpFantasyTool) Run(ctx context.Context, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
|
||||
// Parse and validate JSON arguments
|
||||
var arguments any
|
||||
input := call.Input
|
||||
if input == "" || input == "{}" {
|
||||
arguments = nil
|
||||
} else {
|
||||
var temp any
|
||||
if err := json.Unmarshal([]byte(input), &temp); err != nil {
|
||||
return fantasy.NewTextErrorResponse(fmt.Sprintf("invalid JSON arguments: %v", err)), nil
|
||||
}
|
||||
arguments = json.RawMessage(input)
|
||||
}
|
||||
|
||||
// Get connection from pool with health check
|
||||
conn, err := t.mapping.manager.connectionPool.GetConnectionWithHealthCheck(
|
||||
ctx, t.mapping.serverName, t.mapping.serverConfig,
|
||||
)
|
||||
if err != nil {
|
||||
return fantasy.ToolResponse{}, fmt.Errorf("failed to get healthy connection from pool: %w", err)
|
||||
}
|
||||
|
||||
// Call the MCP tool using the original (unprefixed) name
|
||||
result, err := conn.client.CallTool(ctx, mcp.CallToolRequest{
|
||||
Request: mcp.Request{
|
||||
Method: "tools/call",
|
||||
},
|
||||
Params: struct {
|
||||
Name string `json:"name"`
|
||||
Arguments any `json:"arguments,omitempty"`
|
||||
Meta *mcp.Meta `json:"_meta,omitempty"`
|
||||
}{
|
||||
Name: t.mapping.originalName,
|
||||
Arguments: arguments,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
// Mark connection as unhealthy for automatic recovery
|
||||
t.mapping.manager.connectionPool.HandleConnectionError(t.mapping.serverName, err)
|
||||
return fantasy.ToolResponse{}, fmt.Errorf("failed to call mcp tool: %w", err)
|
||||
}
|
||||
|
||||
// Marshal the MCP result to JSON string
|
||||
marshaledResult, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
return fantasy.ToolResponse{}, fmt.Errorf("failed to marshal mcp tool result: %w", err)
|
||||
}
|
||||
|
||||
// Return as text response, preserving error status from MCP
|
||||
if result.IsError {
|
||||
return fantasy.NewTextErrorResponse(string(marshaledResult)), nil
|
||||
}
|
||||
return fantasy.NewTextResponse(string(marshaledResult)), nil
|
||||
}
|
||||
|
||||
// ProviderOptions returns provider-specific options for this tool.
|
||||
func (t *mcpFantasyTool) ProviderOptions() fantasy.ProviderOptions {
|
||||
return t.providerOptions
|
||||
}
|
||||
|
||||
// SetProviderOptions sets provider-specific options for this tool.
|
||||
func (t *mcpFantasyTool) SetProviderOptions(opts fantasy.ProviderOptions) {
|
||||
t.providerOptions = opts
|
||||
}
|
||||
+72
-168
@@ -4,14 +4,11 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/cloudwego/eino/components/model"
|
||||
"github.com/cloudwego/eino/components/tool"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
"github.com/eino-contrib/jsonschema"
|
||||
"charm.land/fantasy"
|
||||
"github.com/mark3labs/mcp-go/client"
|
||||
"github.com/mark3labs/mcp-go/client/transport"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
@@ -26,9 +23,9 @@ import (
|
||||
// Thread-safe for concurrent tool invocations.
|
||||
type MCPToolManager struct {
|
||||
connectionPool *MCPConnectionPool
|
||||
tools []tool.BaseTool
|
||||
tools []fantasy.AgentTool
|
||||
toolMap map[string]*toolMapping // maps prefixed tool names to their server and original name
|
||||
model model.ToolCallingChatModel // LLM model for sampling
|
||||
model fantasy.LanguageModel // LLM model for sampling
|
||||
config *config.Config
|
||||
debug bool
|
||||
debugLogger DebugLogger
|
||||
@@ -42,18 +39,12 @@ type toolMapping struct {
|
||||
manager *MCPToolManager
|
||||
}
|
||||
|
||||
// mcpToolImpl implements the eino tool interface with server prefixing
|
||||
type mcpToolImpl struct {
|
||||
info *schema.ToolInfo
|
||||
mapping *toolMapping
|
||||
}
|
||||
|
||||
// NewMCPToolManager creates a new MCP tool manager instance.
|
||||
// Returns an initialized manager with empty tool collections ready to load tools from MCP servers.
|
||||
// The manager must be configured with SetModel and LoadTools before use.
|
||||
func NewMCPToolManager() *MCPToolManager {
|
||||
return &MCPToolManager{
|
||||
tools: make([]tool.BaseTool, 0),
|
||||
tools: make([]fantasy.AgentTool, 0),
|
||||
toolMap: make(map[string]*toolMapping),
|
||||
}
|
||||
}
|
||||
@@ -62,7 +53,7 @@ func NewMCPToolManager() *MCPToolManager {
|
||||
// The model is used when MCP servers request sampling operations, allowing them to
|
||||
// leverage the host's LLM capabilities for text generation tasks.
|
||||
// This method should be called before LoadTools if any MCP servers require sampling support.
|
||||
func (m *MCPToolManager) SetModel(model model.ToolCallingChatModel) {
|
||||
func (m *MCPToolManager) SetModel(model fantasy.LanguageModel) {
|
||||
m.model = model
|
||||
}
|
||||
|
||||
@@ -77,13 +68,13 @@ func (m *MCPToolManager) SetDebugLogger(logger DebugLogger) {
|
||||
}
|
||||
}
|
||||
|
||||
// samplingHandler implements the MCP sampling handler interface
|
||||
// samplingHandler implements the MCP sampling handler interface using a fantasy LanguageModel
|
||||
type samplingHandler struct {
|
||||
model model.ToolCallingChatModel
|
||||
model fantasy.LanguageModel
|
||||
}
|
||||
|
||||
// CreateMessage handles sampling requests from MCP servers by forwarding them to the configured LLM model.
|
||||
// It converts MCP message formats to eino message formats, invokes the model for generation,
|
||||
// It converts MCP message formats to fantasy message formats, invokes the model for generation,
|
||||
// and converts the response back to MCP format. Returns an error if no model is available
|
||||
// or if generation fails.
|
||||
func (h *samplingHandler) CreateMessage(ctx context.Context, request mcp.CreateMessageRequest) (*mcp.CreateMessageResult, error) {
|
||||
@@ -91,17 +82,16 @@ func (h *samplingHandler) CreateMessage(ctx context.Context, request mcp.CreateM
|
||||
return nil, fmt.Errorf("no model available for sampling")
|
||||
}
|
||||
|
||||
// Convert MCP messages to eino messages
|
||||
var messages []*schema.Message
|
||||
// Build fantasy messages from MCP sampling request
|
||||
var messages []fantasy.Message
|
||||
|
||||
// Add system message if provided
|
||||
if request.SystemPrompt != "" {
|
||||
messages = append(messages, schema.SystemMessage(request.SystemPrompt))
|
||||
messages = append(messages, fantasy.NewSystemMessage(request.SystemPrompt))
|
||||
}
|
||||
|
||||
// Convert sampling messages
|
||||
for _, msg := range request.Messages {
|
||||
// Extract text content
|
||||
var content string
|
||||
if textContent, ok := msg.Content.(mcp.TextContent); ok {
|
||||
content = textContent.Text
|
||||
@@ -111,30 +101,36 @@ func (h *samplingHandler) CreateMessage(ctx context.Context, request mcp.CreateM
|
||||
|
||||
switch msg.Role {
|
||||
case mcp.RoleUser:
|
||||
messages = append(messages, schema.UserMessage(content))
|
||||
messages = append(messages, fantasy.NewUserMessage(content))
|
||||
case mcp.RoleAssistant:
|
||||
messages = append(messages, schema.AssistantMessage(content, nil))
|
||||
messages = append(messages, fantasy.Message{
|
||||
Role: fantasy.MessageRoleAssistant,
|
||||
Content: []fantasy.MessagePart{fantasy.TextPart{Text: content}},
|
||||
})
|
||||
default:
|
||||
messages = append(messages, schema.UserMessage(content)) // Default to user
|
||||
messages = append(messages, fantasy.NewUserMessage(content))
|
||||
}
|
||||
}
|
||||
|
||||
// Generate response using the model (no config options for now)
|
||||
response, err := h.model.Generate(ctx, messages)
|
||||
// Generate response using the fantasy model
|
||||
call := fantasy.Call{
|
||||
Prompt: fantasy.Prompt(messages),
|
||||
}
|
||||
response, err := h.model.Generate(ctx, call)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("model generation failed: %w", err)
|
||||
}
|
||||
|
||||
// Convert response back to MCP format
|
||||
result := &mcp.CreateMessageResult{
|
||||
Model: "mcphost-model", // Generic model name
|
||||
Model: h.model.Model(),
|
||||
StopReason: "endTurn",
|
||||
}
|
||||
result.SamplingMessage = mcp.SamplingMessage{
|
||||
Role: mcp.RoleAssistant,
|
||||
Content: mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: response.Content,
|
||||
Text: response.Content.Text(),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -202,7 +198,7 @@ func (m *MCPToolManager) loadServerTools(ctx context.Context, serverName string,
|
||||
}
|
||||
}
|
||||
|
||||
// Convert MCP tools to eino tools with prefixed names
|
||||
// Convert MCP tools to fantasy AgentTools with prefixed names
|
||||
for _, mcpTool := range listResults.Tools {
|
||||
// Filter tools based on allowedTools/excludedTools
|
||||
if len(serverConfig.AllowedTools) > 0 {
|
||||
@@ -216,29 +212,40 @@ func (m *MCPToolManager) loadServerTools(ctx context.Context, serverName string,
|
||||
continue
|
||||
}
|
||||
|
||||
// Convert schema
|
||||
marshaledInputSchema, err := sonic.Marshal(mcpTool.InputSchema)
|
||||
// Convert MCP InputSchema to map[string]any for fantasy ToolInfo
|
||||
marshaledSchema, err := json.Marshal(mcpTool.InputSchema)
|
||||
if err != nil {
|
||||
return fmt.Errorf("conv mcp tool input schema fail(marshal): %w, tool name: %s", err, mcpTool.Name)
|
||||
}
|
||||
|
||||
// Fix for JSON Schema draft-07 vs draft-04 compatibility:
|
||||
// Chrome DevTools MCP uses draft-07 where exclusiveMinimum/exclusiveMaximum are numbers,
|
||||
// but kin-openapi (OpenAPI 3.0) expects them as booleans (draft-04 format).
|
||||
// Pre-process the schema to convert numeric exclusive bounds to boolean format.
|
||||
marshaledInputSchema = convertExclusiveBoundsToBoolean(marshaledInputSchema)
|
||||
// Fix for JSON Schema draft-07 vs draft-04 compatibility
|
||||
marshaledSchema = convertExclusiveBoundsToBoolean(marshaledSchema)
|
||||
|
||||
inputSchema := &jsonschema.Schema{}
|
||||
err = sonic.Unmarshal(marshaledInputSchema, inputSchema)
|
||||
if err != nil {
|
||||
// Parse into map[string]any for fantasy's parameters format
|
||||
var schemaMap map[string]any
|
||||
if err := json.Unmarshal(marshaledSchema, &schemaMap); err != nil {
|
||||
return fmt.Errorf("conv mcp tool input schema fail(unmarshal): %w, tool name: %s", err, mcpTool.Name)
|
||||
}
|
||||
|
||||
// Extract properties and required from the schema
|
||||
parameters := make(map[string]any)
|
||||
var required []string
|
||||
|
||||
if props, ok := schemaMap["properties"].(map[string]any); ok {
|
||||
parameters = props
|
||||
}
|
||||
|
||||
// Fix for issue #89: Ensure object schemas have a properties field
|
||||
// OpenAI function calling requires object schemas to have a "properties" field
|
||||
// even if it's empty, otherwise it throws "object schema missing properties" error
|
||||
if inputSchema.Type == "object" && inputSchema.Properties == nil {
|
||||
inputSchema.Properties = jsonschema.NewProperties()
|
||||
if schemaType, ok := schemaMap["type"].(string); ok && schemaType == "object" && len(parameters) == 0 {
|
||||
// Keep empty parameters map - fantasy handles this fine
|
||||
}
|
||||
|
||||
if req, ok := schemaMap["required"].([]any); ok {
|
||||
for _, r := range req {
|
||||
if s, ok := r.(string); ok {
|
||||
required = append(required, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create prefixed tool name
|
||||
@@ -253,89 +260,26 @@ func (m *MCPToolManager) loadServerTools(ctx context.Context, serverName string,
|
||||
}
|
||||
m.toolMap[prefixedName] = mapping
|
||||
|
||||
// Create eino tool
|
||||
einoTool := &mcpToolImpl{
|
||||
info: &schema.ToolInfo{
|
||||
// Create fantasy AgentTool
|
||||
fantasyTool := &mcpFantasyTool{
|
||||
toolInfo: fantasy.ToolInfo{
|
||||
Name: prefixedName,
|
||||
Desc: mcpTool.Description,
|
||||
ParamsOneOf: schema.NewParamsOneOfByJSONSchema(inputSchema),
|
||||
Description: mcpTool.Description,
|
||||
Parameters: parameters,
|
||||
Required: required,
|
||||
},
|
||||
mapping: mapping,
|
||||
}
|
||||
|
||||
m.tools = append(m.tools, einoTool)
|
||||
m.tools = append(m.tools, fantasyTool)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Info returns the tool information including name, description, and parameter schema.
|
||||
// This method implements the eino tool.BaseTool interface.
|
||||
// The returned ToolInfo contains the prefixed tool name to ensure uniqueness across servers.
|
||||
func (t *mcpToolImpl) Info(ctx context.Context) (*schema.ToolInfo, error) {
|
||||
return t.info, nil
|
||||
}
|
||||
|
||||
// InvokableRun executes the tool by mapping the prefixed name back to the original tool name and server.
|
||||
// It retrieves a healthy connection from the pool, invokes the tool on the appropriate MCP server,
|
||||
// and returns the result as a JSON string. The method handles connection errors by marking
|
||||
// connections as unhealthy in the pool for automatic recovery on subsequent requests.
|
||||
// Thread-safe for concurrent invocations.
|
||||
func (t *mcpToolImpl) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) {
|
||||
// Handle empty or invalid JSON arguments
|
||||
var arguments any
|
||||
if argumentsInJSON == "" || argumentsInJSON == "{}" {
|
||||
arguments = nil
|
||||
} else {
|
||||
// Validate that argumentsInJSON is valid JSON before using it
|
||||
var temp any
|
||||
if err := json.Unmarshal([]byte(argumentsInJSON), &temp); err != nil {
|
||||
return "", fmt.Errorf("invalid JSON arguments: %w", err)
|
||||
}
|
||||
arguments = json.RawMessage(argumentsInJSON)
|
||||
}
|
||||
|
||||
// Get connection from pool for this server with health check
|
||||
conn, err := t.mapping.manager.connectionPool.GetConnectionWithHealthCheck(ctx, t.mapping.serverName, t.mapping.serverConfig)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get healthy connection from pool: %w", err)
|
||||
}
|
||||
|
||||
result, err := conn.client.CallTool(ctx, mcp.CallToolRequest{
|
||||
Request: mcp.Request{
|
||||
Method: "tools/call",
|
||||
},
|
||||
Params: struct {
|
||||
Name string `json:"name"`
|
||||
Arguments any `json:"arguments,omitempty"`
|
||||
Meta *mcp.Meta `json:"_meta,omitempty"`
|
||||
}{
|
||||
Name: t.mapping.originalName, // Use original name, not prefixed
|
||||
Arguments: arguments,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
// Handle connection error in pool
|
||||
t.mapping.manager.connectionPool.HandleConnectionError(t.mapping.serverName, err)
|
||||
return "", fmt.Errorf("failed to call mcp tool: %w", err)
|
||||
}
|
||||
|
||||
marshaledResult, err := sonic.MarshalString(result)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to marshal mcp tool result: %w", err)
|
||||
}
|
||||
|
||||
// If the MCP server returned an error, we still return the error content as the response
|
||||
// to the LLM so it can see what went wrong. The error will be shown to the user via
|
||||
// the UI callbacks, but the LLM needs to see the actual error details to continue
|
||||
// the conversation appropriately.
|
||||
return marshaledResult, nil
|
||||
}
|
||||
|
||||
// GetTools returns all loaded tools from all configured MCP servers.
|
||||
// GetTools returns all loaded tools as fantasy AgentTools from all configured MCP servers.
|
||||
// Tools are returned with their prefixed names (serverName__toolName) to ensure uniqueness.
|
||||
// The returned slice is a copy and can be safely modified by the caller.
|
||||
func (m *MCPToolManager) GetTools() []tool.BaseTool {
|
||||
func (m *MCPToolManager) GetTools() []fantasy.AgentTool {
|
||||
return m.tools
|
||||
}
|
||||
|
||||
@@ -360,15 +304,11 @@ func (m *MCPToolManager) Close() error {
|
||||
|
||||
// shouldExcludeTool determines if a tool should be excluded based on excludedTools
|
||||
func (m *MCPToolManager) shouldExcludeTool(toolName string, serverConfig config.MCPServerConfig) bool {
|
||||
// If excludedTools is specified, exclude tools in the list
|
||||
if len(serverConfig.ExcludedTools) > 0 {
|
||||
for _, excludedTool := range serverConfig.ExcludedTools {
|
||||
if excludedTool == toolName {
|
||||
if slices.Contains(serverConfig.ExcludedTools, toolName) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -377,60 +317,44 @@ func (m *MCPToolManager) createMCPClient(ctx context.Context, serverName string,
|
||||
|
||||
switch transportType {
|
||||
case "stdio":
|
||||
// STDIO client
|
||||
var env []string
|
||||
var command string
|
||||
var args []string
|
||||
|
||||
// Handle command and environment
|
||||
if len(serverConfig.Command) > 0 {
|
||||
command = serverConfig.Command[0]
|
||||
if len(serverConfig.Command) > 1 {
|
||||
args = serverConfig.Command[1:]
|
||||
} else if len(serverConfig.Args) > 0 {
|
||||
// Legacy fallback: Command only has the command, Args has the arguments
|
||||
// This handles cases where legacy config conversion didn't work properly
|
||||
args = serverConfig.Args
|
||||
}
|
||||
}
|
||||
|
||||
// Convert environment variables
|
||||
if serverConfig.Environment != nil {
|
||||
for k, v := range serverConfig.Environment {
|
||||
env = append(env, fmt.Sprintf("%s=%s", k, v))
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy environment support
|
||||
if serverConfig.Env != nil {
|
||||
for k, v := range serverConfig.Env {
|
||||
env = append(env, fmt.Sprintf("%s=%v", k, v))
|
||||
}
|
||||
}
|
||||
|
||||
// Create stdio transport
|
||||
stdioTransport := transport.NewStdio(command, env, args...)
|
||||
|
||||
stdioClient := client.NewClient(stdioTransport)
|
||||
|
||||
// Start the transport
|
||||
if err := stdioTransport.Start(ctx); err != nil {
|
||||
return nil, fmt.Errorf("failed to start stdio transport: %v", err)
|
||||
}
|
||||
|
||||
// Add a brief delay to allow the process to start and potentially fail
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// TODO: Add process health check here if the mcp-go library exposes process info
|
||||
// For now, we rely on the timeout in initializeClient to catch dead processes
|
||||
|
||||
return stdioClient, nil
|
||||
|
||||
case "sse":
|
||||
// SSE client
|
||||
var options []transport.ClientOption
|
||||
|
||||
// Add headers if specified
|
||||
if len(serverConfig.Headers) > 0 {
|
||||
headers := make(map[string]string)
|
||||
for _, header := range serverConfig.Headers {
|
||||
@@ -451,7 +375,6 @@ func (m *MCPToolManager) createMCPClient(ctx context.Context, serverName string,
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Start the SSE client
|
||||
if err := sseClient.Start(ctx); err != nil {
|
||||
return nil, fmt.Errorf("failed to start SSE client: %v", err)
|
||||
}
|
||||
@@ -459,10 +382,8 @@ func (m *MCPToolManager) createMCPClient(ctx context.Context, serverName string,
|
||||
return sseClient, nil
|
||||
|
||||
case "streamable":
|
||||
// Streamable HTTP client
|
||||
var options []transport.StreamableHTTPCOption
|
||||
|
||||
// Add headers if specified
|
||||
if len(serverConfig.Headers) > 0 {
|
||||
headers := make(map[string]string)
|
||||
for _, header := range serverConfig.Headers {
|
||||
@@ -483,7 +404,6 @@ func (m *MCPToolManager) createMCPClient(ctx context.Context, serverName string,
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Start the streamable HTTP client
|
||||
if err := streamableClient.Start(ctx); err != nil {
|
||||
return nil, fmt.Errorf("failed to start streamable HTTP client: %v", err)
|
||||
}
|
||||
@@ -491,7 +411,6 @@ func (m *MCPToolManager) createMCPClient(ctx context.Context, serverName string,
|
||||
return streamableClient, nil
|
||||
|
||||
case "inprocess":
|
||||
// Builtin server
|
||||
return m.createBuiltinClient(ctx, serverName, serverConfig)
|
||||
|
||||
default:
|
||||
@@ -500,7 +419,6 @@ func (m *MCPToolManager) createMCPClient(ctx context.Context, serverName string,
|
||||
}
|
||||
|
||||
func (m *MCPToolManager) initializeClient(ctx context.Context, client client.MCPClient) error {
|
||||
// Create a timeout context for initialization to prevent deadlocks
|
||||
initCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
@@ -523,13 +441,11 @@ func (m *MCPToolManager) initializeClient(ctx context.Context, client client.MCP
|
||||
func (m *MCPToolManager) createBuiltinClient(ctx context.Context, serverName string, serverConfig config.MCPServerConfig) (client.MCPClient, error) {
|
||||
registry := builtin.NewRegistry()
|
||||
|
||||
// Create the builtin server, passing the model for servers that need it
|
||||
builtinServer, err := registry.CreateServer(serverConfig.Name, serverConfig.Options, m.model)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create builtin server: %v", err)
|
||||
}
|
||||
|
||||
// Create an in-process client that wraps the builtin server
|
||||
inProcessClient, err := client.NewInProcessClient(builtinServer.GetServer())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create in-process client: %v", err)
|
||||
@@ -566,77 +482,65 @@ func (m *MCPToolManager) debugLogConnectionInfo(serverName string, serverConfig
|
||||
// convertExclusiveBoundsToBoolean converts JSON Schema draft-07 style exclusive bounds
|
||||
// (where exclusiveMinimum/exclusiveMaximum are numbers) to draft-04 style
|
||||
// (where they are booleans that modify minimum/maximum).
|
||||
// This enables compatibility with kin-openapi which uses OpenAPI 3.0 (draft-04 based) schemas.
|
||||
func convertExclusiveBoundsToBoolean(schemaJSON []byte) []byte {
|
||||
var data map[string]interface{}
|
||||
var data map[string]any
|
||||
if err := json.Unmarshal(schemaJSON, &data); err != nil {
|
||||
return schemaJSON // Return unchanged on error
|
||||
return schemaJSON
|
||||
}
|
||||
|
||||
convertSchemaRecursive(data)
|
||||
|
||||
result, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return schemaJSON // Return unchanged on error
|
||||
return schemaJSON
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// convertSchemaRecursive recursively processes a schema map and converts
|
||||
// numeric exclusiveMinimum/exclusiveMaximum to boolean format.
|
||||
func convertSchemaRecursive(schema map[string]interface{}) {
|
||||
// Convert exclusiveMinimum if it's a number
|
||||
func convertSchemaRecursive(schema map[string]any) {
|
||||
if exMin, ok := schema["exclusiveMinimum"]; ok {
|
||||
if num, isNum := exMin.(float64); isNum {
|
||||
// JSON Schema draft-07: exclusiveMinimum is the limit value
|
||||
// Convert to draft-04: set minimum = value, exclusiveMinimum = true
|
||||
schema["minimum"] = num
|
||||
schema["exclusiveMinimum"] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Convert exclusiveMaximum if it's a number
|
||||
if exMax, ok := schema["exclusiveMaximum"]; ok {
|
||||
if num, isNum := exMax.(float64); isNum {
|
||||
// JSON Schema draft-07: exclusiveMaximum is the limit value
|
||||
// Convert to draft-04: set maximum = value, exclusiveMaximum = true
|
||||
schema["maximum"] = num
|
||||
schema["exclusiveMaximum"] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Recursively process properties
|
||||
if props, ok := schema["properties"].(map[string]interface{}); ok {
|
||||
if props, ok := schema["properties"].(map[string]any); ok {
|
||||
for _, prop := range props {
|
||||
if propSchema, ok := prop.(map[string]interface{}); ok {
|
||||
if propSchema, ok := prop.(map[string]any); ok {
|
||||
convertSchemaRecursive(propSchema)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Recursively process items (for arrays)
|
||||
if items, ok := schema["items"].(map[string]interface{}); ok {
|
||||
if items, ok := schema["items"].(map[string]any); ok {
|
||||
convertSchemaRecursive(items)
|
||||
}
|
||||
|
||||
// Recursively process additionalProperties
|
||||
if addProps, ok := schema["additionalProperties"].(map[string]interface{}); ok {
|
||||
if addProps, ok := schema["additionalProperties"].(map[string]any); ok {
|
||||
convertSchemaRecursive(addProps)
|
||||
}
|
||||
|
||||
// Recursively process allOf, anyOf, oneOf
|
||||
for _, key := range []string{"allOf", "anyOf", "oneOf"} {
|
||||
if arr, ok := schema[key].([]interface{}); ok {
|
||||
if arr, ok := schema[key].([]any); ok {
|
||||
for _, item := range arr {
|
||||
if itemSchema, ok := item.(map[string]interface{}); ok {
|
||||
if itemSchema, ok := item.(map[string]any); ok {
|
||||
convertSchemaRecursive(itemSchema)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Recursively process not
|
||||
if not, ok := schema["not"].(map[string]interface{}); ok {
|
||||
if not, ok := schema["not"].(map[string]any); ok {
|
||||
convertSchemaRecursive(not)
|
||||
}
|
||||
}
|
||||
|
||||
+61
-65
@@ -6,8 +6,6 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cloudwego/eino/schema"
|
||||
"github.com/eino-contrib/jsonschema"
|
||||
"github.com/mark3labs/mcphost/internal/config"
|
||||
)
|
||||
|
||||
@@ -84,7 +82,6 @@ func TestMCPToolManager_ToolWithoutProperties(t *testing.T) {
|
||||
manager := NewMCPToolManager()
|
||||
|
||||
// Create a config with a builtin todo server (which has tools with properties)
|
||||
// and test the schema conversion logic
|
||||
cfg := &config.Config{
|
||||
MCPServers: map[string]config.MCPServerConfig{
|
||||
"todo-server": {
|
||||
@@ -111,69 +108,68 @@ func TestMCPToolManager_ToolWithoutProperties(t *testing.T) {
|
||||
|
||||
// Test that we can get tool info for each tool
|
||||
for _, tool := range tools {
|
||||
info, err := tool.Info(ctx)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to get tool info: %v", err)
|
||||
continue
|
||||
info := tool.Info()
|
||||
|
||||
// Check that the tool has a valid name
|
||||
if info.Name == "" {
|
||||
t.Error("Tool has empty name")
|
||||
}
|
||||
|
||||
// Check that the tool has a valid schema
|
||||
if info.ParamsOneOf == nil {
|
||||
t.Errorf("Tool %s has nil ParamsOneOf", info.Name)
|
||||
}
|
||||
|
||||
t.Logf("Tool: %s, Description: %s", info.Name, info.Desc)
|
||||
t.Logf("Tool: %s, Description: %s", info.Name, info.Description)
|
||||
}
|
||||
}
|
||||
|
||||
// TestIssue89_ObjectSchemaMissingProperties tests the fix for issue #89
|
||||
// This is a regression test for the "object schema missing properties" error
|
||||
// that occurs when tools have no input parameters and use OpenAI function calling
|
||||
// This verifies that object schemas with nil properties get an empty properties map
|
||||
func TestIssue89_ObjectSchemaMissingProperties(t *testing.T) {
|
||||
// Create a schema that would cause the OpenAI validation error
|
||||
// This simulates what might happen with tools that have no input properties
|
||||
brokenSchema := &jsonschema.Schema{
|
||||
Type: "object",
|
||||
// Properties is nil - this causes "object schema missing properties" error in OpenAI
|
||||
// Create a schema that would cause issues with tools that have no input properties
|
||||
brokenSchema := map[string]any{
|
||||
"type": "object",
|
||||
// Properties is nil - this used to cause "object schema missing properties" error
|
||||
}
|
||||
|
||||
// Verify the problematic state
|
||||
if brokenSchema.Type == "object" && brokenSchema.Properties == nil {
|
||||
t.Log("Found object schema with nil properties - this causes OpenAI validation error")
|
||||
if brokenSchema["type"] == "object" && brokenSchema["properties"] == nil {
|
||||
t.Log("Found object schema with nil properties - this previously caused validation errors")
|
||||
}
|
||||
|
||||
// Apply the fix from issue #89
|
||||
if brokenSchema.Type == "object" && brokenSchema.Properties == nil {
|
||||
brokenSchema.Properties = jsonschema.NewProperties()
|
||||
// Apply the fix - add empty properties
|
||||
if brokenSchema["type"] == "object" && brokenSchema["properties"] == nil {
|
||||
brokenSchema["properties"] = map[string]any{}
|
||||
}
|
||||
|
||||
// Verify the fix worked
|
||||
if brokenSchema.Type == "object" && brokenSchema.Properties == nil {
|
||||
if brokenSchema["properties"] == nil {
|
||||
t.Error("Fix failed: object schema still has nil properties")
|
||||
}
|
||||
|
||||
// Test that we can create a ParamsOneOf from the fixed schema
|
||||
// This is what would fail before the fix
|
||||
paramsOneOf := schema.NewParamsOneOfByJSONSchema(brokenSchema)
|
||||
if paramsOneOf == nil {
|
||||
t.Error("Failed to create ParamsOneOf from fixed schema - OpenAI function calling would fail")
|
||||
// Verify it marshals cleanly
|
||||
data, err := json.Marshal(brokenSchema)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to marshal fixed schema: %v", err)
|
||||
}
|
||||
|
||||
var result map[string]any
|
||||
if err := json.Unmarshal(data, &result); err != nil {
|
||||
t.Errorf("Failed to unmarshal fixed schema: %v", err)
|
||||
}
|
||||
|
||||
if result["type"] != "object" {
|
||||
t.Error("Schema type should be 'object'")
|
||||
}
|
||||
}
|
||||
|
||||
// TestConvertExclusiveBoundsToBoolean tests the JSON Schema draft-07 to draft-04 conversion
|
||||
// for exclusiveMinimum and exclusiveMaximum fields.
|
||||
// Draft-07: exclusiveMinimum/exclusiveMaximum are numeric values (the actual bounds)
|
||||
// Draft-04: exclusiveMinimum/exclusiveMaximum are booleans that modify minimum/maximum
|
||||
func TestConvertExclusiveBoundsToBoolean(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected map[string]interface{}
|
||||
expected map[string]any
|
||||
}{
|
||||
{
|
||||
name: "exclusiveMinimum as number",
|
||||
input: `{"type": "number", "exclusiveMinimum": 0}`,
|
||||
expected: map[string]interface{}{
|
||||
expected: map[string]any{
|
||||
"type": "number",
|
||||
"minimum": float64(0),
|
||||
"exclusiveMinimum": true,
|
||||
@@ -182,7 +178,7 @@ func TestConvertExclusiveBoundsToBoolean(t *testing.T) {
|
||||
{
|
||||
name: "exclusiveMaximum as number",
|
||||
input: `{"type": "number", "exclusiveMaximum": 100}`,
|
||||
expected: map[string]interface{}{
|
||||
expected: map[string]any{
|
||||
"type": "number",
|
||||
"maximum": float64(100),
|
||||
"exclusiveMaximum": true,
|
||||
@@ -191,7 +187,7 @@ func TestConvertExclusiveBoundsToBoolean(t *testing.T) {
|
||||
{
|
||||
name: "both exclusive bounds as numbers",
|
||||
input: `{"type": "integer", "exclusiveMinimum": 1, "exclusiveMaximum": 10}`,
|
||||
expected: map[string]interface{}{
|
||||
expected: map[string]any{
|
||||
"type": "integer",
|
||||
"minimum": float64(1),
|
||||
"exclusiveMinimum": true,
|
||||
@@ -202,7 +198,7 @@ func TestConvertExclusiveBoundsToBoolean(t *testing.T) {
|
||||
{
|
||||
name: "already boolean exclusiveMinimum (draft-04 style)",
|
||||
input: `{"type": "number", "minimum": 0, "exclusiveMinimum": true}`,
|
||||
expected: map[string]interface{}{
|
||||
expected: map[string]any{
|
||||
"type": "number",
|
||||
"minimum": float64(0),
|
||||
"exclusiveMinimum": true,
|
||||
@@ -211,7 +207,7 @@ func TestConvertExclusiveBoundsToBoolean(t *testing.T) {
|
||||
{
|
||||
name: "no exclusive bounds",
|
||||
input: `{"type": "string", "minLength": 1}`,
|
||||
expected: map[string]interface{}{
|
||||
expected: map[string]any{
|
||||
"type": "string",
|
||||
"minLength": float64(1),
|
||||
},
|
||||
@@ -219,10 +215,10 @@ func TestConvertExclusiveBoundsToBoolean(t *testing.T) {
|
||||
{
|
||||
name: "nested properties with exclusive bounds",
|
||||
input: `{"type": "object", "properties": {"age": {"type": "integer", "exclusiveMinimum": 0}}}`,
|
||||
expected: map[string]interface{}{
|
||||
expected: map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"age": map[string]interface{}{
|
||||
"properties": map[string]any{
|
||||
"age": map[string]any{
|
||||
"type": "integer",
|
||||
"minimum": float64(0),
|
||||
"exclusiveMinimum": true,
|
||||
@@ -233,9 +229,9 @@ func TestConvertExclusiveBoundsToBoolean(t *testing.T) {
|
||||
{
|
||||
name: "array items with exclusive bounds",
|
||||
input: `{"type": "array", "items": {"type": "number", "exclusiveMaximum": 100}}`,
|
||||
expected: map[string]interface{}{
|
||||
expected: map[string]any{
|
||||
"type": "array",
|
||||
"items": map[string]interface{}{
|
||||
"items": map[string]any{
|
||||
"type": "number",
|
||||
"maximum": float64(100),
|
||||
"exclusiveMaximum": true,
|
||||
@@ -245,9 +241,9 @@ func TestConvertExclusiveBoundsToBoolean(t *testing.T) {
|
||||
{
|
||||
name: "allOf with exclusive bounds",
|
||||
input: `{"allOf": [{"type": "number", "exclusiveMinimum": 0}]}`,
|
||||
expected: map[string]interface{}{
|
||||
"allOf": []interface{}{
|
||||
map[string]interface{}{
|
||||
expected: map[string]any{
|
||||
"allOf": []any{
|
||||
map[string]any{
|
||||
"type": "number",
|
||||
"minimum": float64(0),
|
||||
"exclusiveMinimum": true,
|
||||
@@ -258,9 +254,9 @@ func TestConvertExclusiveBoundsToBoolean(t *testing.T) {
|
||||
{
|
||||
name: "additionalProperties with exclusive bounds",
|
||||
input: `{"type": "object", "additionalProperties": {"type": "integer", "exclusiveMinimum": 0, "exclusiveMaximum": 255}}`,
|
||||
expected: map[string]interface{}{
|
||||
expected: map[string]any{
|
||||
"type": "object",
|
||||
"additionalProperties": map[string]interface{}{
|
||||
"additionalProperties": map[string]any{
|
||||
"type": "integer",
|
||||
"minimum": float64(0),
|
||||
"exclusiveMinimum": true,
|
||||
@@ -272,15 +268,15 @@ func TestConvertExclusiveBoundsToBoolean(t *testing.T) {
|
||||
{
|
||||
name: "Chrome DevTools MCP style schema (real-world example)",
|
||||
input: `{"type": "object", "properties": {"timeout": {"type": "integer", "exclusiveMinimum": 0}, "quality": {"type": "number", "minimum": 0, "maximum": 100}}}`,
|
||||
expected: map[string]interface{}{
|
||||
expected: map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"timeout": map[string]interface{}{
|
||||
"properties": map[string]any{
|
||||
"timeout": map[string]any{
|
||||
"type": "integer",
|
||||
"minimum": float64(0),
|
||||
"exclusiveMinimum": true,
|
||||
},
|
||||
"quality": map[string]interface{}{
|
||||
"quality": map[string]any{
|
||||
"type": "number",
|
||||
"minimum": float64(0),
|
||||
"maximum": float64(100),
|
||||
@@ -294,7 +290,7 @@ func TestConvertExclusiveBoundsToBoolean(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := convertExclusiveBoundsToBoolean([]byte(tt.input))
|
||||
|
||||
var got map[string]interface{}
|
||||
var got map[string]any
|
||||
if err := json.Unmarshal(result, &got); err != nil {
|
||||
t.Fatalf("Failed to unmarshal result: %v", err)
|
||||
}
|
||||
@@ -317,7 +313,7 @@ func TestConvertExclusiveBoundsToBoolean_InvalidJSON(t *testing.T) {
|
||||
}
|
||||
|
||||
// deepEqual compares two maps recursively
|
||||
func deepEqual(a, b map[string]interface{}) bool {
|
||||
func deepEqual(a, b map[string]any) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
@@ -327,13 +323,13 @@ func deepEqual(a, b map[string]interface{}) bool {
|
||||
return false
|
||||
}
|
||||
switch av := v.(type) {
|
||||
case map[string]interface{}:
|
||||
bvm, ok := bv.(map[string]interface{})
|
||||
case map[string]any:
|
||||
bvm, ok := bv.(map[string]any)
|
||||
if !ok || !deepEqual(av, bvm) {
|
||||
return false
|
||||
}
|
||||
case []interface{}:
|
||||
bva, ok := bv.([]interface{})
|
||||
case []any:
|
||||
bva, ok := bv.([]any)
|
||||
if !ok || !sliceEqual(av, bva) {
|
||||
return false
|
||||
}
|
||||
@@ -347,19 +343,19 @@ func deepEqual(a, b map[string]interface{}) bool {
|
||||
}
|
||||
|
||||
// sliceEqual compares two slices recursively
|
||||
func sliceEqual(a, b []interface{}) bool {
|
||||
func sliceEqual(a, b []any) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i := range a {
|
||||
switch av := a[i].(type) {
|
||||
case map[string]interface{}:
|
||||
bvm, ok := b[i].(map[string]interface{})
|
||||
case map[string]any:
|
||||
bvm, ok := b[i].(map[string]any)
|
||||
if !ok || !deepEqual(av, bvm) {
|
||||
return false
|
||||
}
|
||||
case []interface{}:
|
||||
bva, ok := b[i].([]interface{})
|
||||
case []any:
|
||||
bva, ok := b[i].([]any)
|
||||
if !ok || !sliceEqual(av, bva) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cloudwego/eino/callbacks"
|
||||
"github.com/cloudwego/eino/components/tool"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
utilCallbacks "github.com/cloudwego/eino/utils/callbacks"
|
||||
)
|
||||
|
||||
// CreateCallbackHandler creates and returns a callbacks.Handler that manages
|
||||
// tool execution callbacks for the CLI. The handler displays tool calls,
|
||||
// handles errors, and manages streaming output for interactive tool operations.
|
||||
// It integrates with the eino callback system to provide real-time UI feedback
|
||||
// during tool execution.
|
||||
func (c *CLI) CreateCallbackHandler() callbacks.Handler {
|
||||
toolHandler := &utilCallbacks.ToolCallbackHandler{
|
||||
OnStart: func(ctx context.Context, runInfo *callbacks.RunInfo, input *tool.CallbackInput) context.Context {
|
||||
// Display the tool call message with the tool name and arguments
|
||||
c.DisplayToolCallMessage(runInfo.Name, input.ArgumentsInJSON)
|
||||
return ctx
|
||||
},
|
||||
OnEnd: func(ctx context.Context, runInfo *callbacks.RunInfo, output *tool.CallbackOutput) context.Context {
|
||||
// Tool execution completed - we could show results here if needed
|
||||
return ctx
|
||||
},
|
||||
OnEndWithStreamOutput: func(ctx context.Context, runInfo *callbacks.RunInfo, output *schema.StreamReader[*tool.CallbackOutput]) context.Context {
|
||||
return ctx
|
||||
},
|
||||
OnError: func(ctx context.Context, runInfo *callbacks.RunInfo, err error) context.Context {
|
||||
// Display error message
|
||||
c.DisplayError(err)
|
||||
return ctx
|
||||
},
|
||||
}
|
||||
|
||||
return utilCallbacks.NewHandlerHelper().
|
||||
Tool(toolHandler).
|
||||
Handler()
|
||||
}
|
||||
+9
-20
@@ -8,8 +8,8 @@ import (
|
||||
"time"
|
||||
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"charm.land/fantasy"
|
||||
"charm.land/lipgloss/v2"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
@@ -498,38 +498,27 @@ func (c *CLI) UpdateUsage(inputText, outputText string) {
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateUsageFromResponse records token usage using metadata from the AI provider's
|
||||
// UpdateUsageFromResponse records token usage using metadata from the fantasy
|
||||
// response when available. Falls back to text-based estimation if the metadata is
|
||||
// missing or appears unreliable. This provides more accurate usage tracking when
|
||||
// providers supply token count information.
|
||||
func (c *CLI) UpdateUsageFromResponse(response *schema.Message, inputText string) {
|
||||
func (c *CLI) UpdateUsageFromResponse(response *fantasy.Response, inputText string) {
|
||||
if c.usageTracker == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Try to extract token usage from response metadata
|
||||
if response.ResponseMeta != nil && response.ResponseMeta.Usage != nil {
|
||||
usage := response.ResponseMeta.Usage
|
||||
|
||||
// Use actual token counts from the response
|
||||
inputTokens := int(usage.PromptTokens)
|
||||
outputTokens := int(usage.CompletionTokens)
|
||||
usage := response.Usage
|
||||
inputTokens := int(usage.InputTokens)
|
||||
outputTokens := int(usage.OutputTokens)
|
||||
|
||||
// Validate that the metadata seems reasonable
|
||||
// If token counts are 0 or seem unrealistic, fall back to estimation
|
||||
if inputTokens > 0 && outputTokens > 0 {
|
||||
// Handle cache tokens if available (some providers support this)
|
||||
cacheReadTokens := 0
|
||||
cacheWriteTokens := 0
|
||||
|
||||
cacheReadTokens := int(usage.CacheReadTokens)
|
||||
cacheWriteTokens := int(usage.CacheCreationTokens)
|
||||
c.usageTracker.UpdateUsage(inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens)
|
||||
} else {
|
||||
// Metadata exists but seems incomplete/unreliable, use estimation
|
||||
c.usageTracker.EstimateAndUpdateUsage(inputText, response.Content)
|
||||
}
|
||||
} else {
|
||||
// Fallback to estimation if no metadata is available
|
||||
c.usageTracker.EstimateAndUpdateUsage(inputText, response.Content)
|
||||
c.usageTracker.EstimateAndUpdateUsage(inputText, response.Content.Text())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package ui
|
||||
|
||||
import "slices"
|
||||
|
||||
// SlashCommand represents a user-invokable slash command with its metadata.
|
||||
// Commands can have multiple aliases and are organized by category for better
|
||||
// discoverability and help display.
|
||||
@@ -68,12 +70,10 @@ func GetCommandByName(name string) *SlashCommand {
|
||||
if cmd.Name == name {
|
||||
return cmd
|
||||
}
|
||||
for _, alias := range cmd.Aliases {
|
||||
if alias == name {
|
||||
if slices.Contains(cmd.Aliases, name) {
|
||||
return cmd
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -296,10 +296,11 @@ func (r *CompactRenderer) formatCompactContent(content string) string {
|
||||
content = strings.TrimSpace(content)
|
||||
|
||||
// Truncate if too long (unless in debug mode)
|
||||
maxLen := r.width - 28 // Reserve space for symbol and label more conservatively
|
||||
if maxLen < 40 {
|
||||
maxLen = 40 // Minimum width for readability
|
||||
}
|
||||
maxLen := max(
|
||||
// Reserve space for symbol and label more conservatively
|
||||
r.width-28,
|
||||
// Minimum width for readability
|
||||
40)
|
||||
if !r.debug && len(content) > maxLen {
|
||||
content = content[:maxLen-3] + "..."
|
||||
}
|
||||
@@ -315,10 +316,9 @@ func (r *CompactRenderer) formatUserAssistantContent(content string) string {
|
||||
|
||||
// Calculate available width more conservatively
|
||||
// Account for: symbol (1) + spaces (2) + label (up to 20 chars) + space (1) + margin (4)
|
||||
availableWidth := r.width - 28
|
||||
if availableWidth < 40 {
|
||||
availableWidth = 40 // Minimum width for readability
|
||||
}
|
||||
availableWidth := max(r.width-28,
|
||||
// Minimum width for readability
|
||||
40)
|
||||
|
||||
// Use glamour to render markdown content with proper width
|
||||
rendered := toMarkdown(content, availableWidth)
|
||||
@@ -407,10 +407,9 @@ func (r *CompactRenderer) formatToolResult(result string) string {
|
||||
}
|
||||
|
||||
// Calculate available width more conservatively
|
||||
availableWidth := r.width - 28
|
||||
if availableWidth < 40 {
|
||||
availableWidth = 40 // Minimum width for readability
|
||||
}
|
||||
availableWidth := max(r.width-28,
|
||||
// Minimum width for readability
|
||||
40)
|
||||
|
||||
// First wrap the text to prevent long lines (tool results are usually plain text, not markdown)
|
||||
wrappedResult := r.wrapText(result, availableWidth)
|
||||
|
||||
@@ -36,12 +36,12 @@ func (l *CLIDebugLogger) LogDebug(message string) {
|
||||
// Check if this is a multi-line debug output (like connection info)
|
||||
if strings.Contains(message, "[DEBUG]") || strings.Contains(message, "[POOL]") {
|
||||
// Extract the tag and content
|
||||
if strings.HasPrefix(message, "[DEBUG]") {
|
||||
content := strings.TrimPrefix(message, "[DEBUG]")
|
||||
if after, ok := strings.CutPrefix(message, "[DEBUG]"); ok {
|
||||
content := after
|
||||
content = strings.TrimSpace(content)
|
||||
formattedMessage = fmt.Sprintf("🔍 DEBUG: %s", content)
|
||||
} else if strings.HasPrefix(message, "[POOL]") {
|
||||
content := strings.TrimPrefix(message, "[POOL]")
|
||||
} else if after, ok := strings.CutPrefix(message, "[POOL]"); ok {
|
||||
content := after
|
||||
content = strings.TrimSpace(content)
|
||||
|
||||
// Add appropriate emoji based on the message content
|
||||
|
||||
@@ -80,10 +80,7 @@ func (m ProgressModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m, nil
|
||||
|
||||
case tea.WindowSizeMsg:
|
||||
newWidth := msg.Width - padding*2 - 4
|
||||
if newWidth > maxWidth {
|
||||
newWidth = maxWidth
|
||||
}
|
||||
newWidth := min(msg.Width-padding*2-4, maxWidth)
|
||||
m.progress.SetWidth(newWidth)
|
||||
return m, nil
|
||||
|
||||
@@ -167,14 +164,12 @@ func NewProgressReader(reader io.Reader) *ProgressReader {
|
||||
}
|
||||
|
||||
// Start the TUI in a goroutine
|
||||
pr.wg.Add(1)
|
||||
go func() {
|
||||
defer pr.wg.Done()
|
||||
pr.wg.Go(func() {
|
||||
if _, err := program.Run(); err != nil {
|
||||
// Handle error silently for now
|
||||
}
|
||||
close(pr.done)
|
||||
}()
|
||||
})
|
||||
|
||||
return pr
|
||||
}
|
||||
|
||||
+69
-121
@@ -1,153 +1,101 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"image/color"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"charm.land/bubbles/v2/spinner"
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"charm.land/lipgloss/v2"
|
||||
)
|
||||
|
||||
// Spinner provides an animated loading indicator that displays while long-running
|
||||
// operations are in progress. It wraps the bubbles spinner component and manages
|
||||
// its lifecycle through a tea.Program for proper terminal handling.
|
||||
// spinnerFrames defines available spinner animation styles.
|
||||
var (
|
||||
pointsFrames = []string{"∙∙∙", "●∙∙", "∙●∙", "∙∙●"}
|
||||
pointsFPS = time.Second / 7
|
||||
|
||||
dotFrames = []string{"⣾ ", "⣽ ", "⣻ ", "⢿ ", "⡿ ", "⣟ ", "⣯ ", "⣷ "}
|
||||
dotFPS = time.Second / 10
|
||||
)
|
||||
|
||||
// Spinner provides an animated loading indicator that displays while
|
||||
// long-running operations are in progress. It writes directly to stderr
|
||||
// using a goroutine-based animation loop, avoiding Bubble Tea's terminal
|
||||
// capability queries that can leak escape sequences (mode 2026 DECRPM).
|
||||
type Spinner struct {
|
||||
model spinner.Model
|
||||
done chan struct{}
|
||||
prog *tea.Program
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
// spinnerModel is the tea.Model for the spinner
|
||||
type spinnerModel struct {
|
||||
spinner spinner.Model
|
||||
message string
|
||||
quitting bool
|
||||
frames []string
|
||||
fps time.Duration
|
||||
color color.Color
|
||||
done chan struct{}
|
||||
once sync.Once
|
||||
}
|
||||
|
||||
func (m spinnerModel) Init() tea.Cmd {
|
||||
return m.spinner.Tick
|
||||
}
|
||||
|
||||
func (m spinnerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyPressMsg:
|
||||
m.quitting = true
|
||||
return m, tea.Quit
|
||||
case spinner.TickMsg:
|
||||
var cmd tea.Cmd
|
||||
m.spinner, cmd = m.spinner.Update(msg)
|
||||
return m, cmd
|
||||
case quitMsg:
|
||||
m.quitting = true
|
||||
return m, tea.Quit
|
||||
default:
|
||||
return m, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (m spinnerModel) View() tea.View {
|
||||
if m.quitting {
|
||||
return tea.NewView("")
|
||||
}
|
||||
|
||||
// Enhanced spinner display with better styling
|
||||
baseStyle := lipgloss.NewStyle()
|
||||
theme := GetTheme()
|
||||
|
||||
spinnerStyle := baseStyle.
|
||||
Foreground(theme.Primary).
|
||||
Bold(true)
|
||||
|
||||
messageStyle := baseStyle.
|
||||
Foreground(theme.Text).
|
||||
Italic(true)
|
||||
|
||||
return tea.NewView(fmt.Sprintf(" %s %s",
|
||||
spinnerStyle.Render(m.spinner.View()),
|
||||
messageStyle.Render(m.message)))
|
||||
}
|
||||
|
||||
// quitMsg is sent when we want to quit the spinner
|
||||
type quitMsg struct{}
|
||||
|
||||
// NewSpinner creates a new animated spinner with the specified message. The spinner
|
||||
// uses the theme's primary color and a modern animation style. It runs in a separate
|
||||
// tea.Program to avoid interfering with other terminal operations.
|
||||
// NewSpinner creates a new animated spinner with the specified message.
|
||||
// The spinner uses the theme's primary color and a points animation style.
|
||||
func NewSpinner(message string) *Spinner {
|
||||
s := spinner.New()
|
||||
s.Spinner = spinner.Points // More modern spinner style
|
||||
theme := GetTheme()
|
||||
s.Style = s.Style.Foreground(theme.Primary)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
model := spinnerModel{
|
||||
spinner: s,
|
||||
message: message,
|
||||
}
|
||||
|
||||
prog := tea.NewProgram(model, tea.WithOutput(os.Stderr), tea.WithoutCatchPanics())
|
||||
|
||||
return &Spinner{
|
||||
model: s,
|
||||
message: message,
|
||||
frames: pointsFrames,
|
||||
fps: pointsFPS,
|
||||
color: GetTheme().Primary,
|
||||
done: make(chan struct{}),
|
||||
prog: prog,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}
|
||||
}
|
||||
|
||||
// NewThemedSpinner creates a new animated spinner with custom color styling.
|
||||
// This allows for different spinner colors based on the operation type or status.
|
||||
// The spinner runs independently in its own tea.Program.
|
||||
func NewThemedSpinner(message string, color color.Color) *Spinner {
|
||||
s := spinner.New()
|
||||
s.Spinner = spinner.Dot
|
||||
s.Style = s.Style.Foreground(color)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
model := spinnerModel{
|
||||
spinner: s,
|
||||
message: message,
|
||||
}
|
||||
|
||||
prog := tea.NewProgram(model, tea.WithOutput(os.Stderr), tea.WithoutCatchPanics())
|
||||
|
||||
func NewThemedSpinner(message string, c color.Color) *Spinner {
|
||||
return &Spinner{
|
||||
model: s,
|
||||
message: message,
|
||||
frames: dotFrames,
|
||||
fps: dotFPS,
|
||||
color: c,
|
||||
done: make(chan struct{}),
|
||||
prog: prog,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}
|
||||
}
|
||||
|
||||
// Start begins the spinner animation in a separate goroutine. The spinner will
|
||||
// continue animating until Stop is called. The animation runs in a separate
|
||||
// tea.Program to maintain smooth animation independent of other operations.
|
||||
// Start begins the spinner animation in a separate goroutine. The spinner
|
||||
// will continue animating until Stop is called.
|
||||
func (s *Spinner) Start() {
|
||||
go func() {
|
||||
defer close(s.done)
|
||||
go func() {
|
||||
<-s.ctx.Done()
|
||||
s.prog.Send(quitMsg{})
|
||||
}()
|
||||
_, err := s.prog.Run()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error running spinner: %v\n", err)
|
||||
}
|
||||
}()
|
||||
go s.run()
|
||||
}
|
||||
|
||||
// Stop halts the spinner animation and cleans up resources. This method blocks
|
||||
// until the spinner has fully stopped and the terminal state is restored.
|
||||
// Stop halts the spinner animation and cleans up. This method blocks until
|
||||
// the animation goroutine has exited and the line is cleared.
|
||||
func (s *Spinner) Stop() {
|
||||
s.cancel()
|
||||
<-s.done
|
||||
s.once.Do(func() { close(s.done) })
|
||||
}
|
||||
|
||||
// run is the animation loop that renders spinner frames to stderr.
|
||||
func (s *Spinner) run() {
|
||||
theme := GetTheme()
|
||||
|
||||
spinnerStyle := lipgloss.NewStyle().
|
||||
Foreground(s.color).
|
||||
Bold(true)
|
||||
|
||||
messageStyle := lipgloss.NewStyle().
|
||||
Foreground(theme.Text).
|
||||
Italic(true)
|
||||
|
||||
ticker := time.NewTicker(s.fps)
|
||||
defer ticker.Stop()
|
||||
|
||||
var frame int
|
||||
for {
|
||||
select {
|
||||
case <-s.done:
|
||||
// Clear the spinner line and return.
|
||||
fmt.Fprint(os.Stderr, "\r\033[K")
|
||||
return
|
||||
case <-ticker.C:
|
||||
f := s.frames[frame%len(s.frames)]
|
||||
fmt.Fprintf(os.Stderr, "\r %s %s",
|
||||
spinnerStyle.Render(f),
|
||||
messageStyle.Render(s.message))
|
||||
frame++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+81
-75
@@ -11,9 +11,15 @@ import (
|
||||
const defaultMargin = 1
|
||||
|
||||
// Helper functions for style pointers
|
||||
func boolPtr(b bool) *bool { return &b }
|
||||
func stringPtr(s string) *string { return &s }
|
||||
func uintPtr(u uint) *uint { return &u }
|
||||
//
|
||||
//go:fix inline
|
||||
func boolPtr(b bool) *bool { return new(b) }
|
||||
|
||||
//go:fix inline
|
||||
func stringPtr(s string) *string { return new(s) }
|
||||
|
||||
//go:fix inline
|
||||
func uintPtr(u uint) *uint { return new(u) }
|
||||
|
||||
// BaseStyle returns a new, empty lipgloss style that can be customized with
|
||||
// additional styling methods. This serves as the foundation for building more
|
||||
@@ -106,101 +112,101 @@ func generateMarkdownStyleConfig() ansi.StyleConfig {
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
BlockPrefix: "",
|
||||
BlockSuffix: "",
|
||||
Color: stringPtr(textColor),
|
||||
Color: new(textColor),
|
||||
},
|
||||
Margin: uintPtr(0), // Remove margin to prevent spacing
|
||||
},
|
||||
BlockQuote: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
Color: stringPtr(mutedColor),
|
||||
Italic: boolPtr(true),
|
||||
Color: new(mutedColor),
|
||||
Italic: new(true),
|
||||
Prefix: "┃ ",
|
||||
},
|
||||
Indent: uintPtr(1),
|
||||
IndentToken: stringPtr(lipgloss.NewStyle().Background(lipgloss.Color(bgColor)).Render(" ")),
|
||||
IndentToken: new(lipgloss.NewStyle().Background(lipgloss.Color(bgColor)).Render(" ")),
|
||||
},
|
||||
List: ansi.StyleList{
|
||||
LevelIndent: 0, // Remove list indentation
|
||||
StyleBlock: ansi.StyleBlock{
|
||||
IndentToken: stringPtr(lipgloss.NewStyle().Background(lipgloss.Color(bgColor)).Render(" ")),
|
||||
IndentToken: new(lipgloss.NewStyle().Background(lipgloss.Color(bgColor)).Render(" ")),
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
Color: stringPtr(textColor),
|
||||
Color: new(textColor),
|
||||
},
|
||||
},
|
||||
},
|
||||
Heading: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
BlockSuffix: "\n",
|
||||
Color: stringPtr(headingColor),
|
||||
Bold: boolPtr(true),
|
||||
Color: new(headingColor),
|
||||
Bold: new(true),
|
||||
},
|
||||
},
|
||||
H1: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
Prefix: "# ",
|
||||
Color: stringPtr(headingColor),
|
||||
Bold: boolPtr(true),
|
||||
Color: new(headingColor),
|
||||
Bold: new(true),
|
||||
},
|
||||
},
|
||||
H2: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
Prefix: "## ",
|
||||
Color: stringPtr(headingColor),
|
||||
Bold: boolPtr(true),
|
||||
Color: new(headingColor),
|
||||
Bold: new(true),
|
||||
},
|
||||
},
|
||||
H3: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
Prefix: "### ",
|
||||
Color: stringPtr(headingColor),
|
||||
Bold: boolPtr(true),
|
||||
Color: new(headingColor),
|
||||
Bold: new(true),
|
||||
},
|
||||
},
|
||||
H4: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
Prefix: "#### ",
|
||||
Color: stringPtr(headingColor),
|
||||
Bold: boolPtr(true),
|
||||
Color: new(headingColor),
|
||||
Bold: new(true),
|
||||
},
|
||||
},
|
||||
H5: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
Prefix: "##### ",
|
||||
Color: stringPtr(headingColor),
|
||||
Bold: boolPtr(true),
|
||||
Color: new(headingColor),
|
||||
Bold: new(true),
|
||||
},
|
||||
},
|
||||
H6: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
Prefix: "###### ",
|
||||
Color: stringPtr(headingColor),
|
||||
Bold: boolPtr(true),
|
||||
Color: new(headingColor),
|
||||
Bold: new(true),
|
||||
},
|
||||
},
|
||||
Strikethrough: ansi.StylePrimitive{
|
||||
CrossedOut: boolPtr(true),
|
||||
Color: stringPtr(mutedColor),
|
||||
CrossedOut: new(true),
|
||||
Color: new(mutedColor),
|
||||
},
|
||||
Emph: ansi.StylePrimitive{
|
||||
Color: stringPtr(emphColor),
|
||||
Color: new(emphColor),
|
||||
|
||||
Italic: boolPtr(true),
|
||||
Italic: new(true),
|
||||
},
|
||||
Strong: ansi.StylePrimitive{
|
||||
Bold: boolPtr(true),
|
||||
Color: stringPtr(strongColor),
|
||||
Bold: new(true),
|
||||
Color: new(strongColor),
|
||||
},
|
||||
HorizontalRule: ansi.StylePrimitive{
|
||||
Color: stringPtr(mutedColor),
|
||||
Color: new(mutedColor),
|
||||
Format: "\n─────────────────────────────────────────\n",
|
||||
},
|
||||
Item: ansi.StylePrimitive{
|
||||
BlockPrefix: "• ",
|
||||
Color: stringPtr(textColor),
|
||||
Color: new(textColor),
|
||||
},
|
||||
Enumeration: ansi.StylePrimitive{
|
||||
BlockPrefix: ". ",
|
||||
Color: stringPtr(textColor),
|
||||
Color: new(textColor),
|
||||
},
|
||||
Task: ansi.StyleTask{
|
||||
StylePrimitive: ansi.StylePrimitive{},
|
||||
@@ -208,29 +214,29 @@ func generateMarkdownStyleConfig() ansi.StyleConfig {
|
||||
Unticked: "[ ] ",
|
||||
},
|
||||
Link: ansi.StylePrimitive{
|
||||
Color: stringPtr(linkColor),
|
||||
Color: new(linkColor),
|
||||
|
||||
Underline: boolPtr(true),
|
||||
Underline: new(true),
|
||||
},
|
||||
LinkText: ansi.StylePrimitive{
|
||||
Color: stringPtr(linkColor),
|
||||
Color: new(linkColor),
|
||||
|
||||
Bold: boolPtr(true),
|
||||
Bold: new(true),
|
||||
},
|
||||
Image: ansi.StylePrimitive{
|
||||
Color: stringPtr(linkColor),
|
||||
Color: new(linkColor),
|
||||
|
||||
Underline: boolPtr(true),
|
||||
Underline: new(true),
|
||||
Format: "🖼 {{.text}}",
|
||||
},
|
||||
ImageText: ansi.StylePrimitive{
|
||||
Color: stringPtr(linkColor),
|
||||
Color: new(linkColor),
|
||||
|
||||
Format: "{{.text}}",
|
||||
},
|
||||
Code: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
Color: stringPtr(codeColor),
|
||||
Color: new(codeColor),
|
||||
|
||||
Prefix: "",
|
||||
Suffix: "",
|
||||
@@ -240,92 +246,92 @@ func generateMarkdownStyleConfig() ansi.StyleConfig {
|
||||
StyleBlock: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
Prefix: "",
|
||||
Color: stringPtr(codeColor),
|
||||
Color: new(codeColor),
|
||||
},
|
||||
Margin: uintPtr(0), // Remove margin
|
||||
},
|
||||
Chroma: &ansi.Chroma{
|
||||
Text: ansi.StylePrimitive{
|
||||
Color: stringPtr(textColor),
|
||||
Color: new(textColor),
|
||||
},
|
||||
Error: ansi.StylePrimitive{
|
||||
Color: stringPtr(errorColor),
|
||||
Color: new(errorColor),
|
||||
},
|
||||
Comment: ansi.StylePrimitive{
|
||||
Color: stringPtr(commentColor),
|
||||
Color: new(commentColor),
|
||||
},
|
||||
CommentPreproc: ansi.StylePrimitive{
|
||||
Color: stringPtr(keywordColor),
|
||||
Color: new(keywordColor),
|
||||
},
|
||||
Keyword: ansi.StylePrimitive{
|
||||
Color: stringPtr(keywordColor),
|
||||
Color: new(keywordColor),
|
||||
},
|
||||
KeywordReserved: ansi.StylePrimitive{
|
||||
Color: stringPtr(keywordColor),
|
||||
Color: new(keywordColor),
|
||||
},
|
||||
KeywordNamespace: ansi.StylePrimitive{
|
||||
Color: stringPtr(keywordColor),
|
||||
Color: new(keywordColor),
|
||||
},
|
||||
KeywordType: ansi.StylePrimitive{
|
||||
Color: stringPtr(keywordColor),
|
||||
Color: new(keywordColor),
|
||||
},
|
||||
Operator: ansi.StylePrimitive{
|
||||
Color: stringPtr(textColor),
|
||||
Color: new(textColor),
|
||||
},
|
||||
Punctuation: ansi.StylePrimitive{
|
||||
Color: stringPtr(textColor),
|
||||
Color: new(textColor),
|
||||
},
|
||||
Name: ansi.StylePrimitive{
|
||||
Color: stringPtr(textColor),
|
||||
Color: new(textColor),
|
||||
},
|
||||
NameBuiltin: ansi.StylePrimitive{
|
||||
Color: stringPtr(textColor),
|
||||
Color: new(textColor),
|
||||
},
|
||||
NameTag: ansi.StylePrimitive{
|
||||
Color: stringPtr(keywordColor),
|
||||
Color: new(keywordColor),
|
||||
},
|
||||
NameAttribute: ansi.StylePrimitive{
|
||||
Color: stringPtr(textColor),
|
||||
Color: new(textColor),
|
||||
},
|
||||
NameClass: ansi.StylePrimitive{
|
||||
Color: stringPtr(keywordColor),
|
||||
Color: new(keywordColor),
|
||||
},
|
||||
NameConstant: ansi.StylePrimitive{
|
||||
Color: stringPtr(textColor),
|
||||
Color: new(textColor),
|
||||
},
|
||||
NameDecorator: ansi.StylePrimitive{
|
||||
Color: stringPtr(textColor),
|
||||
Color: new(textColor),
|
||||
},
|
||||
NameFunction: ansi.StylePrimitive{
|
||||
Color: stringPtr(textColor),
|
||||
Color: new(textColor),
|
||||
},
|
||||
LiteralNumber: ansi.StylePrimitive{
|
||||
Color: stringPtr(numberColor),
|
||||
Color: new(numberColor),
|
||||
},
|
||||
LiteralString: ansi.StylePrimitive{
|
||||
Color: stringPtr(stringColor),
|
||||
Color: new(stringColor),
|
||||
},
|
||||
LiteralStringEscape: ansi.StylePrimitive{
|
||||
Color: stringPtr(keywordColor),
|
||||
Color: new(keywordColor),
|
||||
},
|
||||
GenericDeleted: ansi.StylePrimitive{
|
||||
Color: stringPtr(errorColor),
|
||||
Color: new(errorColor),
|
||||
},
|
||||
GenericEmph: ansi.StylePrimitive{
|
||||
Color: stringPtr(emphColor),
|
||||
Color: new(emphColor),
|
||||
|
||||
Italic: boolPtr(true),
|
||||
Italic: new(true),
|
||||
},
|
||||
GenericInserted: ansi.StylePrimitive{
|
||||
Color: stringPtr(stringColor),
|
||||
Color: new(stringColor),
|
||||
},
|
||||
GenericStrong: ansi.StylePrimitive{
|
||||
Color: stringPtr(strongColor),
|
||||
Color: new(strongColor),
|
||||
|
||||
Bold: boolPtr(true),
|
||||
Bold: new(true),
|
||||
},
|
||||
GenericSubheading: ansi.StylePrimitive{
|
||||
Color: stringPtr(headingColor),
|
||||
Color: new(headingColor),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -336,20 +342,20 @@ func generateMarkdownStyleConfig() ansi.StyleConfig {
|
||||
BlockSuffix: "\n",
|
||||
},
|
||||
},
|
||||
CenterSeparator: stringPtr("┼"),
|
||||
ColumnSeparator: stringPtr("│"),
|
||||
RowSeparator: stringPtr("─"),
|
||||
CenterSeparator: new("┼"),
|
||||
ColumnSeparator: new("│"),
|
||||
RowSeparator: new("─"),
|
||||
},
|
||||
DefinitionDescription: ansi.StylePrimitive{
|
||||
BlockPrefix: "\n ❯ ",
|
||||
Color: stringPtr(linkColor),
|
||||
Color: new(linkColor),
|
||||
},
|
||||
Text: ansi.StylePrimitive{
|
||||
Color: stringPtr(textColor),
|
||||
Color: new(textColor),
|
||||
},
|
||||
Paragraph: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
Color: stringPtr(textColor),
|
||||
Color: new(textColor),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
+8
-9
@@ -4,7 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/cloudwego/eino/schema"
|
||||
"charm.land/fantasy"
|
||||
"github.com/mark3labs/mcphost/cmd"
|
||||
"github.com/mark3labs/mcphost/internal/agent"
|
||||
"github.com/mark3labs/mcphost/internal/config"
|
||||
@@ -78,7 +78,7 @@ func New(ctx context.Context, opts *Options) (*MCPHost, error) {
|
||||
return nil, fmt.Errorf("failed to load system prompt: %v", err)
|
||||
}
|
||||
|
||||
// Create model configuration (same as CLI in root.go:387-406)
|
||||
// Create model configuration (same as CLI in root.go)
|
||||
temperature := float32(viper.GetFloat64("temperature"))
|
||||
topP := float32(viper.GetFloat64("top-p"))
|
||||
topK := int32(viper.GetInt("top-k"))
|
||||
@@ -100,7 +100,7 @@ func New(ctx context.Context, opts *Options) (*MCPHost, error) {
|
||||
TLSSkipVerify: viper.GetBool("tls-skip-verify"),
|
||||
}
|
||||
|
||||
// Create agent using existing factory (same as CLI in root.go:431-440)
|
||||
// Create agent using existing factory
|
||||
a, err := agent.CreateAgent(ctx, &agent.AgentCreationOptions{
|
||||
ModelConfig: modelConfig,
|
||||
MCPConfig: mcpConfig,
|
||||
@@ -132,10 +132,10 @@ func (m *MCPHost) Prompt(ctx context.Context, message string) (string, error) {
|
||||
messages := m.sessionMgr.GetMessages()
|
||||
|
||||
// Add new user message
|
||||
userMsg := schema.UserMessage(message)
|
||||
userMsg := fantasy.NewUserMessage(message)
|
||||
messages = append(messages, userMsg)
|
||||
|
||||
// Call agent (same as CLI does in root.go:902)
|
||||
// Call agent
|
||||
result, err := m.agent.GenerateWithLoop(ctx, messages,
|
||||
nil, // onToolCall
|
||||
nil, // onToolExecution
|
||||
@@ -149,12 +149,11 @@ func (m *MCPHost) Prompt(ctx context.Context, message string) (string, error) {
|
||||
}
|
||||
|
||||
// Update session with all messages from the conversation
|
||||
// This preserves the complete history including tool calls
|
||||
if err := m.sessionMgr.ReplaceAllMessages(result.ConversationMessages); err != nil {
|
||||
return "", fmt.Errorf("failed to update session: %v", err)
|
||||
}
|
||||
|
||||
return result.FinalResponse.Content, nil
|
||||
return result.FinalResponse.Content.Text(), nil
|
||||
}
|
||||
|
||||
// PromptWithCallbacks sends a message with callbacks for monitoring tool execution
|
||||
@@ -171,7 +170,7 @@ func (m *MCPHost) PromptWithCallbacks(
|
||||
messages := m.sessionMgr.GetMessages()
|
||||
|
||||
// Add new user message
|
||||
userMsg := schema.UserMessage(message)
|
||||
userMsg := fantasy.NewUserMessage(message)
|
||||
messages = append(messages, userMsg)
|
||||
|
||||
// Call agent with callbacks
|
||||
@@ -193,7 +192,7 @@ func (m *MCPHost) PromptWithCallbacks(
|
||||
return "", fmt.Errorf("failed to update session: %v", err)
|
||||
}
|
||||
|
||||
return result.FinalResponse.Content, nil
|
||||
return result.FinalResponse.Content.Text(), nil
|
||||
}
|
||||
|
||||
// GetSessionManager returns the current session manager for direct access
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ func TestNewWithOptions(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
opts := &sdk.Options{
|
||||
Model: "anthropic:claude-3-haiku-20240307",
|
||||
Model: "anthropic:claude-sonnet-4-20250514",
|
||||
MaxSteps: 5,
|
||||
Quiet: true,
|
||||
}
|
||||
|
||||
+7
-7
@@ -1,7 +1,7 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"github.com/cloudwego/eino/schema"
|
||||
"charm.land/fantasy"
|
||||
"github.com/mark3labs/mcphost/internal/session"
|
||||
)
|
||||
|
||||
@@ -13,14 +13,14 @@ type Message = session.Message
|
||||
// with its name, arguments, and result within a conversation.
|
||||
type ToolCall = session.ToolCall
|
||||
|
||||
// ConvertToSchemaMessage converts an SDK message to the underlying schema message
|
||||
// ConvertToFantasyMessage converts an SDK message to the underlying fantasy message
|
||||
// format used by the agent for LLM interactions.
|
||||
func ConvertToSchemaMessage(msg *Message) *schema.Message {
|
||||
return msg.ConvertToSchemaMessage()
|
||||
func ConvertToFantasyMessage(msg *Message) fantasy.Message {
|
||||
return msg.ConvertToFantasyMessage()
|
||||
}
|
||||
|
||||
// ConvertFromSchemaMessage converts a schema message from the agent to an SDK
|
||||
// ConvertFromFantasyMessage converts a fantasy message from the agent to an SDK
|
||||
// message format for use in the SDK API.
|
||||
func ConvertFromSchemaMessage(msg *schema.Message) Message {
|
||||
return session.ConvertFromSchemaMessage(msg)
|
||||
func ConvertFromFantasyMessage(msg fantasy.Message) Message {
|
||||
return session.ConvertFromFantasyMessage(msg)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user