first commit
This commit is contained in:
301
core/mcp/codemode/starlark/getdocs.go
Normal file
301
core/mcp/codemode/starlark/getdocs.go
Normal 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 ""
|
||||
}
|
||||
Reference in New Issue
Block a user