mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-06-29 15:31:05 +08:00
Replaces the Python agent canvas runtime with a Go implementation that runs inside `cmd/server_main`. The canvas compiles into an eino Workflow that pauses on wait-for-user via native Interrupt/Resume (no sentinel flag) and resumes from a Redis-backed CheckPointStore. All 21 Python agent components and ~35 tools are ported with functional parity. Sandbox providers now read their JSON config from the admin-panel system_settings table with env fallback. 234 files / +35,413 / -6,111. All Go files are gofmt-clean (CI gate added); drops the v2 DSL E2E step and the gap-analysis plan (both redundant after the port ships). ## Type of change - [x] Refactoring - [x] New feature - [x] Bug fix 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude <noreply@anthropic.com>
180 lines
6.5 KiB
Go
180 lines
6.5 KiB
Go
//
|
|
// Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
//
|
|
|
|
// Package tool — MCP (Model Context Protocol) wrapper.
|
|
//
|
|
// Wraps a single MCP-server-discovered tool (utility/mcpclient.Tool) as
|
|
// an eino BaseTool so it can be invoked from inside the Agent's
|
|
// ReAct loop. The MCP tool list is fetched via utility/mcpclient
|
|
// (which currently only implements tools/list discovery; tools/call
|
|
// invocation is the next step on the MCP client).
|
|
package tool
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/cloudwego/eino/components/tool"
|
|
"github.com/cloudwego/eino/schema"
|
|
|
|
mcpclient "ragflow/internal/utility"
|
|
)
|
|
|
|
// MCPToolAdapter wraps a single MCP-discovered tool descriptor as an
|
|
// eino InvokableTool. The wire format matches what eino's react.Agent
|
|
// expects: a ToolInfo with name/description/params, and an
|
|
// InvokableRun that accepts a JSON arguments string and returns a
|
|
// string result.
|
|
//
|
|
// InvokableRun dispatches through mcpclient.CallTool
|
|
// (streamable-HTTP transport). The MCP server URL + headers
|
|
// are captured on construction so the adapter has everything it
|
|
// needs to call back into the server. Adapters built without
|
|
// a URL (legacy callers) fall back to the "not yet wired"
|
|
// sentinel so existing call sites don't break.
|
|
type MCPToolAdapter struct {
|
|
mcpTool mcpclient.Tool
|
|
serverURL string
|
|
headers map[string]string
|
|
timeout time.Duration
|
|
httpClient *http.Client
|
|
}
|
|
|
|
// NewMCPToolAdapter constructs a wrapper for a single MCP tool.
|
|
// The returned adapter has no MCP server URL and so cannot be
|
|
// invoked — use NewMCPToolAdapterWithServer for adapters that
|
|
// need to call back into the server.
|
|
func NewMCPToolAdapter(t mcpclient.Tool) *MCPToolAdapter {
|
|
return &MCPToolAdapter{mcpTool: t}
|
|
}
|
|
|
|
// NewMCPToolAdapterWithServer constructs a wrapper that knows
|
|
// the MCP server URL + transport headers. InvokableRun uses this
|
|
// to route InvokableRun into mcpclient.CallTool.
|
|
func NewMCPToolAdapterWithServer(t mcpclient.Tool, serverURL string, headers map[string]string, timeout time.Duration) *MCPToolAdapter {
|
|
return &MCPToolAdapter{
|
|
mcpTool: t,
|
|
serverURL: serverURL,
|
|
headers: headers,
|
|
timeout: timeout,
|
|
}
|
|
}
|
|
|
|
// NewMCPToolAdapterFull is the most-configurable constructor;
|
|
// callers can also pass an *http.Client (e.g. an httptest server's
|
|
// Client, or a custom transport with mTLS) so the underlying
|
|
// CallTool call doesn't have to fall back to a pinned client.
|
|
func NewMCPToolAdapterFull(t mcpclient.Tool, serverURL string, headers map[string]string, timeout time.Duration, client *http.Client) *MCPToolAdapter {
|
|
return &MCPToolAdapter{
|
|
mcpTool: t,
|
|
serverURL: serverURL,
|
|
headers: headers,
|
|
timeout: timeout,
|
|
httpClient: client,
|
|
}
|
|
}
|
|
|
|
// Name returns the underlying MCP tool name.
|
|
func (m *MCPToolAdapter) Name() string { return m.mcpTool.Name }
|
|
|
|
// Info returns eino-compatible tool metadata. InputSchema is
|
|
// translated from the MCP tool's JSON Schema.
|
|
func (m *MCPToolAdapter) Info(_ context.Context) (*schema.ToolInfo, error) {
|
|
// eino's schema.ParameterInfo shape: name → description.
|
|
// We translate the MCP tool's inputSchema.properties into a
|
|
// best-effort ParameterInfo map. For tools without a JSON schema
|
|
// the params map is empty — eino falls back to free-form args.
|
|
params := make(map[string]*schema.ParameterInfo, len(m.mcpTool.InputSchema))
|
|
for name := range m.mcpTool.InputSchema {
|
|
params[name] = &schema.ParameterInfo{
|
|
Type: schema.String, // conservative default
|
|
Desc: fmt.Sprintf("MCP tool parameter: %s", name),
|
|
Required: false, // MCP doesn't surface required; we err permissive
|
|
}
|
|
}
|
|
return &schema.ToolInfo{
|
|
Name: m.mcpTool.Name,
|
|
Desc: m.mcpTool.Description,
|
|
ParamsOneOf: schema.NewParamsOneOfByParams(params),
|
|
}, nil
|
|
}
|
|
|
|
// InvokableRun is the eino entry point. When the adapter was
|
|
// built with a server URL, dispatch through mcpclient.CallTool.
|
|
// Legacy adapters (no URL) keep the "not yet wired" sentinel
|
|
// so existing tests that pin the error message don't break.
|
|
func (m *MCPToolAdapter) InvokableRun(ctx context.Context, argumentsInJSON string, _ ...tool.Option) (string, error) {
|
|
if m.serverURL == "" {
|
|
return "", fmt.Errorf("mcp tool %q: tools/call not yet implemented in mcpclient; arguments were: %s",
|
|
m.mcpTool.Name, argumentsInJSON)
|
|
}
|
|
argsJSON, mErr := marshalArguments(argumentsInJSON)
|
|
if mErr != nil {
|
|
return "", mErr
|
|
}
|
|
res, err := mcpclient.CallTool(ctx, mcpclient.CallOptions{
|
|
URL: m.serverURL,
|
|
ServerType: mcpclient.TransportStreamableHTTP,
|
|
Headers: m.headers,
|
|
ToolName: m.mcpTool.Name,
|
|
Arguments: argsJSON,
|
|
Timeout: m.timeout,
|
|
HTTPClient: m.httpClient,
|
|
})
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if res == nil {
|
|
return "", nil
|
|
}
|
|
if res.IsError {
|
|
// Surface the structured tool error under a known prefix
|
|
// so the ReAct loop can route it as a tool-level error
|
|
// rather than a transport failure.
|
|
return "", fmt.Errorf("mcp tool %q returned isError: %s", m.mcpTool.Name, res.Text)
|
|
}
|
|
return res.Text, nil
|
|
}
|
|
|
|
// BuildMCPToolAdapters wraps a slice of mcpclient.Tool descriptors as
|
|
// eino InvokableTool. Returned slice is suitable for handing to
|
|
// agenttool.NewRetrieverTool / NewMCPToolAdapter paths or directly to
|
|
// the Agent's tool list.
|
|
func BuildMCPToolAdapters(tools []mcpclient.Tool) []tool.InvokableTool {
|
|
out := make([]tool.InvokableTool, 0, len(tools))
|
|
for _, t := range tools {
|
|
out = append(out, NewMCPToolAdapter(t))
|
|
}
|
|
return out
|
|
}
|
|
|
|
// marshalArguments is a helper for the future tools/call
|
|
// implementation. The argumentsInJSON string from eino is
|
|
// round-tripped through json.RawMessage before being passed to the
|
|
// MCP server so the server's expected payload structure is preserved.
|
|
func marshalArguments(argumentsInJSON string) (json.RawMessage, error) {
|
|
if argumentsInJSON == "" || argumentsInJSON == "{}" {
|
|
return json.RawMessage("{}"), nil
|
|
}
|
|
if !json.Valid([]byte(argumentsInJSON)) {
|
|
return nil, fmt.Errorf("mcp tool: arguments are not valid JSON: %q", argumentsInJSON)
|
|
}
|
|
return json.RawMessage(argumentsInJSON), nil
|
|
}
|