mirror of
https://github.com/yunionio/cloudpods.git
synced 2026-09-01 15:07:17 +08:00
Fix(llm): fix bugs & apikey encrypt & support history for mcp-agent-chat & delete modelname in llmSku (#24149)
* fix(llm): mcp-agent apiKey encrypt * fix(llm): error-handle when instant-model import * feature(llm): add get-available-network for llm * feature(llm): support history for mcp-agent-chat * fix(llm): llm create with net instead of networktype & networkid * fix(llm): delete modelName in llmSku & fix some bugs
This commit is contained in:
@@ -18,6 +18,7 @@ func init() {
|
||||
cmd.BatchPerform("start", new(options.LLMStartOptions))
|
||||
cmd.Get("probed-models", new(options.LLMIdOptions))
|
||||
cmd.Get("url", new(options.LLMIdOptions))
|
||||
cmd.Custom(shell.CustomActionGet, "available-network", new(options.LLMAvailableNetworkOptions))
|
||||
cmd.Perform("save-instant-model", new(options.LLMSaveInstantModelOptions))
|
||||
cmd.Perform("quick-models", new(options.LLMQuickModelsOptions))
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"yunion.io/x/onecloud/cmd/climc/shell"
|
||||
@@ -36,15 +36,24 @@ func chatStream(s *mcclient.ClientSession, args *options.MCPAgentMCPAgentRequest
|
||||
return err
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("/mcp_agents/%s/chat-stream?message=%s", id, url.QueryEscape(args.Message))
|
||||
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 := fmt.Sprintf("/mcp_agents/%s/chat-stream", id)
|
||||
resp, err := s.RawVersionRequest(
|
||||
modules.MCPAgent.ServiceType(),
|
||||
modules.MCPAgent.EndpointType(),
|
||||
"GET",
|
||||
"POST",
|
||||
path,
|
||||
nil,
|
||||
nil,
|
||||
headers,
|
||||
body,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
+2
-2
@@ -4,6 +4,7 @@ import (
|
||||
"time"
|
||||
|
||||
"yunion.io/x/onecloud/pkg/apis"
|
||||
computeapi "yunion.io/x/onecloud/pkg/apis/compute"
|
||||
"yunion.io/x/onecloud/pkg/cloudcommon/db/taskman"
|
||||
)
|
||||
|
||||
@@ -64,8 +65,7 @@ type LLMBaseCreateInput struct {
|
||||
PreferHost string `json:"prefer_host"`
|
||||
AutoStart bool `json:"auto_start"`
|
||||
|
||||
NetworkType string `json:"network_type"`
|
||||
NetworkId string `json:"network_id"`
|
||||
Nets []*computeapi.NetworkConfig `json:"nets"`
|
||||
|
||||
BandwidthMB int `json:"bandwidth_mb"`
|
||||
DebugMode bool `json:"debug_mode"`
|
||||
|
||||
@@ -98,7 +98,14 @@ type LLMToolRequestInput struct {
|
||||
}
|
||||
|
||||
type LLMMCPAgentRequestInput struct {
|
||||
Message string `json:"message" help:"message to send to MCP agent"`
|
||||
Message string `json:"message" help:"message to send to MCP agent"`
|
||||
History []MCPAgentChatMessage `json:"history" help:"chat history"`
|
||||
}
|
||||
|
||||
type MCPAgentChatMessage struct {
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content"`
|
||||
// ToolCalls []MCPAgentToolCallRecord `json:"tool_calls,omitempty"`
|
||||
}
|
||||
|
||||
// MCPAgentResponse 表示 Agent 响应
|
||||
@@ -115,6 +122,7 @@ type MCPAgentResponse struct {
|
||||
|
||||
// MCPAgentToolCallRecord 记录工具调用
|
||||
type MCPAgentToolCallRecord struct {
|
||||
Id string `json:"id,omitempty"`
|
||||
ToolName string `json:"tool_name"`
|
||||
Arguments map[string]interface{} `json:"arguments"`
|
||||
Result string `json:"result"`
|
||||
|
||||
@@ -17,10 +17,10 @@ const (
|
||||
LLM_OLLAMA_HOST_MANIFESTS_DIR = "/manifests"
|
||||
LLM_OLLAMA_CACHE_DIR = "/.llm_ollama_cache"
|
||||
LLM_OLLAMA_CACHE_MOUNT_PATH = "/usr/local"
|
||||
LLM_OLLAMA_LIBRARY_BASE_URL = `https://registry.ollama.ai/v2/library/%s`
|
||||
LLM_OLLAMA_LIBRARY_BASE_URL = `https://registry.ollama.ai/v2/%s`
|
||||
LLM_OLLAMA_BASE_PATH = "/root/.ollama/models"
|
||||
LLM_OLLAMA_BLOBS_DIR = "/blobs"
|
||||
LLM_OLLAMA_MANIFESTS_BASE_PATH = "/manifests/registry.ollama.ai/library"
|
||||
LLM_OLLAMA_MANIFESTS_BASE_PATH = "/manifests/registry.ollama.ai"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
+4
-5
@@ -166,18 +166,17 @@ type LLMSkuListInput struct {
|
||||
|
||||
type LLMSkuCreateInput struct {
|
||||
LLMSKuBaseCreateInput
|
||||
MountedModelResourceCreateInput
|
||||
|
||||
LLMImageId string `json:"llm_image_id"`
|
||||
LLMType string `json:"llm_type"`
|
||||
LLMModelName string `json:"llm_model_name"`
|
||||
LLMImageId string `json:"llm_image_id"`
|
||||
LLMType string `json:"llm_type"`
|
||||
}
|
||||
|
||||
type LLMSkuUpdateInput struct {
|
||||
LLMSkuBaseUpdateInput
|
||||
MountedModelResourceUpdateInput
|
||||
|
||||
LLMImageId string `json:"llm_image_id"`
|
||||
LLMModelName string `json:"llm_model_name"`
|
||||
LLMImageId string `json:"llm_image_id"`
|
||||
}
|
||||
|
||||
// type LLMModelCloneInput struct {
|
||||
|
||||
@@ -220,6 +220,13 @@ func (o *ollama) NewUserMessage(content string) models.ILLMChatMessage {
|
||||
}
|
||||
}
|
||||
|
||||
func (o *ollama) NewAssistantMessage(content string) models.ILLMChatMessage {
|
||||
return &OllamaChatMessage{
|
||||
Role: "assistant",
|
||||
Content: content,
|
||||
}
|
||||
}
|
||||
|
||||
func (o *ollama) NewAssistantMessageWithToolCalls(toolCalls []models.ILLMToolCall) models.ILLMChatMessage {
|
||||
// to ollama tool calls
|
||||
ollamaToolCalls := make([]OllamaToolCall, len(toolCalls))
|
||||
|
||||
@@ -194,8 +194,12 @@ func (o *openai) doChatStreamRequest(ctx context.Context, mcpAgent *models.SMCPA
|
||||
return errors.Wrap(err, "create request")
|
||||
}
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
if mcpAgent.ApiKey != "" {
|
||||
httpReq.Header.Set("Authorization", "Bearer "+mcpAgent.ApiKey)
|
||||
apiKey, err := mcpAgent.GetApiKey()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if apiKey != "" {
|
||||
httpReq.Header.Set("Authorization", "Bearer "+apiKey)
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
@@ -277,8 +281,12 @@ func (o *openai) doChatRequest(ctx context.Context, mcpAgent *models.SMCPAgent,
|
||||
return nil, errors.Wrap(err, "create request")
|
||||
}
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
if mcpAgent.ApiKey != "" {
|
||||
httpReq.Header.Set("Authorization", "Bearer "+mcpAgent.ApiKey)
|
||||
apiKey, err := mcpAgent.GetApiKey()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "get apiKey")
|
||||
}
|
||||
if apiKey != "" {
|
||||
httpReq.Header.Set("Authorization", "Bearer "+apiKey)
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
@@ -319,6 +327,13 @@ func (o *openai) NewUserMessage(content string) models.ILLMChatMessage {
|
||||
}
|
||||
}
|
||||
|
||||
func (o *openai) NewAssistantMessage(content string) models.ILLMChatMessage {
|
||||
return &OpenAIChatMessage{
|
||||
Role: "assistant",
|
||||
Content: content,
|
||||
}
|
||||
}
|
||||
|
||||
func (o *openai) NewAssistantMessageWithToolCalls(toolCalls []models.ILLMToolCall) models.ILLMChatMessage {
|
||||
openaiToolCalls := make([]OpenAIToolCall, len(toolCalls))
|
||||
for i, tc := range toolCalls {
|
||||
|
||||
@@ -48,7 +48,7 @@ func (o *ollama) GetContainerSpec(ctx context.Context, llm *models.SLLM, image *
|
||||
},
|
||||
}
|
||||
|
||||
if len(devices) == 0 && len(*sku.Devices) > 0 {
|
||||
if len(devices) == 0 && (sku.Devices != nil && len(*sku.Devices) > 0) {
|
||||
for i := range *sku.Devices {
|
||||
index := i
|
||||
spec.Devices = append(spec.Devices, &computeapi.ContainerDevice{
|
||||
@@ -159,7 +159,8 @@ func (o *ollama) GetContainerSpec(ctx context.Context, llm *models.SLLM, image *
|
||||
|
||||
func (o *ollama) DownloadModel(ctx context.Context, userCred mcclient.TokenCredential, llm *models.SLLM, tmpDir string, modelName string, modelTag string) (string, []string, error) {
|
||||
// 1. download manifest from registry
|
||||
manifestsUrl := fmt.Sprintf(api.LLM_OLLAMA_LIBRARY_BASE_URL, fmt.Sprintf("%s/manifests/%s", modelName, modelTag))
|
||||
namespace, repo := getNamespaceAndRepo(modelName)
|
||||
manifestsUrl := fmt.Sprintf(api.LLM_OLLAMA_LIBRARY_BASE_URL, fmt.Sprintf("%s/%s/manifests/%s", namespace, repo, modelTag))
|
||||
log.Infof("Downloading manifest from %s", manifestsUrl)
|
||||
|
||||
manifestContent, err := llm.HttpGet(ctx, manifestsUrl)
|
||||
@@ -187,9 +188,9 @@ func (o *ollama) DownloadModel(ctx context.Context, userCred mcclient.TokenCrede
|
||||
|
||||
// 4. create directory structure
|
||||
// tmpDir/blobs/
|
||||
// tmpDir/manifests/registry.ollama.ai/library/<modelName>/<modelTag>
|
||||
// tmpDir/manifests/registry.ollama.ai/<namespace>/<repo>
|
||||
blobsDir := path.Join(tmpDir, "blobs")
|
||||
manifestsDir := path.Join(tmpDir, api.LLM_OLLAMA_MANIFESTS_BASE_PATH, modelName)
|
||||
manifestsDir := path.Join(tmpDir, api.LLM_OLLAMA_MANIFESTS_BASE_PATH, namespace, repo)
|
||||
if err := os.MkdirAll(blobsDir, 0755); err != nil {
|
||||
return "", nil, errors.Wrapf(err, "failed to create blobs directory %s", blobsDir)
|
||||
}
|
||||
@@ -210,8 +211,8 @@ func (o *ollama) DownloadModel(ctx context.Context, userCred mcclient.TokenCrede
|
||||
}
|
||||
|
||||
// download blob from registry
|
||||
// URL format: https://registry.ollama.ai/v2/library/<modelName>/blobs/<digest>
|
||||
blobUrl := fmt.Sprintf(api.LLM_OLLAMA_LIBRARY_BASE_URL, fmt.Sprintf("%s/blobs/%s", modelName, digest))
|
||||
// URL format: https://registry.ollama.ai/v2/<namespace>/<repo>/blobs/<digest>
|
||||
blobUrl := fmt.Sprintf(api.LLM_OLLAMA_LIBRARY_BASE_URL, fmt.Sprintf("%s/%s/blobs/%s", namespace, repo, digest))
|
||||
log.Infof("Downloading blob %s from %s", blobFileName, blobUrl)
|
||||
|
||||
if err := llm.HttpDownloadFile(ctx, blobUrl, blobPath); err != nil {
|
||||
@@ -232,7 +233,7 @@ func (o *ollama) DownloadModel(ctx context.Context, userCred mcclient.TokenCrede
|
||||
blobFileName := strings.Replace(blob, ":", "-", 1)
|
||||
mounts[i] = path.Join(api.LLM_OLLAMA_BASE_PATH, api.LLM_OLLAMA_BLOBS_DIR, blobFileName)
|
||||
}
|
||||
mounts = append(mounts, path.Join(api.LLM_OLLAMA_BASE_PATH, api.LLM_OLLAMA_MANIFESTS_BASE_PATH, modelName, modelTag))
|
||||
mounts = append(mounts, path.Join(api.LLM_OLLAMA_BASE_PATH, api.LLM_OLLAMA_MANIFESTS_BASE_PATH, namespace, repo, modelTag))
|
||||
|
||||
return modelId, mounts, nil
|
||||
}
|
||||
@@ -245,7 +246,8 @@ func (o *ollama) PreInstallModel(ctx context.Context, userCred mcclient.TokenCre
|
||||
}
|
||||
|
||||
// mkdir llm-registry-base-path / modelname
|
||||
mkdirReigtryBasePaht := fmt.Sprintf("mkdir -p %s", path.Join(api.LLM_OLLAMA_BASE_PATH, api.LLM_OLLAMA_MANIFESTS_BASE_PATH, instMdl.ModelName))
|
||||
namespace, repo := getNamespaceAndRepo(instMdl.ModelName)
|
||||
mkdirReigtryBasePaht := fmt.Sprintf("mkdir -p %s", path.Join(api.LLM_OLLAMA_BASE_PATH, api.LLM_OLLAMA_MANIFESTS_BASE_PATH, namespace, repo))
|
||||
_, err = exec(ctx, lc.CmpId, mkdirReigtryBasePaht, 10)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to mkdir llm-registry-base-path / modelname")
|
||||
@@ -296,13 +298,11 @@ func (o *ollama) GetInstantModelIdByPostOverlay(postOverlay *commonapi.Container
|
||||
for k := range postOverlay.Image.PathMap {
|
||||
idx := strings.Index(k, api.LLM_OLLAMA_MANIFESTS_BASE_PATH)
|
||||
if idx != -1 {
|
||||
suffix := k[idx+len(api.LLM_OLLAMA_MANIFESTS_BASE_PATH):]
|
||||
parts := strings.Split(strings.Trim(suffix, "/"), "/")
|
||||
if len(parts) >= 2 {
|
||||
modelName := parts[len(parts)-2]
|
||||
modelTag := parts[len(parts)-1]
|
||||
log.Infof("In GetInstantModelIdByPostOverlay, Extracted modelName: %s, modelTag: %s, Got modelId: %s", modelName, modelTag, mdlNameToId[modelName+":"+modelTag])
|
||||
return mdlNameToId[modelName+":"+modelTag]
|
||||
path := k[idx:]
|
||||
nameTag := parseModelName(path)
|
||||
if nameTag != "" {
|
||||
log.Infof("In GetInstantModelIdByPostOverlay, Extracted nameTag: %s, Got modelId: %s", nameTag, mdlNameToId[nameTag])
|
||||
return mdlNameToId[nameTag]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -320,7 +320,8 @@ func (o *ollama) DetectModelPaths(ctx context.Context, userCred mcclient.TokenCr
|
||||
for idx, blob := range pkgInfo.Blobs {
|
||||
originBlobs[idx] = path.Join(api.LLM_OLLAMA_BASE_PATH, api.LLM_OLLAMA_BLOBS_DIR, blob)
|
||||
}
|
||||
originManifests := path.Join(api.LLM_OLLAMA_BASE_PATH, api.LLM_OLLAMA_MANIFESTS_BASE_PATH, pkgInfo.Name, pkgInfo.Tag)
|
||||
namespace, repo := getNamespaceAndRepo(pkgInfo.Name)
|
||||
originManifests := path.Join(api.LLM_OLLAMA_BASE_PATH, api.LLM_OLLAMA_MANIFESTS_BASE_PATH, namespace, repo, pkgInfo.Tag)
|
||||
|
||||
var checks []string
|
||||
for _, blob := range originBlobs {
|
||||
@@ -509,7 +510,8 @@ func exec(ctx context.Context, containerId string, cmd string, timeoutSec int64)
|
||||
}
|
||||
|
||||
func getManifestsPath(modelName, modelTag string) string {
|
||||
return path.Join(api.LLM_OLLAMA_BASE_PATH, api.LLM_OLLAMA_MANIFESTS_BASE_PATH, modelName, modelTag)
|
||||
namespace, repo := getNamespaceAndRepo(modelName)
|
||||
return path.Join(api.LLM_OLLAMA_BASE_PATH, api.LLM_OLLAMA_MANIFESTS_BASE_PATH, namespace, repo, modelTag)
|
||||
}
|
||||
|
||||
type Layer struct {
|
||||
@@ -591,6 +593,9 @@ func parseModelName(path string) string {
|
||||
name := model[:lastSlash]
|
||||
tag := model[lastSlash+1:]
|
||||
tag = strings.TrimRight(tag, `\`)
|
||||
if after, ok := strings.CutPrefix(name, "library/"); ok {
|
||||
name = after
|
||||
}
|
||||
return name + ":" + tag
|
||||
}
|
||||
return strings.TrimRight(model, `\`)
|
||||
@@ -642,3 +647,11 @@ func (o *ollama) GetLLMUrl(ctx context.Context, userCred mcclient.TokenCredentia
|
||||
return fmt.Sprintf("http://%s:%d", server.HostAccessIp, accessInfo.AccessPort), nil
|
||||
}
|
||||
}
|
||||
|
||||
func getNamespaceAndRepo(modelName string) (string, string) {
|
||||
if strings.Contains(modelName, "/") {
|
||||
parts := strings.Split(modelName, "/")
|
||||
return parts[0], parts[1]
|
||||
}
|
||||
return "library", modelName
|
||||
}
|
||||
|
||||
@@ -41,15 +41,15 @@ type SDifySkuManager struct {
|
||||
type SDifySku struct {
|
||||
SLLMSkuBase
|
||||
|
||||
PostgresImageId string `width:"128" charset:"ascii" nullable:"false" list:"user" create:"required"`
|
||||
RedisImageId string `width:"128" charset:"ascii" nullable:"false" list:"user" create:"required"`
|
||||
NginxImageId string `width:"128" charset:"ascii" nullable:"false" list:"user" create:"required"`
|
||||
DifyApiImageId string `width:"128" charset:"ascii" nullable:"false" list:"user" create:"required"`
|
||||
DifyPluginImageId string `width:"128" charset:"ascii" nullable:"false" list:"user" create:"required"`
|
||||
DifyWebImageId string `width:"128" charset:"ascii" nullable:"false" list:"user" create:"required"`
|
||||
DifySandboxImageId string `width:"128" charset:"ascii" nullable:"false" list:"user" create:"required"`
|
||||
DifySSRFImageId string `width:"128" charset:"ascii" nullable:"false" list:"user" create:"required"`
|
||||
DifyWeaviateImageId string `width:"128" charset:"ascii" nullable:"false" list:"user" create:"required"`
|
||||
PostgresImageId string `width:"128" charset:"ascii" nullable:"false" list:"user" create:"required" update:"user"`
|
||||
RedisImageId string `width:"128" charset:"ascii" nullable:"false" list:"user" create:"required" update:"user"`
|
||||
NginxImageId string `width:"128" charset:"ascii" nullable:"false" list:"user" create:"required" update:"user"`
|
||||
DifyApiImageId string `width:"128" charset:"ascii" nullable:"false" list:"user" create:"required" update:"user"`
|
||||
DifyPluginImageId string `width:"128" charset:"ascii" nullable:"false" list:"user" create:"required" update:"user"`
|
||||
DifyWebImageId string `width:"128" charset:"ascii" nullable:"false" list:"user" create:"required" update:"user"`
|
||||
DifySandboxImageId string `width:"128" charset:"ascii" nullable:"false" list:"user" create:"required" update:"user"`
|
||||
DifySSRFImageId string `width:"128" charset:"ascii" nullable:"false" list:"user" create:"required" update:"user"`
|
||||
DifyWeaviateImageId string `width:"128" charset:"ascii" nullable:"false" list:"user" create:"required" update:"user"`
|
||||
}
|
||||
|
||||
func (man *SDifySkuManager) ListItemFilter(
|
||||
|
||||
+31
-5
@@ -361,11 +361,7 @@ func (llm *SLLM) GetLLMSku(skuId string) (*SLLMSku, error) {
|
||||
|
||||
func (llm *SLLM) GetLargeLanguageModelName(name string) (modelName string, modelTag string, err error) {
|
||||
if name == "" {
|
||||
sku, err := llm.GetLLMSku("")
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
name = sku.LLMModelName
|
||||
return "", "", errors.Wrap(errors.ErrInvalidStatus, "model name is empty")
|
||||
}
|
||||
parts := strings.Split(name, ":")
|
||||
modelName = parts[0]
|
||||
@@ -578,3 +574,33 @@ func fetchNetworks(ctx context.Context, userCred mcclient.TokenCredential, netwo
|
||||
}
|
||||
return networks, nil
|
||||
}
|
||||
|
||||
func (man *SLLMManager) GetAvailableNetwork(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject) (jsonutils.JSONObject, error) {
|
||||
s := auth.GetSession(ctx, userCred, "")
|
||||
|
||||
ret := jsonutils.NewDict()
|
||||
|
||||
q := jsonutils.NewDict()
|
||||
if query != nil {
|
||||
q.Update(query)
|
||||
}
|
||||
q.Set("server_type", jsonutils.NewString(string(computeapi.NETWORK_TYPE_HOSTLOCAL)))
|
||||
q.Set("is_auto_alloc", jsonutils.NewBool(true))
|
||||
q.Set("status", jsonutils.NewString(computeapi.NETWORK_STATUS_AVAILABLE))
|
||||
q.Set("limit", jsonutils.NewInt(1))
|
||||
|
||||
result, err := compute.Networks.List(s, q)
|
||||
if err == nil && result.Total > 0 {
|
||||
ret.Add(jsonutils.NewInt(int64(result.Total)), "auto_alloc_network_hostlocal_count")
|
||||
}
|
||||
|
||||
q.Set("server_type", jsonutils.NewString(string(computeapi.NETWORK_TYPE_GUEST)))
|
||||
q.Set("vpc_id", jsonutils.NewString(computeapi.DEFAULT_VPC_ID))
|
||||
|
||||
resultGuest, err := compute.Networks.List(s, q)
|
||||
if err == nil && resultGuest.Total > 0 {
|
||||
ret.Add(jsonutils.NewInt(int64(resultGuest.Total)), "auto_alloc_network_guest_count")
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
+46
-11
@@ -97,23 +97,58 @@ func (man *SLLMBaseManager) ValidateCreateData(ctx context.Context, userCred mcc
|
||||
input.PreferHost = hostDetails.Id
|
||||
}
|
||||
|
||||
if len(input.NetworkType) > 0 && !api.IsLLMSkuBaseNetworkType(input.NetworkType) {
|
||||
return input, errors.Wrapf(httperrors.ErrInputParameter, "invalid network type %s", input.NetworkType)
|
||||
}
|
||||
|
||||
if len(input.NetworkId) > 0 {
|
||||
s := auth.GetSession(ctx, userCred, "")
|
||||
netObj, err := compute.Networks.Get(s, input.NetworkId, nil)
|
||||
if err != nil {
|
||||
return input, errors.Wrapf(httperrors.ErrInputParameter, "invalid network_id %s", input.NetworkId)
|
||||
// 处理网络配置
|
||||
var firstNet *computeapi.NetworkConfig
|
||||
if len(input.Nets) > 0 {
|
||||
firstNet = input.Nets[0]
|
||||
firstNet.Index = 0
|
||||
if len(string(firstNet.NetType)) > 0 && !api.IsLLMSkuBaseNetworkType(string(firstNet.NetType)) {
|
||||
return input, errors.Wrapf(httperrors.ErrInputParameter, "invalid network type %s", firstNet.NetType)
|
||||
}
|
||||
input.NetworkId, _ = netObj.GetString("id")
|
||||
input.NetworkType, _ = netObj.GetString("server_type")
|
||||
if len(firstNet.Network) > 0 {
|
||||
s := auth.GetSession(ctx, userCred, "")
|
||||
netObj, err := compute.Networks.Get(s, firstNet.Network, nil)
|
||||
if err != nil {
|
||||
return input, errors.Wrapf(httperrors.ErrInputParameter, "invalid network_id %s", firstNet.Network)
|
||||
}
|
||||
netId, _ := netObj.GetString("id")
|
||||
netType, _ := netObj.GetString("server_type")
|
||||
firstNet.Network = netId
|
||||
if len(string(firstNet.NetType)) == 0 {
|
||||
firstNet.NetType = computeapi.TNetworkType(netType)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return input, errors.Wrap(httperrors.ErrInputParameter, "nets cannot be empty")
|
||||
}
|
||||
|
||||
return input, nil
|
||||
}
|
||||
|
||||
func (llmBase *SLLMBase) CustomizeCreate(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, data jsonutils.JSONObject) error {
|
||||
err := llmBase.SVirtualResourceBase.CustomizeCreate(ctx, userCred, ownerId, query, data)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "SVirtualResourceBase.CustomizeCreate")
|
||||
}
|
||||
|
||||
var input api.LLMBaseCreateInput
|
||||
if err := data.Unmarshal(&input); err != nil {
|
||||
return errors.Wrap(err, "unmarshal LLMBaseCreateInput")
|
||||
}
|
||||
|
||||
if len(input.Nets) > 0 {
|
||||
firstNet := input.Nets[0]
|
||||
if len(string(firstNet.NetType)) > 0 {
|
||||
llmBase.NetworkType = string(firstNet.NetType)
|
||||
}
|
||||
if len(firstNet.Network) > 0 {
|
||||
llmBase.NetworkId = firstNet.Network
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetServerIdsByHost(ctx context.Context, userCred mcclient.TokenCredential, hostId string) ([]string, error) {
|
||||
s := auth.GetSession(ctx, userCred, options.Options.Region)
|
||||
params := computeoptions.ServerListOptions{}
|
||||
|
||||
@@ -109,21 +109,27 @@ func GetLLMBasePodCreateInput(
|
||||
})
|
||||
}
|
||||
}
|
||||
bandwidth := llmBase.BandwidthMb
|
||||
if bandwidth == 0 {
|
||||
bandwidth = skuBase.Bandwidth
|
||||
var network *computeapi.NetworkConfig
|
||||
if len(input.Nets) > 0 {
|
||||
network = input.Nets[0]
|
||||
networkCopy := *network
|
||||
network = &networkCopy
|
||||
network.Index = 0
|
||||
}
|
||||
|
||||
network := &computeapi.NetworkConfig{
|
||||
BwLimit: bandwidth,
|
||||
NetType: computeapi.TNetworkType(llmBase.NetworkType),
|
||||
bandwidth := input.BandwidthMB
|
||||
if bandwidth == 0 && network.BwLimit != 0 {
|
||||
bandwidth = network.BwLimit
|
||||
}
|
||||
if llmBase.NetworkType == string(computeapi.NETWORK_TYPE_HOSTLOCAL) {
|
||||
if bandwidth == 0 && skuBase.Bandwidth != 0 {
|
||||
bandwidth = skuBase.Bandwidth
|
||||
}
|
||||
network.BwLimit = bandwidth
|
||||
|
||||
networkType := string(network.NetType)
|
||||
if networkType == string(computeapi.NETWORK_TYPE_HOSTLOCAL) {
|
||||
network.PortMappings = portMappings
|
||||
}
|
||||
if len(llmBase.NetworkId) > 0 {
|
||||
network.Network = llmBase.NetworkId
|
||||
}
|
||||
|
||||
data.Networks = []*computeapi.NetworkConfig{
|
||||
network,
|
||||
|
||||
@@ -57,6 +57,7 @@ type ILLMClient interface {
|
||||
ChatStream(ctx context.Context, mcpAgent *SMCPAgent, messages interface{}, tools interface{}, onChunk func(ILLMChatResponse) error) error
|
||||
|
||||
NewUserMessage(content string) ILLMChatMessage
|
||||
NewAssistantMessage(content string) ILLMChatMessage
|
||||
NewAssistantMessageWithToolCalls(toolCalls []ILLMToolCall) ILLMChatMessage
|
||||
NewToolMessage(toolId string, toolName string, content string) ILLMChatMessage
|
||||
NewSystemMessage(content string) ILLMChatMessage
|
||||
@@ -64,6 +65,32 @@ type ILLMClient interface {
|
||||
ConvertMCPTools(mcpTools []mcp.Tool) []ILLMTool
|
||||
}
|
||||
|
||||
type SLLMToolCall struct {
|
||||
Id string
|
||||
Function SLLMFunctionCall
|
||||
}
|
||||
|
||||
func (tc *SLLMToolCall) GetId() string {
|
||||
return tc.Id
|
||||
}
|
||||
|
||||
func (tc *SLLMToolCall) GetFunction() ILLMFunctionCall {
|
||||
return &tc.Function
|
||||
}
|
||||
|
||||
type SLLMFunctionCall struct {
|
||||
Name string
|
||||
Arguments map[string]interface{}
|
||||
}
|
||||
|
||||
func (fc *SLLMFunctionCall) GetName() string {
|
||||
return fc.Name
|
||||
}
|
||||
|
||||
func (fc *SLLMFunctionCall) GetArguments() map[string]interface{} {
|
||||
return fc.Arguments
|
||||
}
|
||||
|
||||
var (
|
||||
llmClientDrivers = newDrivers()
|
||||
)
|
||||
|
||||
@@ -207,6 +207,9 @@ func (llm *SLLM) HttpGet(ctx context.Context, url string) ([]byte, error) {
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return nil, httperrors.NewResourceNotFoundError("url %s not found", url)
|
||||
}
|
||||
return nil, errors.Errorf("unexpected status code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
@@ -231,6 +234,9 @@ func (llm *SLLM) HttpDownloadFile(ctx context.Context, url string, filePath stri
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return errors.Wrapf(httperrors.ErrResourceNotFound, "url %s not found", url)
|
||||
}
|
||||
return errors.Errorf("unexpected status code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
|
||||
@@ -52,9 +52,8 @@ type SLLMSku struct {
|
||||
SLLMSkuBase
|
||||
SMountedModelsResource
|
||||
|
||||
LLMImageId string `width:"128" charset:"ascii" nullable:"false" list:"user" create:"required"`
|
||||
LLMType string `width:"128" charset:"ascii" nullable:"false" list:"user" create:"required"`
|
||||
LLMModelName string `width:"128" charset:"ascii" nullable:"false" list:"user" create:"required"`
|
||||
LLMImageId string `width:"128" charset:"ascii" nullable:"false" list:"user" create:"required" update:"user"`
|
||||
LLMType string `width:"128" charset:"ascii" nullable:"false" list:"user" create:"required"`
|
||||
}
|
||||
|
||||
func (man *SLLMSkuManager) ListItemFilter(
|
||||
@@ -161,10 +160,29 @@ func (man *SLLMSkuManager) ValidateCreateData(ctx context.Context, userCred mccl
|
||||
return input, errors.Wrap(httperrors.ErrInputParameter, "llm_type must be one of "+strings.Join(api.LLM_CONTAINER_TYPES.List(), ","))
|
||||
}
|
||||
|
||||
_, err = validators.ValidateModel(ctx, userCred, GetLLMImageManager(), &input.LLMImageId)
|
||||
imgObj, err := validators.ValidateModel(ctx, userCred, GetLLMImageManager(), &input.LLMImageId)
|
||||
if err != nil {
|
||||
return input, errors.Wrapf(err, "validate image_id %s", input.LLMImageId)
|
||||
}
|
||||
llmImage := imgObj.(*SLLMImage)
|
||||
if llmImage.LLMType != input.LLMType {
|
||||
return input, errors.Wrapf(httperrors.ErrInvalidStatus, "image %s is not of type %s", input.LLMImageId, input.LLMType)
|
||||
}
|
||||
input.LLMImageId = llmImage.Id
|
||||
|
||||
if input.MountedModels != nil {
|
||||
for i, mdl := range input.MountedModels {
|
||||
instMdl, err := GetInstantModelManager().FetchByIdOrName(ctx, userCred, mdl)
|
||||
if err != nil {
|
||||
return input, errors.Wrapf(err, "validate mounted model %s", mdl)
|
||||
}
|
||||
instantModle := instMdl.(*SInstantModel)
|
||||
if instantModle.LlmType != input.LLMType {
|
||||
return input, errors.Wrapf(httperrors.ErrInvalidStatus, "mounted model %s is not of type %s", mdl, input.LLMType)
|
||||
}
|
||||
input.MountedModels[i] = instantModle.GetId()
|
||||
}
|
||||
}
|
||||
|
||||
input.Status = api.STATUS_READY
|
||||
return input, nil
|
||||
@@ -200,7 +218,12 @@ func (sku *SLLMSku) ValidateUpdateData(ctx context.Context, userCred mcclient.To
|
||||
if err != nil {
|
||||
return input, errors.Wrapf(err, "validate image_id %s", input.LLMImageId)
|
||||
}
|
||||
input.LLMImageId = imgObj.GetId()
|
||||
llmImage := imgObj.(*SLLMImage)
|
||||
if llmImage.LLMType != sku.LLMType {
|
||||
return input, errors.Wrapf(httperrors.ErrInvalidStatus, "image %s is not of type %s", input.LLMImageId, sku.LLMType)
|
||||
}
|
||||
input.LLMImageId = llmImage.Id
|
||||
log.Infof("update llm_image_id %s to %s", sku.LLMImageId, input.LLMImageId)
|
||||
}
|
||||
|
||||
return input, nil
|
||||
|
||||
+180
-42
@@ -10,6 +10,7 @@ import (
|
||||
"yunion.io/x/jsonutils"
|
||||
"yunion.io/x/log"
|
||||
"yunion.io/x/pkg/errors"
|
||||
seclib "yunion.io/x/pkg/utils"
|
||||
"yunion.io/x/sqlchemy"
|
||||
|
||||
api "yunion.io/x/onecloud/pkg/apis/llm"
|
||||
@@ -68,6 +69,48 @@ type SMCPAgent struct {
|
||||
McpServer string `width:"512" charset:"utf8" nullable:"false" list:"user" create:"optional" update:"user"`
|
||||
}
|
||||
|
||||
func (mcp *SMCPAgent) BeforeInsert() {
|
||||
if len(mcp.Id) == 0 {
|
||||
mcp.Id = db.DefaultUUIDGenerator()
|
||||
}
|
||||
if len(mcp.ApiKey) > 0 {
|
||||
sec, err := seclib.EncryptAESBase64(mcp.Id, mcp.ApiKey)
|
||||
if err != nil {
|
||||
log.Errorf("EncryptAESBase64 fail %s", err)
|
||||
} else {
|
||||
mcp.ApiKey = sec
|
||||
}
|
||||
}
|
||||
mcp.SSharableVirtualResourceBase.BeforeInsert()
|
||||
}
|
||||
|
||||
func (mcp *SMCPAgent) BeforeUpdate() {
|
||||
if len(mcp.ApiKey) > 0 {
|
||||
// heuristic to check if it is plaintext
|
||||
_, err := seclib.DescryptAESBase64(mcp.Id, mcp.ApiKey)
|
||||
if err != nil {
|
||||
sec, err := seclib.EncryptAESBase64(mcp.Id, mcp.ApiKey)
|
||||
if err != nil {
|
||||
log.Errorf("EncryptAESBase64 fail %s", err)
|
||||
} else {
|
||||
mcp.ApiKey = sec
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (mcp *SMCPAgent) GetApiKey() (string, error) {
|
||||
if len(mcp.ApiKey) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
// try decrypt
|
||||
key, err := seclib.DescryptAESBase64(mcp.Id, mcp.ApiKey)
|
||||
if err == nil {
|
||||
return key, nil
|
||||
}
|
||||
return mcp.ApiKey, nil
|
||||
}
|
||||
|
||||
func (man *SMCPAgentManager) CustomizeHandlerInfo(info *appsrv.SHandlerInfo) {
|
||||
man.SSharableVirtualResourceBaseManager.CustomizeHandlerInfo(info)
|
||||
|
||||
@@ -100,12 +143,20 @@ func (man *SMCPAgentManager) ValidateCreateData(ctx context.Context, userCred mc
|
||||
}
|
||||
input.LLMUrl = llmUrl
|
||||
|
||||
sku, err := llm.GetLLMSku("")
|
||||
if err != nil {
|
||||
return input, errors.Wrapf(err, "get LLM Sku from LLM %s", input.LLMId)
|
||||
}
|
||||
if len(input.Model) == 0 {
|
||||
input.Model = sku.LLMModelName
|
||||
mdlInfos, err := llm.getProbedInstantModelsExt(ctx, userCred)
|
||||
if err != nil {
|
||||
return input, errors.Wrap(err, "get probed models from LLM instance")
|
||||
}
|
||||
if len(mdlInfos) == 0 {
|
||||
return input, httperrors.NewBadRequestError("no available models found in LLM instance %s", input.LLMId)
|
||||
}
|
||||
var firstModel api.LLMInternalInstantMdlInfo
|
||||
for _, mdlInfo := range mdlInfos {
|
||||
firstModel = mdlInfo
|
||||
break
|
||||
}
|
||||
input.Model = fmt.Sprintf("%s:%s", firstModel.Name, firstModel.Tag)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,11 +210,22 @@ func (man *SMCPAgentManager) ValidateUpdateData(ctx context.Context, userCred mc
|
||||
}
|
||||
input.LLMUrl = &llmUrl
|
||||
|
||||
sku, err := llm.GetLLMSku("")
|
||||
if err != nil {
|
||||
return input, errors.Wrapf(err, "get LLM Sku from LLM %s", *input.LLMId)
|
||||
if input.Model == nil || len(*input.Model) == 0 {
|
||||
mdlInfos, err := llm.getProbedInstantModelsExt(ctx, userCred)
|
||||
if err != nil {
|
||||
return input, errors.Wrap(err, "get probed models from LLM instance")
|
||||
}
|
||||
if len(mdlInfos) == 0 {
|
||||
return input, httperrors.NewBadRequestError("no available models found in LLM instance %s", *input.LLMId)
|
||||
}
|
||||
var firstModel api.LLMInternalInstantMdlInfo
|
||||
for _, mdlInfo := range mdlInfos {
|
||||
firstModel = mdlInfo
|
||||
break
|
||||
}
|
||||
modelStr := fmt.Sprintf("%s:%s", firstModel.Name, firstModel.Tag)
|
||||
input.Model = &modelStr
|
||||
}
|
||||
input.Model = &sku.LLMModelName
|
||||
}
|
||||
|
||||
// 如果更新 llm_driver,验证其值
|
||||
@@ -294,9 +356,10 @@ func (mcp *SMCPAgent) GetDetailsToolRequest(
|
||||
// return jsonutils.Marshal(result), nil
|
||||
// }
|
||||
|
||||
func (mcp *SMCPAgent) GetDetailsChatStream(
|
||||
func (mcp *SMCPAgent) PerformChatStream(
|
||||
ctx context.Context,
|
||||
userCred mcclient.TokenCredential,
|
||||
query jsonutils.JSONObject,
|
||||
input api.LLMMCPAgentRequestInput,
|
||||
) (jsonutils.JSONObject, error) {
|
||||
appParams := appsrv.AppContextGetParams(ctx)
|
||||
@@ -361,11 +424,22 @@ func (mcp *SMCPAgent) process(ctx context.Context, userCred mcclient.TokenCreden
|
||||
systemPrompt := buildSystemPrompt()
|
||||
|
||||
// 初始化消息历史
|
||||
messages := []ILLMChatMessage{
|
||||
llmClient.NewSystemMessage(systemPrompt),
|
||||
llmClient.NewUserMessage(req.Message),
|
||||
messages := make([]ILLMChatMessage, 0)
|
||||
messages = append(messages, llmClient.NewSystemMessage(systemPrompt))
|
||||
|
||||
// 处理历史消息
|
||||
if len(req.History) > 0 {
|
||||
historyMessages := processHistoryMessages(
|
||||
req.History,
|
||||
llmClient,
|
||||
options.Options.MCPAgentUserCharLimit,
|
||||
options.Options.MCPAgentAssistantCharLimit,
|
||||
)
|
||||
messages = append(messages, historyMessages...)
|
||||
}
|
||||
|
||||
messages = append(messages, llmClient.NewUserMessage(req.Message))
|
||||
|
||||
// 记录工具调用
|
||||
var toolCallRecords []api.MCPAgentToolCallRecord
|
||||
|
||||
@@ -406,37 +480,14 @@ func (mcp *SMCPAgent) process(ctx context.Context, userCred mcclient.TokenCreden
|
||||
toolCalls := resp.GetToolCalls()
|
||||
log.Infof("Got %d tool calls from Phase 1", len(toolCalls))
|
||||
|
||||
// 将助手决定调用工具的消息加入历史
|
||||
messages = append(messages, llmClient.NewAssistantMessageWithToolCalls(toolCalls))
|
||||
|
||||
// 执行每个工具调用
|
||||
for _, tc := range toolCalls {
|
||||
fc := tc.GetFunction()
|
||||
toolName := fc.GetName()
|
||||
arguments := fc.GetArguments()
|
||||
|
||||
if arguments == nil {
|
||||
arguments = make(map[string]interface{})
|
||||
}
|
||||
|
||||
log.Infof("Calling tool: %s with arguments: %v", toolName, arguments)
|
||||
|
||||
// 调用 MCP 工具
|
||||
result, err := mcpClient.CallTool(ctx, toolName, arguments)
|
||||
resultText := utils.FormatToolResult(toolName, result, err)
|
||||
log.Infoln("Get result from mcp query", resultText)
|
||||
|
||||
// 记录
|
||||
toolCallRecords = append(toolCallRecords, api.MCPAgentToolCallRecord{
|
||||
ToolName: toolName,
|
||||
Arguments: arguments,
|
||||
Result: resultText,
|
||||
})
|
||||
|
||||
// 将工具执行结果加入历史
|
||||
messages = append(messages, llmClient.NewToolMessage(tc.GetId(), toolName, resultText))
|
||||
toolCallRecords, toolMessages, err := processToolCalls(ctx, toolCalls, mcpClient, llmClient)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "process tool calls")
|
||||
}
|
||||
|
||||
// 将工具调用相关的消息加入历史
|
||||
messages = append(messages, toolMessages...)
|
||||
|
||||
log.Infof("Phase 2: Streaming Response...")
|
||||
|
||||
var finalAnswer strings.Builder
|
||||
@@ -472,3 +523,90 @@ func (mcp *SMCPAgent) process(ctx context.Context, userCred mcclient.TokenCreden
|
||||
func buildSystemPrompt() string {
|
||||
return api.MCP_AGENT_SYSTEM_PROMPT
|
||||
}
|
||||
|
||||
func processHistoryMessages(
|
||||
history []api.MCPAgentChatMessage,
|
||||
llmClient ILLMClient,
|
||||
maxUserChars int,
|
||||
maxAssistantChars int,
|
||||
) []ILLMChatMessage {
|
||||
if len(history) == 0 {
|
||||
return []ILLMChatMessage{}
|
||||
}
|
||||
|
||||
var userChars, assistantChars int
|
||||
processedMessages := make([]ILLMChatMessage, 0)
|
||||
|
||||
// 从最新的消息开始遍历,保留最新消息,丢弃最旧消息
|
||||
for i := len(history) - 1; i >= 0; i-- {
|
||||
msg := history[i]
|
||||
msgChars := len(msg.Content)
|
||||
|
||||
switch msg.Role {
|
||||
case "user":
|
||||
if userChars+msgChars > maxUserChars {
|
||||
break
|
||||
}
|
||||
userChars += msgChars
|
||||
processedMessages = append(processedMessages, llmClient.NewUserMessage(msg.Content))
|
||||
case "assistant":
|
||||
if assistantChars+msgChars > maxAssistantChars {
|
||||
break
|
||||
}
|
||||
assistantChars += msgChars
|
||||
|
||||
if len(msg.Content) > 0 {
|
||||
processedMessages = append(processedMessages, llmClient.NewAssistantMessage(msg.Content))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for i, j := 0, len(processedMessages)-1; i < j; i, j = i+1, j-1 {
|
||||
processedMessages[i], processedMessages[j] = processedMessages[j], processedMessages[i]
|
||||
}
|
||||
|
||||
return processedMessages
|
||||
}
|
||||
|
||||
// processToolCalls 处理工具调用
|
||||
func processToolCalls(
|
||||
ctx context.Context,
|
||||
toolCalls []ILLMToolCall,
|
||||
mcpClient *utils.MCPClient,
|
||||
llmClient ILLMClient,
|
||||
) ([]api.MCPAgentToolCallRecord, []ILLMChatMessage, error) {
|
||||
toolCallRecords := make([]api.MCPAgentToolCallRecord, 0)
|
||||
messagesToAdd := make([]ILLMChatMessage, 0)
|
||||
|
||||
messagesToAdd = append(messagesToAdd, llmClient.NewAssistantMessageWithToolCalls(toolCalls))
|
||||
|
||||
// 执行每个工具调用
|
||||
for _, tc := range toolCalls {
|
||||
fc := tc.GetFunction()
|
||||
toolName := fc.GetName()
|
||||
arguments := fc.GetArguments()
|
||||
|
||||
if arguments == nil {
|
||||
arguments = make(map[string]interface{})
|
||||
}
|
||||
|
||||
log.Infof("Calling tool: %s with arguments: %v", toolName, arguments)
|
||||
|
||||
// 调用 MCP 工具
|
||||
result, err := mcpClient.CallTool(ctx, toolName, arguments)
|
||||
resultText := utils.FormatToolResult(toolName, result, err)
|
||||
log.Infoln("Get result from mcp query", resultText)
|
||||
|
||||
toolCallRecords = append(toolCallRecords, api.MCPAgentToolCallRecord{
|
||||
Id: tc.GetId(),
|
||||
ToolName: toolName,
|
||||
Arguments: arguments,
|
||||
Result: resultText,
|
||||
})
|
||||
|
||||
// 将工具执行结果加入历史
|
||||
messagesToAdd = append(messagesToAdd, llmClient.NewToolMessage(tc.GetId(), toolName, resultText))
|
||||
}
|
||||
|
||||
return toolCallRecords, messagesToAdd, nil
|
||||
}
|
||||
|
||||
@@ -30,6 +30,9 @@ type LLMOptions struct {
|
||||
// MCP Agent 配置
|
||||
MCPServerURL string `help:"MCP Server URL" default:"http://default-mcp-server:30876"`
|
||||
MCPAgentTimeout int `help:"MCP Agent request timeout in seconds" default:"120"`
|
||||
|
||||
MCPAgentUserCharLimit int `help:"MCP Agent user char limit" default:"3200"`
|
||||
MCPAgentAssistantCharLimit int `help:"MCP Agent assistant char limit" default:"6400"`
|
||||
}
|
||||
|
||||
var (
|
||||
|
||||
@@ -2,13 +2,18 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"yunion.io/x/jsonutils"
|
||||
"yunion.io/x/onecloud/pkg/appsrv"
|
||||
"yunion.io/x/onecloud/pkg/appsrv/dispatcher"
|
||||
"yunion.io/x/onecloud/pkg/cloudcommon/db"
|
||||
"yunion.io/x/onecloud/pkg/cloudcommon/db/taskman"
|
||||
"yunion.io/x/onecloud/pkg/cloudcommon/policy"
|
||||
"yunion.io/x/onecloud/pkg/httperrors"
|
||||
"yunion.io/x/onecloud/pkg/llm/models"
|
||||
"yunion.io/x/onecloud/pkg/mcclient/auth"
|
||||
)
|
||||
|
||||
func handleOllamaRegistryYAML(ctx context.Context, w http.ResponseWriter, r *http.Request) {
|
||||
@@ -17,6 +22,34 @@ func handleOllamaRegistryYAML(ctx context.Context, w http.ResponseWriter, r *htt
|
||||
appsrv.Send(w, yamlContent)
|
||||
}
|
||||
|
||||
func AddAvailableNetworkHandler(prefix string, app *appsrv.Application) {
|
||||
app.AddHandler2("GET", fmt.Sprintf("%s/available-network", prefix), auth.Authenticate(handleLLMAvailableNetwork), nil, "get_llm_available_network", nil)
|
||||
}
|
||||
|
||||
func handleLLMAvailableNetwork(ctx context.Context, w http.ResponseWriter, r *http.Request) {
|
||||
userCred := auth.FetchUserCredential(ctx, policy.FilterPolicyCredential)
|
||||
if userCred == nil {
|
||||
httperrors.UnauthorizedError(ctx, w, "Unauthorized")
|
||||
return
|
||||
}
|
||||
query, err := jsonutils.ParseQueryString(r.URL.RawQuery)
|
||||
if err != nil {
|
||||
httperrors.InvalidInputError(ctx, w, "Parse query string %q: %v", r.URL.RawQuery, err)
|
||||
return
|
||||
}
|
||||
ret, err := models.GetLLMManager().GetAvailableNetwork(ctx, userCred, query)
|
||||
if err != nil {
|
||||
httperrors.GeneralServerError(ctx, w, err)
|
||||
return
|
||||
}
|
||||
|
||||
wrapped := jsonutils.NewDict()
|
||||
if ret != nil {
|
||||
wrapped.Add(ret, "llm")
|
||||
}
|
||||
appsrv.SendJSON(w, wrapped)
|
||||
}
|
||||
|
||||
func InitHandlers(app *appsrv.Application, isSlave bool) {
|
||||
db.InitAllManagers()
|
||||
db.RegistUserCredCacheUpdater()
|
||||
@@ -25,6 +58,8 @@ func InitHandlers(app *appsrv.Application, isSlave bool) {
|
||||
|
||||
app.AddHandler("GET", "/ollama-registry.yaml", handleOllamaRegistryYAML)
|
||||
|
||||
AddAvailableNetworkHandler(models.GetLLMManager().KeywordPlural(), app)
|
||||
|
||||
for _, manager := range []db.IModelManager{
|
||||
taskman.TaskManager,
|
||||
taskman.SubTaskManager,
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
package llm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"yunion.io/x/jsonutils"
|
||||
|
||||
"yunion.io/x/onecloud/pkg/mcclient"
|
||||
"yunion.io/x/onecloud/pkg/mcclient/modulebase"
|
||||
"yunion.io/x/onecloud/pkg/mcclient/modules"
|
||||
)
|
||||
@@ -21,3 +26,14 @@ func init() {
|
||||
type LLMManager struct {
|
||||
modulebase.ResourceManager
|
||||
}
|
||||
|
||||
func (this *LLMManager) GetAvailableNetwork(session *mcclient.ClientSession, params jsonutils.JSONObject) (jsonutils.JSONObject, error) {
|
||||
path := fmt.Sprintf("/%s/available-network", this.KeywordPlural)
|
||||
if params != nil {
|
||||
qs := params.QueryString()
|
||||
if len(qs) > 0 {
|
||||
path = fmt.Sprintf("%s?%s", path, qs)
|
||||
}
|
||||
}
|
||||
return modulebase.Get(this.ResourceManager, session, path, this.Keyword)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,10 @@ package llm
|
||||
|
||||
import (
|
||||
"yunion.io/x/jsonutils"
|
||||
"yunion.io/x/pkg/errors"
|
||||
|
||||
computeapi "yunion.io/x/onecloud/pkg/apis/compute"
|
||||
"yunion.io/x/onecloud/pkg/cloudcommon/cmdline"
|
||||
"yunion.io/x/onecloud/pkg/mcclient/options"
|
||||
)
|
||||
|
||||
@@ -38,7 +41,21 @@ type DifyCreateOptions struct {
|
||||
}
|
||||
|
||||
func (o *DifyCreateOptions) Params() (jsonutils.JSONObject, error) {
|
||||
return jsonutils.Marshal(o), nil
|
||||
params := jsonutils.Marshal(o)
|
||||
|
||||
if len(o.Net) > 0 {
|
||||
nets := make([]*computeapi.NetworkConfig, 0)
|
||||
for i, n := range o.Net {
|
||||
net, err := cmdline.ParseNetworkConfig(n, i)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "parse network config %s", n)
|
||||
}
|
||||
nets = append(nets, net)
|
||||
}
|
||||
params.(*jsonutils.JSONDict).Add(jsonutils.Marshal(nets), "nets")
|
||||
}
|
||||
|
||||
return params, nil
|
||||
}
|
||||
|
||||
func (o *DifyCreateOptions) GetCountParam() int {
|
||||
|
||||
@@ -2,8 +2,11 @@ package llm
|
||||
|
||||
import (
|
||||
"yunion.io/x/jsonutils"
|
||||
"yunion.io/x/pkg/errors"
|
||||
|
||||
computeapi "yunion.io/x/onecloud/pkg/apis/compute"
|
||||
api "yunion.io/x/onecloud/pkg/apis/llm"
|
||||
"yunion.io/x/onecloud/pkg/cloudcommon/cmdline"
|
||||
"yunion.io/x/onecloud/pkg/mcclient/options"
|
||||
)
|
||||
|
||||
@@ -55,8 +58,7 @@ type LLMBaseCreateOptions struct {
|
||||
ProjectId string
|
||||
PreferHost string
|
||||
|
||||
NETWORK_TYPE string `json:"network_type" choices:"guest|hostlocal"`
|
||||
NetworkId string `help:"id of network" json:"network_id"`
|
||||
Net []string `help:"Network descriptions"`
|
||||
|
||||
BandwidthMb int
|
||||
|
||||
@@ -70,7 +72,21 @@ type LLMCreateOptions struct {
|
||||
}
|
||||
|
||||
func (o *LLMCreateOptions) Params() (jsonutils.JSONObject, error) {
|
||||
return jsonutils.Marshal(o), nil
|
||||
params := jsonutils.Marshal(o)
|
||||
|
||||
if len(o.Net) > 0 {
|
||||
nets := make([]*computeapi.NetworkConfig, 0)
|
||||
for i, n := range o.Net {
|
||||
net, err := cmdline.ParseNetworkConfig(n, i)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "parse network config %s", n)
|
||||
}
|
||||
nets = append(nets, net)
|
||||
}
|
||||
params.(*jsonutils.JSONDict).Add(jsonutils.Marshal(nets), "nets")
|
||||
}
|
||||
|
||||
return params, nil
|
||||
}
|
||||
|
||||
func (o *LLMCreateOptions) GetCountParam() int {
|
||||
@@ -117,6 +133,19 @@ func (opts *LLMIdOptions) Params() (jsonutils.JSONObject, error) {
|
||||
return jsonutils.Marshal(opts), nil
|
||||
}
|
||||
|
||||
type LLMAvailableNetworkOptions struct {
|
||||
NetworkType string `help:"network server_type filter, e.g. guest|hostlocal"`
|
||||
VpcId string `help:"vpc id filter"`
|
||||
}
|
||||
|
||||
func (opts *LLMAvailableNetworkOptions) GetId() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (opts *LLMAvailableNetworkOptions) Params() (jsonutils.JSONObject, error) {
|
||||
return options.StructToParams(opts)
|
||||
}
|
||||
|
||||
type LLMSaveInstantModelOptions struct {
|
||||
LLMIdOptions
|
||||
|
||||
|
||||
@@ -27,9 +27,10 @@ func (o *LLMSkuShowOptions) Params() (jsonutils.JSONObject, error) {
|
||||
type LLMSkuCreateOptions struct {
|
||||
LLMSkuBaseCreateOptions
|
||||
|
||||
LLM_IMAGE_ID string `json:"llm_image_id"`
|
||||
LLM_TYPE string `json:"llm_type" choices:"ollama"`
|
||||
LLM_MODEL_NAME string `help:"specific model of large language model, for example: qwen3:32b" json:"llm_model_name"`
|
||||
MountedModels []string `help:"mounted models, <model_id> e.g. qwen2:0.5b-dup" json:"mounted_models"`
|
||||
|
||||
LLM_IMAGE_ID string `json:"llm_image_id"`
|
||||
LLM_TYPE string `json:"llm_type" choices:"ollama"`
|
||||
}
|
||||
|
||||
func (o *LLMSkuCreateOptions) Params() (jsonutils.JSONObject, error) {
|
||||
@@ -38,6 +39,7 @@ func (o *LLMSkuCreateOptions) Params() (jsonutils.JSONObject, error) {
|
||||
obj.Unmarshal(dict)
|
||||
|
||||
o.LLMSkuBaseCreateOptions.Params(dict)
|
||||
fetchMountedModels(o.MountedModels, dict)
|
||||
return dict, nil
|
||||
}
|
||||
|
||||
@@ -56,10 +58,9 @@ func (o *LLMSkuDeleteOptions) Params() (jsonutils.JSONObject, error) {
|
||||
type LLMSkuUpdateOptions struct {
|
||||
LLMSkuBaseUpdateOptions
|
||||
|
||||
MountedModels []string `help:"mounted models, <model_id>@<model_name>:<model_tag> e.g. 6f48b936a09f@qwen2:0.5b" json:"mounted_models"`
|
||||
MountedModels []string `help:"mounted models, <model_id> e.g. qwen2:0.5b-dup" json:"mounted_models"`
|
||||
|
||||
LlmImageId string
|
||||
LlmModelName string
|
||||
LlmImageId string `json:"llm_image_id"`
|
||||
}
|
||||
|
||||
func (o *LLMSkuUpdateOptions) GetId() string {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package llm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"yunion.io/x/jsonutils"
|
||||
@@ -143,12 +144,28 @@ func (opts *MCPAgentToolRequestOptions) Params() (jsonutils.JSONObject, error) {
|
||||
type MCPAgentMCPAgentRequestOptions struct {
|
||||
MCPAgentIdOptions
|
||||
|
||||
Message string `help:"message to send to MCP agent" json:"message"`
|
||||
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 *MCPAgentMCPAgentRequestOptions) Params() (jsonutils.JSONObject, error) {
|
||||
input := api.LLMMCPAgentRequestInput{
|
||||
Message: opts.Message,
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user