first commit

This commit is contained in:
Beyhan Oğur
2026-04-26 21:52:23 +03:00
commit 880f412e2c
2662 changed files with 866266 additions and 0 deletions

View File

@@ -0,0 +1,301 @@
//go:build !tinygo && !wasm
package starlark
import (
"context"
"encoding/json"
"fmt"
"strings"
codemcp "github.com/maximhq/bifrost/core/mcp"
"github.com/maximhq/bifrost/core/schemas"
)
// createGetToolDocsTool creates the getToolDocs tool definition for code mode.
// This tool provides detailed documentation for a specific tool when the compact
// signatures from readToolFile are not sufficient to understand how to use it.
func (s *StarlarkCodeMode) createGetToolDocsTool() schemas.ChatTool {
getToolDocsProps := schemas.NewOrderedMapFromPairs(
schemas.KV("server", map[string]interface{}{
"type": "string",
"description": "The server name (e.g., 'calculator'). Use listToolFiles to see available servers.",
}),
schemas.KV("tool", map[string]interface{}{
"type": "string",
"description": "The tool name (e.g., 'add'). Use readToolFile to see available tools for a server.",
}),
)
return schemas.ChatTool{
Type: schemas.ChatToolTypeFunction,
Function: &schemas.ChatToolFunction{
Name: codemcp.ToolTypeGetToolDocs,
Description: schemas.Ptr(
"Get detailed documentation for a specific tool including full parameter descriptions, " +
"types, and usage examples. Use this when the compact signature from readToolFile " +
"is not sufficient to understand how to use a tool. " +
"Requires both server name and tool name as parameters.",
),
Parameters: &schemas.ToolFunctionParameters{
Type: "object",
Properties: getToolDocsProps,
Required: []string{"server", "tool"},
},
},
}
}
// handleGetToolDocs handles the getToolDocs tool call.
func (s *StarlarkCodeMode) handleGetToolDocs(ctx context.Context, toolCall schemas.ChatAssistantMessageToolCall) (*schemas.ChatMessage, error) {
// Parse tool arguments
var arguments map[string]interface{}
if err := json.Unmarshal([]byte(toolCall.Function.Arguments), &arguments); err != nil {
return nil, fmt.Errorf("failed to parse tool arguments: %v", err)
}
serverName, ok := arguments["server"].(string)
if !ok || serverName == "" {
return nil, fmt.Errorf("server parameter is required and must be a string")
}
toolName, ok := arguments["tool"].(string)
if !ok || toolName == "" {
return nil, fmt.Errorf("tool parameter is required and must be a string")
}
// Get available tools per client
availableToolsPerClient := s.clientManager.GetToolPerClient(ctx)
// Find matching client
var matchedClientName string
var matchedTool *schemas.ChatTool
serverNameLower := strings.ToLower(serverName)
for clientName, tools := range availableToolsPerClient {
client := s.clientManager.GetClientByName(clientName)
if client == nil {
s.logger.Warn("%s Client %s not found, skipping", codemcp.CodeModeLogPrefix, clientName)
continue
}
if !client.ExecutionConfig.IsCodeModeClient || len(tools) == 0 {
continue
}
clientNameLower := strings.ToLower(clientName)
if clientNameLower == serverNameLower {
matchedClientName = clientName
// Find the specific tool
for i, tool := range tools {
if tool.Function != nil {
if matchesToolReference(toolName, clientName, tool.Function.Name) {
matchedTool = &tools[i]
break
}
}
}
break
}
}
// Handle server not found
if matchedClientName == "" {
var availableServers []string
for name := range availableToolsPerClient {
client := s.clientManager.GetClientByName(name)
if client != nil && client.ExecutionConfig.IsCodeModeClient {
availableServers = append(availableServers, name)
}
}
errorMsg := fmt.Sprintf("Server '%s' not found. Available servers are:\n", serverName)
for _, sn := range availableServers {
errorMsg += fmt.Sprintf(" - %s\n", sn)
}
return createToolResponseMessage(toolCall, errorMsg), nil
}
// Handle tool not found
if matchedTool == nil {
tools := availableToolsPerClient[matchedClientName]
var availableTools []string
for _, tool := range tools {
if tool.Function != nil {
availableTools = append(availableTools, getCanonicalToolName(matchedClientName, tool.Function.Name))
}
}
errorMsg := fmt.Sprintf("Tool '%s' not found in server '%s'. Available tools are:\n", toolName, matchedClientName)
for _, t := range availableTools {
errorMsg += fmt.Sprintf(" - %s\n", t)
}
return createToolResponseMessage(toolCall, errorMsg), nil
}
// Generate detailed documentation using generateTypeDefinitions
docContent := generateTypeDefinitions(matchedClientName, []schemas.ChatTool{*matchedTool}, true)
return createToolResponseMessage(toolCall, docContent), nil
}
// generateTypeDefinitions generates Python documentation with docstrings from ChatTool schemas.
func generateTypeDefinitions(clientName string, tools []schemas.ChatTool, isToolLevel bool) string {
var sb strings.Builder
// Write comprehensive header
sb.WriteString("# ============================================================================\n")
if isToolLevel && len(tools) == 1 && tools[0].Function != nil {
sb.WriteString(fmt.Sprintf("# Documentation for %s.%s tool\n", clientName, getCanonicalToolName(clientName, tools[0].Function.Name)))
} else {
sb.WriteString(fmt.Sprintf("# Documentation for %s MCP server\n", clientName))
}
sb.WriteString("# ============================================================================\n")
sb.WriteString("#\n")
if isToolLevel && len(tools) == 1 {
sb.WriteString("# This file contains Python documentation for a specific tool on this MCP server.\n")
} else {
sb.WriteString("# This file contains Python documentation for all tools available on this MCP server.\n")
}
sb.WriteString("#\n")
sb.WriteString("# USAGE INSTRUCTIONS:\n")
sb.WriteString(fmt.Sprintf("# Call tools using: result = %s.tool_name(param=value)\n", clientName))
sb.WriteString("# No async/await needed - calls are synchronous.\n")
sb.WriteString("#\n")
sb.WriteString("# STARLARK DIFFERENCE FROM PYTHON:\n")
sb.WriteString("# for/if/while at top level MUST be inside a function.\n")
sb.WriteString("# Wrap loops: def main(): for x in items: ... then result = main()\n")
sb.WriteString("#\n")
sb.WriteString("# CRITICAL - HANDLING RESPONSES:\n")
sb.WriteString("# Tool responses are dicts. To avoid runtime errors:\n")
sb.WriteString("# 1. Use print(result) to inspect the response structure first\n")
sb.WriteString("# 2. Access dict values with brackets: result[\"key\"] NOT result.key\n")
sb.WriteString("# 3. Use .get() for safe access: result.get(\"key\", default)\n")
sb.WriteString("#\n")
sb.WriteString("# Common error: \"key not found\" or \"has no attribute\"\n")
sb.WriteString("# Fix: Use print() to see actual structure, then use result[\"key\"] or .get()\n")
sb.WriteString("# ============================================================================\n\n")
// Generate function definitions for each tool
for _, tool := range tools {
if tool.Function == nil || tool.Function.Name == "" {
continue
}
originalToolName := tool.Function.Name
toolName := getCanonicalToolName(clientName, originalToolName)
description := ""
if tool.Function.Description != nil {
description = *tool.Function.Description
}
// Generate function signature
params := formatPythonParams(tool.Function.Parameters)
sb.WriteString(fmt.Sprintf("def %s(%s) -> dict:\n", toolName, params))
// Generate docstring
sb.WriteString(" \"\"\"\n")
if description != "" {
sb.WriteString(fmt.Sprintf(" %s\n", description))
sb.WriteString("\n")
}
// Args section
if tool.Function.Parameters != nil && tool.Function.Parameters.Properties != nil {
props := tool.Function.Parameters.Properties
required := make(map[string]bool)
if tool.Function.Parameters.Required != nil {
for _, req := range tool.Function.Parameters.Required {
required[req] = true
}
}
if props.Len() > 0 {
sb.WriteString(" Args:\n")
// Sort properties for consistent output
propNames := make([]string, 0, props.Len())
props.Range(func(name string, _ interface{}) bool {
propNames = append(propNames, name)
return true
})
for i := 0; i < len(propNames)-1; i++ {
for j := i + 1; j < len(propNames); j++ {
if propNames[i] > propNames[j] {
propNames[i], propNames[j] = propNames[j], propNames[i]
}
}
}
for _, propName := range propNames {
prop, _ := props.Get(propName)
propMap, ok := prop.(map[string]interface{})
if !ok {
continue
}
pyType := jsonSchemaToPython(propMap)
propDesc := ""
if desc, ok := propMap["description"].(string); ok && desc != "" {
propDesc = desc
} else {
propDesc = fmt.Sprintf("%s parameter", propName)
}
requiredNote := ""
if required[propName] {
requiredNote = " (required)"
} else {
requiredNote = " (optional)"
}
sb.WriteString(fmt.Sprintf(" %s (%s): %s%s\n", propName, pyType, propDesc, requiredNote))
}
sb.WriteString("\n")
}
}
// Returns section
sb.WriteString(" Returns:\n")
sb.WriteString(" dict: Response from the tool. Structure varies by tool.\n")
sb.WriteString(" Use print(result) to inspect the actual structure.\n")
sb.WriteString("\n")
// Example section
sb.WriteString(" Example:\n")
sb.WriteString(fmt.Sprintf(" result = %s.%s(%s)\n", clientName, toolName, getExampleParams(tool.Function.Parameters)))
sb.WriteString(" print(result) # Always inspect response first!\n")
sb.WriteString(" value = result.get(\"key\", default) # Safe access\n")
sb.WriteString(" \"\"\"\n")
sb.WriteString(" ...\n\n")
}
return sb.String()
}
// getExampleParams generates example parameter usage for a function.
func getExampleParams(params *schemas.ToolFunctionParameters) string {
if params == nil || params.Properties == nil || params.Properties.Len() == 0 {
return ""
}
required := make(map[string]bool)
if params.Required != nil {
for _, req := range params.Required {
required[req] = true
}
}
keys := params.Properties.Keys()
// Get first required param as example
for _, name := range keys {
if required[name] {
return fmt.Sprintf("%s=\"...\"", name)
}
}
// If no required, get first param
if len(keys) > 0 {
return fmt.Sprintf("%s=\"...\"", keys[0])
}
return ""
}