fix: some bugs in mcp-agent & llm (#24355)

* fix(mcp-agent): encoding issue when simulated streaming

* fix(mcp-agent): use chat-stream in phase tool-choose

* fix(mcp-agent): support reasoning model

* fix(llm): add pre-delete detection

* feat(mcp-agent): add default mcp-agent

* feat(mcp-agent): add default-mcp-tools
This commit is contained in:
cwz_eikoh
2026-03-03 16:07:32 +08:00
committed by GitHub
parent efea8bdbf7
commit a279c4f89f
15 changed files with 675 additions and 96 deletions

View File

@@ -28,6 +28,7 @@ func init() {
// cmd.Get("chat-test", new(options.MCPAgentChatTestOptions))
cmd.Get("request", new(options.MCPAgentMCPAgentRequestOptions))
shell.R(&options.MCPAgentMCPAgentRequestOptions{}, "mcp-agent-chat", "Chat with MCP Agent (Stream)", chatStream)
shell.R(&options.MCPAgentDefaultChatOptions{}, "mcp-agent-chat-default", "Chat with default MCP Agent (Stream, no ID required)", chatStreamDefault)
}
func chatStream(s *mcclient.ClientSession, args *options.MCPAgentMCPAgentRequestOptions) error {
@@ -88,3 +89,50 @@ func chatStream(s *mcclient.ClientSession, args *options.MCPAgentMCPAgentRequest
fmt.Println()
return nil
}
func chatStreamDefault(s *mcclient.ClientSession, args *options.MCPAgentDefaultChatOptions) error {
bodyJSON, err := args.Params()
if err != nil {
return fmt.Errorf("failed to build request params: %v", err)
}
headers := http.Header{}
headers.Set("Content-Type", "application/json")
body := strings.NewReader(bodyJSON.String())
path := "/mcp_agents/default/chat-stream"
resp, err := s.RawVersionRequest(
modules.MCPAgent.ServiceType(),
modules.MCPAgent.EndpointType(),
"POST",
path,
headers,
body,
)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
respBody, _ := io.ReadAll(resp.Body)
return fmt.Errorf("Error: %s %s", resp.Status, string(respBody))
}
scanner := bufio.NewScanner(resp.Body)
var eventData []string
for scanner.Scan() {
line := scanner.Text()
if line == "" {
if len(eventData) > 0 {
fmt.Print(strings.Join(eventData, "\n"))
eventData = nil
}
continue
}
if after, found := strings.CutPrefix(line, "data: "); found {
eventData = append(eventData, after)
}
}
if err := scanner.Err(); err != nil {
return err
}
fmt.Println()
return nil
}

View File

@@ -149,3 +149,106 @@ func mcpAgentChatStreamHandler(ctx context.Context, w http.ResponseWriter, r *ht
}
}
}
// mcpAgentDefaultChatStreamHandler 将请求转发到 region 的 default-chat-stream使用 default_agent=true 的条目)
func mcpAgentDefaultChatStreamHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) {
token := AppContextToken(ctx)
s := auth.GetSession(ctx, token, FetchRegion(r))
headers := http.Header{}
headers.Set("Content-Type", "application/json")
var bodyReader io.Reader
if r.Body != nil {
bodyReader = r.Body
}
path := "/mcp_agents/default-chat-stream"
resp, err := s.RawVersionRequest(
modules.MCPAgent.ServiceType(),
modules.MCPAgent.EndpointType(),
"POST",
path,
headers,
bodyReader,
)
if err != nil {
httperrors.GeneralServerError(ctx, w, errors.Wrap(err, "request backend"))
return
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
respBody, _ := io.ReadAll(resp.Body)
if resp.StatusCode >= 400 && resp.StatusCode < 500 {
httperrors.InputParameterError(ctx, w, "backend error: %s", string(respBody))
} else {
httperrors.GeneralServerError(ctx, w, fmt.Errorf("backend error %d: %s", resp.StatusCode, string(respBody)))
}
return
}
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
if f, ok := w.(http.Flusher); ok {
f.Flush()
}
buf := make([]byte, 1024)
for {
n, err := resp.Body.Read(buf)
if n > 0 {
if _, wErr := w.Write(buf[:n]); wErr != nil {
log.Errorf("write response error: %v", wErr)
return
}
if f, ok := w.(http.Flusher); ok {
f.Flush()
}
}
if err != nil {
if err != io.EOF {
log.Errorf("read backend response error: %v", err)
}
break
}
}
}
// mcpAgentDefaultToolsHandler 将 GET 请求转发到 region 的 default-mcp-tools仅使用 options.MCPServerURL不通过 mcp_agent 条目)
func mcpAgentDefaultToolsHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) {
token := AppContextToken(ctx)
s := auth.GetSession(ctx, token, FetchRegion(r))
path := "/mcp_agents/default-mcp-tools"
resp, err := s.RawVersionRequest(
modules.MCPAgent.ServiceType(),
modules.MCPAgent.EndpointType(),
"GET",
path,
nil,
nil,
)
if err != nil {
httperrors.GeneralServerError(ctx, w, errors.Wrap(err, "request backend"))
return
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
respBody, _ := io.ReadAll(resp.Body)
if resp.StatusCode >= 400 && resp.StatusCode < 500 {
httperrors.InputParameterError(ctx, w, "backend error: %s", string(respBody))
} else {
httperrors.GeneralServerError(ctx, w, fmt.Errorf("backend error %d: %s", resp.StatusCode, string(respBody)))
}
return
}
w.Header().Set("Content-Type", "application/json")
_, err = io.Copy(w, resp.Body)
if err != nil {
log.Errorf("write default mcp tools response error: %v", err)
}
}

View File

