mirror of
https://github.com/mark3labs/kit.git
synced 2026-06-14 03:30:26 +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",
|
"type": "git",
|
||||||
"name": "glamour",
|
"name": "glamour",
|
||||||
"url": "https://github.com/charmbracelet/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"
|
"branch": "main"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
+38
-30
@@ -10,7 +10,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/cloudwego/eino/schema"
|
"charm.land/fantasy"
|
||||||
"github.com/mark3labs/mcphost/internal/agent"
|
"github.com/mark3labs/mcphost/internal/agent"
|
||||||
"github.com/mark3labs/mcphost/internal/config"
|
"github.com/mark3labs/mcphost/internal/config"
|
||||||
"github.com/mark3labs/mcphost/internal/hooks"
|
"github.com/mark3labs/mcphost/internal/hooks"
|
||||||
@@ -608,13 +608,12 @@ func runNormalMode(ctx context.Context) error {
|
|||||||
tools := mcpAgent.GetTools()
|
tools := mcpAgent.GetTools()
|
||||||
var toolNames []string
|
var toolNames []string
|
||||||
for _, tool := range tools {
|
for _, tool := range tools {
|
||||||
if info, err := tool.Info(ctx); err == nil {
|
info := tool.Info()
|
||||||
toolNames = append(toolNames, info.Name)
|
toolNames = append(toolNames, info.Name)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Main interaction logic
|
// Main interaction logic
|
||||||
var messages []*schema.Message
|
var messages []fantasy.Message
|
||||||
var sessionManager *session.Manager
|
var sessionManager *session.Manager
|
||||||
if sessionPath != "" {
|
if sessionPath != "" {
|
||||||
_, err := os.Stat(sessionPath)
|
_, err := os.Stat(sessionPath)
|
||||||
@@ -637,7 +636,8 @@ func runNormalMode(ctx context.Context) error {
|
|||||||
|
|
||||||
// Convert session messages to schema messages
|
// Convert session messages to schema messages
|
||||||
for _, msg := range loadedSession.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
|
// 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
|
// Display all previous messages as they would have appeared
|
||||||
for _, sessionMsg := range loadedSession.Messages {
|
for _, sessionMsg := range loadedSession.Messages {
|
||||||
if sessionMsg.Role == "user" {
|
switch sessionMsg.Role {
|
||||||
|
case "user":
|
||||||
cli.DisplayUserMessage(sessionMsg.Content)
|
cli.DisplayUserMessage(sessionMsg.Content)
|
||||||
} else if sessionMsg.Role == "assistant" {
|
case "assistant":
|
||||||
// Display tool calls if present
|
// Display tool calls if present
|
||||||
if len(sessionMsg.ToolCalls) > 0 {
|
if len(sessionMsg.ToolCalls) > 0 {
|
||||||
for _, tc := range sessionMsg.ToolCalls {
|
for _, tc := range sessionMsg.ToolCalls {
|
||||||
@@ -679,7 +680,7 @@ func runNormalMode(ctx context.Context) error {
|
|||||||
if sessionMsg.Content != "" {
|
if sessionMsg.Content != "" {
|
||||||
cli.DisplayAssistantMessage(sessionMsg.Content)
|
cli.DisplayAssistantMessage(sessionMsg.Content)
|
||||||
}
|
}
|
||||||
} else if sessionMsg.Role == "tool" {
|
case "tool":
|
||||||
// Display tool result
|
// Display tool result
|
||||||
if sessionMsg.ToolCallID != "" {
|
if sessionMsg.ToolCallID != "" {
|
||||||
if toolCall, exists := toolCallMap[sessionMsg.ToolCallID]; exists {
|
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
|
// 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
|
// Add to local history
|
||||||
*messages = append(*messages, newMessages...)
|
*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
|
// 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
|
// Replace local history
|
||||||
*messages = newMessages
|
*messages = newMessages
|
||||||
|
|
||||||
@@ -807,7 +808,7 @@ func replaceMessagesHistory(messages *[]*schema.Message, sessionManager *session
|
|||||||
}
|
}
|
||||||
|
|
||||||
// runAgenticLoop handles all execution modes with a single unified loop
|
// 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
|
// Handle initial prompt for non-interactive modes
|
||||||
if !config.IsInteractive && config.InitialPrompt != "" {
|
if !config.IsInteractive && config.InitialPrompt != "" {
|
||||||
// Execute UserPromptSubmit hooks for non-interactive mode
|
// 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)
|
// 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
|
// Process the initial prompt with tool calls
|
||||||
_, conversationMessages, err := runAgenticStep(ctx, mcpAgent, cli, tempMessages, config, hookExecutor)
|
_, 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)
|
// 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
|
var currentSpinner *ui.Spinner
|
||||||
|
|
||||||
// Start initial spinner (skip if quiet)
|
// 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 {
|
if len(messages) > 0 {
|
||||||
// Find the last user message
|
// Find the last user message
|
||||||
for i := len(messages) - 1; i >= 0; i-- {
|
for i := len(messages) - 1; i >= 0; i-- {
|
||||||
if messages[i].Role == schema.User {
|
if messages[i].Role == fantasy.MessageRoleUser {
|
||||||
lastUserMessage = messages[i].Content
|
// Extract text from message parts
|
||||||
|
for _, part := range messages[i].Content {
|
||||||
|
if tp, ok := part.(fantasy.TextPart); ok {
|
||||||
|
lastUserMessage = tp.Text
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get text content from response
|
||||||
|
responseText := response.Content.Text()
|
||||||
|
|
||||||
// Update usage tracking for ALL responses (streaming and non-streaming)
|
// Update usage tracking for ALL responses (streaming and non-streaming)
|
||||||
if !config.Quiet && cli != nil {
|
if !config.Quiet && cli != nil {
|
||||||
cli.UpdateUsageFromResponse(response, lastUserMessage)
|
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
|
// Display assistant response with model name
|
||||||
// Skip if: quiet mode, same content already displayed, or if streaming completed the full response
|
// Skip if: quiet mode, same content already displayed, or if streaming completed the full response
|
||||||
streamedFullResponse := responseWasStreamed && streamingContent.String() == response.Content
|
streamedFullResponse := responseWasStreamed && streamingContent.String() == responseText
|
||||||
if !config.Quiet && cli != nil && response.Content != lastDisplayedContent && response.Content != "" && !streamedFullResponse {
|
if !config.Quiet && cli != nil && responseText != lastDisplayedContent && responseText != "" && !streamedFullResponse {
|
||||||
if err := cli.DisplayAssistantMessageWithModel(response.Content, config.ModelName); err != nil {
|
if err := cli.DisplayAssistantMessageWithModel(responseText, config.ModelName); err != nil {
|
||||||
cli.DisplayError(fmt.Errorf("display error: %v", err))
|
cli.DisplayError(fmt.Errorf("display error: %v", err))
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
} else if config.Quiet {
|
} else if config.Quiet {
|
||||||
// In quiet mode, only output the final response content to stdout
|
// 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)
|
// 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
|
// 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 {
|
if hookExecutor != nil {
|
||||||
// Prepare metadata
|
// Prepare metadata
|
||||||
var meta json.RawMessage
|
var meta json.RawMessage
|
||||||
if response != nil {
|
if response != nil {
|
||||||
metaData := map[string]interface{}{
|
metaData := map[string]any{
|
||||||
"model": modelName,
|
"model": modelName,
|
||||||
"role": string(response.Role),
|
"has_tool_calls": len(response.Content.ToolCalls()) > 0,
|
||||||
"has_tool_calls": len(response.ToolCalls) > 0,
|
|
||||||
}
|
}
|
||||||
if metaBytes, err := json.Marshal(metaData); err == nil {
|
if metaBytes, err := json.Marshal(metaData); err == nil {
|
||||||
meta = json.RawMessage(metaBytes)
|
meta = json.RawMessage(metaBytes)
|
||||||
@@ -1208,7 +1217,7 @@ func executeStopHook(hookExecutor *hooks.Executor, response *schema.Message, sto
|
|||||||
|
|
||||||
responseContent := ""
|
responseContent := ""
|
||||||
if response != nil {
|
if response != nil {
|
||||||
responseContent = response.Content
|
responseContent = response.Content.Text()
|
||||||
}
|
}
|
||||||
|
|
||||||
input := &hooks.StopInput{
|
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
|
// 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 {
|
for {
|
||||||
// Get user input
|
// Get user input
|
||||||
prompt, err := cli.GetPrompt()
|
prompt, err := cli.GetPrompt()
|
||||||
@@ -1292,7 +1301,7 @@ func runInteractiveLoop(ctx context.Context, mcpAgent *agent.Agent, cli *ui.CLI,
|
|||||||
cli.DisplayUserMessage(prompt)
|
cli.DisplayUserMessage(prompt)
|
||||||
|
|
||||||
// Create temporary messages with user input for processing
|
// 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
|
// Process the user input with tool calls
|
||||||
_, conversationMessages, err := runAgenticStep(ctx, mcpAgent, cli, tempMessages, config, hookExecutor)
|
_, conversationMessages, err := runAgenticStep(ctx, mcpAgent, cli, tempMessages, config, hookExecutor)
|
||||||
if err != nil {
|
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
|
// 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)
|
// Prepare data for slash commands (needed if continuing to interactive mode)
|
||||||
var serverNames []string
|
var serverNames []string
|
||||||
for name := range mcpConfig.MCPServers {
|
for name := range mcpConfig.MCPServers {
|
||||||
@@ -1322,10 +1331,9 @@ func runNonInteractiveMode(ctx context.Context, mcpAgent *agent.Agent, cli *ui.C
|
|||||||
tools := mcpAgent.GetTools()
|
tools := mcpAgent.GetTools()
|
||||||
var toolNames []string
|
var toolNames []string
|
||||||
for _, tool := range tools {
|
for _, tool := range tools {
|
||||||
if info, err := tool.Info(ctx); err == nil {
|
info := tool.Info()
|
||||||
toolNames = append(toolNames, info.Name)
|
toolNames = append(toolNames, info.Name)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Configure and run unified agentic loop
|
// Configure and run unified agentic loop
|
||||||
config := AgenticLoopConfig{
|
config := AgenticLoopConfig{
|
||||||
@@ -1345,7 +1353,7 @@ func runNonInteractiveMode(ctx context.Context, mcpAgent *agent.Agent, cli *ui.C
|
|||||||
}
|
}
|
||||||
|
|
||||||
// runInteractiveMode handles the interactive mode execution
|
// 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
|
// Configure and run unified agentic loop
|
||||||
config := AgenticLoopConfig{
|
config := AgenticLoopConfig{
|
||||||
IsInteractive: true,
|
IsInteractive: true,
|
||||||
|
|||||||
+6
-7
@@ -10,7 +10,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/cloudwego/eino/schema"
|
"charm.land/fantasy"
|
||||||
"github.com/mark3labs/mcphost/internal/agent"
|
"github.com/mark3labs/mcphost/internal/agent"
|
||||||
"github.com/mark3labs/mcphost/internal/config"
|
"github.com/mark3labs/mcphost/internal/config"
|
||||||
"github.com/mark3labs/mcphost/internal/hooks"
|
"github.com/mark3labs/mcphost/internal/hooks"
|
||||||
@@ -177,8 +177,8 @@ func parseCustomVariables(_ *cobra.Command) map[string]string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Parse custom variables with --args: prefix
|
// Parse custom variables with --args: prefix
|
||||||
if strings.HasPrefix(arg, "--args:") {
|
if after, ok := strings.CutPrefix(arg, "--args:"); ok {
|
||||||
varName := strings.TrimPrefix(arg, "--args:")
|
varName := after
|
||||||
if varName == "" {
|
if varName == "" {
|
||||||
continue // Skip malformed --args: without name
|
continue // Skip malformed --args: without name
|
||||||
}
|
}
|
||||||
@@ -312,7 +312,7 @@ func parseScriptContent(content string, variables map[string]string) (*config.Co
|
|||||||
var promptLines []string
|
var promptLines []string
|
||||||
var inFrontmatter bool
|
var inFrontmatter bool
|
||||||
var foundFrontmatter bool
|
var foundFrontmatter bool
|
||||||
var frontmatterEnd int = -1
|
var frontmatterEnd = -1
|
||||||
|
|
||||||
for i, line := range lines {
|
for i, line := range lines {
|
||||||
trimmed := strings.TrimSpace(line)
|
trimmed := strings.TrimSpace(line)
|
||||||
@@ -699,13 +699,12 @@ func runScriptMode(ctx context.Context, mcpConfig *config.Config, prompt string,
|
|||||||
tools := mcpAgent.GetTools()
|
tools := mcpAgent.GetTools()
|
||||||
var toolNames []string
|
var toolNames []string
|
||||||
for _, tool := range tools {
|
for _, tool := range tools {
|
||||||
if info, err := tool.Info(ctx); err == nil {
|
info := tool.Info()
|
||||||
toolNames = append(toolNames, info.Name)
|
toolNames = append(toolNames, info.Name)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Configure and run unified agentic loop
|
// Configure and run unified agentic loop
|
||||||
var messages []*schema.Message
|
var messages []fantasy.Message
|
||||||
config := AgenticLoopConfig{
|
config := AgenticLoopConfig{
|
||||||
IsInteractive: prompt == "", // If no prompt, start in interactive mode
|
IsInteractive: prompt == "", // If no prompt, start in interactive mode
|
||||||
InitialPrompt: prompt,
|
InitialPrompt: prompt,
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ Working directory is ${env://WORK_DIR:-/tmp}.
|
|||||||
t.Fatal("Filesystem server not found in script config")
|
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 {
|
if !ok {
|
||||||
t.Fatal("allowed_directories should be an array")
|
t.Fatal("allowed_directories should be an array")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,150 +1,129 @@
|
|||||||
module github.com/mark3labs/mcphost
|
module github.com/mark3labs/mcphost
|
||||||
|
|
||||||
go 1.24.2
|
go 1.26.0
|
||||||
|
|
||||||
toolchain go1.24.5
|
|
||||||
|
|
||||||
require (
|
require (
|
||||||
charm.land/bubbles/v2 v2.0.0
|
charm.land/bubbles/v2 v2.0.0
|
||||||
charm.land/bubbletea/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
|
charm.land/lipgloss/v2 v2.0.0
|
||||||
github.com/JohannesKaufmann/html-to-markdown v1.6.0
|
github.com/JohannesKaufmann/html-to-markdown v1.6.0
|
||||||
github.com/PuerkitoBio/goquery v1.10.3
|
github.com/PuerkitoBio/goquery v1.10.3
|
||||||
github.com/bytedance/sonic v1.15.0
|
|
||||||
github.com/charmbracelet/fang v0.4.4
|
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-filesystem-server v0.11.1
|
||||||
github.com/mark3labs/mcp-go v0.44.0-beta.2
|
github.com/mark3labs/mcp-go v0.44.0-beta.2
|
||||||
github.com/spf13/cobra v1.10.1
|
github.com/spf13/cobra v1.10.1
|
||||||
github.com/spf13/viper v1.20.1
|
github.com/spf13/viper v1.20.1
|
||||||
github.com/tidwall/gjson v1.18.0
|
github.com/tidwall/gjson v1.18.0
|
||||||
golang.org/x/term v0.37.0
|
golang.org/x/term v0.40.0
|
||||||
google.golang.org/genai v1.22.0
|
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
cloud.google.com/go v0.121.6 // indirect
|
cloud.google.com/go v0.123.0 // indirect
|
||||||
cloud.google.com/go/auth v0.16.5 // indirect
|
cloud.google.com/go/auth v0.18.2 // indirect
|
||||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // 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/alecthomas/chroma/v2 v2.20.0 // indirect
|
||||||
github.com/andybalholm/cascadia v1.3.3 // 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/atotto/clipboard v0.1.4 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2 v1.38.3 // indirect
|
github.com/aws/aws-sdk-go-v2 v1.41.1 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.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.31.6 // indirect
|
github.com/aws/aws-sdk-go-v2/config v1.32.9 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.10 // 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.6 // 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.6 // 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.6 // 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.3 // 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.1 // 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.6 // indirect
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/sso v1.29.1 // indirect
|
github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.2 // indirect
|
github.com/aws/aws-sdk-go-v2/service/sso v1.30.10 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.38.2 // indirect
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.14 // indirect
|
||||||
github.com/aws/smithy-go v1.23.0 // 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/aymerick/douceur v0.2.0 // indirect
|
||||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||||
github.com/buger/jsonparser v1.1.1 // indirect
|
github.com/buger/jsonparser v1.1.1 // indirect
|
||||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.5.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/colorprofile v0.4.2 // indirect
|
||||||
github.com/charmbracelet/harmonica v0.2.0 // indirect
|
github.com/charmbracelet/harmonica v0.2.0 // indirect
|
||||||
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 // 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/ultraviolet v0.0.0-20260205113103-524a6607adb8 // indirect
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.15 // 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/charmtone v0.0.0-20250902204034-1cdc10c66d5b // indirect
|
||||||
github.com/charmbracelet/x/exp/color 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/termios v0.1.1 // indirect
|
||||||
github.com/charmbracelet/x/windows v0.2.2 // indirect
|
github.com/charmbracelet/x/windows v0.2.2 // indirect
|
||||||
github.com/clipperhouse/displaywidth v0.11.0 // indirect
|
github.com/clipperhouse/displaywidth v0.11.0 // indirect
|
||||||
github.com/clipperhouse/uax29/v2 v2.7.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/djherbis/times v1.6.0 // indirect
|
||||||
github.com/dlclark/regexp2 v1.11.5 // 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/felixge/httpsnoop v1.0.4 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.10 // 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/logr v1.4.3 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/go-openapi/jsonpointer v0.22.0 // indirect
|
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||||
github.com/go-openapi/swag/jsonname v0.24.0 // indirect
|
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
|
||||||
github.com/gobwas/glob v0.2.3 // 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/go-cmp v0.7.0 // indirect
|
||||||
github.com/google/s2a-go v0.1.9 // indirect
|
github.com/google/s2a-go v0.1.9 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
|
github.com/googleapis/enterprise-certificate-proxy v0.3.12 // indirect
|
||||||
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
|
github.com/googleapis/gax-go/v2 v2.17.0 // indirect
|
||||||
github.com/goph/emperror v0.17.2 // indirect
|
|
||||||
github.com/gorilla/css v1.0.1 // indirect
|
github.com/gorilla/css v1.0.1 // indirect
|
||||||
github.com/gorilla/websocket v1.5.3 // indirect
|
github.com/gorilla/websocket v1.5.3 // indirect
|
||||||
github.com/invopop/jsonschema v0.13.0 // indirect
|
github.com/invopop/jsonschema v0.13.0 // indirect
|
||||||
github.com/josharian/intern v1.0.0 // indirect
|
github.com/kaptinlin/go-i18n v0.2.11 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/kaptinlin/jsonpointer v0.4.16 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 // 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/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/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 v0.2.0 // indirect
|
||||||
github.com/muesli/mango-cobra v1.2.0 // indirect
|
github.com/muesli/mango-cobra v1.2.0 // indirect
|
||||||
github.com/muesli/mango-pflag v0.1.0 // indirect
|
github.com/muesli/mango-pflag v0.1.0 // indirect
|
||||||
github.com/muesli/reflow v0.3.0 // indirect
|
github.com/muesli/reflow v0.3.0 // indirect
|
||||||
github.com/muesli/roff v0.1.0 // indirect
|
github.com/muesli/roff v0.1.0 // indirect
|
||||||
github.com/nikolalohinski/gonja v1.5.3 // indirect
|
github.com/openai/openai-go/v2 v2.7.1 // indirect
|
||||||
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect
|
|
||||||
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect
|
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 // 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/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/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/spf13/cast v1.9.2 // indirect
|
||||||
github.com/subosito/gotenv v1.6.0 // indirect
|
github.com/subosito/gotenv v1.6.0 // indirect
|
||||||
github.com/tidwall/match v1.1.1 // indirect
|
github.com/tidwall/match v1.1.1 // indirect
|
||||||
github.com/tidwall/pretty v1.2.1 // indirect
|
github.com/tidwall/pretty v1.2.1 // indirect
|
||||||
github.com/tidwall/sjson v1.2.5 // 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/wk8/go-ordered-map/v2 v2.1.8 // indirect
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // 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/yosida95/uritemplate/v3 v3.0.2 // indirect
|
||||||
github.com/yuin/goldmark v1.7.13 // indirect
|
github.com/yuin/goldmark v1.7.13 // indirect
|
||||||
github.com/yuin/goldmark-emoji v1.0.6 // indirect
|
github.com/yuin/goldmark-emoji v1.0.6 // indirect
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect
|
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0 // indirect
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect
|
||||||
go.opentelemetry.io/otel v1.38.0 // indirect
|
go.opentelemetry.io/otel v1.40.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
go.opentelemetry.io/otel/metric v1.40.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
go.opentelemetry.io/otel/trace v1.40.0 // indirect
|
||||||
golang.org/x/arch v0.23.0 // indirect
|
golang.org/x/crypto v0.48.0 // indirect
|
||||||
golang.org/x/crypto v0.45.0 // indirect
|
golang.org/x/net v0.50.0 // indirect
|
||||||
golang.org/x/net v0.47.0 // indirect
|
golang.org/x/oauth2 v0.35.0 // indirect
|
||||||
golang.org/x/oauth2 v0.30.0 // indirect
|
golang.org/x/time v0.14.0 // indirect
|
||||||
golang.org/x/time v0.12.0 // indirect
|
google.golang.org/api v0.267.0 // indirect
|
||||||
google.golang.org/api v0.246.0 // indirect
|
google.golang.org/genai v1.47.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d // indirect
|
||||||
google.golang.org/grpc v1.75.0 // indirect
|
google.golang.org/grpc v1.79.1 // indirect
|
||||||
google.golang.org/protobuf v1.36.8 // indirect
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -160,8 +139,7 @@ require (
|
|||||||
github.com/muesli/termenv v0.16.0 // indirect
|
github.com/muesli/termenv v0.16.0 // indirect
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/spf13/pflag v1.0.10 // 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/sync v0.19.0 // indirect
|
||||||
golang.org/x/sys v0.41.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/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 h1:p0d6CtWyJXJ9GfzMpUUqbP/XUUhhlk06+vCKWmox1wQ=
|
||||||
charm.land/bubbletea/v2 v2.0.0/go.mod h1:3LRff2U4WIYXy7MTxfbAQ+AdfM3D8Xuvz2wbsOD9OHQ=
|
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 h1:sd8N/B3x892oiOjFfBQdXBQp3cAkvjGaU5TvVZC3ivo=
|
||||||
charm.land/lipgloss/v2 v2.0.0/go.mod h1:w6SnmsBFBmEFBodiEDurGS/sdUY/u1+v72DqUzc6J14=
|
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.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=
|
||||||
cloud.google.com/go v0.121.6/go.mod h1:coChdst4Ea5vUpiALcYKXEpR1S9ZgXbhEzzMcMR66vI=
|
cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
|
||||||
cloud.google.com/go/auth v0.16.5 h1:mFWNQ2FEVWAliEQWpAdH80omXFokmrnbDhUS9cBywsI=
|
cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM=
|
||||||
cloud.google.com/go/auth v0.16.5/go.mod h1:utzRfHMP+Vv0mpOkTRQoWD2q3BatTOoWbA7gCc2dUhQ=
|
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 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
|
||||||
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
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.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
|
||||||
cloud.google.com/go/compute/metadata v0.8.0/go.mod h1:sYOGTp851OV9bOFJ9CH7elVvyzopvWQFNNghtDQ/Biw=
|
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 h1:04VXMiE50YYfCfLboJCLcgqF5x+rHJnb1ssNmqpLH/k=
|
||||||
github.com/JohannesKaufmann/html-to-markdown v1.6.0/go.mod h1:NUI78lGg/a7vpEJTz/0uOcYMaibytE4BUOQS8k78yPQ=
|
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=
|
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.9.2/go.mod h1:GHPCaP0ODyyxqcNoFGYlAprUFH81NuRPd0GX3Zu2Mvk=
|
||||||
github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo=
|
github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo=
|
||||||
github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=
|
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 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
||||||
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
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=
|
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.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
|
||||||
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
|
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/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 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
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.41.1 h1:ABlyEARCDLN034NhxlRUSZr4l71mh+T5KAeGh6cerhU=
|
||||||
github.com/aws/aws-sdk-go-v2 v1.38.3/go.mod h1:sDioUELIUO9Znk23YVmIk86/9DOpkbyyVb1i/gUNFXY=
|
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.1 h1:i8p8P4diljCr60PpJp6qZXNlgX4m2yQFpYk+9ZT+J4E=
|
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.1/go.mod h1:ddqbooRZYNoJ2dsTwOty16rM+/Aqmk/GOXrK8cg7V00=
|
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.31.6 h1:a1t8fXY4GT4xjyJExz4knbuoxSCacB5hT/WgtfPyLjo=
|
github.com/aws/aws-sdk-go-v2/config v1.32.9 h1:ktda/mtAydeObvJXlHzyGpK1xcsLaP16zfUPDGoW90A=
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.31.6/go.mod h1:5ByscNi7R+ztvOGzeUaIu49vkMk2soq5NaH5PYe33MQ=
|
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.18.10 h1:xdJnXCouCx8Y0NncgoptztUocIYLKeQxrCgN6x9sdhg=
|
github.com/aws/aws-sdk-go-v2/credentials v1.19.9 h1:sWvTKsyrMlJGEuj/WgrwilpoJ6Xa1+KhIpGdzw7mMU8=
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.10/go.mod h1:7tQk08ntj914F/5i9jC4+2HQTAuJirq7m1vZVIhEkWs=
|
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.6 h1:wbjnrrMnKew78/juW7I2BtKQwa1qlf6EjQgS69uYY14=
|
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.6/go.mod h1:AtiqqNrDioJXuUgz3+3T0mBWN7Hro2n9wll2zRUc0ww=
|
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.6 h1:uF68eJA6+S9iVr9WgX1NaRGyQ/6MdIyc4JNUo6TN1FA=
|
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.6/go.mod h1:qlPeVZCGPiobx8wb1ft0GHT5l+dc6ldnwInDFaMvC7Y=
|
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.6 h1:pa1DEC6JoI0zduhZePp3zmhWvk/xxm4NB8Hy/Tlsgos=
|
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.6/go.mod h1:gxEjPebnhWGJoaDdtDkA0JX46VRg1wcTHYe63OfX5pE=
|
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.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
|
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.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
|
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.1 h1:oegbebPEMA/1Jny7kvwejowCaHz1FWZAQ94WXFNCyTM=
|
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.1/go.mod h1:kemo5Myr9ac0U9JfSjMo9yHLtw+pECEHsFtJ9tqCEI8=
|
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.6 h1:LHS1YAIJXJ4K9zS+1d/xa9JAA9sL2QyXIQCQFQW/X08=
|
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.6/go.mod h1:c9PCiTEuh0wQID5/KqA32J+HAgZxN9tOGXKCiYJjTZI=
|
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/sso v1.29.1 h1:8OLZnVJPvjnrxEwHFg9hVUof/P4sibH+Ea4KKuqAGSg=
|
github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 h1:VrhDvQib/i0lxvr3zqlUwLwJP4fpmpyD9wYG1vfSu+Y=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sso v1.29.1/go.mod h1:27M3BpVi0C02UiQh1w9nsBEit6pLhlaH3NHna6WUbDE=
|
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/ssooidc v1.34.2 h1:gKWSTnqudpo8dAxqBqZnDoDWCiEh/40FziUjr/mo6uA=
|
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/ssooidc v1.34.2/go.mod h1:x7+rkNmRoEN1U13A6JE2fXne9EWyJy54o3n6d4mGaXQ=
|
github.com/aws/aws-sdk-go-v2/service/sso v1.30.10/go.mod h1:yifAsgBxgJWn3ggx70A3urX2AN49Y5sJTD1UQFlfqBw=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.38.2 h1:YZPjhyaGzhDQEvsffDEcpycq49nl7fiGcfJTIo8BszI=
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.14 h1:0jbJeuEHlwKJ9PfXtpSFc4MF+WIWORdhN1n30ITZGFM=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.38.2/go.mod h1:2dIN8qhQfv37BdUYGgEC8Q3tteM3zFxTI1MLO2O3J3c=
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.14/go.mod h1:sTGThjphYE4Ohw8vJiRStAcu3rbjtXRsdNB0TvZ5wwo=
|
||||||
github.com/aws/smithy-go v1.23.0 h1:8n6I3gXzWJB2DxBDnfxgBaSX6oe0d/t10qGz7OKqMCE=
|
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 h1:5fFjR/ToSOzB2OQ/XqWpZBmNvmP/pJ1jOWYlFDJTjRQ=
|
||||||
github.com/aws/smithy-go v1.23.0/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
|
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 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||||
github.com/aymanbagabas/go-udiff v0.4.0 h1:TKnLPh7IbnizJIBKFWa9mKayRUBQ9Kh1BPCk6w2PnYM=
|
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/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 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
|
||||||
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
|
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 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
|
||||||
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/bugsnag/panicwrap v1.2.0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE=
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
github.com/charmbracelet/anthropic-sdk-go v0.0.0-20260223140439-63879b0b8dab h1:J7XQLgl9sefgTnTGrmX3xqvp5o6MCiBzEjGv5igAlc4=
|
||||||
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
github.com/charmbracelet/anthropic-sdk-go v0.0.0-20260223140439-63879b0b8dab/go.mod h1:hqlYqR7uPKOKfnNeicUbZp0Ps0GeYFlKYtwh5HGDCx8=
|
||||||
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/charmbracelet/colorprofile v0.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY=
|
github.com/charmbracelet/colorprofile v0.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY=
|
||||||
github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8=
|
github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8=
|
||||||
github.com/charmbracelet/fang v0.4.4 h1:G4qKxF6or/eTPgmAolwPuRNyuci3hTUGGX1rj1YkHJY=
|
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/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 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
|
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 h1:U9SnQTnrxy8y3gEpxhpBS3ztHAR7IvL0CjvFHOR4sbE=
|
||||||
github.com/charmbracelet/x/exp/charmtone v0.0.0-20250902204034-1cdc10c66d5b/go.mod h1:T9jr8CzFpjhFVHjNjKwbAD7KwBNyFnj2pntAO7F2zw0=
|
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 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/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 h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA=
|
||||||
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I=
|
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-20250904123553-b4e2667e5ad5 h1:DTSZxdV9qQagD4iGcAt9RgaRBZtJl01bfKgdLzUzUPI=
|
||||||
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/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 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
||||||
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
||||||
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
|
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/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 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
|
||||||
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
||||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik=
|
||||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4=
|
||||||
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/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
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.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.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 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
|
||||||
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
|
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 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
||||||
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI=
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
|
||||||
github.com/eino-contrib/jsonschema v1.0.3 h1:2Kfsm1xlMV0ssY2nuxshS4AwbLFuqmPmzIjLVJ1Fsp0=
|
github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA=
|
||||||
github.com/eino-contrib/jsonschema v1.0.3/go.mod h1:cpnX4SyKjWjGC7iN2EbhxaTdLqGjCi0e9DxpLYxddD4=
|
github.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNfdSbEPe9Yyl09/B6wBrQ=
|
||||||
github.com/eino-contrib/ollama v0.1.0 h1:z1NaMdKW6X1ftP8g5xGGR5zDRPUtuTKFq35vBQgxsN4=
|
github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A=
|
||||||
github.com/eino-contrib/ollama v0.1.0/go.mod h1:mYsQ7b3DeqY8bHPuD3MZJYTqkgyL6LoemxoP/B7ZNhA=
|
github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds=
|
||||||
github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k=
|
github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0=
|
||||||
github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ=
|
|
||||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
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 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
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 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
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 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
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/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e h1:Lf/gRkoycfOBPa42vU2bbgPurFong6zXeFtPoxholzU=
|
||||||
github.com/getkin/kin-openapi v0.131.0/go.mod h1:3OlG51PCYNsPByuiMB0t4fjnNlIDnaEDsjiKUV8nL58=
|
github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e/go.mod h1:uNVvRXArCGbZ508SxYYTC5v1JWoz2voff5pm25jU1Ok=
|
||||||
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-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
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 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
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 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
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-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
|
||||||
github.com/go-openapi/jsonpointer v0.22.0/go.mod h1:xt3jV88UtExdIkkL7NloURjRQjbeUgcxFblMjq2iaiU=
|
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||||
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/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
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/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/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
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 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
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.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 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
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 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
||||||
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
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 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
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.12 h1:Fg+zsqzYEs1ZnvmcztTYxhgCBsx3eEhEwQ1W/lHq/sQ=
|
||||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
|
github.com/googleapis/enterprise-certificate-proxy v0.3.12/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=
|
||||||
github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=
|
github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc=
|
||||||
github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc=
|
github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY=
|
||||||
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/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
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/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 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
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 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||||
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
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 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
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 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
|
||||||
github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
|
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/kaptinlin/go-i18n v0.2.11 h1:OayNt8mWt8nDaqAOp09/C1VG9Y5u8LpQnnxbyGARDV4=
|
||||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
github.com/kaptinlin/go-i18n v0.2.11/go.mod h1:pVcu9qsW5pOIOoZFJXesRYmLos1vMQrby70JPAoWmJU=
|
||||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
github.com/kaptinlin/jsonpointer v0.4.16 h1:Ux4w4FY+uLv+K+TxaCJtM/TpPv+1+eS6gH4Z9/uhOuA=
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
github.com/kaptinlin/jsonpointer v0.4.16/go.mod h1:SsfsjqnHG5zuKo1DTBzk1VknaHlL4osHw+X9kZKukpU=
|
||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
github.com/kaptinlin/jsonschema v0.7.3 h1:kyIydij76ORiSxmfy0xFYy0cOx8MwG6pyyaSoQshsK4=
|
||||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
github.com/kaptinlin/jsonschema v0.7.3/go.mod h1:Ys6zr+W6/1330FzZEouFrAYImK+AmYt5HQVTHQQXQo8=
|
||||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
github.com/kaptinlin/messageformat-go v0.4.18 h1:RBlHVWgZyoxTcUgGWBsl2AcyScq/urqbLZvzgryTmSI=
|
||||||
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
|
github.com/kaptinlin/messageformat-go v0.4.18/go.mod h1:ntI3154RnqJgr7GaC+vZBnIExl2V3sv9selvRNNEM24=
|
||||||
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/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
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 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
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.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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
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 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
||||||
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
|
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-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 h1:gfUT0m77E4odfgiHkqV/E+MQVaQ06rbutW7Ln0JRkBA=
|
||||||
github.com/mark3labs/mcp-go v0.44.0-beta.2/go.mod h1:YnJfOL382MIWDx1kMY+2zsRHU/q78dBg9aFb8W6Thdw=
|
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 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
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.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||||
github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ=
|
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/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 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
|
||||||
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
|
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 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||||
github.com/muesli/mango v0.2.0 h1:iNNc0c5VLQ6fsMgAqGQofByNUBH2Q2nEbD6TaI+5yyQ=
|
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/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 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
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/openai/openai-go/v2 v2.7.1 h1:/tfvTJhfv7hTSL8mWwc5VL4WLLSDL5yn9VqVykdu9r8=
|
||||||
github.com/nikolalohinski/gonja v1.5.3/go.mod h1:RmjwxNiXAEqcq1HeK5SSMmqFJvKOfTfXhkJv6YBtPa4=
|
github.com/openai/openai-go/v2 v2.7.1/go.mod h1:jrJs23apqJKKbT+pqtFgNKpRju/KP9zpUTZhz3GElQE=
|
||||||
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/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
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/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s=
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||||
github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
|
||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
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.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rivo/uniseg v0.2.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 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
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.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
github.com/rollbar/rollbar-go v1.0.2/go.mod h1:AcFs5f0I+c71bpHlXNNDbOWJiKwjFDtISeXco0L5PKQ=
|
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
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 h1:FM8Cv6j2KqIhM2ZK7HZjm4mpj9NBktLgowT1aN9q5Cc=
|
||||||
github.com/sagikazarmark/locafero v0.10.0/go.mod h1:Ieo3EUsjifvQu4NZwV5sPd4dwvu0OCgEQV7vjc9yDjw=
|
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.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 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
|
||||||
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
|
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 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
|
||||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
|
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.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||||
github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo=
|
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 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE=
|
||||||
github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
||||||
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
|
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 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
|
||||||
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
|
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.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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
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 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
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/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 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
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 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
|
||||||
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
|
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 h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
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 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
|
||||||
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
|
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=
|
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 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 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs=
|
||||||
github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA=
|
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.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
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.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=
|
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.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
|
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.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0=
|
||||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
|
||||||
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
|
||||||
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
|
||||||
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
|
||||||
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
|
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
|
||||||
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
|
go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
|
go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
|
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
|
||||||
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
|
||||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
|
||||||
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=
|
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
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.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
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.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.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
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.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||||
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b h1:DXr+pvt3nC887026GRP39Ej11UATqWDmWuS99x26cD0=
|
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0=
|
||||||
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
|
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.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.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.12.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.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
golang.org/x/mod v0.17.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-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-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
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.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
|
||||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
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.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
||||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
||||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
|
||||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
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.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.1.0/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.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 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
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-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-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-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-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-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.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.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.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.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
|
||||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
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.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.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
|
||||||
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
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.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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
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.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/text v0.15.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.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
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-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.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
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=
|
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 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
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.267.0 h1:w+vfWPMPYeRs8qH1aYYsFX68jMls5acWl/jocfLomwE=
|
||||||
google.golang.org/api v0.246.0/go.mod h1:dMVhVcylamkirHdzEBAIQWUCgqY885ivNeZYd7VAVr8=
|
google.golang.org/api v0.267.0/go.mod h1:Jzc0+ZfLnyvXma3UtaTl023TdhZu6OMBP9tJ+0EmFD0=
|
||||||
google.golang.org/genai v1.22.0 h1:5hrEhXXWJQZa3tdPocl4vQ/0w6myEAxdNns2Kmx0f4Y=
|
google.golang.org/genai v1.47.0 h1:iWCS7gEdO6rctOqfCYLOrZGKu2D+N42aTnCEcBvB1jo=
|
||||||
google.golang.org/genai v1.22.0/go.mod h1:QPj5NGJw+3wEOHg+PrsWwJKvG6UC84ex5FR7qAYsN/M=
|
google.golang.org/genai v1.47.0/go.mod h1:A3kkl0nyBjyFlNjgxIwKq70julKbIxpSxqKO5gw/gmk=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1 h1:pmJpJEvT846VzausCQ5d7KreSROcDqmO388w5YbnltA=
|
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-20250826171959-ef028d996bc1/go.mod h1:GmFNa4BdJZ2a8G+wCe9Bg3wwThLrJun751XstdJt5Og=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||||
google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4=
|
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
|
||||||
google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
|
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||||
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
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 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-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 h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
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.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 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
+219
-329
@@ -8,102 +8,114 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
tea "charm.land/bubbletea/v2"
|
tea "charm.land/bubbletea/v2"
|
||||||
"github.com/cloudwego/eino/components/model"
|
"charm.land/fantasy"
|
||||||
"github.com/cloudwego/eino/components/tool"
|
|
||||||
"github.com/cloudwego/eino/schema"
|
|
||||||
"github.com/mark3labs/mcp-go/mcp"
|
|
||||||
"github.com/mark3labs/mcphost/internal/config"
|
"github.com/mark3labs/mcphost/internal/config"
|
||||||
"github.com/mark3labs/mcphost/internal/models"
|
"github.com/mark3labs/mcphost/internal/models"
|
||||||
"github.com/mark3labs/mcphost/internal/tools"
|
"github.com/mark3labs/mcphost/internal/tools"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AgentConfig holds configuration options for creating a new Agent.
|
// AgentConfig holds configuration options for creating a new Agent.
|
||||||
// It includes model configuration, MCP settings, and various behavioral options.
|
|
||||||
type AgentConfig struct {
|
type AgentConfig struct {
|
||||||
// ModelConfig specifies the LLM provider and model to use
|
|
||||||
ModelConfig *models.ProviderConfig
|
ModelConfig *models.ProviderConfig
|
||||||
// MCPConfig contains MCP server configurations
|
|
||||||
MCPConfig *config.Config
|
MCPConfig *config.Config
|
||||||
// SystemPrompt is the initial system message for the agent
|
|
||||||
SystemPrompt string
|
SystemPrompt string
|
||||||
// MaxSteps limits the number of tool calls (0 for unlimited)
|
|
||||||
MaxSteps int
|
MaxSteps int
|
||||||
// StreamingEnabled controls whether responses are streamed
|
|
||||||
StreamingEnabled bool
|
StreamingEnabled bool
|
||||||
// DebugLogger is an optional logger for debugging MCP communications
|
DebugLogger tools.DebugLogger
|
||||||
DebugLogger tools.DebugLogger // Optional debug logger
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToolCallHandler is a function type for handling tool calls as they happen.
|
// 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)
|
type ToolCallHandler func(toolName, toolArgs string)
|
||||||
|
|
||||||
// ToolExecutionHandler is a function type for handling tool execution start/end events.
|
// 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)
|
type ToolExecutionHandler func(toolName string, isStarting bool)
|
||||||
|
|
||||||
// ToolResultHandler is a function type for handling tool results.
|
// 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)
|
type ToolResultHandler func(toolName, toolArgs, result string, isError bool)
|
||||||
|
|
||||||
// ResponseHandler is a function type for handling LLM responses.
|
// ResponseHandler is a function type for handling LLM responses.
|
||||||
// It receives the complete response content from the model.
|
|
||||||
type ResponseHandler func(content string)
|
type ResponseHandler func(content string)
|
||||||
|
|
||||||
// StreamingResponseHandler is a function type for handling streaming LLM responses.
|
// 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)
|
type StreamingResponseHandler func(content string)
|
||||||
|
|
||||||
// ToolCallContentHandler is a function type for handling content that accompanies tool calls.
|
// 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)
|
type ToolCallContentHandler func(content string)
|
||||||
|
|
||||||
// ToolApprovalHandler is a function type for handling user approval of tool calls.
|
// 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)
|
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.
|
// It manages the interaction between an LLM and various tools through the MCP protocol.
|
||||||
type Agent struct {
|
type Agent struct {
|
||||||
toolManager *tools.MCPToolManager
|
toolManager *tools.MCPToolManager
|
||||||
model model.ToolCallingChatModel
|
fantasyAgent fantasy.Agent
|
||||||
|
model fantasy.LanguageModel
|
||||||
maxSteps int
|
maxSteps int
|
||||||
systemPrompt string
|
systemPrompt string
|
||||||
loadingMessage string // Message from provider loading (e.g., GPU fallback info)
|
loadingMessage string
|
||||||
providerType string // Provider type for streaming behavior
|
providerType string
|
||||||
streamingEnabled bool // Whether streaming is enabled
|
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.
|
// NewAgent creates a new Agent with MCP tool integration and streaming support.
|
||||||
// It initializes the LLM provider, loads MCP tools, and configures the agent
|
func NewAgent(ctx context.Context, agentConfig *AgentConfig) (*Agent, error) {
|
||||||
// based on the provided configuration. Returns an error if provider creation
|
// Create the LLM provider via fantasy
|
||||||
// or tool loading fails.
|
providerResult, err := models.CreateProvider(ctx, agentConfig.ModelConfig)
|
||||||
func NewAgent(ctx context.Context, config *AgentConfig) (*Agent, error) {
|
|
||||||
// Create the LLM provider
|
|
||||||
providerResult, err := models.CreateProvider(ctx, config.ModelConfig)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create model provider: %v", err)
|
return nil, fmt.Errorf("failed to create model provider: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create and load MCP tools
|
// Create and load MCP tools
|
||||||
toolManager := tools.NewMCPToolManager()
|
toolManager := tools.NewMCPToolManager()
|
||||||
|
|
||||||
// Set the model for sampling support
|
|
||||||
toolManager.SetModel(providerResult.Model)
|
toolManager.SetModel(providerResult.Model)
|
||||||
|
|
||||||
// Set the debug logger if provided
|
if agentConfig.DebugLogger != nil {
|
||||||
if config.DebugLogger != nil {
|
toolManager.SetDebugLogger(agentConfig.DebugLogger)
|
||||||
toolManager.SetDebugLogger(config.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)
|
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
|
// Determine provider type from model string
|
||||||
providerType := "default"
|
providerType := "default"
|
||||||
if config.ModelConfig != nil && config.ModelConfig.ModelString != "" {
|
if agentConfig.ModelConfig != nil && agentConfig.ModelConfig.ModelString != "" {
|
||||||
parts := strings.SplitN(config.ModelConfig.ModelString, ":", 2)
|
parts := strings.SplitN(agentConfig.ModelConfig.ModelString, ":", 2)
|
||||||
if len(parts) >= 1 {
|
if len(parts) >= 1 {
|
||||||
providerType = parts[0]
|
providerType = parts[0]
|
||||||
}
|
}
|
||||||
@@ -111,345 +123,239 @@ func NewAgent(ctx context.Context, config *AgentConfig) (*Agent, error) {
|
|||||||
|
|
||||||
return &Agent{
|
return &Agent{
|
||||||
toolManager: toolManager,
|
toolManager: toolManager,
|
||||||
|
fantasyAgent: fantasyAgent,
|
||||||
model: providerResult.Model,
|
model: providerResult.Model,
|
||||||
maxSteps: config.MaxSteps, // Keep 0 for infinite, handle in loop
|
maxSteps: agentConfig.MaxSteps,
|
||||||
systemPrompt: config.SystemPrompt,
|
systemPrompt: agentConfig.SystemPrompt,
|
||||||
loadingMessage: providerResult.Message,
|
loadingMessage: providerResult.Message,
|
||||||
providerType: providerType,
|
providerType: providerType,
|
||||||
streamingEnabled: config.StreamingEnabled,
|
streamingEnabled: agentConfig.StreamingEnabled,
|
||||||
}, nil
|
}, 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.
|
// 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.
|
func (a *Agent) GenerateWithLoop(ctx context.Context, messages []fantasy.Message,
|
||||||
// This method does not support streaming responses; use GenerateWithLoopAndStreaming for streaming support.
|
onToolCall ToolCallHandler, onToolExecution ToolExecutionHandler, onToolResult ToolResultHandler,
|
||||||
func (a *Agent) GenerateWithLoop(ctx context.Context, messages []*schema.Message,
|
onResponse ResponseHandler, onToolCallContent ToolCallContentHandler, onToolApproval ToolApprovalHandler,
|
||||||
onToolCall ToolCallHandler, onToolExecution ToolExecutionHandler, onToolResult ToolResultHandler, onResponse ResponseHandler, onToolCallContent ToolCallContentHandler, onToolApproval ToolApprovalHandler,
|
|
||||||
) (*GenerateWithLoopResult, error) {
|
) (*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.
|
// GenerateWithLoopAndStreaming processes messages using the fantasy agent with streaming and callbacks.
|
||||||
// It handles the conversation flow, executing tools as needed and invoking callbacks for various events including streaming chunks.
|
// Fantasy handles the tool call loop internally. We map fantasy's rich callback system
|
||||||
// The onStreamingResponse callback is invoked for each content chunk during streaming if streaming is enabled.
|
// to mcphost's existing callback interface for UI integration.
|
||||||
func (a *Agent) GenerateWithLoopAndStreaming(ctx context.Context, messages []*schema.Message,
|
func (a *Agent) GenerateWithLoopAndStreaming(ctx context.Context, messages []fantasy.Message,
|
||||||
onToolCall ToolCallHandler, onToolExecution ToolExecutionHandler, onToolResult ToolResultHandler, onResponse ResponseHandler, onToolCallContent ToolCallContentHandler, onStreamingResponse StreamingResponseHandler, onToolApproval ToolApprovalHandler,
|
onToolCall ToolCallHandler, onToolExecution ToolExecutionHandler, onToolResult ToolResultHandler,
|
||||||
|
onResponse ResponseHandler, onToolCallContent ToolCallContentHandler,
|
||||||
|
onStreamingResponse StreamingResponseHandler, onToolApproval ToolApprovalHandler,
|
||||||
) (*GenerateWithLoopResult, error) {
|
) (*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
|
// Fantasy requires the current user input as Prompt, with prior messages as history.
|
||||||
if a.systemPrompt != "" {
|
// Extract the last user message text as the prompt, and pass everything before it as Messages.
|
||||||
hasSystemMessage := false
|
prompt, history := splitPromptAndHistory(messages)
|
||||||
if len(workingMessages) > 0 && workingMessages[0].Role == schema.System {
|
|
||||||
hasSystemMessage = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if !hasSystemMessage {
|
// Track current tool call info for callbacks
|
||||||
systemMsg := schema.SystemMessage(a.systemPrompt)
|
var currentToolName string
|
||||||
workingMessages = append([]*schema.Message{systemMsg}, workingMessages...)
|
var currentToolArgs string
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get available tools
|
if a.streamingEnabled {
|
||||||
availableTools := a.toolManager.GetTools()
|
// Use fantasy's streaming agent
|
||||||
var toolInfos []*schema.ToolInfo
|
result, err := a.fantasyAgent.Stream(ctx, fantasy.AgentStreamCall{
|
||||||
toolMap := make(map[string]tool.BaseTool)
|
Prompt: prompt,
|
||||||
|
Messages: history,
|
||||||
|
|
||||||
for _, t := range availableTools {
|
// Text streaming callback
|
||||||
info, err := t.Info(ctx)
|
OnTextDelta: func(id, text string) error {
|
||||||
if err != nil {
|
if onStreamingResponse != nil {
|
||||||
continue
|
onStreamingResponse(text)
|
||||||
}
|
|
||||||
if info == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
toolInfos = append(toolInfos, info)
|
|
||||||
toolMap[info.Name] = t
|
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
|
||||||
// Main loop
|
// Tool call complete - the tool has been parsed and is about to execute
|
||||||
for step := 0; a.maxSteps == 0 || step < a.maxSteps; step++ {
|
OnToolCall: func(tc fantasy.ToolCallContent) error {
|
||||||
// Check if context was cancelled before making LLM call
|
currentToolName = tc.ToolName
|
||||||
select {
|
currentToolArgs = tc.Input
|
||||||
case <-ctx.Done():
|
|
||||||
return nil, ctx.Err()
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call the LLM with cancellation support
|
// Check approval if handler is set
|
||||||
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 {
|
|
||||||
if onToolApproval != nil {
|
if onToolApproval != nil {
|
||||||
approved, err := onToolApproval(toolCall.Function.Name, toolCall.Function.Arguments)
|
approved, err := onToolApproval(tc.ToolName, tc.Input)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
if !approved {
|
if !approved {
|
||||||
rejectedMsg := fmt.Sprintf("The user did not allow tool call %s. Reason: User cancelled.", toolCall.Function.Name)
|
return fmt.Errorf("tool call %s rejected by user", tc.ToolName)
|
||||||
toolMessage := schema.ToolMessage(rejectedMsg, toolCall.ID)
|
|
||||||
workingMessages = append(workingMessages, toolMessage)
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notify about tool call
|
// Notify about the tool call
|
||||||
if onToolCall != nil {
|
if onToolCall != nil {
|
||||||
onToolCall(toolCall.Function.Name, toolCall.Function.Arguments)
|
onToolCall(tc.ToolName, tc.Input)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute the tool
|
// Notify tool execution starting
|
||||||
if selectedTool, exists := toolMap[toolCall.Function.Name]; exists {
|
|
||||||
// Notify tool execution start
|
|
||||||
if onToolExecution != nil {
|
if onToolExecution != nil {
|
||||||
onToolExecution(toolCall.Function.Name, true)
|
onToolExecution(tc.ToolName, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sanitize arguments for common LLM junk like "}{"
|
return nil
|
||||||
arguments := toolCall.Function.Arguments
|
},
|
||||||
if len(arguments) > 0 && strings.Trim(arguments, " \t\n\r{}") == "" {
|
|
||||||
arguments = "{}"
|
|
||||||
}
|
|
||||||
|
|
||||||
output, err := selectedTool.(tool.InvokableTool).InvokableRun(ctx, arguments)
|
// Tool result - tool execution completed
|
||||||
|
OnToolResult: func(tr fantasy.ToolResultContent) error {
|
||||||
// Notify tool execution end
|
// Notify tool execution finished
|
||||||
if onToolExecution != nil {
|
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 {
|
if err != nil {
|
||||||
errorMsg := fmt.Sprintf("Tool execution error: %v", err)
|
return nil, err
|
||||||
toolMessage := schema.ToolMessage(errorMsg, toolCall.ID)
|
|
||||||
workingMessages = append(workingMessages, toolMessage)
|
|
||||||
|
|
||||||
if onToolResult != nil {
|
|
||||||
onToolResult(toolCall.Function.Name, toolCall.Function.Arguments, errorMsg, true)
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// Check if this is an MCP tool response with an error
|
return convertAgentResult(result, messages), nil
|
||||||
isError := false
|
}
|
||||||
if output != "" {
|
|
||||||
var mcpResult mcp.CallToolResult
|
// Non-streaming path
|
||||||
if err := json.Unmarshal([]byte(output), &mcpResult); err == nil && mcpResult.IsError {
|
result, err := a.fantasyAgent.Generate(ctx, fantasy.AgentCall{
|
||||||
isError = true
|
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)
|
// No user message found — use the last message's text as prompt
|
||||||
workingMessages = append(workingMessages, toolMessage)
|
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 {
|
return "", messages
|
||||||
onToolResult(toolCall.Function.Name, toolCall.Function.Arguments, output, isError)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} else {
|
|
||||||
errorMsg := fmt.Sprintf("Tool not found: %s", toolCall.Function.Name)
|
|
||||||
toolMessage := schema.ToolMessage(errorMsg, toolCall.ID)
|
|
||||||
workingMessages = append(workingMessages, toolMessage)
|
|
||||||
|
|
||||||
if onToolResult != nil {
|
// convertAgentResult converts a fantasy AgentResult to our GenerateWithLoopResult.
|
||||||
onToolResult(toolCall.Function.Name, toolCall.Function.Arguments, errorMsg, true)
|
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...)
|
||||||
} else {
|
|
||||||
// This is a final response
|
for _, step := range result.Steps {
|
||||||
if onResponse != nil && response.Content != "" {
|
allMessages = append(allMessages, step.Messages...)
|
||||||
onResponse(response.Content)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return &GenerateWithLoopResult{
|
return &GenerateWithLoopResult{
|
||||||
FinalResponse: response,
|
FinalResponse: &result.Response,
|
||||||
ConversationMessages: workingMessages,
|
ConversationMessages: allMessages,
|
||||||
}, nil
|
TotalUsage: result.TotalUsage,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we reach here, we've exceeded max steps
|
// extractToolResultText extracts the text and error status from a fantasy ToolResultContent.
|
||||||
finalResponse := schema.AssistantMessage("Maximum number of steps reached.", nil)
|
func extractToolResultText(tr fantasy.ToolResultContent) (string, bool) {
|
||||||
return &GenerateWithLoopResult{
|
if tr.Result == nil {
|
||||||
FinalResponse: finalResponse,
|
return "", false
|
||||||
ConversationMessages: workingMessages,
|
}
|
||||||
}, nil
|
|
||||||
|
// 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.
|
// 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() []fantasy.AgentTool {
|
||||||
func (a *Agent) GetTools() []tool.BaseTool {
|
|
||||||
return a.toolManager.GetTools()
|
return a.toolManager.GetTools()
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetLoadingMessage returns the loading message from provider creation.
|
// 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 {
|
func (a *Agent) GetLoadingMessage() string {
|
||||||
return a.loadingMessage
|
return a.loadingMessage
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetLoadedServerNames returns the names of successfully loaded MCP servers.
|
// GetLoadedServerNames returns the names of successfully loaded MCP servers.
|
||||||
// This includes both builtin servers and external MCP server configurations.
|
|
||||||
func (a *Agent) GetLoadedServerNames() []string {
|
func (a *Agent) GetLoadedServerNames() []string {
|
||||||
return a.toolManager.GetLoadedServerNames()
|
return a.toolManager.GetLoadedServerNames()
|
||||||
}
|
}
|
||||||
|
|
||||||
// generateWithCancellationAndStreaming calls the LLM with ESC key cancellation support and streaming callbacks
|
// GetModel returns the underlying fantasy LanguageModel.
|
||||||
func (a *Agent) generateWithCancellationAndStreaming(ctx context.Context, messages []*schema.Message, toolInfos []*schema.ToolInfo, streamingCallback StreamingResponseHandler) (*schema.Message, error) {
|
func (a *Agent) GetModel() fantasy.LanguageModel {
|
||||||
// Check if streaming is enabled
|
return a.model
|
||||||
if !a.streamingEnabled {
|
|
||||||
// Use traditional non-streaming approach
|
|
||||||
return a.generateWithoutStreaming(ctx, messages, toolInfos)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try streaming first if no tools are expected or if we can detect tool calls early
|
// Close closes the agent and cleans up resources.
|
||||||
if len(toolInfos) == 0 {
|
func (a *Agent) Close() error {
|
||||||
// No tools available, use streaming directly
|
return a.toolManager.Close()
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// escListenerModel is a simple Bubble Tea model for ESC key detection
|
// 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) {
|
func (m escListenerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
switch msg := msg.(type) {
|
if msg, ok := msg.(tea.KeyPressMsg); ok {
|
||||||
case tea.KeyPressMsg:
|
|
||||||
if msg.String() == "esc" {
|
if msg.String() == "esc" {
|
||||||
// Signal ESC was pressed
|
|
||||||
select {
|
select {
|
||||||
case m.escPressed <- true:
|
case m.escPressed <- true:
|
||||||
default:
|
default:
|
||||||
@@ -477,7 +381,7 @@ func (m escListenerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m escListenerModel) View() tea.View {
|
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
|
// 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,
|
escPressed: escPressed,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a Bubble Tea program
|
|
||||||
p := tea.NewProgram(model, tea.WithoutRenderer())
|
p := tea.NewProgram(model, tea.WithoutRenderer())
|
||||||
|
|
||||||
// Start the program in a goroutine
|
|
||||||
go func() {
|
go func() {
|
||||||
if _, err := p.Run(); err != nil {
|
if _, err := p.Run(); err != nil {
|
||||||
// Program failed, try to signal completion
|
|
||||||
select {
|
select {
|
||||||
case escPressed <- false:
|
case escPressed <- false:
|
||||||
default:
|
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() {
|
go func() {
|
||||||
time.Sleep(10 * time.Millisecond)
|
time.Sleep(10 * time.Millisecond)
|
||||||
select {
|
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 {
|
select {
|
||||||
case <-stopChan:
|
case <-stopChan:
|
||||||
p.Kill()
|
p.Kill()
|
||||||
// Give the program time to fully terminate
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
time.Sleep(50 * time.Millisecond)
|
||||||
return false
|
return false
|
||||||
case pressed := <-escPressed:
|
case pressed := <-escPressed:
|
||||||
p.Kill()
|
p.Kill()
|
||||||
// Give the program time to fully terminate
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
time.Sleep(50 * time.Millisecond)
|
||||||
return pressed
|
return pressed
|
||||||
case <-time.After(30 * time.Second):
|
case <-time.After(30 * time.Second):
|
||||||
// Timeout after 30 seconds to prevent hanging
|
|
||||||
p.Kill()
|
p.Kill()
|
||||||
time.Sleep(50 * time.Millisecond)
|
time.Sleep(50 * time.Millisecond)
|
||||||
return false
|
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)
|
parsedCode, parsedState := c.parseCodeAndState(code)
|
||||||
|
|
||||||
// Build request body
|
// Build request body
|
||||||
reqBody := map[string]interface{}{
|
reqBody := map[string]any{
|
||||||
"code": parsedCode,
|
"code": parsedCode,
|
||||||
"grant_type": "authorization_code",
|
"grant_type": "authorization_code",
|
||||||
"client_id": c.ClientID,
|
"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
|
// rotated), and new expiration timestamp. Returns an error if the refresh fails or
|
||||||
// the refresh token is invalid.
|
// the refresh token is invalid.
|
||||||
func (c *OAuthClient) RefreshToken(refreshToken string) (*AnthropicCredentials, error) {
|
func (c *OAuthClient) RefreshToken(refreshToken string) (*AnthropicCredentials, error) {
|
||||||
reqBody := map[string]interface{}{
|
reqBody := map[string]any{
|
||||||
"grant_type": "refresh_token",
|
"grant_type": "refresh_token",
|
||||||
"refresh_token": refreshToken,
|
"refresh_token": refreshToken,
|
||||||
"client_id": c.ClientID,
|
"client_id": c.ClientID,
|
||||||
@@ -141,7 +141,7 @@ func (c *OAuthClient) RefreshToken(refreshToken string) (*AnthropicCredentials,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// makeTokenRequest makes a token request to the OAuth server
|
// 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)
|
jsonBody, err := json.Marshal(body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to marshal request body: %w", err)
|
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()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
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 {
|
if err := json.NewDecoder(resp.Body).Decode(&errorResp); err == nil {
|
||||||
return nil, fmt.Errorf("token request failed: %v", errorResp)
|
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
|
timeout := defaultTimeout
|
||||||
if timeoutMs := request.GetFloat("timeout", 0); timeoutMs > 0 {
|
if timeoutMs := request.GetFloat("timeout", 0); timeoutMs > 0 {
|
||||||
timeoutDuration := time.Duration(timeoutMs) * time.Millisecond
|
timeoutDuration := time.Duration(timeoutMs) * time.Millisecond
|
||||||
if timeoutDuration > maxTimeout {
|
timeout = min(timeoutDuration, maxTimeout)
|
||||||
timeout = maxTimeout
|
|
||||||
} else {
|
|
||||||
timeout = timeoutDuration
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for banned commands
|
// Check for banned commands
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package builtin
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"slices"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/mark3labs/mcp-go/mcp"
|
"github.com/mark3labs/mcp-go/mcp"
|
||||||
@@ -23,13 +24,7 @@ func TestBashServerRegistry(t *testing.T) {
|
|||||||
|
|
||||||
// Test that bash server is registered
|
// Test that bash server is registered
|
||||||
servers := registry.ListServers()
|
servers := registry.ListServers()
|
||||||
found := false
|
found := slices.Contains(servers, "bash")
|
||||||
for _, name := range servers {
|
|
||||||
if name == "bash" {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !found {
|
if !found {
|
||||||
t.Error("bash server not found in registry")
|
t.Error("bash server not found in registry")
|
||||||
|
|||||||
@@ -73,11 +73,7 @@ func executeFetch(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallTo
|
|||||||
timeout := defaultFetchTimeout
|
timeout := defaultFetchTimeout
|
||||||
if timeoutSec := request.GetFloat("timeout", 0); timeoutSec > 0 {
|
if timeoutSec := request.GetFloat("timeout", 0); timeoutSec > 0 {
|
||||||
timeoutDuration := time.Duration(timeoutSec) * time.Second
|
timeoutDuration := time.Duration(timeoutSec) * time.Second
|
||||||
if timeoutDuration > maxFetchTimeout {
|
timeout = min(timeoutDuration, maxFetchTimeout)
|
||||||
timeout = maxFetchTimeout
|
|
||||||
} else {
|
|
||||||
timeout = timeoutDuration
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate URL
|
// Validate URL
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -26,13 +27,7 @@ func TestFetchServerRegistry(t *testing.T) {
|
|||||||
|
|
||||||
// Test that fetch server is registered
|
// Test that fetch server is registered
|
||||||
servers := registry.ListServers()
|
servers := registry.ListServers()
|
||||||
found := false
|
found := slices.Contains(servers, "fetch")
|
||||||
for _, name := range servers {
|
|
||||||
if name == "fetch" {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !found {
|
if !found {
|
||||||
t.Error("fetch server not found in registry")
|
t.Error("fetch server not found in registry")
|
||||||
|
|||||||
+22
-99
@@ -10,13 +10,13 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/JohannesKaufmann/html-to-markdown"
|
md "github.com/JohannesKaufmann/html-to-markdown"
|
||||||
"github.com/PuerkitoBio/goquery"
|
"github.com/PuerkitoBio/goquery"
|
||||||
"github.com/cloudwego/eino/components/model"
|
"github.com/tidwall/gjson"
|
||||||
"github.com/cloudwego/eino/schema"
|
|
||||||
|
"charm.land/fantasy"
|
||||||
"github.com/mark3labs/mcp-go/mcp"
|
"github.com/mark3labs/mcp-go/mcp"
|
||||||
"github.com/mark3labs/mcp-go/server"
|
"github.com/mark3labs/mcp-go/server"
|
||||||
"github.com/tidwall/gjson"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -26,15 +26,14 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// httpServerModel holds the model for the HTTP server
|
// 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.
|
// NewHTTPServer creates a new MCP server providing advanced HTTP fetching capabilities.
|
||||||
// The server includes tools for fetching web content, summarizing pages, extracting
|
// The server includes tools for fetching web content, summarizing pages, extracting
|
||||||
// specific information, and filtering JSON responses. If an LLM model is provided,
|
// specific information, and filtering JSON responses. If an LLM model is provided,
|
||||||
// AI-powered summarization and extraction tools are enabled. Returns an error if
|
// AI-powered summarization and extraction tools are enabled. Returns an error if
|
||||||
// server initialization fails.
|
// server initialization fails.
|
||||||
func NewHTTPServer(llmModel model.ToolCallingChatModel) (*server.MCPServer, error) {
|
func NewHTTPServer(llmModel fantasy.LanguageModel) (*server.MCPServer, error) {
|
||||||
// Store the model globally for use in tool handlers
|
|
||||||
httpServerModel = llmModel
|
httpServerModel = llmModel
|
||||||
|
|
||||||
s := server.NewMCPServer("http-server", "1.0.0", server.WithToolCapabilities(true))
|
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)
|
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 {
|
if llmModel != nil {
|
||||||
summarizeTool := mcp.NewTool("fetch_summarize",
|
summarizeTool := mcp.NewTool("fetch_summarize",
|
||||||
mcp.WithDescription(httpSummarizeDescription),
|
mcp.WithDescription(httpSummarizeDescription),
|
||||||
@@ -114,7 +113,6 @@ func NewHTTPServer(llmModel model.ToolCallingChatModel) (*server.MCPServer, erro
|
|||||||
|
|
||||||
// executeHTTPFetch handles the fetch tool execution
|
// executeHTTPFetch handles the fetch tool execution
|
||||||
func executeHTTPFetch(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
func executeHTTPFetch(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
// Extract parameters
|
|
||||||
urlStr, err := request.RequireString("url")
|
urlStr, err := request.RequireString("url")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return mcp.NewToolResultError("url parameter is required and must be a string"), 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
|
return mcp.NewToolResultError("format parameter is required and must be a string"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate format
|
|
||||||
if format != "html" && format != "markdown" {
|
if format != "html" && format != "markdown" {
|
||||||
return mcp.NewToolResultError("format must be 'html' or 'markdown'"), nil
|
return mcp.NewToolResultError("format must be 'html' or 'markdown'"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get bodyOnly parameter (optional, defaults to false)
|
|
||||||
bodyOnly := request.GetBool("bodyOnly", false)
|
bodyOnly := request.GetBool("bodyOnly", false)
|
||||||
|
|
||||||
// Parse timeout (optional)
|
|
||||||
timeout := httpDefaultFetchTimeout
|
timeout := httpDefaultFetchTimeout
|
||||||
if timeoutSec := request.GetFloat("timeout", 0); timeoutSec > 0 {
|
if timeoutSec := request.GetFloat("timeout", 0); timeoutSec > 0 {
|
||||||
timeoutDuration := time.Duration(timeoutSec) * time.Second
|
timeoutDuration := time.Duration(timeoutSec) * time.Second
|
||||||
if timeoutDuration > httpMaxFetchTimeout {
|
timeout = min(timeoutDuration, httpMaxFetchTimeout)
|
||||||
timeout = httpMaxFetchTimeout
|
|
||||||
} else {
|
|
||||||
timeout = timeoutDuration
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate URL
|
|
||||||
parsedURL, err := url.Parse(urlStr)
|
parsedURL, err := url.Parse(urlStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return mcp.NewToolResultError(fmt.Sprintf("invalid URL: %v", err)), nil
|
return mcp.NewToolResultError(fmt.Sprintf("invalid URL: %v", err)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure URL has a scheme
|
|
||||||
if parsedURL.Scheme == "" {
|
if parsedURL.Scheme == "" {
|
||||||
urlStr = "https://" + urlStr
|
urlStr = "https://" + urlStr
|
||||||
parsedURL, err = url.Parse(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" {
|
if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" {
|
||||||
return mcp.NewToolResultError("URL must use http:// or https://"), nil
|
return mcp.NewToolResultError("URL must use http:// or https://"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create HTTP client with timeout
|
|
||||||
client := &http.Client{
|
client := &http.Client{
|
||||||
Timeout: timeout,
|
Timeout: timeout,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create request with context
|
|
||||||
req, err := http.NewRequestWithContext(ctx, "GET", urlStr, nil)
|
req, err := http.NewRequestWithContext(ctx, "GET", urlStr, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return mcp.NewToolResultError(fmt.Sprintf("failed to create request: %v", 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("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", "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")
|
req.Header.Set("Accept-Language", "en-US,en;q=0.9")
|
||||||
|
|
||||||
// Make the request
|
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return mcp.NewToolResultError(fmt.Sprintf("request failed: %v", err)), nil
|
return mcp.NewToolResultError(fmt.Sprintf("request failed: %v", err)), nil
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
// Check status code
|
|
||||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||||
return mcp.NewToolResultError(fmt.Sprintf("request failed with status code: %d", resp.StatusCode)), nil
|
return mcp.NewToolResultError(fmt.Sprintf("request failed with status code: %d", resp.StatusCode)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check content length
|
|
||||||
if resp.ContentLength > httpMaxResponseSize {
|
if resp.ContentLength > httpMaxResponseSize {
|
||||||
return mcp.NewToolResultError("response too large (exceeds 5MB limit)"), nil
|
return mcp.NewToolResultError("response too large (exceeds 5MB limit)"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read response body with size limit
|
|
||||||
limitedReader := io.LimitReader(resp.Body, httpMaxResponseSize+1)
|
limitedReader := io.LimitReader(resp.Body, httpMaxResponseSize+1)
|
||||||
bodyBytes, err := io.ReadAll(limitedReader)
|
bodyBytes, err := io.ReadAll(limitedReader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return mcp.NewToolResultError(fmt.Sprintf("failed to read response: %v", 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 {
|
if len(bodyBytes) > httpMaxResponseSize {
|
||||||
return mcp.NewToolResultError("response too large (exceeds 5MB limit)"), nil
|
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"
|
contentType = "unknown"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract body content if requested
|
|
||||||
if bodyOnly && strings.Contains(contentType, "text/html") {
|
if bodyOnly && strings.Contains(contentType, "text/html") {
|
||||||
content, err = extractBodyContent(content)
|
content, err = extractBodyContent(content)
|
||||||
if err != nil {
|
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
|
var output string
|
||||||
switch format {
|
switch format {
|
||||||
case "html":
|
case "html":
|
||||||
output = content
|
output = content
|
||||||
|
|
||||||
case "markdown":
|
case "markdown":
|
||||||
if strings.Contains(contentType, "text/html") {
|
if strings.Contains(contentType, "text/html") {
|
||||||
output, err = httpConvertHTMLToMarkdown(content)
|
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
|
return mcp.NewToolResultError(fmt.Sprintf("failed to convert HTML to markdown: %v", err)), nil
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Non-HTML content, wrap in code block
|
|
||||||
output = "```\n" + content + "\n```"
|
output = "```\n" + content + "\n```"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create result with metadata
|
|
||||||
title := fmt.Sprintf("%s (%s)", urlStr, contentType)
|
title := fmt.Sprintf("%s (%s)", urlStr, contentType)
|
||||||
result := mcp.NewToolResultText(output)
|
result := mcp.NewToolResultText(output)
|
||||||
result.Meta = &mcp.Meta{
|
result.Meta = &mcp.Meta{
|
||||||
@@ -263,14 +238,11 @@ func extractBodyContent(htmlContent string) (string, error) {
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the body tag
|
|
||||||
bodySelection := doc.Find("body")
|
bodySelection := doc.Find("body")
|
||||||
if bodySelection.Length() == 0 {
|
if bodySelection.Length() == 0 {
|
||||||
// No body tag found, return the original content
|
|
||||||
return htmlContent, nil
|
return htmlContent, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the inner HTML of the body tag
|
|
||||||
bodyHTML, err := bodySelection.Html()
|
bodyHTML, err := bodySelection.Html()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@@ -283,7 +255,6 @@ func extractBodyContent(htmlContent string) (string, error) {
|
|||||||
func httpConvertHTMLToMarkdown(htmlContent string) (string, error) {
|
func httpConvertHTMLToMarkdown(htmlContent string) (string, error) {
|
||||||
converter := md.NewConverter("", true, nil)
|
converter := md.NewConverter("", true, nil)
|
||||||
|
|
||||||
// Remove unwanted elements
|
|
||||||
converter.Remove("script")
|
converter.Remove("script")
|
||||||
converter.Remove("style")
|
converter.Remove("style")
|
||||||
converter.Remove("meta")
|
converter.Remove("meta")
|
||||||
@@ -300,43 +271,39 @@ func httpConvertHTMLToMarkdown(htmlContent string) (string, error) {
|
|||||||
|
|
||||||
// executeHTTPFetchSummarize handles the fetch_summarize tool execution
|
// executeHTTPFetchSummarize handles the fetch_summarize tool execution
|
||||||
func executeHTTPFetchSummarize(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
func executeHTTPFetchSummarize(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
// Get URL
|
|
||||||
urlStr, err := request.RequireString("url")
|
urlStr, err := request.RequireString("url")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return mcp.NewToolResultError("url parameter is required and must be a string"), 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.")
|
instructions := request.GetString("instructions", "Provide a concise summary of this content.")
|
||||||
|
|
||||||
// Fetch content as text (reuse existing logic)
|
|
||||||
content, err := httpFetchAndExtractText(ctx, urlStr)
|
content, err := httpFetchAndExtractText(ctx, urlStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return mcp.NewToolResultError(fmt.Sprintf("Failed to fetch content: %v", err)), nil
|
return mcp.NewToolResultError(fmt.Sprintf("Failed to fetch content: %v", err)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if we have a model available
|
|
||||||
if httpServerModel == nil {
|
if httpServerModel == nil {
|
||||||
return mcp.NewToolResultError("LLM model not available for summarization"), nil
|
return mcp.NewToolResultError("LLM model not available for summarization"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create messages for the LLM
|
// Use fantasy model for summarization
|
||||||
messages := []*schema.Message{
|
call := fantasy.Call{
|
||||||
schema.UserMessage(fmt.Sprintf("%s\n\nContent to summarize:\n%s", instructions, content)),
|
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, call)
|
||||||
response, err := httpServerModel.Generate(ctx, messages)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return mcp.NewToolResultError(fmt.Sprintf("Summarization failed: %v", err)), nil
|
return mcp.NewToolResultError(fmt.Sprintf("Summarization failed: %v", err)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return summary
|
|
||||||
return &mcp.CallToolResult{
|
return &mcp.CallToolResult{
|
||||||
Content: []mcp.Content{
|
Content: []mcp.Content{
|
||||||
mcp.TextContent{
|
mcp.TextContent{
|
||||||
Type: "text",
|
Type: "text",
|
||||||
Text: response.Content,
|
Text: response.Content.Text(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}, nil
|
}, nil
|
||||||
@@ -344,30 +311,25 @@ func executeHTTPFetchSummarize(ctx context.Context, request mcp.CallToolRequest)
|
|||||||
|
|
||||||
// executeHTTPFetchExtract handles the fetch_extract tool execution
|
// executeHTTPFetchExtract handles the fetch_extract tool execution
|
||||||
func executeHTTPFetchExtract(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
func executeHTTPFetchExtract(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
// Get URL
|
|
||||||
urlStr, err := request.RequireString("url")
|
urlStr, err := request.RequireString("url")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return mcp.NewToolResultError("url parameter is required and must be a string"), nil
|
return mcp.NewToolResultError("url parameter is required and must be a string"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get extraction instructions
|
|
||||||
instructions, err := request.RequireString("instructions")
|
instructions, err := request.RequireString("instructions")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return mcp.NewToolResultError("instructions parameter is required and must be a string"), 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)
|
content, err := httpFetchAndExtractText(ctx, urlStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return mcp.NewToolResultError(fmt.Sprintf("Failed to fetch content: %v", err)), nil
|
return mcp.NewToolResultError(fmt.Sprintf("Failed to fetch content: %v", err)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if we have a model available
|
|
||||||
if httpServerModel == nil {
|
if httpServerModel == nil {
|
||||||
return mcp.NewToolResultError("LLM model not available for extraction"), 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.
|
extractionPrompt := fmt.Sprintf(`Extract the requested information from the following web content.
|
||||||
|
|
||||||
Extraction Instructions: %s
|
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)
|
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
|
call := fantasy.Call{
|
||||||
messages := []*schema.Message{
|
Prompt: fantasy.Prompt{
|
||||||
schema.UserMessage(extractionPrompt),
|
fantasy.NewUserMessage(extractionPrompt),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate extraction using the model directly
|
response, err := httpServerModel.Generate(ctx, call)
|
||||||
response, err := httpServerModel.Generate(ctx, messages)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return mcp.NewToolResultError(fmt.Sprintf("Extraction failed: %v", err)), nil
|
return mcp.NewToolResultError(fmt.Sprintf("Extraction failed: %v", err)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return extracted data
|
|
||||||
return &mcp.CallToolResult{
|
return &mcp.CallToolResult{
|
||||||
Content: []mcp.Content{
|
Content: []mcp.Content{
|
||||||
mcp.TextContent{
|
mcp.TextContent{
|
||||||
Type: "text",
|
Type: "text",
|
||||||
Text: response.Content,
|
Text: response.Content.Text(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}, nil
|
}, 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
|
// httpFetchAndExtractText fetches content from URL and extracts as text
|
||||||
func httpFetchAndExtractText(ctx context.Context, urlStr string) (string, error) {
|
func httpFetchAndExtractText(ctx context.Context, urlStr string) (string, error) {
|
||||||
// Parse timeout (use default)
|
|
||||||
timeout := httpDefaultFetchTimeout
|
timeout := httpDefaultFetchTimeout
|
||||||
|
|
||||||
// Validate URL
|
|
||||||
parsedURL, err := url.Parse(urlStr)
|
parsedURL, err := url.Parse(urlStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("invalid URL: %v", err)
|
return "", fmt.Errorf("invalid URL: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure URL has a scheme
|
|
||||||
if parsedURL.Scheme == "" {
|
if parsedURL.Scheme == "" {
|
||||||
urlStr = "https://" + urlStr
|
urlStr = "https://" + urlStr
|
||||||
parsedURL, err = url.Parse(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" {
|
if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" {
|
||||||
return "", fmt.Errorf("URL must use http:// or https://")
|
return "", fmt.Errorf("URL must use http:// or https://")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create HTTP client with timeout
|
|
||||||
client := &http.Client{
|
client := &http.Client{
|
||||||
Timeout: timeout,
|
Timeout: timeout,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create request with context
|
|
||||||
req, err := http.NewRequestWithContext(ctx, "GET", urlStr, nil)
|
req, err := http.NewRequestWithContext(ctx, "GET", urlStr, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to create request: %v", err)
|
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("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", "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")
|
req.Header.Set("Accept-Language", "en-US,en;q=0.9")
|
||||||
|
|
||||||
// Make the request
|
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("request failed: %v", err)
|
return "", fmt.Errorf("request failed: %v", err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
// Check status code
|
|
||||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||||
return "", fmt.Errorf("request failed with status code: %d", resp.StatusCode)
|
return "", fmt.Errorf("request failed with status code: %d", resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check content length
|
|
||||||
if resp.ContentLength > httpMaxResponseSize {
|
if resp.ContentLength > httpMaxResponseSize {
|
||||||
return "", fmt.Errorf("response too large (exceeds 5MB limit)")
|
return "", fmt.Errorf("response too large (exceeds 5MB limit)")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read response body with size limit
|
|
||||||
limitedReader := io.LimitReader(resp.Body, httpMaxResponseSize+1)
|
limitedReader := io.LimitReader(resp.Body, httpMaxResponseSize+1)
|
||||||
bodyBytes, err := io.ReadAll(limitedReader)
|
bodyBytes, err := io.ReadAll(limitedReader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to read response: %v", err)
|
return "", fmt.Errorf("failed to read response: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if we exceeded the size limit
|
|
||||||
if len(bodyBytes) > httpMaxResponseSize {
|
if len(bodyBytes) > httpMaxResponseSize {
|
||||||
return "", fmt.Errorf("response too large (exceeds 5MB limit)")
|
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)
|
content := string(bodyBytes)
|
||||||
contentType := resp.Header.Get("Content-Type")
|
contentType := resp.Header.Get("Content-Type")
|
||||||
|
|
||||||
// Extract text content
|
|
||||||
if strings.Contains(contentType, "text/html") {
|
if strings.Contains(contentType, "text/html") {
|
||||||
return httpExtractTextFromHTML(content)
|
return httpExtractTextFromHTML(content)
|
||||||
}
|
}
|
||||||
@@ -486,13 +434,10 @@ func httpExtractTextFromHTML(htmlContent string) (string, error) {
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove script, style, and other non-content elements
|
|
||||||
doc.Find("script, style, noscript, iframe, object, embed").Remove()
|
doc.Find("script, style, noscript, iframe, object, embed").Remove()
|
||||||
|
|
||||||
// Extract text content
|
|
||||||
text := doc.Text()
|
text := doc.Text()
|
||||||
|
|
||||||
// Clean up whitespace
|
|
||||||
lines := strings.Split(text, "\n")
|
lines := strings.Split(text, "\n")
|
||||||
var cleanLines []string
|
var cleanLines []string
|
||||||
for _, line := range lines {
|
for _, line := range lines {
|
||||||
@@ -507,7 +452,6 @@ func httpExtractTextFromHTML(htmlContent string) (string, error) {
|
|||||||
|
|
||||||
// executeHTTPFetchFilteredJSON handles the fetch_filtered_json tool execution
|
// executeHTTPFetchFilteredJSON handles the fetch_filtered_json tool execution
|
||||||
func executeHTTPFetchFilteredJSON(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
func executeHTTPFetchFilteredJSON(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
// Extract parameters
|
|
||||||
urlStr, err := request.RequireString("url")
|
urlStr, err := request.RequireString("url")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return mcp.NewToolResultError("url parameter is required and must be a string"), 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
|
return mcp.NewToolResultError("path parameter is required and must be a string"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse timeout (optional)
|
|
||||||
timeout := httpDefaultFetchTimeout
|
timeout := httpDefaultFetchTimeout
|
||||||
if timeoutSec := request.GetFloat("timeout", 0); timeoutSec > 0 {
|
if timeoutSec := request.GetFloat("timeout", 0); timeoutSec > 0 {
|
||||||
timeoutDuration := time.Duration(timeoutSec) * time.Second
|
timeoutDuration := time.Duration(timeoutSec) * time.Second
|
||||||
if timeoutDuration > httpMaxFetchTimeout {
|
timeout = min(timeoutDuration, httpMaxFetchTimeout)
|
||||||
timeout = httpMaxFetchTimeout
|
|
||||||
} else {
|
|
||||||
timeout = timeoutDuration
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate URL
|
|
||||||
parsedURL, err := url.Parse(urlStr)
|
parsedURL, err := url.Parse(urlStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return mcp.NewToolResultError(fmt.Sprintf("invalid URL: %v", err)), nil
|
return mcp.NewToolResultError(fmt.Sprintf("invalid URL: %v", err)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure URL has a scheme
|
|
||||||
if parsedURL.Scheme == "" {
|
if parsedURL.Scheme == "" {
|
||||||
urlStr = "https://" + urlStr
|
urlStr = "https://" + urlStr
|
||||||
parsedURL, err = url.Parse(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" {
|
if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" {
|
||||||
return mcp.NewToolResultError("URL must use http:// or https://"), nil
|
return mcp.NewToolResultError("URL must use http:// or https://"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create HTTP client with timeout
|
|
||||||
client := &http.Client{
|
client := &http.Client{
|
||||||
Timeout: timeout,
|
Timeout: timeout,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create request with context
|
|
||||||
req, err := http.NewRequestWithContext(ctx, "GET", urlStr, nil)
|
req, err := http.NewRequestWithContext(ctx, "GET", urlStr, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return mcp.NewToolResultError(fmt.Sprintf("failed to create request: %v", 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("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", "application/json, text/plain, */*")
|
||||||
req.Header.Set("Accept-Language", "en-US,en;q=0.9")
|
req.Header.Set("Accept-Language", "en-US,en;q=0.9")
|
||||||
|
|
||||||
// Make the request
|
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return mcp.NewToolResultError(fmt.Sprintf("request failed: %v", err)), nil
|
return mcp.NewToolResultError(fmt.Sprintf("request failed: %v", err)), nil
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
// Check status code
|
|
||||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||||
return mcp.NewToolResultError(fmt.Sprintf("request failed with status code: %d", resp.StatusCode)), nil
|
return mcp.NewToolResultError(fmt.Sprintf("request failed with status code: %d", resp.StatusCode)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check content length
|
|
||||||
if resp.ContentLength > httpMaxResponseSize {
|
if resp.ContentLength > httpMaxResponseSize {
|
||||||
return mcp.NewToolResultError("response too large (exceeds 5MB limit)"), nil
|
return mcp.NewToolResultError("response too large (exceeds 5MB limit)"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read response body with size limit
|
|
||||||
limitedReader := io.LimitReader(resp.Body, httpMaxResponseSize+1)
|
limitedReader := io.LimitReader(resp.Body, httpMaxResponseSize+1)
|
||||||
bodyBytes, err := io.ReadAll(limitedReader)
|
bodyBytes, err := io.ReadAll(limitedReader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return mcp.NewToolResultError(fmt.Sprintf("failed to read response: %v", 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 {
|
if len(bodyBytes) > httpMaxResponseSize {
|
||||||
return mcp.NewToolResultError("response too large (exceeds 5MB limit)"), nil
|
return mcp.NewToolResultError("response too large (exceeds 5MB limit)"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
content := string(bodyBytes)
|
content := string(bodyBytes)
|
||||||
|
|
||||||
// Validate that the content is valid JSON
|
|
||||||
if !json.Valid(bodyBytes) {
|
if !json.Valid(bodyBytes) {
|
||||||
return mcp.NewToolResultError("response is not valid JSON"), nil
|
return mcp.NewToolResultError("response is not valid JSON"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply gjson path to filter the JSON
|
|
||||||
result := gjson.Get(content, path)
|
result := gjson.Get(content, path)
|
||||||
if !result.Exists() {
|
if !result.Exists() {
|
||||||
return mcp.NewToolResultError(fmt.Sprintf("gjson path '%s' did not match any data", path)), nil
|
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
|
var filteredJSON string
|
||||||
if result.IsArray() || result.IsObject() {
|
if result.IsArray() || result.IsObject() {
|
||||||
filteredJSON = result.Raw
|
filteredJSON = result.Raw
|
||||||
} else {
|
} else {
|
||||||
// For primitive values, wrap in quotes if it's a string
|
|
||||||
if result.Type == gjson.String {
|
if result.Type == gjson.String {
|
||||||
filteredJSON = fmt.Sprintf(`"%s"`, result.Str)
|
filteredJSON = fmt.Sprintf(`"%s"`, result.Str)
|
||||||
} else {
|
} else {
|
||||||
@@ -620,7 +544,6 @@ func executeHTTPFetchFilteredJSON(ctx context.Context, request mcp.CallToolReque
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create result with metadata
|
|
||||||
contentType := resp.Header.Get("Content-Type")
|
contentType := resp.Header.Get("Content-Type")
|
||||||
if contentType == "" {
|
if contentType == "" {
|
||||||
contentType = "application/json"
|
contentType = "application/json"
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -26,13 +27,7 @@ func TestHTTPServerRegistry(t *testing.T) {
|
|||||||
|
|
||||||
// Test that HTTP server is registered
|
// Test that HTTP server is registered
|
||||||
servers := registry.ListServers()
|
servers := registry.ListServers()
|
||||||
found := false
|
found := slices.Contains(servers, "http")
|
||||||
for _, name := range servers {
|
|
||||||
if name == "http" {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !found {
|
if !found {
|
||||||
t.Error("http server not found in registry")
|
t.Error("http server not found in registry")
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/cloudwego/eino/components/model"
|
"charm.land/fantasy"
|
||||||
"github.com/mark3labs/mcp-filesystem-server/filesystemserver"
|
"github.com/mark3labs/mcp-filesystem-server/filesystemserver"
|
||||||
"github.com/mark3labs/mcp-go/server"
|
"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
|
// a no-op as the server is initialized during creation. Returns an error if
|
||||||
// initialization fails.
|
// initialization fails.
|
||||||
func (w *BuiltinServerWrapper) Initialize() error {
|
func (w *BuiltinServerWrapper) Initialize() error {
|
||||||
// The server is already initialized when created
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,7 +32,7 @@ func (w *BuiltinServerWrapper) GetServer() *server.MCPServer {
|
|||||||
// It provides a centralized registry for creating instances of builtin MCP servers
|
// It provides a centralized registry for creating instances of builtin MCP servers
|
||||||
// with their respective configurations.
|
// with their respective configurations.
|
||||||
type Registry struct {
|
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
|
// NewRegistry creates a new builtin server registry with all available builtin
|
||||||
@@ -41,10 +40,9 @@ type Registry struct {
|
|||||||
// and HTTP servers.
|
// and HTTP servers.
|
||||||
func NewRegistry() *Registry {
|
func NewRegistry() *Registry {
|
||||||
r := &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.registerFilesystemServer()
|
||||||
r.registerBashServer()
|
r.registerBashServer()
|
||||||
r.registerTodoServer()
|
r.registerTodoServer()
|
||||||
@@ -58,7 +56,7 @@ func NewRegistry() *Registry {
|
|||||||
// parameter provides server-specific configuration, and the model parameter provides
|
// parameter provides server-specific configuration, and the model parameter provides
|
||||||
// an optional LLM for AI-powered features. Returns an error if the server name
|
// an optional LLM for AI-powered features. Returns an error if the server name
|
||||||
// is unknown or if creation fails.
|
// 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]
|
factory, exists := r.servers[name]
|
||||||
if !exists {
|
if !exists {
|
||||||
return nil, fmt.Errorf("unknown builtin server: %s", name)
|
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)
|
return factory(options, model)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListServers returns a list of all available builtin server names that can be
|
// ListServers returns a list of all available builtin server names.
|
||||||
// created using CreateServer. The order of names is not guaranteed.
|
|
||||||
func (r *Registry) ListServers() []string {
|
func (r *Registry) ListServers() []string {
|
||||||
names := make([]string, 0, len(r.servers))
|
names := make([]string, 0, len(r.servers))
|
||||||
for name := range r.servers {
|
for name := range r.servers {
|
||||||
@@ -79,8 +76,7 @@ func (r *Registry) ListServers() []string {
|
|||||||
|
|
||||||
// registerFilesystemServer registers the filesystem server
|
// registerFilesystemServer registers the filesystem server
|
||||||
func (r *Registry) registerFilesystemServer() {
|
func (r *Registry) registerFilesystemServer() {
|
||||||
r.servers["fs"] = func(options map[string]any, model model.ToolCallingChatModel) (*BuiltinServerWrapper, error) {
|
r.servers["fs"] = func(options map[string]any, model fantasy.LanguageModel) (*BuiltinServerWrapper, error) {
|
||||||
// Extract allowed directories from options
|
|
||||||
var allowedDirs []string
|
var allowedDirs []string
|
||||||
if dirs, ok := options["allowed_directories"]; ok {
|
if dirs, ok := options["allowed_directories"]; ok {
|
||||||
switch v := dirs.(type) {
|
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")
|
return nil, fmt.Errorf("allowed_directories must be a string or array of strings")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Default to current working directory if no directories specified
|
|
||||||
cwd, err := os.Getwd()
|
cwd, err := os.Getwd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get current working directory: %v", err)
|
return nil, fmt.Errorf("failed to get current working directory: %v", err)
|
||||||
@@ -109,7 +104,6 @@ func (r *Registry) registerFilesystemServer() {
|
|||||||
allowedDirs = []string{cwd}
|
allowedDirs = []string{cwd}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the filesystem server
|
|
||||||
server, err := filesystemserver.NewFilesystemServer(allowedDirs)
|
server, err := filesystemserver.NewFilesystemServer(allowedDirs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create filesystem server: %v", err)
|
return nil, fmt.Errorf("failed to create filesystem server: %v", err)
|
||||||
@@ -121,8 +115,7 @@ func (r *Registry) registerFilesystemServer() {
|
|||||||
|
|
||||||
// registerBashServer registers the bash server
|
// registerBashServer registers the bash server
|
||||||
func (r *Registry) registerBashServer() {
|
func (r *Registry) registerBashServer() {
|
||||||
r.servers["bash"] = func(options map[string]any, model model.ToolCallingChatModel) (*BuiltinServerWrapper, error) {
|
r.servers["bash"] = func(options map[string]any, model fantasy.LanguageModel) (*BuiltinServerWrapper, error) {
|
||||||
// Create the bash server
|
|
||||||
server, err := NewBashServer()
|
server, err := NewBashServer()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create bash server: %v", err)
|
return nil, fmt.Errorf("failed to create bash server: %v", err)
|
||||||
@@ -134,8 +127,7 @@ func (r *Registry) registerBashServer() {
|
|||||||
|
|
||||||
// registerTodoServer registers the todo server
|
// registerTodoServer registers the todo server
|
||||||
func (r *Registry) registerTodoServer() {
|
func (r *Registry) registerTodoServer() {
|
||||||
r.servers["todo"] = func(options map[string]any, model model.ToolCallingChatModel) (*BuiltinServerWrapper, error) {
|
r.servers["todo"] = func(options map[string]any, model fantasy.LanguageModel) (*BuiltinServerWrapper, error) {
|
||||||
// Create the todo server
|
|
||||||
server, err := NewTodoServer()
|
server, err := NewTodoServer()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create todo server: %v", err)
|
return nil, fmt.Errorf("failed to create todo server: %v", err)
|
||||||
@@ -147,8 +139,7 @@ func (r *Registry) registerTodoServer() {
|
|||||||
|
|
||||||
// registerFetchServer registers the fetch server
|
// registerFetchServer registers the fetch server
|
||||||
func (r *Registry) registerFetchServer() {
|
func (r *Registry) registerFetchServer() {
|
||||||
r.servers["fetch"] = func(options map[string]any, model model.ToolCallingChatModel) (*BuiltinServerWrapper, error) {
|
r.servers["fetch"] = func(options map[string]any, model fantasy.LanguageModel) (*BuiltinServerWrapper, error) {
|
||||||
// Create the fetch server
|
|
||||||
server, err := NewFetchServer()
|
server, err := NewFetchServer()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create fetch server: %v", err)
|
return nil, fmt.Errorf("failed to create fetch server: %v", err)
|
||||||
@@ -160,8 +151,7 @@ func (r *Registry) registerFetchServer() {
|
|||||||
|
|
||||||
// registerHTTPServer registers the HTTP server
|
// registerHTTPServer registers the HTTP server
|
||||||
func (r *Registry) registerHTTPServer() {
|
func (r *Registry) registerHTTPServer() {
|
||||||
r.servers["http"] = func(options map[string]any, model model.ToolCallingChatModel) (*BuiltinServerWrapper, error) {
|
r.servers["http"] = func(options map[string]any, model fantasy.LanguageModel) (*BuiltinServerWrapper, error) {
|
||||||
// Create the HTTP server
|
|
||||||
server, err := NewHTTPServer(model)
|
server, err := NewHTTPServer(model)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create HTTP server: %v", err)
|
return nil, fmt.Errorf("failed to create HTTP server: %v", err)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package builtin
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"slices"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/mark3labs/mcp-go/mcp"
|
"github.com/mark3labs/mcp-go/mcp"
|
||||||
@@ -23,13 +24,7 @@ func TestTodoServerRegistry(t *testing.T) {
|
|||||||
|
|
||||||
// Test that todo server is registered
|
// Test that todo server is registered
|
||||||
servers := registry.ListServers()
|
servers := registry.ListServers()
|
||||||
found := false
|
found := slices.Contains(servers, "todo")
|
||||||
for _, name := range servers {
|
|
||||||
if name == "todo" {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !found {
|
if !found {
|
||||||
t.Error("todo server not found in registry")
|
t.Error("todo server not found in registry")
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package config
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -77,13 +78,7 @@ mcpServers:
|
|||||||
|
|
||||||
// Check command has substituted value
|
// Check command has substituted value
|
||||||
expectedInCommand := "GITHUB_PERSONAL_ACCESS_TOKEN=ghp_test_token_123"
|
expectedInCommand := "GITHUB_PERSONAL_ACCESS_TOKEN=ghp_test_token_123"
|
||||||
found := false
|
found := slices.Contains(githubServer.Command, expectedInCommand)
|
||||||
for _, arg := range githubServer.Command {
|
|
||||||
if arg == expectedInCommand {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !found {
|
if !found {
|
||||||
t.Errorf("Expected '%s' in command, got: %v", expectedInCommand, githubServer.Command)
|
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
|
// Check if we have mcpServers in the raw config
|
||||||
if mcpServersRaw, ok := rawConfig["mcpservers"]; ok {
|
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
|
// Iterate through each server
|
||||||
for serverName, serverDataRaw := range mcpServersMap {
|
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
|
// Check if this server has an environment field
|
||||||
if _, hasEnv := serverData["environment"]; hasEnv {
|
if _, hasEnv := serverData["environment"]; hasEnv {
|
||||||
// Get the server config from our parsed config
|
// Get the server config from our parsed config
|
||||||
|
|||||||
@@ -24,15 +24,15 @@ mcpServers:
|
|||||||
|
|
||||||
// Test 1: Direct YAML parsing
|
// Test 1: Direct YAML parsing
|
||||||
t.Run("DirectYAMLParsing", func(t *testing.T) {
|
t.Run("DirectYAMLParsing", func(t *testing.T) {
|
||||||
var yamlData map[string]interface{}
|
var yamlData map[string]any
|
||||||
err := yaml.Unmarshal([]byte(yamlContent), &yamlData)
|
err := yaml.Unmarshal([]byte(yamlContent), &yamlData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("YAML unmarshal error: %v", err)
|
t.Fatalf("YAML unmarshal error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
servers := yamlData["mcpServers"].(map[string]interface{})
|
servers := yamlData["mcpServers"].(map[string]any)
|
||||||
testServer := servers["test"].(map[string]interface{})
|
testServer := servers["test"].(map[string]any)
|
||||||
env := testServer["environment"].(map[string]interface{})
|
env := testServer["environment"].(map[string]any)
|
||||||
|
|
||||||
if env["KEY1"] != "value1" {
|
if env["KEY1"] != "value1" {
|
||||||
t.Errorf("Expected KEY1=value1, got %v", env["KEY1"])
|
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
|
// it matches hooks based on tool name patterns. Hooks are executed in parallel
|
||||||
// with configurable timeouts. Returns a combined HookOutput from all executed
|
// with configurable timeouts. Returns a combined HookOutput from all executed
|
||||||
// hooks, with blocking decisions taking precedence.
|
// 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]
|
matchers, ok := e.config.Hooks[event]
|
||||||
if !ok || len(matchers) == 0 {
|
if !ok || len(matchers) == 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@@ -119,7 +119,7 @@ func (e *Executor) ExecuteHooks(ctx context.Context, event HookEvent, input inte
|
|||||||
}
|
}
|
||||||
|
|
||||||
// executeHook runs a single hook command
|
// 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
|
// Prepare input JSON
|
||||||
inputJSON, err := json.Marshal(input)
|
inputJSON, err := json.Marshal(input)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -187,7 +187,7 @@ func matchesPattern(pattern, toolName string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// extractToolName gets the tool name from various input types
|
// extractToolName gets the tool name from various input types
|
||||||
func extractToolName(input interface{}) string {
|
func extractToolName(input any) string {
|
||||||
switch v := input.(type) {
|
switch v := input.(type) {
|
||||||
case *PreToolUseInput:
|
case *PreToolUseInput:
|
||||||
return v.ToolName
|
return v.ToolName
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ echo '{"decision": "approve", "reason": "Approved by test"}'
|
|||||||
name string
|
name string
|
||||||
config *HookConfig
|
config *HookConfig
|
||||||
event HookEvent
|
event HookEvent
|
||||||
input interface{}
|
input any
|
||||||
expected *HookOutput
|
expected *HookOutput
|
||||||
wantErr bool
|
wantErr bool
|
||||||
}{
|
}{
|
||||||
@@ -87,7 +87,7 @@ echo '{"decision": "approve", "reason": "Approved by test"}'
|
|||||||
expected: &HookOutput{
|
expected: &HookOutput{
|
||||||
Decision: "block",
|
Decision: "block",
|
||||||
Reason: "Blocked by policy\n",
|
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 {
|
func boolPtr(b bool) *bool {
|
||||||
return &b
|
return new(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
func compareHookOutputs(a, b *HookOutput) bool {
|
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
|
package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"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.
|
// ModelsRegistry provides validation and information about models.
|
||||||
// It maintains a registry of all supported LLM providers and their models,
|
// It maintains a registry of all supported LLM providers and their models,
|
||||||
// including capabilities, pricing, and configuration requirements.
|
// including capabilities, pricing, and configuration requirements.
|
||||||
// The registry data is generated from models.dev and provides a single
|
// The registry data comes from the catwalk embedded database.
|
||||||
// source of truth for model validation and discovery.
|
|
||||||
type ModelsRegistry struct {
|
type ModelsRegistry struct {
|
||||||
// providers maps provider IDs to their information and available models
|
|
||||||
providers map[string]ProviderInfo
|
providers map[string]ProviderInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewModelsRegistry creates a new models registry with static data.
|
// NewModelsRegistry creates a new models registry populated from the catwalk embedded database.
|
||||||
// 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
|
|
||||||
func NewModelsRegistry() *ModelsRegistry {
|
func NewModelsRegistry() *ModelsRegistry {
|
||||||
return &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.
|
// 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) {
|
func (r *ModelsRegistry) ValidateModel(provider, modelID string) (*ModelInfo, error) {
|
||||||
providerInfo, exists := r.providers[provider]
|
providerInfo, exists := r.providers[provider]
|
||||||
if !exists {
|
if !exists {
|
||||||
@@ -56,20 +174,6 @@ func (r *ModelsRegistry) ValidateModel(provider, modelID string) (*ModelInfo, er
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetRequiredEnvVars returns the required environment variables for a provider.
|
// 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) {
|
func (r *ModelsRegistry) GetRequiredEnvVars(provider string) ([]string, error) {
|
||||||
providerInfo, exists := r.providers[provider]
|
providerInfo, exists := r.providers[provider]
|
||||||
if !exists {
|
if !exists {
|
||||||
@@ -80,28 +184,17 @@ func (r *ModelsRegistry) GetRequiredEnvVars(provider string) ([]string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ValidateEnvironment checks if required environment variables are set.
|
// 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 {
|
func (r *ModelsRegistry) ValidateEnvironment(provider string, apiKey string) error {
|
||||||
envVars, err := r.GetRequiredEnvVars(provider)
|
envVars, err := r.GetRequiredEnvVars(provider)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// If API key is provided via config, we don't need to check env vars
|
|
||||||
if apiKey != "" {
|
if apiKey != "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add alternative environment variable names for google-vertex-anthropic
|
// 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" {
|
if provider == "google-vertex-anthropic" {
|
||||||
envVars = append(envVars,
|
envVars = append(envVars,
|
||||||
"ANTHROPIC_VERTEX_PROJECT_ID",
|
"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
|
var foundVar bool
|
||||||
for _, envVar := range envVars {
|
for _, envVar := range envVars {
|
||||||
if os.Getenv(envVar) != "" {
|
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.
|
// 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 {
|
func (r *ModelsRegistry) SuggestModels(provider, invalidModel string) []string {
|
||||||
providerInfo, exists := r.providers[provider]
|
providerInfo, exists := r.providers[provider]
|
||||||
if !exists {
|
if !exists {
|
||||||
@@ -149,12 +232,10 @@ func (r *ModelsRegistry) SuggestModels(provider, invalidModel string) []string {
|
|||||||
var suggestions []string
|
var suggestions []string
|
||||||
invalidLower := strings.ToLower(invalidModel)
|
invalidLower := strings.ToLower(invalidModel)
|
||||||
|
|
||||||
// Look for models that contain parts of the invalid model name
|
|
||||||
for modelID, modelInfo := range providerInfo.Models {
|
for modelID, modelInfo := range providerInfo.Models {
|
||||||
modelIDLower := strings.ToLower(modelID)
|
modelIDLower := strings.ToLower(modelID)
|
||||||
modelNameLower := strings.ToLower(modelInfo.Name)
|
modelNameLower := strings.ToLower(modelInfo.Name)
|
||||||
|
|
||||||
// Check if the invalid model is a substring of existing models
|
|
||||||
if strings.Contains(modelIDLower, invalidLower) ||
|
if strings.Contains(modelIDLower, invalidLower) ||
|
||||||
strings.Contains(modelNameLower, invalidLower) ||
|
strings.Contains(modelNameLower, invalidLower) ||
|
||||||
strings.Contains(invalidLower, strings.ToLower(strings.Split(modelID, "-")[0])) {
|
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 {
|
if len(suggestions) > 5 {
|
||||||
suggestions = 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.
|
// 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 {
|
func (r *ModelsRegistry) GetSupportedProviders() []string {
|
||||||
var providers []string
|
var providers []string
|
||||||
for providerID := range r.providers {
|
for providerID := range r.providers {
|
||||||
@@ -185,15 +260,6 @@ func (r *ModelsRegistry) GetSupportedProviders() []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetModelsForProvider returns all models for a specific provider.
|
// 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) {
|
func (r *ModelsRegistry) GetModelsForProvider(provider string) (map[string]ModelInfo, error) {
|
||||||
providerInfo, exists := r.providers[provider]
|
providerInfo, exists := r.providers[provider]
|
||||||
if !exists {
|
if !exists {
|
||||||
@@ -207,12 +273,6 @@ func (r *ModelsRegistry) GetModelsForProvider(provider string) (map[string]Model
|
|||||||
var globalRegistry = NewModelsRegistry()
|
var globalRegistry = NewModelsRegistry()
|
||||||
|
|
||||||
// GetGlobalRegistry returns the global models registry instance.
|
// 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 {
|
func GetGlobalRegistry() *ModelsRegistry {
|
||||||
return globalRegistry
|
return globalRegistry
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-57
@@ -4,14 +4,12 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/cloudwego/eino/schema"
|
"charm.land/fantasy"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Manager manages session state and auto-saving functionality.
|
// Manager manages session state and auto-saving functionality.
|
||||||
// It provides thread-safe operations for managing a conversation session,
|
// It provides thread-safe operations for managing a conversation session,
|
||||||
// including automatic persistence to disk after each modification.
|
// 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 {
|
type Manager struct {
|
||||||
session *Session
|
session *Session
|
||||||
filePath string
|
filePath string
|
||||||
@@ -19,9 +17,6 @@ type Manager struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewManager creates a new session manager with a fresh session.
|
// 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 {
|
func NewManager(filePath string) *Manager {
|
||||||
return &Manager{
|
return &Manager{
|
||||||
session: NewSession(),
|
session: NewSession(),
|
||||||
@@ -30,10 +25,6 @@ func NewManager(filePath string) *Manager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewManagerWithSession creates a new session manager with an existing session.
|
// 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 {
|
func NewManagerWithSession(session *Session, filePath string) *Manager {
|
||||||
return &Manager{
|
return &Manager{
|
||||||
session: session,
|
session: session,
|
||||||
@@ -41,17 +32,12 @@ func NewManagerWithSession(session *Session, filePath string) *Manager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddMessage adds a message to the session and auto-saves.
|
// AddMessage adds a fantasy message to the session and auto-saves.
|
||||||
// The message is converted from schema.Message format to the internal
|
func (m *Manager) AddMessage(msg fantasy.Message) error {
|
||||||
// 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 {
|
|
||||||
m.mutex.Lock()
|
m.mutex.Lock()
|
||||||
defer m.mutex.Unlock()
|
defer m.mutex.Unlock()
|
||||||
|
|
||||||
sessionMsg := ConvertFromSchemaMessage(msg)
|
sessionMsg := ConvertFromFantasyMessage(msg)
|
||||||
m.session.AddMessage(sessionMsg)
|
m.session.AddMessage(sessionMsg)
|
||||||
|
|
||||||
if m.filePath != "" {
|
if m.filePath != "" {
|
||||||
@@ -61,17 +47,13 @@ func (m *Manager) AddMessage(msg *schema.Message) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddMessages adds multiple messages to the session and auto-saves.
|
// AddMessages adds multiple fantasy messages to the session and auto-saves.
|
||||||
// All messages are added in order and then the session is saved once.
|
func (m *Manager) AddMessages(msgs []fantasy.Message) error {
|
||||||
// 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 {
|
|
||||||
m.mutex.Lock()
|
m.mutex.Lock()
|
||||||
defer m.mutex.Unlock()
|
defer m.mutex.Unlock()
|
||||||
|
|
||||||
for _, msg := range msgs {
|
for _, msg := range msgs {
|
||||||
sessionMsg := ConvertFromSchemaMessage(msg)
|
sessionMsg := ConvertFromFantasyMessage(msg)
|
||||||
m.session.AddMessage(sessionMsg)
|
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.
|
// ReplaceAllMessages replaces all messages in the session with the provided messages.
|
||||||
// This method completely clears the existing message history and replaces it with
|
func (m *Manager) ReplaceAllMessages(msgs []fantasy.Message) error {
|
||||||
// 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 {
|
|
||||||
m.mutex.Lock()
|
m.mutex.Lock()
|
||||||
defer m.mutex.Unlock()
|
defer m.mutex.Unlock()
|
||||||
|
|
||||||
@@ -97,7 +74,7 @@ func (m *Manager) ReplaceAllMessages(msgs []*schema.Message) error {
|
|||||||
|
|
||||||
// Add all new messages
|
// Add all new messages
|
||||||
for _, msg := range msgs {
|
for _, msg := range msgs {
|
||||||
sessionMsg := ConvertFromSchemaMessage(msg)
|
sessionMsg := ConvertFromFantasyMessage(msg)
|
||||||
m.session.AddMessage(sessionMsg)
|
m.session.AddMessage(sessionMsg)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,10 +86,6 @@ func (m *Manager) ReplaceAllMessages(msgs []*schema.Message) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SetMetadata sets the session metadata.
|
// 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 {
|
func (m *Manager) SetMetadata(metadata Metadata) error {
|
||||||
m.mutex.Lock()
|
m.mutex.Lock()
|
||||||
defer m.mutex.Unlock()
|
defer m.mutex.Unlock()
|
||||||
@@ -126,33 +99,24 @@ func (m *Manager) SetMetadata(metadata Metadata) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetMessages returns all messages as a schema.Message slice.
|
// GetMessages returns all messages as fantasy.Message slice.
|
||||||
// This method converts all stored session messages to the schema format
|
func (m *Manager) GetMessages() []fantasy.Message {
|
||||||
// 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 {
|
|
||||||
m.mutex.RLock()
|
m.mutex.RLock()
|
||||||
defer m.mutex.RUnlock()
|
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 {
|
for i, msg := range m.session.Messages {
|
||||||
messages[i] = msg.ConvertToSchemaMessage()
|
messages[i] = msg.ConvertToFantasyMessage()
|
||||||
}
|
}
|
||||||
|
|
||||||
return messages
|
return messages
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSession returns a copy of the current session.
|
// 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 {
|
func (m *Manager) GetSession() *Session {
|
||||||
m.mutex.RLock()
|
m.mutex.RLock()
|
||||||
defer m.mutex.RUnlock()
|
defer m.mutex.RUnlock()
|
||||||
|
|
||||||
// Return a copy to prevent external modification
|
|
||||||
sessionCopy := *m.session
|
sessionCopy := *m.session
|
||||||
sessionCopy.Messages = make([]Message, len(m.session.Messages))
|
sessionCopy.Messages = make([]Message, len(m.session.Messages))
|
||||||
copy(sessionCopy.Messages, 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.
|
// 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 {
|
func (m *Manager) Save() error {
|
||||||
m.mutex.RLock()
|
m.mutex.RLock()
|
||||||
defer m.mutex.RUnlock()
|
defer m.mutex.RUnlock()
|
||||||
@@ -177,15 +137,11 @@ func (m *Manager) Save() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetFilePath returns the file path for this session.
|
// 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 {
|
func (m *Manager) GetFilePath() string {
|
||||||
return m.filePath
|
return m.filePath
|
||||||
}
|
}
|
||||||
|
|
||||||
// MessageCount returns the number of messages in the session.
|
// 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 {
|
func (m *Manager) MessageCount() int {
|
||||||
m.mutex.RLock()
|
m.mutex.RLock()
|
||||||
defer m.mutex.RUnlock()
|
defer m.mutex.RUnlock()
|
||||||
|
|||||||
+71
-77
@@ -8,7 +8,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/cloudwego/eino/schema"
|
"charm.land/fantasy"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Session represents a complete conversation session with metadata.
|
// Session represents a complete conversation session with metadata.
|
||||||
@@ -30,9 +30,7 @@ type Session struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Metadata contains session metadata that provides context about the
|
// Metadata contains session metadata that provides context about the
|
||||||
// environment and configuration used during the conversation. This helps
|
// environment and configuration used during the conversation.
|
||||||
// with debugging and understanding the session's context when reviewing
|
|
||||||
// conversation history.
|
|
||||||
type Metadata struct {
|
type Metadata struct {
|
||||||
// MCPHostVersion is the version of MCPHost used for this session
|
// MCPHostVersion is the version of MCPHost used for this session
|
||||||
MCPHostVersion string `json:"mcphost_version"`
|
MCPHostVersion string `json:"mcphost_version"`
|
||||||
@@ -61,8 +59,6 @@ type Message struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ToolCall represents a tool invocation within an assistant message.
|
// 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 {
|
type ToolCall struct {
|
||||||
// ID is a unique identifier for this tool call, used to link results
|
// ID is a unique identifier for this tool call, used to link results
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
@@ -73,9 +69,6 @@ type ToolCall struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewSession creates a new session with default values.
|
// 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 {
|
func NewSession() *Session {
|
||||||
return &Session{
|
return &Session{
|
||||||
Version: "1.0",
|
Version: "1.0",
|
||||||
@@ -87,9 +80,6 @@ func NewSession() *Session {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// AddMessage adds a message to the 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) {
|
func (s *Session) AddMessage(msg Message) {
|
||||||
if msg.ID == "" {
|
if msg.ID == "" {
|
||||||
msg.ID = generateMessageID()
|
msg.ID = generateMessageID()
|
||||||
@@ -103,20 +93,12 @@ func (s *Session) AddMessage(msg Message) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SetMetadata sets the session metadata.
|
// 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) {
|
func (s *Session) SetMetadata(metadata Metadata) {
|
||||||
s.Metadata = metadata
|
s.Metadata = metadata
|
||||||
s.UpdatedAt = time.Now()
|
s.UpdatedAt = time.Now()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SaveToFile saves the session to a JSON file.
|
// 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 {
|
func (s *Session) SaveToFile(filePath string) error {
|
||||||
s.UpdatedAt = time.Now()
|
s.UpdatedAt = time.Now()
|
||||||
|
|
||||||
@@ -129,11 +111,6 @@ func (s *Session) SaveToFile(filePath string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// LoadFromFile loads a session from a JSON file.
|
// 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) {
|
func LoadFromFile(filePath string) (*Session, error) {
|
||||||
data, err := os.ReadFile(filePath)
|
data, err := os.ReadFile(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -148,85 +125,102 @@ func LoadFromFile(filePath string) (*Session, error) {
|
|||||||
return &session, nil
|
return &session, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConvertFromSchemaMessage converts a schema.Message to a session Message.
|
// ConvertFromFantasyMessage converts a fantasy.Message to a session Message.
|
||||||
// This function bridges between the eino schema message format and the
|
// This function bridges between the fantasy message format and the
|
||||||
// session's internal message format. It preserves role, content, and
|
// session's internal message format for JSON persistence.
|
||||||
// tool-related information while adding a timestamp.
|
func ConvertFromFantasyMessage(msg fantasy.Message) Message {
|
||||||
// Tool calls from assistant messages and tool call IDs from tool messages
|
|
||||||
// are properly converted and preserved.
|
|
||||||
func ConvertFromSchemaMessage(msg *schema.Message) Message {
|
|
||||||
sessionMsg := Message{
|
sessionMsg := Message{
|
||||||
Role: string(msg.Role),
|
Role: string(msg.Role),
|
||||||
Content: msg.Content,
|
|
||||||
Timestamp: time.Now(),
|
Timestamp: time.Now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert tool calls if present (for assistant messages)
|
// Extract text content and tool calls from message parts
|
||||||
if len(msg.ToolCalls) > 0 {
|
var textParts []string
|
||||||
sessionMsg.ToolCalls = make([]ToolCall, len(msg.ToolCalls))
|
for _, part := range msg.Content {
|
||||||
for i, tc := range msg.ToolCalls {
|
switch p := part.(type) {
|
||||||
sessionMsg.ToolCalls[i] = ToolCall{
|
case fantasy.TextPart:
|
||||||
ID: tc.ID,
|
textParts = append(textParts, p.Text)
|
||||||
Name: tc.Function.Name,
|
case fantasy.ToolCallPart:
|
||||||
Arguments: tc.Function.Arguments,
|
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
|
// Join all text parts
|
||||||
if msg.Role == schema.Tool && msg.ToolCallID != "" {
|
for i, t := range textParts {
|
||||||
sessionMsg.ToolCallID = msg.ToolCallID
|
if i > 0 {
|
||||||
|
sessionMsg.Content += "\n"
|
||||||
|
}
|
||||||
|
sessionMsg.Content += t
|
||||||
}
|
}
|
||||||
|
|
||||||
return sessionMsg
|
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
|
// This method bridges between the session's internal message format and
|
||||||
// the eino schema message format used by the LLM providers.
|
// the fantasy message format used by the LLM providers.
|
||||||
// It properly handles tool calls for assistant messages and tool call IDs
|
func (m *Message) ConvertToFantasyMessage() fantasy.Message {
|
||||||
// for tool result messages. Arguments are converted to string format as
|
msg := fantasy.Message{
|
||||||
// required by the schema.
|
Role: fantasy.MessageRole(m.Role),
|
||||||
func (m *Message) ConvertToSchemaMessage() *schema.Message {
|
|
||||||
msg := &schema.Message{
|
|
||||||
Role: schema.RoleType(m.Role),
|
|
||||||
Content: m.Content,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert tool calls if present (for assistant messages)
|
// Build content parts based on role
|
||||||
if len(m.ToolCalls) > 0 {
|
switch m.Role {
|
||||||
msg.ToolCalls = make([]schema.ToolCall, len(m.ToolCalls))
|
case "assistant":
|
||||||
for i, tc := range m.ToolCalls {
|
// Add text content if present
|
||||||
// Arguments are already stored as a string, use them directly
|
if m.Content != "" {
|
||||||
var argsStr string
|
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 {
|
if str, ok := tc.Arguments.(string); ok {
|
||||||
argsStr = str
|
inputStr = str
|
||||||
} else {
|
} else if argBytes, err := json.Marshal(tc.Arguments); err == nil {
|
||||||
// Fallback: marshal to JSON if not a string
|
inputStr = string(argBytes)
|
||||||
if argBytes, err := json.Marshal(tc.Arguments); err == nil {
|
|
||||||
argsStr = string(argBytes)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
msg.ToolCalls[i] = schema.ToolCall{
|
msg.Content = append(msg.Content, fantasy.ToolCallPart{
|
||||||
ID: tc.ID,
|
ToolCallID: tc.ID,
|
||||||
Function: schema.FunctionCall{
|
ToolName: tc.Name,
|
||||||
Name: tc.Name,
|
Input: inputStr,
|
||||||
Arguments: argsStr,
|
})
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
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
|
msg.Content = append(msg.Content, fantasy.ToolResultPart{
|
||||||
if m.Role == "tool" && m.ToolCallID != "" {
|
ToolCallID: m.ToolCallID,
|
||||||
msg.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
|
return msg
|
||||||
}
|
}
|
||||||
|
|
||||||
// generateMessageID generates a unique message ID
|
// generateMessageID generates a unique message ID.
|
||||||
func generateMessageID() string {
|
func generateMessageID() string {
|
||||||
bytes := make([]byte, 8)
|
bytes := make([]byte, 8)
|
||||||
rand.Read(bytes)
|
rand.Read(bytes)
|
||||||
|
|||||||
@@ -3,11 +3,12 @@ package tools
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"maps"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/cloudwego/eino/components/model"
|
"charm.land/fantasy"
|
||||||
"github.com/mark3labs/mcp-go/client"
|
"github.com/mark3labs/mcp-go/client"
|
||||||
"github.com/mark3labs/mcp-go/client/transport"
|
"github.com/mark3labs/mcp-go/client/transport"
|
||||||
"github.com/mark3labs/mcp-go/mcp"
|
"github.com/mark3labs/mcp-go/mcp"
|
||||||
@@ -63,7 +64,7 @@ type MCPConnectionPool struct {
|
|||||||
connections map[string]*MCPConnection
|
connections map[string]*MCPConnection
|
||||||
config *ConnectionPoolConfig
|
config *ConnectionPoolConfig
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
model model.ToolCallingChatModel
|
model fantasy.LanguageModel
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
debug bool
|
debug bool
|
||||||
@@ -75,7 +76,7 @@ type MCPConnectionPool struct {
|
|||||||
// goroutine for periodic health checks that runs until Close is called.
|
// goroutine for periodic health checks that runs until Close is called.
|
||||||
// The model parameter is used for MCP servers that require sampling support.
|
// The model parameter is used for MCP servers that require sampling support.
|
||||||
// Thread-safe for concurrent use immediately after creation.
|
// 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 {
|
if config == nil {
|
||||||
config = DefaultConnectionPoolConfig()
|
config = DefaultConnectionPoolConfig()
|
||||||
}
|
}
|
||||||
@@ -406,7 +407,7 @@ func (p *MCPConnectionPool) initializeClient(ctx context.Context, client client.
|
|||||||
}
|
}
|
||||||
|
|
||||||
if p.debugLogger != nil && p.debugLogger.IsDebugEnabled() {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
@@ -430,9 +431,7 @@ func (p *MCPConnectionPool) startHealthCheck() {
|
|||||||
func (p *MCPConnectionPool) checkConnectionsHealth() {
|
func (p *MCPConnectionPool) checkConnectionsHealth() {
|
||||||
p.mu.RLock()
|
p.mu.RLock()
|
||||||
connections := make(map[string]*MCPConnection)
|
connections := make(map[string]*MCPConnection)
|
||||||
for k, v := range p.connections {
|
maps.Copy(connections, p.connections)
|
||||||
connections[k] = v
|
|
||||||
}
|
|
||||||
p.mu.RUnlock()
|
p.mu.RUnlock()
|
||||||
|
|
||||||
for serverName, conn := range connections {
|
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
|
// The returned map includes health status, last usage time, error counts, and
|
||||||
// last error for each connection. Useful for monitoring and debugging connection
|
// last error for each connection. Useful for monitoring and debugging connection
|
||||||
// pool behavior. The returned data is a snapshot and safe for concurrent access.
|
// 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()
|
p.mu.RLock()
|
||||||
defer p.mu.RUnlock()
|
defer p.mu.RUnlock()
|
||||||
|
|
||||||
stats := make(map[string]interface{})
|
stats := make(map[string]any)
|
||||||
for serverName, conn := range p.connections {
|
for serverName, conn := range p.connections {
|
||||||
conn.mu.RLock()
|
conn.mu.RLock()
|
||||||
stats[serverName] = map[string]interface{}{
|
stats[serverName] = map[string]any{
|
||||||
"is_healthy": conn.isHealthy,
|
"is_healthy": conn.isHealthy,
|
||||||
"last_used": conn.lastUsed,
|
"last_used": conn.lastUsed,
|
||||||
"last_error": conn.lastError,
|
"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"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/bytedance/sonic"
|
"charm.land/fantasy"
|
||||||
"github.com/cloudwego/eino/components/model"
|
|
||||||
"github.com/cloudwego/eino/components/tool"
|
|
||||||
"github.com/cloudwego/eino/schema"
|
|
||||||
"github.com/eino-contrib/jsonschema"
|
|
||||||
"github.com/mark3labs/mcp-go/client"
|
"github.com/mark3labs/mcp-go/client"
|
||||||
"github.com/mark3labs/mcp-go/client/transport"
|
"github.com/mark3labs/mcp-go/client/transport"
|
||||||
"github.com/mark3labs/mcp-go/mcp"
|
"github.com/mark3labs/mcp-go/mcp"
|
||||||
@@ -26,9 +23,9 @@ import (
|
|||||||
// Thread-safe for concurrent tool invocations.
|
// Thread-safe for concurrent tool invocations.
|
||||||
type MCPToolManager struct {
|
type MCPToolManager struct {
|
||||||
connectionPool *MCPConnectionPool
|
connectionPool *MCPConnectionPool
|
||||||
tools []tool.BaseTool
|
tools []fantasy.AgentTool
|
||||||
toolMap map[string]*toolMapping // maps prefixed tool names to their server and original name
|
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
|
config *config.Config
|
||||||
debug bool
|
debug bool
|
||||||
debugLogger DebugLogger
|
debugLogger DebugLogger
|
||||||
@@ -42,18 +39,12 @@ type toolMapping struct {
|
|||||||
manager *MCPToolManager
|
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.
|
// NewMCPToolManager creates a new MCP tool manager instance.
|
||||||
// Returns an initialized manager with empty tool collections ready to load tools from MCP servers.
|
// 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.
|
// The manager must be configured with SetModel and LoadTools before use.
|
||||||
func NewMCPToolManager() *MCPToolManager {
|
func NewMCPToolManager() *MCPToolManager {
|
||||||
return &MCPToolManager{
|
return &MCPToolManager{
|
||||||
tools: make([]tool.BaseTool, 0),
|
tools: make([]fantasy.AgentTool, 0),
|
||||||
toolMap: make(map[string]*toolMapping),
|
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
|
// The model is used when MCP servers request sampling operations, allowing them to
|
||||||
// leverage the host's LLM capabilities for text generation tasks.
|
// leverage the host's LLM capabilities for text generation tasks.
|
||||||
// This method should be called before LoadTools if any MCP servers require sampling support.
|
// 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
|
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 {
|
type samplingHandler struct {
|
||||||
model model.ToolCallingChatModel
|
model fantasy.LanguageModel
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateMessage handles sampling requests from MCP servers by forwarding them to the configured LLM model.
|
// 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
|
// and converts the response back to MCP format. Returns an error if no model is available
|
||||||
// or if generation fails.
|
// or if generation fails.
|
||||||
func (h *samplingHandler) CreateMessage(ctx context.Context, request mcp.CreateMessageRequest) (*mcp.CreateMessageResult, error) {
|
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")
|
return nil, fmt.Errorf("no model available for sampling")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert MCP messages to eino messages
|
// Build fantasy messages from MCP sampling request
|
||||||
var messages []*schema.Message
|
var messages []fantasy.Message
|
||||||
|
|
||||||
// Add system message if provided
|
// Add system message if provided
|
||||||
if request.SystemPrompt != "" {
|
if request.SystemPrompt != "" {
|
||||||
messages = append(messages, schema.SystemMessage(request.SystemPrompt))
|
messages = append(messages, fantasy.NewSystemMessage(request.SystemPrompt))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert sampling messages
|
// Convert sampling messages
|
||||||
for _, msg := range request.Messages {
|
for _, msg := range request.Messages {
|
||||||
// Extract text content
|
|
||||||
var content string
|
var content string
|
||||||
if textContent, ok := msg.Content.(mcp.TextContent); ok {
|
if textContent, ok := msg.Content.(mcp.TextContent); ok {
|
||||||
content = textContent.Text
|
content = textContent.Text
|
||||||
@@ -111,30 +101,36 @@ func (h *samplingHandler) CreateMessage(ctx context.Context, request mcp.CreateM
|
|||||||
|
|
||||||
switch msg.Role {
|
switch msg.Role {
|
||||||
case mcp.RoleUser:
|
case mcp.RoleUser:
|
||||||
messages = append(messages, schema.UserMessage(content))
|
messages = append(messages, fantasy.NewUserMessage(content))
|
||||||
case mcp.RoleAssistant:
|
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:
|
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)
|
// Generate response using the fantasy model
|
||||||
response, err := h.model.Generate(ctx, messages)
|
call := fantasy.Call{
|
||||||
|
Prompt: fantasy.Prompt(messages),
|
||||||
|
}
|
||||||
|
response, err := h.model.Generate(ctx, call)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("model generation failed: %w", err)
|
return nil, fmt.Errorf("model generation failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert response back to MCP format
|
// Convert response back to MCP format
|
||||||
result := &mcp.CreateMessageResult{
|
result := &mcp.CreateMessageResult{
|
||||||
Model: "mcphost-model", // Generic model name
|
Model: h.model.Model(),
|
||||||
StopReason: "endTurn",
|
StopReason: "endTurn",
|
||||||
}
|
}
|
||||||
result.SamplingMessage = mcp.SamplingMessage{
|
result.SamplingMessage = mcp.SamplingMessage{
|
||||||
Role: mcp.RoleAssistant,
|
Role: mcp.RoleAssistant,
|
||||||
Content: mcp.TextContent{
|
Content: mcp.TextContent{
|
||||||
Type: "text",
|
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 {
|
for _, mcpTool := range listResults.Tools {
|
||||||
// Filter tools based on allowedTools/excludedTools
|
// Filter tools based on allowedTools/excludedTools
|
||||||
if len(serverConfig.AllowedTools) > 0 {
|
if len(serverConfig.AllowedTools) > 0 {
|
||||||
@@ -216,29 +212,40 @@ func (m *MCPToolManager) loadServerTools(ctx context.Context, serverName string,
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert schema
|
// Convert MCP InputSchema to map[string]any for fantasy ToolInfo
|
||||||
marshaledInputSchema, err := sonic.Marshal(mcpTool.InputSchema)
|
marshaledSchema, err := json.Marshal(mcpTool.InputSchema)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("conv mcp tool input schema fail(marshal): %w, tool name: %s", err, mcpTool.Name)
|
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:
|
// Fix for JSON Schema draft-07 vs draft-04 compatibility
|
||||||
// Chrome DevTools MCP uses draft-07 where exclusiveMinimum/exclusiveMaximum are numbers,
|
marshaledSchema = convertExclusiveBoundsToBoolean(marshaledSchema)
|
||||||
// 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)
|
|
||||||
|
|
||||||
inputSchema := &jsonschema.Schema{}
|
// Parse into map[string]any for fantasy's parameters format
|
||||||
err = sonic.Unmarshal(marshaledInputSchema, inputSchema)
|
var schemaMap map[string]any
|
||||||
if err != nil {
|
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)
|
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
|
// Fix for issue #89: Ensure object schemas have a properties field
|
||||||
// OpenAI function calling requires object schemas to have a "properties" field
|
if schemaType, ok := schemaMap["type"].(string); ok && schemaType == "object" && len(parameters) == 0 {
|
||||||
// even if it's empty, otherwise it throws "object schema missing properties" error
|
// Keep empty parameters map - fantasy handles this fine
|
||||||
if inputSchema.Type == "object" && inputSchema.Properties == nil {
|
}
|
||||||
inputSchema.Properties = jsonschema.NewProperties()
|
|
||||||
|
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
|
// Create prefixed tool name
|
||||||
@@ -253,89 +260,26 @@ func (m *MCPToolManager) loadServerTools(ctx context.Context, serverName string,
|
|||||||
}
|
}
|
||||||
m.toolMap[prefixedName] = mapping
|
m.toolMap[prefixedName] = mapping
|
||||||
|
|
||||||
// Create eino tool
|
// Create fantasy AgentTool
|
||||||
einoTool := &mcpToolImpl{
|
fantasyTool := &mcpFantasyTool{
|
||||||
info: &schema.ToolInfo{
|
toolInfo: fantasy.ToolInfo{
|
||||||
Name: prefixedName,
|
Name: prefixedName,
|
||||||
Desc: mcpTool.Description,
|
Description: mcpTool.Description,
|
||||||
ParamsOneOf: schema.NewParamsOneOfByJSONSchema(inputSchema),
|
Parameters: parameters,
|
||||||
|
Required: required,
|
||||||
},
|
},
|
||||||
mapping: mapping,
|
mapping: mapping,
|
||||||
}
|
}
|
||||||
|
|
||||||
m.tools = append(m.tools, einoTool)
|
m.tools = append(m.tools, fantasyTool)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Info returns the tool information including name, description, and parameter schema.
|
// GetTools returns all loaded tools as fantasy AgentTools from all configured MCP servers.
|
||||||
// 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.
|
|
||||||
// Tools are returned with their prefixed names (serverName__toolName) to ensure uniqueness.
|
// 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() []fantasy.AgentTool {
|
||||||
func (m *MCPToolManager) GetTools() []tool.BaseTool {
|
|
||||||
return m.tools
|
return m.tools
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -360,15 +304,11 @@ func (m *MCPToolManager) Close() error {
|
|||||||
|
|
||||||
// shouldExcludeTool determines if a tool should be excluded based on excludedTools
|
// shouldExcludeTool determines if a tool should be excluded based on excludedTools
|
||||||
func (m *MCPToolManager) shouldExcludeTool(toolName string, serverConfig config.MCPServerConfig) bool {
|
func (m *MCPToolManager) shouldExcludeTool(toolName string, serverConfig config.MCPServerConfig) bool {
|
||||||
// If excludedTools is specified, exclude tools in the list
|
|
||||||
if len(serverConfig.ExcludedTools) > 0 {
|
if len(serverConfig.ExcludedTools) > 0 {
|
||||||
for _, excludedTool := range serverConfig.ExcludedTools {
|
if slices.Contains(serverConfig.ExcludedTools, toolName) {
|
||||||
if excludedTool == toolName {
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -377,60 +317,44 @@ func (m *MCPToolManager) createMCPClient(ctx context.Context, serverName string,
|
|||||||
|
|
||||||
switch transportType {
|
switch transportType {
|
||||||
case "stdio":
|
case "stdio":
|
||||||
// STDIO client
|
|
||||||
var env []string
|
var env []string
|
||||||
var command string
|
var command string
|
||||||
var args []string
|
var args []string
|
||||||
|
|
||||||
// Handle command and environment
|
|
||||||
if len(serverConfig.Command) > 0 {
|
if len(serverConfig.Command) > 0 {
|
||||||
command = serverConfig.Command[0]
|
command = serverConfig.Command[0]
|
||||||
if len(serverConfig.Command) > 1 {
|
if len(serverConfig.Command) > 1 {
|
||||||
args = serverConfig.Command[1:]
|
args = serverConfig.Command[1:]
|
||||||
} else if len(serverConfig.Args) > 0 {
|
} 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
|
args = serverConfig.Args
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert environment variables
|
|
||||||
if serverConfig.Environment != nil {
|
if serverConfig.Environment != nil {
|
||||||
for k, v := range serverConfig.Environment {
|
for k, v := range serverConfig.Environment {
|
||||||
env = append(env, fmt.Sprintf("%s=%s", k, v))
|
env = append(env, fmt.Sprintf("%s=%s", k, v))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Legacy environment support
|
|
||||||
if serverConfig.Env != nil {
|
if serverConfig.Env != nil {
|
||||||
for k, v := range serverConfig.Env {
|
for k, v := range serverConfig.Env {
|
||||||
env = append(env, fmt.Sprintf("%s=%v", k, v))
|
env = append(env, fmt.Sprintf("%s=%v", k, v))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create stdio transport
|
|
||||||
stdioTransport := transport.NewStdio(command, env, args...)
|
stdioTransport := transport.NewStdio(command, env, args...)
|
||||||
|
|
||||||
stdioClient := client.NewClient(stdioTransport)
|
stdioClient := client.NewClient(stdioTransport)
|
||||||
|
|
||||||
// Start the transport
|
|
||||||
if err := stdioTransport.Start(ctx); err != nil {
|
if err := stdioTransport.Start(ctx); err != nil {
|
||||||
return nil, fmt.Errorf("failed to start stdio transport: %v", err)
|
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)
|
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
|
return stdioClient, nil
|
||||||
|
|
||||||
case "sse":
|
case "sse":
|
||||||
// SSE client
|
|
||||||
var options []transport.ClientOption
|
var options []transport.ClientOption
|
||||||
|
|
||||||
// Add headers if specified
|
|
||||||
if len(serverConfig.Headers) > 0 {
|
if len(serverConfig.Headers) > 0 {
|
||||||
headers := make(map[string]string)
|
headers := make(map[string]string)
|
||||||
for _, header := range serverConfig.Headers {
|
for _, header := range serverConfig.Headers {
|
||||||
@@ -451,7 +375,6 @@ func (m *MCPToolManager) createMCPClient(ctx context.Context, serverName string,
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start the SSE client
|
|
||||||
if err := sseClient.Start(ctx); err != nil {
|
if err := sseClient.Start(ctx); err != nil {
|
||||||
return nil, fmt.Errorf("failed to start SSE client: %v", err)
|
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
|
return sseClient, nil
|
||||||
|
|
||||||
case "streamable":
|
case "streamable":
|
||||||
// Streamable HTTP client
|
|
||||||
var options []transport.StreamableHTTPCOption
|
var options []transport.StreamableHTTPCOption
|
||||||
|
|
||||||
// Add headers if specified
|
|
||||||
if len(serverConfig.Headers) > 0 {
|
if len(serverConfig.Headers) > 0 {
|
||||||
headers := make(map[string]string)
|
headers := make(map[string]string)
|
||||||
for _, header := range serverConfig.Headers {
|
for _, header := range serverConfig.Headers {
|
||||||
@@ -483,7 +404,6 @@ func (m *MCPToolManager) createMCPClient(ctx context.Context, serverName string,
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start the streamable HTTP client
|
|
||||||
if err := streamableClient.Start(ctx); err != nil {
|
if err := streamableClient.Start(ctx); err != nil {
|
||||||
return nil, fmt.Errorf("failed to start streamable HTTP client: %v", err)
|
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
|
return streamableClient, nil
|
||||||
|
|
||||||
case "inprocess":
|
case "inprocess":
|
||||||
// Builtin server
|
|
||||||
return m.createBuiltinClient(ctx, serverName, serverConfig)
|
return m.createBuiltinClient(ctx, serverName, serverConfig)
|
||||||
|
|
||||||
default:
|
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 {
|
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)
|
initCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
||||||
defer cancel()
|
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) {
|
func (m *MCPToolManager) createBuiltinClient(ctx context.Context, serverName string, serverConfig config.MCPServerConfig) (client.MCPClient, error) {
|
||||||
registry := builtin.NewRegistry()
|
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)
|
builtinServer, err := registry.CreateServer(serverConfig.Name, serverConfig.Options, m.model)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create builtin server: %v", err)
|
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())
|
inProcessClient, err := client.NewInProcessClient(builtinServer.GetServer())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create in-process client: %v", err)
|
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
|
// convertExclusiveBoundsToBoolean converts JSON Schema draft-07 style exclusive bounds
|
||||||
// (where exclusiveMinimum/exclusiveMaximum are numbers) to draft-04 style
|
// (where exclusiveMinimum/exclusiveMaximum are numbers) to draft-04 style
|
||||||
// (where they are booleans that modify minimum/maximum).
|
// (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 {
|
func convertExclusiveBoundsToBoolean(schemaJSON []byte) []byte {
|
||||||
var data map[string]interface{}
|
var data map[string]any
|
||||||
if err := json.Unmarshal(schemaJSON, &data); err != nil {
|
if err := json.Unmarshal(schemaJSON, &data); err != nil {
|
||||||
return schemaJSON // Return unchanged on error
|
return schemaJSON
|
||||||
}
|
}
|
||||||
|
|
||||||
convertSchemaRecursive(data)
|
convertSchemaRecursive(data)
|
||||||
|
|
||||||
result, err := json.Marshal(data)
|
result, err := json.Marshal(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return schemaJSON // Return unchanged on error
|
return schemaJSON
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// convertSchemaRecursive recursively processes a schema map and converts
|
// convertSchemaRecursive recursively processes a schema map and converts
|
||||||
// numeric exclusiveMinimum/exclusiveMaximum to boolean format.
|
// numeric exclusiveMinimum/exclusiveMaximum to boolean format.
|
||||||
func convertSchemaRecursive(schema map[string]interface{}) {
|
func convertSchemaRecursive(schema map[string]any) {
|
||||||
// Convert exclusiveMinimum if it's a number
|
|
||||||
if exMin, ok := schema["exclusiveMinimum"]; ok {
|
if exMin, ok := schema["exclusiveMinimum"]; ok {
|
||||||
if num, isNum := exMin.(float64); isNum {
|
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["minimum"] = num
|
||||||
schema["exclusiveMinimum"] = true
|
schema["exclusiveMinimum"] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert exclusiveMaximum if it's a number
|
|
||||||
if exMax, ok := schema["exclusiveMaximum"]; ok {
|
if exMax, ok := schema["exclusiveMaximum"]; ok {
|
||||||
if num, isNum := exMax.(float64); isNum {
|
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["maximum"] = num
|
||||||
schema["exclusiveMaximum"] = true
|
schema["exclusiveMaximum"] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recursively process properties
|
if props, ok := schema["properties"].(map[string]any); ok {
|
||||||
if props, ok := schema["properties"].(map[string]interface{}); ok {
|
|
||||||
for _, prop := range props {
|
for _, prop := range props {
|
||||||
if propSchema, ok := prop.(map[string]interface{}); ok {
|
if propSchema, ok := prop.(map[string]any); ok {
|
||||||
convertSchemaRecursive(propSchema)
|
convertSchemaRecursive(propSchema)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recursively process items (for arrays)
|
if items, ok := schema["items"].(map[string]any); ok {
|
||||||
if items, ok := schema["items"].(map[string]interface{}); ok {
|
|
||||||
convertSchemaRecursive(items)
|
convertSchemaRecursive(items)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recursively process additionalProperties
|
if addProps, ok := schema["additionalProperties"].(map[string]any); ok {
|
||||||
if addProps, ok := schema["additionalProperties"].(map[string]interface{}); ok {
|
|
||||||
convertSchemaRecursive(addProps)
|
convertSchemaRecursive(addProps)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recursively process allOf, anyOf, oneOf
|
|
||||||
for _, key := range []string{"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 {
|
for _, item := range arr {
|
||||||
if itemSchema, ok := item.(map[string]interface{}); ok {
|
if itemSchema, ok := item.(map[string]any); ok {
|
||||||
convertSchemaRecursive(itemSchema)
|
convertSchemaRecursive(itemSchema)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recursively process not
|
if not, ok := schema["not"].(map[string]any); ok {
|
||||||
if not, ok := schema["not"].(map[string]interface{}); ok {
|
|
||||||
convertSchemaRecursive(not)
|
convertSchemaRecursive(not)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+61
-65
@@ -6,8 +6,6 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/cloudwego/eino/schema"
|
|
||||||
"github.com/eino-contrib/jsonschema"
|
|
||||||
"github.com/mark3labs/mcphost/internal/config"
|
"github.com/mark3labs/mcphost/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -84,7 +82,6 @@ func TestMCPToolManager_ToolWithoutProperties(t *testing.T) {
|
|||||||
manager := NewMCPToolManager()
|
manager := NewMCPToolManager()
|
||||||
|
|
||||||
// Create a config with a builtin todo server (which has tools with properties)
|
// Create a config with a builtin todo server (which has tools with properties)
|
||||||
// and test the schema conversion logic
|
|
||||||
cfg := &config.Config{
|
cfg := &config.Config{
|
||||||
MCPServers: map[string]config.MCPServerConfig{
|
MCPServers: map[string]config.MCPServerConfig{
|
||||||
"todo-server": {
|
"todo-server": {
|
||||||
@@ -111,69 +108,68 @@ func TestMCPToolManager_ToolWithoutProperties(t *testing.T) {
|
|||||||
|
|
||||||
// Test that we can get tool info for each tool
|
// Test that we can get tool info for each tool
|
||||||
for _, tool := range tools {
|
for _, tool := range tools {
|
||||||
info, err := tool.Info(ctx)
|
info := tool.Info()
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Failed to get tool info: %v", err)
|
// Check that the tool has a valid name
|
||||||
continue
|
if info.Name == "" {
|
||||||
|
t.Error("Tool has empty name")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check that the tool has a valid schema
|
t.Logf("Tool: %s, Description: %s", info.Name, info.Description)
|
||||||
if info.ParamsOneOf == nil {
|
|
||||||
t.Errorf("Tool %s has nil ParamsOneOf", info.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Logf("Tool: %s, Description: %s", info.Name, info.Desc)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestIssue89_ObjectSchemaMissingProperties tests the fix for issue #89
|
// TestIssue89_ObjectSchemaMissingProperties tests the fix for issue #89
|
||||||
// This is a regression test for the "object schema missing properties" error
|
// This verifies that object schemas with nil properties get an empty properties map
|
||||||
// that occurs when tools have no input parameters and use OpenAI function calling
|
|
||||||
func TestIssue89_ObjectSchemaMissingProperties(t *testing.T) {
|
func TestIssue89_ObjectSchemaMissingProperties(t *testing.T) {
|
||||||
// Create a schema that would cause the OpenAI validation error
|
// Create a schema that would cause issues with tools that have no input properties
|
||||||
// This simulates what might happen with tools that have no input properties
|
brokenSchema := map[string]any{
|
||||||
brokenSchema := &jsonschema.Schema{
|
"type": "object",
|
||||||
Type: "object",
|
// Properties is nil - this used to cause "object schema missing properties" error
|
||||||
// Properties is nil - this causes "object schema missing properties" error in OpenAI
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify the problematic state
|
// Verify the problematic state
|
||||||
if brokenSchema.Type == "object" && brokenSchema.Properties == nil {
|
if brokenSchema["type"] == "object" && brokenSchema["properties"] == nil {
|
||||||
t.Log("Found object schema with nil properties - this causes OpenAI validation error")
|
t.Log("Found object schema with nil properties - this previously caused validation errors")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply the fix from issue #89
|
// Apply the fix - add empty properties
|
||||||
if brokenSchema.Type == "object" && brokenSchema.Properties == nil {
|
if brokenSchema["type"] == "object" && brokenSchema["properties"] == nil {
|
||||||
brokenSchema.Properties = jsonschema.NewProperties()
|
brokenSchema["properties"] = map[string]any{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify the fix worked
|
// 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")
|
t.Error("Fix failed: object schema still has nil properties")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test that we can create a ParamsOneOf from the fixed schema
|
// Verify it marshals cleanly
|
||||||
// This is what would fail before the fix
|
data, err := json.Marshal(brokenSchema)
|
||||||
paramsOneOf := schema.NewParamsOneOfByJSONSchema(brokenSchema)
|
if err != nil {
|
||||||
if paramsOneOf == nil {
|
t.Errorf("Failed to marshal fixed schema: %v", err)
|
||||||
t.Error("Failed to create ParamsOneOf from fixed schema - OpenAI function calling would fail")
|
}
|
||||||
|
|
||||||
|
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
|
// 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) {
|
func TestConvertExclusiveBoundsToBoolean(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
input string
|
input string
|
||||||
expected map[string]interface{}
|
expected map[string]any
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "exclusiveMinimum as number",
|
name: "exclusiveMinimum as number",
|
||||||
input: `{"type": "number", "exclusiveMinimum": 0}`,
|
input: `{"type": "number", "exclusiveMinimum": 0}`,
|
||||||
expected: map[string]interface{}{
|
expected: map[string]any{
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"minimum": float64(0),
|
"minimum": float64(0),
|
||||||
"exclusiveMinimum": true,
|
"exclusiveMinimum": true,
|
||||||
@@ -182,7 +178,7 @@ func TestConvertExclusiveBoundsToBoolean(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "exclusiveMaximum as number",
|
name: "exclusiveMaximum as number",
|
||||||
input: `{"type": "number", "exclusiveMaximum": 100}`,
|
input: `{"type": "number", "exclusiveMaximum": 100}`,
|
||||||
expected: map[string]interface{}{
|
expected: map[string]any{
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"maximum": float64(100),
|
"maximum": float64(100),
|
||||||
"exclusiveMaximum": true,
|
"exclusiveMaximum": true,
|
||||||
@@ -191,7 +187,7 @@ func TestConvertExclusiveBoundsToBoolean(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "both exclusive bounds as numbers",
|
name: "both exclusive bounds as numbers",
|
||||||
input: `{"type": "integer", "exclusiveMinimum": 1, "exclusiveMaximum": 10}`,
|
input: `{"type": "integer", "exclusiveMinimum": 1, "exclusiveMaximum": 10}`,
|
||||||
expected: map[string]interface{}{
|
expected: map[string]any{
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"minimum": float64(1),
|
"minimum": float64(1),
|
||||||
"exclusiveMinimum": true,
|
"exclusiveMinimum": true,
|
||||||
@@ -202,7 +198,7 @@ func TestConvertExclusiveBoundsToBoolean(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "already boolean exclusiveMinimum (draft-04 style)",
|
name: "already boolean exclusiveMinimum (draft-04 style)",
|
||||||
input: `{"type": "number", "minimum": 0, "exclusiveMinimum": true}`,
|
input: `{"type": "number", "minimum": 0, "exclusiveMinimum": true}`,
|
||||||
expected: map[string]interface{}{
|
expected: map[string]any{
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"minimum": float64(0),
|
"minimum": float64(0),
|
||||||
"exclusiveMinimum": true,
|
"exclusiveMinimum": true,
|
||||||
@@ -211,7 +207,7 @@ func TestConvertExclusiveBoundsToBoolean(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "no exclusive bounds",
|
name: "no exclusive bounds",
|
||||||
input: `{"type": "string", "minLength": 1}`,
|
input: `{"type": "string", "minLength": 1}`,
|
||||||
expected: map[string]interface{}{
|
expected: map[string]any{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"minLength": float64(1),
|
"minLength": float64(1),
|
||||||
},
|
},
|
||||||
@@ -219,10 +215,10 @@ func TestConvertExclusiveBoundsToBoolean(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "nested properties with exclusive bounds",
|
name: "nested properties with exclusive bounds",
|
||||||
input: `{"type": "object", "properties": {"age": {"type": "integer", "exclusiveMinimum": 0}}}`,
|
input: `{"type": "object", "properties": {"age": {"type": "integer", "exclusiveMinimum": 0}}}`,
|
||||||
expected: map[string]interface{}{
|
expected: map[string]any{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": map[string]interface{}{
|
"properties": map[string]any{
|
||||||
"age": map[string]interface{}{
|
"age": map[string]any{
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"minimum": float64(0),
|
"minimum": float64(0),
|
||||||
"exclusiveMinimum": true,
|
"exclusiveMinimum": true,
|
||||||
@@ -233,9 +229,9 @@ func TestConvertExclusiveBoundsToBoolean(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "array items with exclusive bounds",
|
name: "array items with exclusive bounds",
|
||||||
input: `{"type": "array", "items": {"type": "number", "exclusiveMaximum": 100}}`,
|
input: `{"type": "array", "items": {"type": "number", "exclusiveMaximum": 100}}`,
|
||||||
expected: map[string]interface{}{
|
expected: map[string]any{
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": map[string]interface{}{
|
"items": map[string]any{
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"maximum": float64(100),
|
"maximum": float64(100),
|
||||||
"exclusiveMaximum": true,
|
"exclusiveMaximum": true,
|
||||||
@@ -245,9 +241,9 @@ func TestConvertExclusiveBoundsToBoolean(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "allOf with exclusive bounds",
|
name: "allOf with exclusive bounds",
|
||||||
input: `{"allOf": [{"type": "number", "exclusiveMinimum": 0}]}`,
|
input: `{"allOf": [{"type": "number", "exclusiveMinimum": 0}]}`,
|
||||||
expected: map[string]interface{}{
|
expected: map[string]any{
|
||||||
"allOf": []interface{}{
|
"allOf": []any{
|
||||||
map[string]interface{}{
|
map[string]any{
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"minimum": float64(0),
|
"minimum": float64(0),
|
||||||
"exclusiveMinimum": true,
|
"exclusiveMinimum": true,
|
||||||
@@ -258,9 +254,9 @@ func TestConvertExclusiveBoundsToBoolean(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "additionalProperties with exclusive bounds",
|
name: "additionalProperties with exclusive bounds",
|
||||||
input: `{"type": "object", "additionalProperties": {"type": "integer", "exclusiveMinimum": 0, "exclusiveMaximum": 255}}`,
|
input: `{"type": "object", "additionalProperties": {"type": "integer", "exclusiveMinimum": 0, "exclusiveMaximum": 255}}`,
|
||||||
expected: map[string]interface{}{
|
expected: map[string]any{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": map[string]interface{}{
|
"additionalProperties": map[string]any{
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"minimum": float64(0),
|
"minimum": float64(0),
|
||||||
"exclusiveMinimum": true,
|
"exclusiveMinimum": true,
|
||||||
@@ -272,15 +268,15 @@ func TestConvertExclusiveBoundsToBoolean(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "Chrome DevTools MCP style schema (real-world example)",
|
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}}}`,
|
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",
|
"type": "object",
|
||||||
"properties": map[string]interface{}{
|
"properties": map[string]any{
|
||||||
"timeout": map[string]interface{}{
|
"timeout": map[string]any{
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"minimum": float64(0),
|
"minimum": float64(0),
|
||||||
"exclusiveMinimum": true,
|
"exclusiveMinimum": true,
|
||||||
},
|
},
|
||||||
"quality": map[string]interface{}{
|
"quality": map[string]any{
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"minimum": float64(0),
|
"minimum": float64(0),
|
||||||
"maximum": float64(100),
|
"maximum": float64(100),
|
||||||
@@ -294,7 +290,7 @@ func TestConvertExclusiveBoundsToBoolean(t *testing.T) {
|
|||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
result := convertExclusiveBoundsToBoolean([]byte(tt.input))
|
result := convertExclusiveBoundsToBoolean([]byte(tt.input))
|
||||||
|
|
||||||
var got map[string]interface{}
|
var got map[string]any
|
||||||
if err := json.Unmarshal(result, &got); err != nil {
|
if err := json.Unmarshal(result, &got); err != nil {
|
||||||
t.Fatalf("Failed to unmarshal result: %v", err)
|
t.Fatalf("Failed to unmarshal result: %v", err)
|
||||||
}
|
}
|
||||||
@@ -317,7 +313,7 @@ func TestConvertExclusiveBoundsToBoolean_InvalidJSON(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// deepEqual compares two maps recursively
|
// 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) {
|
if len(a) != len(b) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -327,13 +323,13 @@ func deepEqual(a, b map[string]interface{}) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
switch av := v.(type) {
|
switch av := v.(type) {
|
||||||
case map[string]interface{}:
|
case map[string]any:
|
||||||
bvm, ok := bv.(map[string]interface{})
|
bvm, ok := bv.(map[string]any)
|
||||||
if !ok || !deepEqual(av, bvm) {
|
if !ok || !deepEqual(av, bvm) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
case []interface{}:
|
case []any:
|
||||||
bva, ok := bv.([]interface{})
|
bva, ok := bv.([]any)
|
||||||
if !ok || !sliceEqual(av, bva) {
|
if !ok || !sliceEqual(av, bva) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -347,19 +343,19 @@ func deepEqual(a, b map[string]interface{}) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// sliceEqual compares two slices recursively
|
// sliceEqual compares two slices recursively
|
||||||
func sliceEqual(a, b []interface{}) bool {
|
func sliceEqual(a, b []any) bool {
|
||||||
if len(a) != len(b) {
|
if len(a) != len(b) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
for i := range a {
|
for i := range a {
|
||||||
switch av := a[i].(type) {
|
switch av := a[i].(type) {
|
||||||
case map[string]interface{}:
|
case map[string]any:
|
||||||
bvm, ok := b[i].(map[string]interface{})
|
bvm, ok := b[i].(map[string]any)
|
||||||
if !ok || !deepEqual(av, bvm) {
|
if !ok || !deepEqual(av, bvm) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
case []interface{}:
|
case []any:
|
||||||
bva, ok := b[i].([]interface{})
|
bva, ok := b[i].([]any)
|
||||||
if !ok || !sliceEqual(av, bva) {
|
if !ok || !sliceEqual(av, bva) {
|
||||||
return false
|
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"
|
"time"
|
||||||
|
|
||||||
tea "charm.land/bubbletea/v2"
|
tea "charm.land/bubbletea/v2"
|
||||||
|
"charm.land/fantasy"
|
||||||
"charm.land/lipgloss/v2"
|
"charm.land/lipgloss/v2"
|
||||||
"github.com/cloudwego/eino/schema"
|
|
||||||
"golang.org/x/term"
|
"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
|
// response when available. Falls back to text-based estimation if the metadata is
|
||||||
// missing or appears unreliable. This provides more accurate usage tracking when
|
// missing or appears unreliable. This provides more accurate usage tracking when
|
||||||
// providers supply token count information.
|
// 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 {
|
if c.usageTracker == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to extract token usage from response metadata
|
usage := response.Usage
|
||||||
if response.ResponseMeta != nil && response.ResponseMeta.Usage != nil {
|
inputTokens := int(usage.InputTokens)
|
||||||
usage := response.ResponseMeta.Usage
|
outputTokens := int(usage.OutputTokens)
|
||||||
|
|
||||||
// Use actual token counts from the response
|
|
||||||
inputTokens := int(usage.PromptTokens)
|
|
||||||
outputTokens := int(usage.CompletionTokens)
|
|
||||||
|
|
||||||
// Validate that the metadata seems reasonable
|
// Validate that the metadata seems reasonable
|
||||||
// If token counts are 0 or seem unrealistic, fall back to estimation
|
|
||||||
if inputTokens > 0 && outputTokens > 0 {
|
if inputTokens > 0 && outputTokens > 0 {
|
||||||
// Handle cache tokens if available (some providers support this)
|
cacheReadTokens := int(usage.CacheReadTokens)
|
||||||
cacheReadTokens := 0
|
cacheWriteTokens := int(usage.CacheCreationTokens)
|
||||||
cacheWriteTokens := 0
|
|
||||||
|
|
||||||
c.usageTracker.UpdateUsage(inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens)
|
c.usageTracker.UpdateUsage(inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens)
|
||||||
} else {
|
|
||||||
// Metadata exists but seems incomplete/unreliable, use estimation
|
|
||||||
c.usageTracker.EstimateAndUpdateUsage(inputText, response.Content)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Fallback to estimation if no metadata is available
|
// 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
|
package ui
|
||||||
|
|
||||||
|
import "slices"
|
||||||
|
|
||||||
// SlashCommand represents a user-invokable slash command with its metadata.
|
// SlashCommand represents a user-invokable slash command with its metadata.
|
||||||
// Commands can have multiple aliases and are organized by category for better
|
// Commands can have multiple aliases and are organized by category for better
|
||||||
// discoverability and help display.
|
// discoverability and help display.
|
||||||
@@ -68,12 +70,10 @@ func GetCommandByName(name string) *SlashCommand {
|
|||||||
if cmd.Name == name {
|
if cmd.Name == name {
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
for _, alias := range cmd.Aliases {
|
if slices.Contains(cmd.Aliases, name) {
|
||||||
if alias == name {
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -296,10 +296,11 @@ func (r *CompactRenderer) formatCompactContent(content string) string {
|
|||||||
content = strings.TrimSpace(content)
|
content = strings.TrimSpace(content)
|
||||||
|
|
||||||
// Truncate if too long (unless in debug mode)
|
// Truncate if too long (unless in debug mode)
|
||||||
maxLen := r.width - 28 // Reserve space for symbol and label more conservatively
|
maxLen := max(
|
||||||
if maxLen < 40 {
|
// Reserve space for symbol and label more conservatively
|
||||||
maxLen = 40 // Minimum width for readability
|
r.width-28,
|
||||||
}
|
// Minimum width for readability
|
||||||
|
40)
|
||||||
if !r.debug && len(content) > maxLen {
|
if !r.debug && len(content) > maxLen {
|
||||||
content = content[:maxLen-3] + "..."
|
content = content[:maxLen-3] + "..."
|
||||||
}
|
}
|
||||||
@@ -315,10 +316,9 @@ func (r *CompactRenderer) formatUserAssistantContent(content string) string {
|
|||||||
|
|
||||||
// Calculate available width more conservatively
|
// Calculate available width more conservatively
|
||||||
// Account for: symbol (1) + spaces (2) + label (up to 20 chars) + space (1) + margin (4)
|
// Account for: symbol (1) + spaces (2) + label (up to 20 chars) + space (1) + margin (4)
|
||||||
availableWidth := r.width - 28
|
availableWidth := max(r.width-28,
|
||||||
if availableWidth < 40 {
|
// Minimum width for readability
|
||||||
availableWidth = 40 // Minimum width for readability
|
40)
|
||||||
}
|
|
||||||
|
|
||||||
// Use glamour to render markdown content with proper width
|
// Use glamour to render markdown content with proper width
|
||||||
rendered := toMarkdown(content, availableWidth)
|
rendered := toMarkdown(content, availableWidth)
|
||||||
@@ -407,10 +407,9 @@ func (r *CompactRenderer) formatToolResult(result string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Calculate available width more conservatively
|
// Calculate available width more conservatively
|
||||||
availableWidth := r.width - 28
|
availableWidth := max(r.width-28,
|
||||||
if availableWidth < 40 {
|
// Minimum width for readability
|
||||||
availableWidth = 40 // Minimum width for readability
|
40)
|
||||||
}
|
|
||||||
|
|
||||||
// First wrap the text to prevent long lines (tool results are usually plain text, not markdown)
|
// First wrap the text to prevent long lines (tool results are usually plain text, not markdown)
|
||||||
wrappedResult := r.wrapText(result, availableWidth)
|
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)
|
// Check if this is a multi-line debug output (like connection info)
|
||||||
if strings.Contains(message, "[DEBUG]") || strings.Contains(message, "[POOL]") {
|
if strings.Contains(message, "[DEBUG]") || strings.Contains(message, "[POOL]") {
|
||||||
// Extract the tag and content
|
// Extract the tag and content
|
||||||
if strings.HasPrefix(message, "[DEBUG]") {
|
if after, ok := strings.CutPrefix(message, "[DEBUG]"); ok {
|
||||||
content := strings.TrimPrefix(message, "[DEBUG]")
|
content := after
|
||||||
content = strings.TrimSpace(content)
|
content = strings.TrimSpace(content)
|
||||||
formattedMessage = fmt.Sprintf("🔍 DEBUG: %s", content)
|
formattedMessage = fmt.Sprintf("🔍 DEBUG: %s", content)
|
||||||
} else if strings.HasPrefix(message, "[POOL]") {
|
} else if after, ok := strings.CutPrefix(message, "[POOL]"); ok {
|
||||||
content := strings.TrimPrefix(message, "[POOL]")
|
content := after
|
||||||
content = strings.TrimSpace(content)
|
content = strings.TrimSpace(content)
|
||||||
|
|
||||||
// Add appropriate emoji based on the message 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
|
return m, nil
|
||||||
|
|
||||||
case tea.WindowSizeMsg:
|
case tea.WindowSizeMsg:
|
||||||
newWidth := msg.Width - padding*2 - 4
|
newWidth := min(msg.Width-padding*2-4, maxWidth)
|
||||||
if newWidth > maxWidth {
|
|
||||||
newWidth = maxWidth
|
|
||||||
}
|
|
||||||
m.progress.SetWidth(newWidth)
|
m.progress.SetWidth(newWidth)
|
||||||
return m, nil
|
return m, nil
|
||||||
|
|
||||||
@@ -167,14 +164,12 @@ func NewProgressReader(reader io.Reader) *ProgressReader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Start the TUI in a goroutine
|
// Start the TUI in a goroutine
|
||||||
pr.wg.Add(1)
|
pr.wg.Go(func() {
|
||||||
go func() {
|
|
||||||
defer pr.wg.Done()
|
|
||||||
if _, err := program.Run(); err != nil {
|
if _, err := program.Run(); err != nil {
|
||||||
// Handle error silently for now
|
// Handle error silently for now
|
||||||
}
|
}
|
||||||
close(pr.done)
|
close(pr.done)
|
||||||
}()
|
})
|
||||||
|
|
||||||
return pr
|
return pr
|
||||||
}
|
}
|
||||||
|
|||||||
+69
-121
@@ -1,153 +1,101 @@
|
|||||||
package ui
|
package ui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"image/color"
|
"image/color"
|
||||||
"os"
|
"os"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"charm.land/bubbles/v2/spinner"
|
|
||||||
tea "charm.land/bubbletea/v2"
|
|
||||||
"charm.land/lipgloss/v2"
|
"charm.land/lipgloss/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Spinner provides an animated loading indicator that displays while long-running
|
// spinnerFrames defines available spinner animation styles.
|
||||||
// operations are in progress. It wraps the bubbles spinner component and manages
|
var (
|
||||||
// its lifecycle through a tea.Program for proper terminal handling.
|
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 {
|
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
|
message string
|
||||||
quitting bool
|
frames []string
|
||||||
|
fps time.Duration
|
||||||
|
color color.Color
|
||||||
|
done chan struct{}
|
||||||
|
once sync.Once
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m spinnerModel) Init() tea.Cmd {
|
// NewSpinner creates a new animated spinner with the specified message.
|
||||||
return m.spinner.Tick
|
// The spinner uses the theme's primary color and a points animation style.
|
||||||
}
|
|
||||||
|
|
||||||
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.
|
|
||||||
func NewSpinner(message string) *Spinner {
|
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{
|
return &Spinner{
|
||||||
model: s,
|
message: message,
|
||||||
|
frames: pointsFrames,
|
||||||
|
fps: pointsFPS,
|
||||||
|
color: GetTheme().Primary,
|
||||||
done: make(chan struct{}),
|
done: make(chan struct{}),
|
||||||
prog: prog,
|
|
||||||
ctx: ctx,
|
|
||||||
cancel: cancel,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewThemedSpinner creates a new animated spinner with custom color styling.
|
// NewThemedSpinner creates a new animated spinner with custom color styling.
|
||||||
// This allows for different spinner colors based on the operation type or status.
|
// 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, c color.Color) *Spinner {
|
||||||
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())
|
|
||||||
|
|
||||||
return &Spinner{
|
return &Spinner{
|
||||||
model: s,
|
message: message,
|
||||||
|
frames: dotFrames,
|
||||||
|
fps: dotFPS,
|
||||||
|
color: c,
|
||||||
done: make(chan struct{}),
|
done: make(chan struct{}),
|
||||||
prog: prog,
|
|
||||||
ctx: ctx,
|
|
||||||
cancel: cancel,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start begins the spinner animation in a separate goroutine. The spinner will
|
// Start begins the spinner animation in a separate goroutine. The spinner
|
||||||
// continue animating until Stop is called. The animation runs in a separate
|
// will continue animating until Stop is called.
|
||||||
// tea.Program to maintain smooth animation independent of other operations.
|
|
||||||
func (s *Spinner) Start() {
|
func (s *Spinner) Start() {
|
||||||
go func() {
|
go s.run()
|
||||||
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)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop halts the spinner animation and cleans up resources. This method blocks
|
// Stop halts the spinner animation and cleans up. This method blocks until
|
||||||
// until the spinner has fully stopped and the terminal state is restored.
|
// the animation goroutine has exited and the line is cleared.
|
||||||
func (s *Spinner) Stop() {
|
func (s *Spinner) Stop() {
|
||||||
s.cancel()
|
s.once.Do(func() { close(s.done) })
|
||||||
<-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
|
const defaultMargin = 1
|
||||||
|
|
||||||
// Helper functions for style pointers
|
// Helper functions for style pointers
|
||||||
func boolPtr(b bool) *bool { return &b }
|
//
|
||||||
func stringPtr(s string) *string { return &s }
|
//go:fix inline
|
||||||
func uintPtr(u uint) *uint { return &u }
|
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
|
// BaseStyle returns a new, empty lipgloss style that can be customized with
|
||||||
// additional styling methods. This serves as the foundation for building more
|
// additional styling methods. This serves as the foundation for building more
|
||||||
@@ -106,101 +112,101 @@ func generateMarkdownStyleConfig() ansi.StyleConfig {
|
|||||||
StylePrimitive: ansi.StylePrimitive{
|
StylePrimitive: ansi.StylePrimitive{
|
||||||
BlockPrefix: "",
|
BlockPrefix: "",
|
||||||
BlockSuffix: "",
|
BlockSuffix: "",
|
||||||
Color: stringPtr(textColor),
|
Color: new(textColor),
|
||||||
},
|
},
|
||||||
Margin: uintPtr(0), // Remove margin to prevent spacing
|
Margin: uintPtr(0), // Remove margin to prevent spacing
|
||||||
},
|
},
|
||||||
BlockQuote: ansi.StyleBlock{
|
BlockQuote: ansi.StyleBlock{
|
||||||
StylePrimitive: ansi.StylePrimitive{
|
StylePrimitive: ansi.StylePrimitive{
|
||||||
Color: stringPtr(mutedColor),
|
Color: new(mutedColor),
|
||||||
Italic: boolPtr(true),
|
Italic: new(true),
|
||||||
Prefix: "┃ ",
|
Prefix: "┃ ",
|
||||||
},
|
},
|
||||||
Indent: uintPtr(1),
|
Indent: uintPtr(1),
|
||||||
IndentToken: stringPtr(lipgloss.NewStyle().Background(lipgloss.Color(bgColor)).Render(" ")),
|
IndentToken: new(lipgloss.NewStyle().Background(lipgloss.Color(bgColor)).Render(" ")),
|
||||||
},
|
},
|
||||||
List: ansi.StyleList{
|
List: ansi.StyleList{
|
||||||
LevelIndent: 0, // Remove list indentation
|
LevelIndent: 0, // Remove list indentation
|
||||||
StyleBlock: ansi.StyleBlock{
|
StyleBlock: ansi.StyleBlock{
|
||||||
IndentToken: stringPtr(lipgloss.NewStyle().Background(lipgloss.Color(bgColor)).Render(" ")),
|
IndentToken: new(lipgloss.NewStyle().Background(lipgloss.Color(bgColor)).Render(" ")),
|
||||||
StylePrimitive: ansi.StylePrimitive{
|
StylePrimitive: ansi.StylePrimitive{
|
||||||
Color: stringPtr(textColor),
|
Color: new(textColor),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Heading: ansi.StyleBlock{
|
Heading: ansi.StyleBlock{
|
||||||
StylePrimitive: ansi.StylePrimitive{
|
StylePrimitive: ansi.StylePrimitive{
|
||||||
BlockSuffix: "\n",
|
BlockSuffix: "\n",
|
||||||
Color: stringPtr(headingColor),
|
Color: new(headingColor),
|
||||||
Bold: boolPtr(true),
|
Bold: new(true),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
H1: ansi.StyleBlock{
|
H1: ansi.StyleBlock{
|
||||||
StylePrimitive: ansi.StylePrimitive{
|
StylePrimitive: ansi.StylePrimitive{
|
||||||
Prefix: "# ",
|
Prefix: "# ",
|
||||||
Color: stringPtr(headingColor),
|
Color: new(headingColor),
|
||||||
Bold: boolPtr(true),
|
Bold: new(true),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
H2: ansi.StyleBlock{
|
H2: ansi.StyleBlock{
|
||||||
StylePrimitive: ansi.StylePrimitive{
|
StylePrimitive: ansi.StylePrimitive{
|
||||||
Prefix: "## ",
|
Prefix: "## ",
|
||||||
Color: stringPtr(headingColor),
|
Color: new(headingColor),
|
||||||
Bold: boolPtr(true),
|
Bold: new(true),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
H3: ansi.StyleBlock{
|
H3: ansi.StyleBlock{
|
||||||
StylePrimitive: ansi.StylePrimitive{
|
StylePrimitive: ansi.StylePrimitive{
|
||||||
Prefix: "### ",
|
Prefix: "### ",
|
||||||
Color: stringPtr(headingColor),
|
Color: new(headingColor),
|
||||||
Bold: boolPtr(true),
|
Bold: new(true),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
H4: ansi.StyleBlock{
|
H4: ansi.StyleBlock{
|
||||||
StylePrimitive: ansi.StylePrimitive{
|
StylePrimitive: ansi.StylePrimitive{
|
||||||
Prefix: "#### ",
|
Prefix: "#### ",
|
||||||
Color: stringPtr(headingColor),
|
Color: new(headingColor),
|
||||||
Bold: boolPtr(true),
|
Bold: new(true),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
H5: ansi.StyleBlock{
|
H5: ansi.StyleBlock{
|
||||||
StylePrimitive: ansi.StylePrimitive{
|
StylePrimitive: ansi.StylePrimitive{
|
||||||
Prefix: "##### ",
|
Prefix: "##### ",
|
||||||
Color: stringPtr(headingColor),
|
Color: new(headingColor),
|
||||||
Bold: boolPtr(true),
|
Bold: new(true),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
H6: ansi.StyleBlock{
|
H6: ansi.StyleBlock{
|
||||||
StylePrimitive: ansi.StylePrimitive{
|
StylePrimitive: ansi.StylePrimitive{
|
||||||
Prefix: "###### ",
|
Prefix: "###### ",
|
||||||
Color: stringPtr(headingColor),
|
Color: new(headingColor),
|
||||||
Bold: boolPtr(true),
|
Bold: new(true),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Strikethrough: ansi.StylePrimitive{
|
Strikethrough: ansi.StylePrimitive{
|
||||||
CrossedOut: boolPtr(true),
|
CrossedOut: new(true),
|
||||||
Color: stringPtr(mutedColor),
|
Color: new(mutedColor),
|
||||||
},
|
},
|
||||||
Emph: ansi.StylePrimitive{
|
Emph: ansi.StylePrimitive{
|
||||||
Color: stringPtr(emphColor),
|
Color: new(emphColor),
|
||||||
|
|
||||||
Italic: boolPtr(true),
|
Italic: new(true),
|
||||||
},
|
},
|
||||||
Strong: ansi.StylePrimitive{
|
Strong: ansi.StylePrimitive{
|
||||||
Bold: boolPtr(true),
|
Bold: new(true),
|
||||||
Color: stringPtr(strongColor),
|
Color: new(strongColor),
|
||||||
},
|
},
|
||||||
HorizontalRule: ansi.StylePrimitive{
|
HorizontalRule: ansi.StylePrimitive{
|
||||||
Color: stringPtr(mutedColor),
|
Color: new(mutedColor),
|
||||||
Format: "\n─────────────────────────────────────────\n",
|
Format: "\n─────────────────────────────────────────\n",
|
||||||
},
|
},
|
||||||
Item: ansi.StylePrimitive{
|
Item: ansi.StylePrimitive{
|
||||||
BlockPrefix: "• ",
|
BlockPrefix: "• ",
|
||||||
Color: stringPtr(textColor),
|
Color: new(textColor),
|
||||||
},
|
},
|
||||||
Enumeration: ansi.StylePrimitive{
|
Enumeration: ansi.StylePrimitive{
|
||||||
BlockPrefix: ". ",
|
BlockPrefix: ". ",
|
||||||
Color: stringPtr(textColor),
|
Color: new(textColor),
|
||||||
},
|
},
|
||||||
Task: ansi.StyleTask{
|
Task: ansi.StyleTask{
|
||||||
StylePrimitive: ansi.StylePrimitive{},
|
StylePrimitive: ansi.StylePrimitive{},
|
||||||
@@ -208,29 +214,29 @@ func generateMarkdownStyleConfig() ansi.StyleConfig {
|
|||||||
Unticked: "[ ] ",
|
Unticked: "[ ] ",
|
||||||
},
|
},
|
||||||
Link: ansi.StylePrimitive{
|
Link: ansi.StylePrimitive{
|
||||||
Color: stringPtr(linkColor),
|
Color: new(linkColor),
|
||||||
|
|
||||||
Underline: boolPtr(true),
|
Underline: new(true),
|
||||||
},
|
},
|
||||||
LinkText: ansi.StylePrimitive{
|
LinkText: ansi.StylePrimitive{
|
||||||
Color: stringPtr(linkColor),
|
Color: new(linkColor),
|
||||||
|
|
||||||
Bold: boolPtr(true),
|
Bold: new(true),
|
||||||
},
|
},
|
||||||
Image: ansi.StylePrimitive{
|
Image: ansi.StylePrimitive{
|
||||||
Color: stringPtr(linkColor),
|
Color: new(linkColor),
|
||||||
|
|
||||||
Underline: boolPtr(true),
|
Underline: new(true),
|
||||||
Format: "🖼 {{.text}}",
|
Format: "🖼 {{.text}}",
|
||||||
},
|
},
|
||||||
ImageText: ansi.StylePrimitive{
|
ImageText: ansi.StylePrimitive{
|
||||||
Color: stringPtr(linkColor),
|
Color: new(linkColor),
|
||||||
|
|
||||||
Format: "{{.text}}",
|
Format: "{{.text}}",
|
||||||
},
|
},
|
||||||
Code: ansi.StyleBlock{
|
Code: ansi.StyleBlock{
|
||||||
StylePrimitive: ansi.StylePrimitive{
|
StylePrimitive: ansi.StylePrimitive{
|
||||||
Color: stringPtr(codeColor),
|
Color: new(codeColor),
|
||||||
|
|
||||||
Prefix: "",
|
Prefix: "",
|
||||||
Suffix: "",
|
Suffix: "",
|
||||||
@@ -240,92 +246,92 @@ func generateMarkdownStyleConfig() ansi.StyleConfig {
|
|||||||
StyleBlock: ansi.StyleBlock{
|
StyleBlock: ansi.StyleBlock{
|
||||||
StylePrimitive: ansi.StylePrimitive{
|
StylePrimitive: ansi.StylePrimitive{
|
||||||
Prefix: "",
|
Prefix: "",
|
||||||
Color: stringPtr(codeColor),
|
Color: new(codeColor),
|
||||||
},
|
},
|
||||||
Margin: uintPtr(0), // Remove margin
|
Margin: uintPtr(0), // Remove margin
|
||||||
},
|
},
|
||||||
Chroma: &ansi.Chroma{
|
Chroma: &ansi.Chroma{
|
||||||
Text: ansi.StylePrimitive{
|
Text: ansi.StylePrimitive{
|
||||||
Color: stringPtr(textColor),
|
Color: new(textColor),
|
||||||
},
|
},
|
||||||
Error: ansi.StylePrimitive{
|
Error: ansi.StylePrimitive{
|
||||||
Color: stringPtr(errorColor),
|
Color: new(errorColor),
|
||||||
},
|
},
|
||||||
Comment: ansi.StylePrimitive{
|
Comment: ansi.StylePrimitive{
|
||||||
Color: stringPtr(commentColor),
|
Color: new(commentColor),
|
||||||
},
|
},
|
||||||
CommentPreproc: ansi.StylePrimitive{
|
CommentPreproc: ansi.StylePrimitive{
|
||||||
Color: stringPtr(keywordColor),
|
Color: new(keywordColor),
|
||||||
},
|
},
|
||||||
Keyword: ansi.StylePrimitive{
|
Keyword: ansi.StylePrimitive{
|
||||||
Color: stringPtr(keywordColor),
|
Color: new(keywordColor),
|
||||||
},
|
},
|
||||||
KeywordReserved: ansi.StylePrimitive{
|
KeywordReserved: ansi.StylePrimitive{
|
||||||
Color: stringPtr(keywordColor),
|
Color: new(keywordColor),
|
||||||
},
|
},
|
||||||
KeywordNamespace: ansi.StylePrimitive{
|
KeywordNamespace: ansi.StylePrimitive{
|
||||||
Color: stringPtr(keywordColor),
|
Color: new(keywordColor),
|
||||||
},
|
},
|
||||||
KeywordType: ansi.StylePrimitive{
|
KeywordType: ansi.StylePrimitive{
|
||||||
Color: stringPtr(keywordColor),
|
Color: new(keywordColor),
|
||||||
},
|
},
|
||||||
Operator: ansi.StylePrimitive{
|
Operator: ansi.StylePrimitive{
|
||||||
Color: stringPtr(textColor),
|
Color: new(textColor),
|
||||||
},
|
},
|
||||||
Punctuation: ansi.StylePrimitive{
|
Punctuation: ansi.StylePrimitive{
|
||||||
Color: stringPtr(textColor),
|
Color: new(textColor),
|
||||||
},
|
},
|
||||||
Name: ansi.StylePrimitive{
|
Name: ansi.StylePrimitive{
|
||||||
Color: stringPtr(textColor),
|
Color: new(textColor),
|
||||||
},
|
},
|
||||||
NameBuiltin: ansi.StylePrimitive{
|
NameBuiltin: ansi.StylePrimitive{
|
||||||
Color: stringPtr(textColor),
|
Color: new(textColor),
|
||||||
},
|
},
|
||||||
NameTag: ansi.StylePrimitive{
|
NameTag: ansi.StylePrimitive{
|
||||||
Color: stringPtr(keywordColor),
|
Color: new(keywordColor),
|
||||||
},
|
},
|
||||||
NameAttribute: ansi.StylePrimitive{
|
NameAttribute: ansi.StylePrimitive{
|
||||||
Color: stringPtr(textColor),
|
Color: new(textColor),
|
||||||
},
|
},
|
||||||
NameClass: ansi.StylePrimitive{
|
NameClass: ansi.StylePrimitive{
|
||||||
Color: stringPtr(keywordColor),
|
Color: new(keywordColor),
|
||||||
},
|
},
|
||||||
NameConstant: ansi.StylePrimitive{
|
NameConstant: ansi.StylePrimitive{
|
||||||
Color: stringPtr(textColor),
|
Color: new(textColor),
|
||||||
},
|
},
|
||||||
NameDecorator: ansi.StylePrimitive{
|
NameDecorator: ansi.StylePrimitive{
|
||||||
Color: stringPtr(textColor),
|
Color: new(textColor),
|
||||||
},
|
},
|
||||||
NameFunction: ansi.StylePrimitive{
|
NameFunction: ansi.StylePrimitive{
|
||||||
Color: stringPtr(textColor),
|
Color: new(textColor),
|
||||||
},
|
},
|
||||||
LiteralNumber: ansi.StylePrimitive{
|
LiteralNumber: ansi.StylePrimitive{
|
||||||
Color: stringPtr(numberColor),
|
Color: new(numberColor),
|
||||||
},
|
},
|
||||||
LiteralString: ansi.StylePrimitive{
|
LiteralString: ansi.StylePrimitive{
|
||||||
Color: stringPtr(stringColor),
|
Color: new(stringColor),
|
||||||
},
|
},
|
||||||
LiteralStringEscape: ansi.StylePrimitive{
|
LiteralStringEscape: ansi.StylePrimitive{
|
||||||
Color: stringPtr(keywordColor),
|
Color: new(keywordColor),
|
||||||
},
|
},
|
||||||
GenericDeleted: ansi.StylePrimitive{
|
GenericDeleted: ansi.StylePrimitive{
|
||||||
Color: stringPtr(errorColor),
|
Color: new(errorColor),
|
||||||
},
|
},
|
||||||
GenericEmph: ansi.StylePrimitive{
|
GenericEmph: ansi.StylePrimitive{
|
||||||
Color: stringPtr(emphColor),
|
Color: new(emphColor),
|
||||||
|
|
||||||
Italic: boolPtr(true),
|
Italic: new(true),
|
||||||
},
|
},
|
||||||
GenericInserted: ansi.StylePrimitive{
|
GenericInserted: ansi.StylePrimitive{
|
||||||
Color: stringPtr(stringColor),
|
Color: new(stringColor),
|
||||||
},
|
},
|
||||||
GenericStrong: ansi.StylePrimitive{
|
GenericStrong: ansi.StylePrimitive{
|
||||||
Color: stringPtr(strongColor),
|
Color: new(strongColor),
|
||||||
|
|
||||||
Bold: boolPtr(true),
|
Bold: new(true),
|
||||||
},
|
},
|
||||||
GenericSubheading: ansi.StylePrimitive{
|
GenericSubheading: ansi.StylePrimitive{
|
||||||
Color: stringPtr(headingColor),
|
Color: new(headingColor),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -336,20 +342,20 @@ func generateMarkdownStyleConfig() ansi.StyleConfig {
|
|||||||
BlockSuffix: "\n",
|
BlockSuffix: "\n",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
CenterSeparator: stringPtr("┼"),
|
CenterSeparator: new("┼"),
|
||||||
ColumnSeparator: stringPtr("│"),
|
ColumnSeparator: new("│"),
|
||||||
RowSeparator: stringPtr("─"),
|
RowSeparator: new("─"),
|
||||||
},
|
},
|
||||||
DefinitionDescription: ansi.StylePrimitive{
|
DefinitionDescription: ansi.StylePrimitive{
|
||||||
BlockPrefix: "\n ❯ ",
|
BlockPrefix: "\n ❯ ",
|
||||||
Color: stringPtr(linkColor),
|
Color: new(linkColor),
|
||||||
},
|
},
|
||||||
Text: ansi.StylePrimitive{
|
Text: ansi.StylePrimitive{
|
||||||
Color: stringPtr(textColor),
|
Color: new(textColor),
|
||||||
},
|
},
|
||||||
Paragraph: ansi.StyleBlock{
|
Paragraph: ansi.StyleBlock{
|
||||||
StylePrimitive: ansi.StylePrimitive{
|
StylePrimitive: ansi.StylePrimitive{
|
||||||
Color: stringPtr(textColor),
|
Color: new(textColor),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-9
@@ -4,7 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/cloudwego/eino/schema"
|
"charm.land/fantasy"
|
||||||
"github.com/mark3labs/mcphost/cmd"
|
"github.com/mark3labs/mcphost/cmd"
|
||||||
"github.com/mark3labs/mcphost/internal/agent"
|
"github.com/mark3labs/mcphost/internal/agent"
|
||||||
"github.com/mark3labs/mcphost/internal/config"
|
"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)
|
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"))
|
temperature := float32(viper.GetFloat64("temperature"))
|
||||||
topP := float32(viper.GetFloat64("top-p"))
|
topP := float32(viper.GetFloat64("top-p"))
|
||||||
topK := int32(viper.GetInt("top-k"))
|
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"),
|
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{
|
a, err := agent.CreateAgent(ctx, &agent.AgentCreationOptions{
|
||||||
ModelConfig: modelConfig,
|
ModelConfig: modelConfig,
|
||||||
MCPConfig: mcpConfig,
|
MCPConfig: mcpConfig,
|
||||||
@@ -132,10 +132,10 @@ func (m *MCPHost) Prompt(ctx context.Context, message string) (string, error) {
|
|||||||
messages := m.sessionMgr.GetMessages()
|
messages := m.sessionMgr.GetMessages()
|
||||||
|
|
||||||
// Add new user message
|
// Add new user message
|
||||||
userMsg := schema.UserMessage(message)
|
userMsg := fantasy.NewUserMessage(message)
|
||||||
messages = append(messages, userMsg)
|
messages = append(messages, userMsg)
|
||||||
|
|
||||||
// Call agent (same as CLI does in root.go:902)
|
// Call agent
|
||||||
result, err := m.agent.GenerateWithLoop(ctx, messages,
|
result, err := m.agent.GenerateWithLoop(ctx, messages,
|
||||||
nil, // onToolCall
|
nil, // onToolCall
|
||||||
nil, // onToolExecution
|
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
|
// 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 {
|
if err := m.sessionMgr.ReplaceAllMessages(result.ConversationMessages); err != nil {
|
||||||
return "", fmt.Errorf("failed to update session: %v", err)
|
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
|
// PromptWithCallbacks sends a message with callbacks for monitoring tool execution
|
||||||
@@ -171,7 +170,7 @@ func (m *MCPHost) PromptWithCallbacks(
|
|||||||
messages := m.sessionMgr.GetMessages()
|
messages := m.sessionMgr.GetMessages()
|
||||||
|
|
||||||
// Add new user message
|
// Add new user message
|
||||||
userMsg := schema.UserMessage(message)
|
userMsg := fantasy.NewUserMessage(message)
|
||||||
messages = append(messages, userMsg)
|
messages = append(messages, userMsg)
|
||||||
|
|
||||||
// Call agent with callbacks
|
// Call agent with callbacks
|
||||||
@@ -193,7 +192,7 @@ func (m *MCPHost) PromptWithCallbacks(
|
|||||||
return "", fmt.Errorf("failed to update session: %v", err)
|
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
|
// GetSessionManager returns the current session manager for direct access
|
||||||
|
|||||||
+1
-1
@@ -35,7 +35,7 @@ func TestNewWithOptions(t *testing.T) {
|
|||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
opts := &sdk.Options{
|
opts := &sdk.Options{
|
||||||
Model: "anthropic:claude-3-haiku-20240307",
|
Model: "anthropic:claude-sonnet-4-20250514",
|
||||||
MaxSteps: 5,
|
MaxSteps: 5,
|
||||||
Quiet: true,
|
Quiet: true,
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-7
@@ -1,7 +1,7 @@
|
|||||||
package sdk
|
package sdk
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/cloudwego/eino/schema"
|
"charm.land/fantasy"
|
||||||
"github.com/mark3labs/mcphost/internal/session"
|
"github.com/mark3labs/mcphost/internal/session"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -13,14 +13,14 @@ type Message = session.Message
|
|||||||
// with its name, arguments, and result within a conversation.
|
// with its name, arguments, and result within a conversation.
|
||||||
type ToolCall = session.ToolCall
|
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.
|
// format used by the agent for LLM interactions.
|
||||||
func ConvertToSchemaMessage(msg *Message) *schema.Message {
|
func ConvertToFantasyMessage(msg *Message) fantasy.Message {
|
||||||
return msg.ConvertToSchemaMessage()
|
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.
|
// message format for use in the SDK API.
|
||||||
func ConvertFromSchemaMessage(msg *schema.Message) Message {
|
func ConvertFromFantasyMessage(msg fantasy.Message) Message {
|
||||||
return session.ConvertFromSchemaMessage(msg)
|
return session.ConvertFromFantasyMessage(msg)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user