@@ -108,6 +108,11 @@ func (h *MiscHandler) Bind(app *appsrv.Application) {
// mcp agent chat stream
chatStream := chatHandlerInfo("POST", prefix+"mcp_agents/<id>/chat-stream", FetchAuthToken(mcpAgentChatStreamHandler))
app.AddHandler3(chatStream)
// mcp agent default chat stream (uses agent with default_agent=true)
defaultChatStream := chatHandlerInfo("POST", prefix+"mcp_agents/default/chat-stream", FetchAuthToken(mcpAgentDefaultChatStreamHandler))
app.AddHandler3(defaultChatStream)
// mcp agent default MCP server tools (options.MCPServerURL only, no mcp_agent entry)
app.AddHandler(GET, prefix+"mcp_agents/default-mcp-tools", FetchAuthToken(mcpAgentDefaultToolsHandler))
// syslog webservice handlers
app.AddHandler(POST, prefix+"syslog/token", handleSyslogWebServiceToken)

View File

@@ -60,36 +60,40 @@ func IsLLMClientType(t string) bool {
type MCPAgentListInput struct {
apis.SharableVirtualResourceListInput
LLMDriver string `json:"llm_driver"`
LLMDriver string `json:"llm_driver"`
DefaultAgent *bool `json:"default_agent,omitempty" help:"filter by default agent (true to get the default one)"`
}
type MCPAgentCreateInput struct {
apis.SharableVirtualResourceCreateInput
LLMId string `json:"llm_id" help:"LLM 实例 ID如果提供则自动获取 llm_url"`
LLMUrl string `json:"llm_url" help:"后端大模型的 base 请求地址"`
LLMDriver string `json:"llm_driver" help:"使用的大模型驱动,可以是 ollama 或 openai"`
Model string `json:"model" help:"使用的模型名称"`
ApiKey string `json:"api_key" help:"在 llm_driver 中需要用到的认证"`
McpServer string `json:"mcp_server" help:"mcp 服务器的后端地址"`
LLMId string `json:"llm_id" help:"LLM 实例 ID如果提供则自动获取 llm_url"`
LLMUrl string `json:"llm_url" help:"后端大模型的 base 请求地址"`
LLMDriver string `json:"llm_driver" help:"使用的大模型驱动,可以是 ollama 或 openai"`
Model string `json:"model" help:"使用的模型名称"`
ApiKey string `json:"api_key" help:"在 llm_driver 中需要用到的认证"`
McpServer string `json:"mcp_server" help:"mcp 服务器的后端地址"`
DefaultAgent *bool `json:"default_agent,omitempty" help:"set as default MCP agent (only one can be true globally)"`
}
type MCPAgentUpdateInput struct {
apis.SharableVirtualResourceCreateInput
LLMId *string `json:"llm_id,omitempty" help:"LLM 实例 ID如果提供则自动获取 llm_url"`
LLMUrl *string `json:"llm_url,omitempty" help:"后端大模型的 base 请求地址"`
LLMDriver *string `json:"llm_driver,omitempty" help:"使用的大模型驱动,可以是 ollama 或 openai"`
Model *string `json:"model,omitempty" help:"使用的模型名称"`
ApiKey *string `json:"api_key,omitempty" help:"在 llm_driver 中需要用到的认证"`
McpServer *string `json:"mcp_server,omitempty" help:"mcp 服务器的后端地址"`
LLMId *string `json:"llm_id,omitempty" help:"LLM 实例 ID如果提供则自动获取 llm_url"`
LLMUrl *string `json:"llm_url,omitempty" help:"后端大模型的 base 请求地址"`
LLMDriver *string `json:"llm_driver,omitempty" help:"使用的大模型驱动,可以是 ollama 或 openai"`
Model *string `json:"model,omitempty" help:"使用的模型名称"`
ApiKey *string `json:"api_key,omitempty" help:"在 llm_driver 中需要用到的认证"`
McpServer *string `json:"mcp_server,omitempty" help:"mcp 服务器的后端地址"`
DefaultAgent *bool `json:"default_agent,omitempty" help:"set as default MCP agent (only one can be true globally)"`
}
type MCPAgentDetails struct {
apis.SharableVirtualResourceDetails
LLMId string `json:"llm_id"`
LLMName string `json:"llm_name"`
LLMId string `json:"llm_id"`
LLMName string `json:"llm_name"`
DefaultAgent bool `json:"default_agent"`
}
type LLMToolRequestInput struct {

View File

@@ -252,6 +252,29 @@ func (o *ollama) NewAssistantMessageWithToolCalls(toolCalls []models.ILLMToolCal
}
}
func (o *ollama) NewAssistantMessageWithToolCallsAndReasoning(reasoningContent, content string, toolCalls []models.ILLMToolCall) models.ILLMChatMessage {
ollamaToolCalls := make([]OllamaToolCall, len(toolCalls))
for i, tc := range toolCalls {
if otc, ok := tc.(*OllamaToolCall); ok {
ollamaToolCalls[i] = *otc
} else {
fc := tc.GetFunction()
ollamaToolCalls[i] = OllamaToolCall{
Function: OllamaFunctionCall{
Name: fc.GetName(),
Arguments: fc.GetArguments(),
},
}
}
}
_ = reasoningContent // Ollama does not use reasoning_content; ignore for compatibility
return &OllamaChatMessage{
Role: "assistant",
Content: content,
ToolCalls: ollamaToolCalls,
}
}
func (o *ollama) NewToolMessage(toolId string, toolName string, content string) models.ILLMChatMessage {
return &OllamaChatMessage{
Role: "tool",
@@ -323,6 +346,7 @@ func (m OllamaChatMessage) GetToolCalls() []models.ILLMToolCall {
// OllamaToolCall 表示工具调用
// 实现 ILLMToolCall 接口
type OllamaToolCall struct {
Index int `json:"-"`
Function OllamaFunctionCall `json:"function"`
}
@@ -331,6 +355,11 @@ func (tc *OllamaToolCall) GetFunction() models.ILLMFunctionCall {
return &tc.Function
}
// GetIndex 实现 ILLMToolCall 接口
func (tc *OllamaToolCall) GetIndex() int {
return tc.Index
}
// GetId 实现 ILLMToolCall 接口
func (tc *OllamaToolCall) GetId() string {
return ""
@@ -348,6 +377,15 @@ func (fc *OllamaFunctionCall) GetName() string {
return fc.Name
}
// GetRawArguments 实现 ILLMFunctionCall 接口
func (fc *OllamaFunctionCall) GetRawArguments() string {
if fc.Arguments == nil {
return ""
}
bytes, _ := json.Marshal(fc.Arguments)
return string(bytes)
}
// GetArguments 实现 ILLMFunctionCall 接口
func (fc *OllamaFunctionCall) GetArguments() map[string]interface{} {
return fc.Arguments
@@ -415,6 +453,11 @@ func (r *OllamaChatResponse) GetContent() string {
return r.Message.Content
}
// GetReasoningContent 获取推理内容Ollama 不支持,返回空)
func (r *OllamaChatResponse) GetReasoningContent() string {
return ""
}
// HasToolCalls 检查响应是否包含工具调用
func (r *OllamaChatResponse) HasToolCalls() bool {
return len(r.Message.ToolCalls) > 0
@@ -427,6 +470,7 @@ func (r *OllamaChatResponse) GetToolCalls() []models.ILLMToolCall {
}
toolCalls := make([]models.ILLMToolCall, len(r.Message.ToolCalls))
for i := range r.Message.ToolCalls {
r.Message.ToolCalls[i].Index = i
toolCalls[i] = &r.Message.ToolCalls[i]
}
return toolCalls

View File

@@ -40,7 +40,7 @@ func (o *openai) Chat(ctx context.Context, mcpAgent *models.SMCPAgent, messages
} else if msgs, ok := messages.([]models.ILLMChatMessage); ok {
openaiMessages = make([]OpenAIChatMessage, len(msgs))
for i, msg := range msgs {
// Check if it's an OpenAIChatMessage to preserve ToolCallID
// Check if it's an OpenAIChatMessage to preserve ToolCallID and ReasoningContent
if om, ok := msg.(*OpenAIChatMessage); ok {
openaiMessages[i] = *om
} else {
@@ -113,7 +113,7 @@ func (o *openai) ChatStream(ctx context.Context, mcpAgent *models.SMCPAgent, mes
openaiMessages = make([]OpenAIChatMessage, len(ilMsgs))
for i, msg := range ilMsgs {
// Check if it's an OpenAIChatMessage to preserve ToolCallID
// Check if it's an OpenAIChatMessage to preserve ToolCallID and ReasoningContent
if om, ok := msg.(*OpenAIChatMessage); ok {
openaiMessages[i] = *om
} else {
@@ -359,6 +359,32 @@ func (o *openai) NewAssistantMessageWithToolCalls(toolCalls []models.ILLMToolCal
}
}
func (o *openai) NewAssistantMessageWithToolCallsAndReasoning(reasoningContent, content string, toolCalls []models.ILLMToolCall) models.ILLMChatMessage {
openaiToolCalls := make([]OpenAIToolCall, len(toolCalls))
for i, tc := range toolCalls {
if otc, ok := tc.(*OpenAIToolCall); ok {
openaiToolCalls[i] = *otc
} else {
fc := tc.GetFunction()
argsBytes, _ := json.Marshal(fc.GetArguments())
openaiToolCalls[i] = OpenAIToolCall{
ID: tc.GetId(),
Type: "function",
Function: OpenAIFunctionCall{
Name: fc.GetName(),
Arguments: string(argsBytes),
},
}
}
}
return &OpenAIChatMessage{
Role: "assistant",
Content: content,
ReasoningContent: reasoningContent,
ToolCalls: openaiToolCalls,
}
}
func (o *openai) NewToolMessage(toolId string, toolName string, content string) models.ILLMChatMessage {
return &OpenAIChatMessage{
Role: "tool",
@@ -399,10 +425,11 @@ func (o *openai) ConvertMCPTools(mcpTools []mcp.Tool) []models.ILLMTool {
// Structures
type OpenAIChatMessage struct {
Role string `json:"role"`
Content string `json:"content"`
ToolCalls []OpenAIToolCall `json:"tool_calls,omitempty"`
ToolCallID string `json:"tool_call_id,omitempty"`
Role string `json:"role"`
Content string `json:"content"`
ReasoningContent string `json:"reasoning_content,omitempty"`
ToolCalls []OpenAIToolCall `json:"tool_calls,omitempty"`
ToolCallID string `json:"tool_call_id,omitempty"`
}
func (m *OpenAIChatMessage) GetRole() string { return m.Role }
@@ -420,6 +447,7 @@ func (m *OpenAIChatMessage) GetToolCalls() []models.ILLMToolCall {
}
type OpenAIToolCall struct {
Index int `json:"index"`
ID string `json:"id"`
Type string `json:"type"`
Function OpenAIFunctionCall `json:"function"`
@@ -427,13 +455,15 @@ type OpenAIToolCall struct {
func (tc *OpenAIToolCall) GetFunction() models.ILLMFunctionCall { return &tc.Function }
func (tc *OpenAIToolCall) GetId() string { return tc.ID }
func (tc *OpenAIToolCall) GetIndex() int { return tc.Index }
type OpenAIFunctionCall struct {
Name string `json:"name"`
Arguments string `json:"arguments"`
}
func (fc *OpenAIFunctionCall) GetName() string { return fc.Name }
func (fc *OpenAIFunctionCall) GetName() string { return fc.Name }
func (fc *OpenAIFunctionCall) GetRawArguments() string { return fc.Arguments }
func (fc *OpenAIFunctionCall) GetArguments() map[string]interface{} {
var args map[string]interface{}
_ = json.Unmarshal([]byte(fc.Arguments), &args)
@@ -482,6 +512,13 @@ func (r *OpenAIChatResponse) GetContent() string {
return ""
}
func (r *OpenAIChatResponse) GetReasoningContent() string {
if len(r.Choices) > 0 {
return r.Choices[0].Message.ReasoningContent
}
return ""
}
func (r *OpenAIChatResponse) HasToolCalls() bool {
return len(r.Choices) > 0 && len(r.Choices[0].Message.ToolCalls) > 0
}
@@ -504,9 +541,10 @@ type OpenAIChatStreamChoice struct {
}
type OpenAIChatStreamDelta struct {
Role string `json:"role,omitempty"`
Content string `json:"content,omitempty"`
ToolCalls []OpenAIToolCall `json:"tool_calls,omitempty"`
Role string `json:"role,omitempty"`
Content string `json:"content,omitempty"`
ReasoningContent string `json:"reasoning_content,omitempty"`
ToolCalls []OpenAIToolCall `json:"tool_calls,omitempty"`
}
func (r *OpenAIChatStreamResponse) GetContent() string {
@@ -516,6 +554,13 @@ func (r *OpenAIChatStreamResponse) GetContent() string {
return ""
}
func (r *OpenAIChatStreamResponse) GetReasoningContent() string {
if len(r.Choices) > 0 {
return r.Choices[0].Delta.ReasoningContent
}
return ""
}
func (r *OpenAIChatStreamResponse) HasToolCalls() bool {
return len(r.Choices) > 0 && len(r.Choices[0].Delta.ToolCalls) > 0
}

View File

@@ -667,15 +667,31 @@ func (model *SInstantModel) PerformPrivate(
func (model *SInstantModel) ValidateDeleteCondition(ctx context.Context, info jsonutils.JSONObject) error {
if model.Enabled.IsTrue() {
for _, man := range []MountedModelModelManager{GetLLMSkuManager(), GetVolumeManager()} {
// volume/sku 存储格式为 modelFullName-instantModelId
used, err := man.IsPremountedModelName(model.ModelName + ":" + model.ModelTag + "-" + model.Id)
if err != nil {
return errors.Wrap(err, "IsPremountedModelName")
}
if used {
return errors.Wrap(errors.ErrInvalidStatus, "cannot delete when model is used by other resources")
}
// check if used by llm sku
used, err := GetLLMSkuManager().IsPremountedModelName(model.Id)
if err != nil {
return errors.Wrap(err, "GetLLMSkuManager().IsPremountedModelName")
}
if used {
return errors.Wrap(errors.ErrInvalidStatus, "cannot delete when model is used by llm sku")
}
// check if used by volume
used, err = GetVolumeManager().IsPremountedModelName(model.Id)
if err != nil {
return errors.Wrap(err, "GetVolumeManager().IsPremountedModelName")
}
if used {
return errors.Wrap(errors.ErrInvalidStatus, "cannot delete when model is used by volume")
}
// check if used by llm instance
cnt, err := GetLLMInstantModelManager().Query().Equals("model_id", model.Id).IsFalse("deleted").CountWithError()
if err != nil {
return errors.Wrap(err, "GetLLMInstantModelManager().CountWithError")
}
if cnt > 0 {
return errors.Wrap(errors.ErrInvalidStatus, "cannot delete when model is used by llm instance")
}
}

View File

@@ -499,16 +499,20 @@ func (llm *SLLM) StartLLMStopTask(ctx context.Context, userCred mcclient.TokenCr
return nil
}
// func (llm *SLLM) ValidateDeleteCondition(ctx context.Context, info jsonutils.JSONObject) error {
// instanceId, isBound, err := llm.IsBoundToInstance()
// if err != nil {
// return errors.Wrap(err, "IsBoundToInstance")
// }
// if isBound {
// return httperrors.NewBadRequestError("llm is bound to instance %s", instanceId)
// }
// return nil
// }
func (llm *SLLM) ValidateDeleteCondition(ctx context.Context, info jsonutils.JSONObject) error {
if err := llm.SLLMBase.ValidateDeleteCondition(ctx, info); err != nil {
return err
}
// Check for associated MCPAgents
cnt, err := GetMCPAgentManager().Query().Equals("llm_id", llm.Id).CountWithError()
if err != nil {
return errors.Wrap(err, "GetMCPAgentManager().Query().CountWithError")
}
if cnt > 0 {
return httperrors.NewConflictError("LLM is being used by %d MCPAgents", cnt)
}
return nil
}
func (llm *SLLM) WaitContainerStatus(ctx context.Context, userCred mcclient.TokenCredential, targetStatus []string, timeoutSecs int) (*computeapi.SContainer, error) {
llmCtr, err := llm.GetLLMContainer()

View File

@@ -18,12 +18,14 @@ type ILLMChatMessage interface {
type ILLMToolCall interface {
GetId() string
GetFunction() ILLMFunctionCall
GetIndex() int
}
// ILLMFunctionCall 表示函数调用详情接口
type ILLMFunctionCall interface {
GetName() string
GetArguments() map[string]interface{}
GetRawArguments() string
}
// ILLMTool 表示工具定义接口
@@ -48,6 +50,8 @@ type ILLMChatResponse interface {
GetToolCalls() []ILLMToolCall
// GetContent 获取响应内容
GetContent() string
// GetReasoningContent 获取推理/思考内容(如 DeepSeek reasoning_content
GetReasoningContent() string
}
type ILLMClient interface {
@@ -59,6 +63,7 @@ type ILLMClient interface {
NewUserMessage(content string) ILLMChatMessage
NewAssistantMessage(content string) ILLMChatMessage
NewAssistantMessageWithToolCalls(toolCalls []ILLMToolCall) ILLMChatMessage
NewAssistantMessageWithToolCallsAndReasoning(reasoningContent, content string, toolCalls []ILLMToolCall) ILLMChatMessage
NewToolMessage(toolId string, toolName string, content string) ILLMChatMessage
NewSystemMessage(content string) ILLMChatMessage
@@ -74,6 +79,10 @@ func (tc *SLLMToolCall) GetId() string {
return tc.Id
}
func (tc *SLLMToolCall) GetIndex() int {
return 0
}
func (tc *SLLMToolCall) GetFunction() ILLMFunctionCall {
return &tc.Function
}
@@ -87,6 +96,10 @@ func (fc *SLLMFunctionCall) GetName() string {
return fc.Name
}
func (fc *SLLMFunctionCall) GetRawArguments() string {
return ""
}
func (fc *SLLMFunctionCall) GetArguments() map[string]interface{} {
return fc.Arguments
}

View File

@@ -237,3 +237,21 @@ func (mdl *SLLMInstantModel) getMountPathsFromImage(isInstall bool) ([]api.LLMMo
})
return info, nil
}
func (man *SLLMInstantModelManager) DeleteByLlmId(ctx context.Context, llmId string) error {
q := man.Query().Equals("llm_id", llmId)
models := make([]SLLMInstantModel, 0)
err := db.FetchModelObjects(man, q, &models)
if err != nil {
return errors.Wrap(err, "FetchModelObjects")
}
for i := range models {
_, err := db.Update(&models[i], func() error {
return models[i].MarkDelete()
})
if err != nil {
return errors.Wrapf(err, "delete llm model %s", models[i].ModelName)
}
}
return nil
}

View File

@@ -3,7 +3,6 @@ package models
import (
"context"
"database/sql"
"fmt"
"net/http"
"slices"
"strings"
@@ -347,7 +346,7 @@ func (llm *SLLM) FetchModelsFullName(isProbed, isMounted *bool) ([]string, error
}
mdlFullNames := make([]string, len(models))
for idx, mdl := range models {
mdlFullNames[idx] = mdl.ModelName + ":" + mdl.Tag + "-" + mdl.InstantModelId
mdlFullNames[idx] = mdl.InstantModelId
}
return mdlFullNames, nil
}
@@ -690,7 +689,7 @@ func (llm *SLLM) UpdateMountedModelFullNames(ctx context.Context, userCred mccli
installModelFullNames := make([]string, 0)
for _, mdlFullNameInfo := range mdlFullNameInfos {
installModelFullNames = append(installModelFullNames, fmt.Sprintf("%s-%s", mdlFullNameInfo.ModelFullName, mdlFullNameInfo.InstantModelId))
installModelFullNames = append(installModelFullNames, mdlFullNameInfo.InstantModelId)
if !mdlFullNameInfo.IsMounted {
modelName, modelTag, _ := llm.GetLargeLanguageModelName(mdlFullNameInfo.ModelFullName)
_, err := GetLLMInstantModelManager().updateInstantModel(ctx, llm.Id, mdlFullNameInfo.InstantModelId, modelName, modelTag, &boolFalse, &boolTrue)

View File

@@ -2,6 +2,8 @@ package models
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"net/http"
"strings"
@@ -16,6 +18,7 @@ import (
api "yunion.io/x/onecloud/pkg/apis/llm"
"yunion.io/x/onecloud/pkg/appsrv"
"yunion.io/x/onecloud/pkg/cloudcommon/db"
"yunion.io/x/onecloud/pkg/cloudcommon/policy"
"yunion.io/x/onecloud/pkg/httperrors"
"yunion.io/x/onecloud/pkg/llm/options"
"yunion.io/x/onecloud/pkg/llm/utils"
@@ -31,6 +34,10 @@ var mcpAgentManager *SMCPAgentManager
var mcpAgentWorkerMan *appsrv.SWorkerManager
func GetMCPAgentWorkerManager() *appsrv.SWorkerManager {
return mcpAgentWorkerMan
}
func GetMCPAgentManager() *SMCPAgentManager {
if mcpAgentManager != nil {
return mcpAgentManager
@@ -51,6 +58,64 @@ type SMCPAgentManager struct {
db.SSharableVirtualResourceBaseManager
}
// unsetOtherDefaultAgents 将除 excludeId 外所有条目的 default_agent 置为 false保证全局唯一
func (man *SMCPAgentManager) unsetOtherDefaultAgents(ctx context.Context, excludeId string) error {
q := man.Query().IsTrue("default_agent")
if len(excludeId) > 0 {
q = q.NotEquals("id", excludeId)
}
agents := make([]SMCPAgent, 0)
err := db.FetchModelObjects(man, q, &agents)
if err != nil {
return errors.Wrap(err, "FetchModelObjects")
}
for i := range agents {
_, err := db.Update(&agents[i], func() error {
agents[i].DefaultAgent = false
return nil
})
if err != nil {
return errors.Wrapf(err, "Update agent %s", agents[i].Id)
}
}
return nil
}
// GetDefaultAgent 返回当前用户可见的、default_agent=true 的那条 MCP Agent仅一条
func (man *SMCPAgentManager) GetDefaultAgent(ctx context.Context, userCred mcclient.TokenCredential) (*SMCPAgent, error) {
query := jsonutils.NewDict()
query.Set("default_agent", jsonutils.JSONTrue)
ownerId, scope, err, _ := db.FetchCheckQueryOwnerScope(ctx, userCred, query, man, policy.PolicyActionList, true)
if err != nil {
return nil, errors.Wrap(err, "FetchCheckQueryOwnerScope")
}
q := man.Query()
q = man.FilterByOwner(ctx, q, man, userCred, ownerId, scope)
q = q.IsTrue("default_agent")
var agent SMCPAgent
err = q.First(&agent)
if err != nil {
if errors.Cause(err) == sql.ErrNoRows {
return nil, nil
}
return nil, errors.Wrap(err, "First default agent")
}
return &agent, nil
}
// GetDefaultMcpServerTools 返回默认 MCP 服务器options.Options.MCPServerURL的 tools不依赖任何 mcp_agent 记录
func (man *SMCPAgentManager) GetDefaultMcpServerTools(ctx context.Context, userCred mcclient.TokenCredential) (jsonutils.JSONObject, error) {
timeout := time.Duration(options.Options.MCPAgentTimeout) * time.Second
mcpClient := utils.NewMCPClient(options.Options.MCPServerURL, timeout, userCred)
defer mcpClient.Close()
tools, err := mcpClient.ListTools(ctx)
if err != nil {
return nil, errors.Wrap(err, "list default MCP tools")
}
return jsonutils.Marshal(tools), nil
}
type SMCPAgent struct {
db.SSharableVirtualResourceBase
@@ -67,6 +132,8 @@ type SMCPAgent struct {
ApiKey string `width:"512" charset:"utf8" nullable:"true" list:"user" create:"optional" update:"user"`
// McpServer 即 mcp 服务器的后端地址
McpServer string `width:"512" charset:"utf8" nullable:"false" list:"user" create:"optional" update:"user"`
// DefaultAgent 是否为默认 Agent全局仅允许一条为 true
DefaultAgent bool `default:"false" list:"user" create:"optional" update:"user"`
}
func (mcp *SMCPAgent) BeforeInsert() {
@@ -99,6 +166,24 @@ func (mcp *SMCPAgent) BeforeUpdate() {
}
}
func (mcp *SMCPAgent) PostCreate(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, data jsonutils.JSONObject) {
mcp.SSharableVirtualResourceBase.PostCreate(ctx, userCred, ownerId, query, data)
if mcp.DefaultAgent {
if err := GetMCPAgentManager().unsetOtherDefaultAgents(ctx, mcp.Id); err != nil {
log.Errorf("unsetOtherDefaultAgents after create: %v", err)
}
}
}
func (mcp *SMCPAgent) PostUpdate(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) {
mcp.SSharableVirtualResourceBase.PostUpdate(ctx, userCred, query, data)
if mcp.DefaultAgent {
if err := GetMCPAgentManager().unsetOtherDefaultAgents(ctx, mcp.Id); err != nil {
log.Errorf("unsetOtherDefaultAgents after update: %v", err)
}
}
}
func (mcp *SMCPAgent) GetApiKey() (string, error) {
if len(mcp.ApiKey) == 0 {
return "", nil
@@ -253,6 +338,9 @@ func (man *SMCPAgentManager) ListItemFilter(
if len(input.LLMDriver) > 0 {
q = q.Equals("llm_driver", strings.ToLower(strings.TrimSpace(input.LLMDriver)))
}
if input.DefaultAgent != nil && *input.DefaultAgent {
q = q.IsTrue("default_agent")
}
return q, nil
}
@@ -293,6 +381,7 @@ func (manager *SMCPAgentManager) FetchCustomizeColumns(
if name, ok := llmIdNameMap[agents[i].LLMId]; ok {
rows[i].LLMName = name
}
rows[i].DefaultAgent = agents[i].DefaultAgent
}
}
@@ -303,10 +392,21 @@ func (mcp *SMCPAgent) GetLLMClientDriver() ILLMClient {
return GetLLMClientDriver(api.LLMClientType(mcp.LLMDriver))
}
func (mcp *SMCPAgent) GetMcpServerUrl(ctx context.Context, userCred mcclient.TokenCredential) (string, error) {
if len(mcp.McpServer) > 0 {
return mcp.McpServer, nil
}
return options.Options.MCPServerURL, nil
}
func (mcp *SMCPAgent) GetDetailsMcpTools(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject) (jsonutils.JSONObject, error) {
// 创建 MCP 客户端
timeout := time.Duration(options.Options.MCPAgentTimeout) * time.Second
mcpClient := utils.NewMCPClient(options.Options.MCPServerURL, timeout, userCred)
mcpServerUrl, err := mcp.GetMcpServerUrl(ctx, userCred)
if err != nil {
return nil, errors.Wrap(err, "GetMcpServerUrl")
}
mcpClient := utils.NewMCPClient(mcpServerUrl, timeout, userCred)
// 获取工具列表
tools, err := mcpClient.ListTools(ctx)
@@ -324,7 +424,11 @@ func (mcp *SMCPAgent) GetDetailsToolRequest(
) (jsonutils.JSONObject, error) {
// 创建 MCP 客户端
timeout := time.Duration(options.Options.MCPAgentTimeout) * time.Second
mcpClient := utils.NewMCPClient(options.Options.MCPServerURL, timeout, userCred)
mcpServerUrl, err := mcp.GetMcpServerUrl(ctx, userCred)
if err != nil {
return nil, errors.Wrap(err, "GetMcpServerUrl")
}
mcpClient := utils.NewMCPClient(mcpServerUrl, timeout, userCred)
defer mcpClient.Close()
// 调用工具
@@ -399,12 +503,13 @@ func (mcp *SMCPAgent) PerformChatStream(
}
// process 处理用户请求
// 强制分为两个阶段:
// 阶段一:使用 Chat 非流式获取工具调用参数,并执行工具
// 阶段二:使用 ChatStream 流式获取最终响应
func (mcp *SMCPAgent) process(ctx context.Context, userCred mcclient.TokenCredential, req *api.LLMMCPAgentRequestInput, onStream func(string) error) (*api.MCPAgentResponse, error) {
// 获取 MCP Server 的工具列表
mcpClient := utils.NewMCPClient(mcp.McpServer, 10*time.Minute, userCred)
mcpServerUrl, err := mcp.GetMcpServerUrl(ctx, userCred)
if err != nil {
return nil, errors.Wrap(err, "GetMcpServerUrl")
}
mcpClient := utils.NewMCPClient(mcpServerUrl, 10*time.Minute, userCred)
defer mcpClient.Close()
mcpTools, err := mcpClient.ListTools(ctx)
if err != nil {
@@ -444,43 +549,107 @@ func (mcp *SMCPAgent) process(ctx context.Context, userCred mcclient.TokenCreden
var toolCallRecords []api.MCPAgentToolCallRecord
log.Infof("Phase 1: Thinking & Acting...")
resp, err := llmClient.Chat(ctx, mcp, messages, tools)
// 处理流式的工具调用参数
type accumToolCall struct {
Id string
Name string
RawArguments strings.Builder
}
accToolCalls := make(map[int]*accumToolCall)
var accumulatedContent strings.Builder
var accumulatedReasoning strings.Builder
hasToolCalls := false
err = llmClient.ChatStream(ctx, mcp, messages, tools, func(chunk ILLMChatResponse) error {
if chunk.HasToolCalls() {
hasToolCalls = true
for _, tc := range chunk.GetToolCalls() {
idx := tc.GetIndex()
if _, exists := accToolCalls[idx]; !exists {
accToolCalls[idx] = &accumToolCall{
Id: tc.GetId(),
}
}
atc := accToolCalls[idx]
if id := tc.GetId(); id != "" {
atc.Id = id
}
if name := tc.GetFunction().GetName(); name != "" {
atc.Name = name
}
if args := tc.GetFunction().GetRawArguments(); args != "" {
atc.RawArguments.WriteString(args)
}
}
}
if r := chunk.GetReasoningContent(); len(r) > 0 {
accumulatedReasoning.WriteString(r)
}
content := chunk.GetContent()
if len(content) > 0 {
accumulatedContent.WriteString(content)
if onStream != nil {
if err := onStream(content); err != nil {
return err
}
}
}
return nil
})
if err != nil {
return nil, errors.Wrap(err, "phase 1 chat error")
return nil, errors.Wrap(err, "phase 1 chat stream error")
}
// 检查是否有工具调用
if !resp.HasToolCalls() {
// 如果阶段一没有调用工具,模拟推流返回结果
content := resp.GetContent()
if onStream != nil && len(content) > 0 {
// 模拟流式输出:按字符逐块推送
chunkSize := 10 // 每次推送10个字符
for i := 0; i < len(content); i += chunkSize {
end := i + chunkSize
if end > len(content) {
end = len(content)
}
chunk := content[i:end]
if err := onStream(chunk); err != nil {
return nil, errors.Wrap(err, "stream content error")
}
// 添加小延迟模拟真实流式输出
time.Sleep(10 * time.Millisecond)
}
}
if !hasToolCalls {
// 如果阶段一没有调用工具,直接返回结果
return &api.MCPAgentResponse{
Success: true,
Answer: content,
Answer: accumulatedContent.String(),
ToolCalls: toolCallRecords,
}, nil
}
// 处理工具调用
toolCalls := resp.GetToolCalls()
// Convert accumulated tool calls to ILLMToolCall
var toolCalls []ILLMToolCall
// Find max index
maxIdx := -1
for idx := range accToolCalls {
if idx > maxIdx {
maxIdx = idx
}
}
for i := 0; i <= maxIdx; i++ {
if atc, ok := accToolCalls[i]; ok {
var args map[string]interface{}
rawArgs := atc.RawArguments.String()
if len(rawArgs) > 0 {
if err := json.Unmarshal([]byte(rawArgs), &args); err != nil {
log.Errorf("Failed to unmarshal arguments for tool %s: %v. Raw: %s", atc.Name, err, rawArgs)
args = make(map[string]interface{})
}
} else {
args = make(map[string]interface{})
}
toolCalls = append(toolCalls, &SLLMToolCall{
Id: atc.Id,
Function: SLLMFunctionCall{
Name: atc.Name,
Arguments: args,
},
})
}
}
log.Infof("Got %d tool calls from Phase 1", len(toolCalls))
toolCallRecords, toolMessages, err := processToolCalls(ctx, toolCalls, mcpClient, llmClient)
toolCallRecords, toolMessages, err := processToolCalls(ctx, toolCalls, accumulatedReasoning.String(), accumulatedContent.String(), mcpClient, llmClient)
if err != nil {
return nil, errors.Wrap(err, "process tool calls")
}
@@ -572,13 +741,15 @@ func processHistoryMessages(
func processToolCalls(
ctx context.Context,
toolCalls []ILLMToolCall,
reasoningContent, content string,
mcpClient *utils.MCPClient,
llmClient ILLMClient,
) ([]api.MCPAgentToolCallRecord, []ILLMChatMessage, error) {
toolCallRecords := make([]api.MCPAgentToolCallRecord, 0)
messagesToAdd := make([]ILLMChatMessage, 0)
messagesToAdd = append(messagesToAdd, llmClient.NewAssistantMessageWithToolCalls(toolCalls))
// 使用带 reasoning_content 的 assistant 消息,满足 DeepSeek thinking mode + tool calls 要求
messagesToAdd = append(messagesToAdd, llmClient.NewAssistantMessageWithToolCallsAndReasoning(reasoningContent, content, toolCalls))
// 执行每个工具调用
for _, tc := range toolCalls {

View File

@@ -3,9 +3,12 @@ package service
import (
"context"
"fmt"
"io"
"net/http"
"time"
"yunion.io/x/jsonutils"
api "yunion.io/x/onecloud/pkg/apis/llm"
"yunion.io/x/onecloud/pkg/appsrv"
"yunion.io/x/onecloud/pkg/appsrv/dispatcher"
"yunion.io/x/onecloud/pkg/cloudcommon/db"
@@ -50,6 +53,64 @@ func handleLLMAvailableNetwork(ctx context.Context, w http.ResponseWriter, r *ht
appsrv.SendJSON(w, wrapped)
}
func handleDefaultChatStream(ctx context.Context, w http.ResponseWriter, r *http.Request) {
userCred := auth.FetchUserCredential(ctx, policy.FilterPolicyCredential)
if userCred == nil {
httperrors.UnauthorizedError(ctx, w, "Unauthorized")
return
}
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
httperrors.GeneralServerError(ctx, w, err)
return
}
body, err := jsonutils.Parse(bodyBytes)
if err != nil {
httperrors.InvalidInputError(ctx, w, "invalid body: %v", err)
return
}
var input api.LLMMCPAgentRequestInput
if body.Contains(models.GetMCPAgentManager().Keyword()) {
agentObj, _ := body.Get(models.GetMCPAgentManager().Keyword())
if agentObj != nil {
body = agentObj
}
}
if err := body.Unmarshal(&input); err != nil {
httperrors.InvalidInputError(ctx, w, "invalid input: %v", err)
return
}
defaultAgent, err := models.GetMCPAgentManager().GetDefaultAgent(ctx, userCred)
if err != nil {
httperrors.GeneralServerError(ctx, w, err)
return
}
if defaultAgent == nil {
httperrors.NotFoundError(ctx, w, "no default MCP agent set (set one agent with default_agent=true)")
return
}
query := jsonutils.NewDict()
_, err = defaultAgent.PerformChatStream(ctx, userCred, query, input)
if err != nil {
httperrors.GeneralServerError(ctx, w, err)
return
}
}
func handleDefaultMcpTools(ctx context.Context, w http.ResponseWriter, r *http.Request) {
userCred := auth.FetchUserCredential(ctx, policy.FilterPolicyCredential)
if userCred == nil {
httperrors.UnauthorizedError(ctx, w, "Unauthorized")
return
}
result, err := models.GetMCPAgentManager().GetDefaultMcpServerTools(ctx, userCred)
if err != nil {
httperrors.GeneralServerError(ctx, w, err)
return
}
appsrv.SendJSON(w, result)
}
func InitHandlers(app *appsrv.Application, isSlave bool) {
db.InitAllManagers()
db.RegistUserCredCacheUpdater()
@@ -60,6 +121,16 @@ func InitHandlers(app *appsrv.Application, isSlave bool) {
AddAvailableNetworkHandler(models.GetLLMManager().KeywordPlural(), app)
// 默认 Agent 聊天流:优先于 dispatcher 注册,避免被 performClassAction 的 sendJSON 覆盖。
// 注册两种路径default-chat-streamapigateway 转发用)与 default/chat-streamclimc 直连 region 时用,否则会被当作 resid=default 的 perform 导致 404
defaultChatStream := app.AddHandler2("POST", "/mcp_agents/default-chat-stream", auth.Authenticate(handleDefaultChatStream), nil, "default_chat_stream", nil)
defaultChatStream.SetProcessTimeout(time.Hour * 4).SetWorkerManager(models.GetMCPAgentWorkerManager())
defaultChatStreamSlash := app.AddHandler2("POST", "/mcp_agents/default/chat-stream", auth.Authenticate(handleDefaultChatStream), nil, "default_chat_stream_slash", nil)
defaultChatStreamSlash.SetProcessTimeout(time.Hour * 4).SetWorkerManager(models.GetMCPAgentWorkerManager())
// 默认 MCP 服务器 tools仅使用 options.MCPServerURL不依赖 mcp_agent 条目
app.AddHandler2("GET", "/mcp_agents/default-mcp-tools", auth.Authenticate(handleDefaultMcpTools), nil, "default_mcp_tools", nil)
for _, manager := range []db.IModelManager{
taskman.TaskManager,
taskman.SubTaskManager,

View File

@@ -107,7 +107,13 @@ func (task *LLMDeleteTask) OnLLMContainerDeleteCompleteFailed(ctx context.Contex
}
func (task *LLMDeleteTask) OnLLMContainerDeleteComplete(ctx context.Context, llm *models.SLLM, body jsonutils.JSONObject) {
err := llm.RealDelete(ctx, task.UserCred)
err := models.GetLLMInstantModelManager().DeleteByLlmId(ctx, llm.Id)
if err != nil {
task.taskFailed(ctx, llm, err)
return
}
err = llm.RealDelete(ctx, task.UserCred)
if err != nil {
task.taskFailed(ctx, llm, err)
return

View File

@@ -14,7 +14,8 @@ import (
type MCPAgentListOptions struct {
options.BaseListOptions
LLMDriver string `json:"llm_driver" help:"filter by llm driver (ollama or openai)"`
LLMDriver string `json:"llm_driver" help:"filter by llm driver (ollama or openai)"`
DefaultAgent *bool `json:"default_agent,omitempty" help:"filter by default agent (true to list the default one)"`
}
func (o *MCPAgentListOptions) Params() (jsonutils.JSONObject, error) {
@@ -32,12 +33,13 @@ func (o *MCPAgentShowOptions) Params() (jsonutils.JSONObject, error) {
type MCPAgentCreateOptions struct {
apis.SharableVirtualResourceCreateInput
LlmId string `help:"LLM 实例 ID如果提供则自动获取 llm_url" json:"llm_id"`
LLM_URL string `help:"后端大模型的 base 请求地址" json:"llm_url"`
LLM_DRIVER string `help:"使用的大模型驱动,可以是 ollama 或 openai" json:"llm_driver" choices:"ollama|openai"`
MODEL string `help:"使用的模型名称" json:"model"`
API_KEY string `help:"访问大模型的密钥" json:"api_key"`
McpServer string `help:"mcp 服务器的后端地址" json:"mcp_server"`
LlmId string `help:"LLM 实例 ID如果提供则自动获取 llm_url" json:"llm_id"`
LLM_URL string `help:"后端大模型的 base 请求地址" json:"llm_url"`
LLM_DRIVER string `help:"使用的大模型驱动,可以是 ollama 或 openai" json:"llm_driver" choices:"ollama|openai"`
MODEL string `help:"使用的模型名称" json:"model"`
API_KEY string `help:"访问大模型的密钥" json:"api_key"`
McpServer string `help:"mcp 服务器的后端地址" json:"mcp_server"`
DefaultAgent bool `help:"set as default MCP agent (only one can be true globally)" json:"default_agent"`
}
func (o *MCPAgentCreateOptions) Params() (jsonutils.JSONObject, error) {
@@ -45,15 +47,16 @@ func (o *MCPAgentCreateOptions) Params() (jsonutils.JSONObject, error) {
}
type MCPAgentUpdateOptions struct {
apis.SharableVirtualResourceCreateInput
apis.SharableVirtualResourceBaseUpdateInput
ID string
LlmId *string `help:"LLM 实例 ID如果提供则自动获取 llm_url" json:"llm_id,omitempty"`
LlmUrl *string `help:"后端大模型的 base 请求地址" json:"llm_url,omitempty"`
LlmDriver *string `help:"使用的大模型驱动,可以是 ollama 或 openai" json:"llm_driver,omitempty" choices:"ollama|openai"`
Model *string `help:"使用的模型名称" json:"model,omitempty"`
ApiKey *string `help:"访问大模型的密钥" json:"api_key,omitempty"`
McpServer *string `help:"mcp 服务器的后端地址" json:"mcp_server,omitempty"`
ID string
LlmId *string `help:"LLM 实例 ID如果提供则自动获取 llm_url" json:"llm_id,omitempty"`
LlmUrl *string `help:"后端大模型的 base 请求地址" json:"llm_url,omitempty"`
LlmDriver *string `help:"使用的大模型驱动,可以是 ollama 或 openai" json:"llm_driver,omitempty" choices:"ollama|openai"`
Model *string `help:"使用的模型名称" json:"model,omitempty"`
ApiKey *string `help:"访问大模型的密钥" json:"api_key,omitempty"`
McpServer *string `help:"mcp 服务器的后端地址" json:"mcp_server,omitempty"`
DefaultAgent *bool `help:"set as default MCP agent (only one can be true globally)" json:"default_agent,omitempty"`
}
func (o *MCPAgentUpdateOptions) GetId() string {
@@ -81,9 +84,12 @@ func (o *MCPAgentUpdateOptions) Params() (jsonutils.JSONObject, error) {
if o.McpServer != nil && len(*o.McpServer) > 0 {
params.Set("mcp_server", jsonutils.NewString(*o.McpServer))
}
if o.DefaultAgent != nil {
params.Set("default_agent", jsonutils.NewBool(*o.DefaultAgent))
}
// 添加基础字段
baseParams, err := options.StructToParams(&o.SharableVirtualResourceCreateInput)
baseParams, err := options.StructToParams(&o.SharableVirtualResourceBaseUpdateInput)
if err != nil {
return nil, err
}
@@ -169,3 +175,29 @@ func (opts *MCPAgentMCPAgentRequestOptions) Params() (jsonutils.JSONObject, erro
return jsonutils.Marshal(input), nil
}
// MCPAgentDefaultChatOptions 用于默认 Agent 聊天(不传 ID使用 default_agent=true 的条目)
type MCPAgentDefaultChatOptions struct {
MESSAGE string `help:"message to send to MCP agent" json:"message"`
History string `help:"chat history as JSON string, e.g. '[{\"role\":\"user\",\"content\":\"hello\"}]'" json:"history,omitempty"`
}
func (opts *MCPAgentDefaultChatOptions) Params() (jsonutils.JSONObject, error) {
input := api.LLMMCPAgentRequestInput{
Message: opts.MESSAGE,
History: []api.MCPAgentChatMessage{},
}
if len(opts.History) > 0 {
historyJSON, err := jsonutils.ParseString(opts.History)
if err != nil {
return nil, fmt.Errorf("failed to parse history JSON: %v", err)
}
if historyJSON != nil {
err = historyJSON.Unmarshal(&input.History)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal history: %v", err)
}
}
}
return jsonutils.Marshal(input), nil
}