From 6be6db21941e76fe2f0255b7cef88d98dcb0233c Mon Sep 17 00:00:00 2001 From: PeterZhong Date: Mon, 19 May 2025 12:10:18 +0800 Subject: [PATCH 1/5] =?UTF-8?q?=E6=8E=A5=E5=85=A5MCP=20ListTool=E8=83=BD?= =?UTF-8?q?=E5=8A=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/tool/ArcGisPro.cs | 6 ++++++ host/McpServerList.cs | 6 ++++++ host/mcp/InnerMcpServer.cs | 6 ++++++ host/mcp/McpToolDefinition.cs | 6 ++++++ 4 files changed, 24 insertions(+) create mode 100644 client/tool/ArcGisPro.cs create mode 100644 host/McpServerList.cs create mode 100644 host/mcp/InnerMcpServer.cs create mode 100644 host/mcp/McpToolDefinition.cs diff --git a/client/tool/ArcGisPro.cs b/client/tool/ArcGisPro.cs new file mode 100644 index 0000000..64e38d9 --- /dev/null +++ b/client/tool/ArcGisPro.cs @@ -0,0 +1,6 @@ +namespace LinkToolAddin.client.tool; + +public class ArcGisPro +{ + +} \ No newline at end of file diff --git a/host/McpServerList.cs b/host/McpServerList.cs new file mode 100644 index 0000000..48f66c4 --- /dev/null +++ b/host/McpServerList.cs @@ -0,0 +1,6 @@ +namespace LinkToolAddin.host; + +public class McpServerList +{ + +} \ No newline at end of file diff --git a/host/mcp/InnerMcpServer.cs b/host/mcp/InnerMcpServer.cs new file mode 100644 index 0000000..ccf5884 --- /dev/null +++ b/host/mcp/InnerMcpServer.cs @@ -0,0 +1,6 @@ +namespace LinkToolAddin.host.mcp; + +public class InnerMcpServer +{ + +} \ No newline at end of file diff --git a/host/mcp/McpToolDefinition.cs b/host/mcp/McpToolDefinition.cs new file mode 100644 index 0000000..3bd9adc --- /dev/null +++ b/host/mcp/McpToolDefinition.cs @@ -0,0 +1,6 @@ +namespace LinkToolAddin.host.mcp; + +public class McpDefinition +{ + +} \ No newline at end of file From b3e2664acd2f4a55248f14ff6e419748cfcb2fea Mon Sep 17 00:00:00 2001 From: PeterZhong Date: Mon, 19 May 2025 12:14:55 +0800 Subject: [PATCH 2/5] =?UTF-8?q?=E6=8E=A5=E5=85=A5MCP=20ListTool=E8=83=BD?= =?UTF-8?q?=E5=8A=9B=E8=A1=A5=E5=85=85=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- LinkToolAddin.csproj | 1 + Properties/launchSettings.json | 2 +- client/tool/ArcGisPro.cs | 19 +++- host/CallMcp.cs | 4 +- host/Gateway.cs | 183 ++++++++++++++++++++++++++++++- host/McpServerList.cs | 50 ++++++++- host/mcp/InnerMcpServer.cs | 26 ++++- host/mcp/McpToolDefinition.cs | 29 ++++- host/prompt/SystemPrompt.cs | 8 ++ server/CallArcGISPro.cs | 25 ++++- server/JsonRpcErrorEntity.cs | 4 +- ui/dockpane/TestDockpane.xaml.cs | 2 +- 12 files changed, 327 insertions(+), 26 deletions(-) diff --git a/LinkToolAddin.csproj b/LinkToolAddin.csproj index e0d0d0e..c53da32 100644 --- a/LinkToolAddin.csproj +++ b/LinkToolAddin.csproj @@ -106,6 +106,7 @@ + diff --git a/Properties/launchSettings.json b/Properties/launchSettings.json index 5a2c881..e98eaad 100644 --- a/Properties/launchSettings.json +++ b/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "LinkToolAddin": { "commandName": "Executable", - "executablePath": "C:\\Program Files\\ArcGIS\\Pro\\bin\\ArcGISPro.exe", + "executablePath": "C:\\Users\\PeterZhong\\AppData\\Local\\Programs\\ArcGIS\\Pro\\bin\\ArcGISPro.exe", "applicationUrl": "https://localhost:5001", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" diff --git a/client/tool/ArcGisPro.cs b/client/tool/ArcGisPro.cs index 64e38d9..c94f2e3 100644 --- a/client/tool/ArcGisPro.cs +++ b/client/tool/ArcGisPro.cs @@ -1,6 +1,21 @@ -namespace LinkToolAddin.client.tool; +using System.Collections.Generic; +using System.ComponentModel; +using System.Threading.Tasks; +using LinkToolAddin.server; +using ModelContextProtocol.Server; +using Newtonsoft.Json; + +namespace LinkToolAddin.client.tool; public class ArcGisPro { - + [McpServerTool, Description("ArcGIS Pro Tool")] + public static async Task ArcGisProTool(string toolName, List toolParams) + { + // Call the ArcGIS Pro method and get the result + var result = await server.CallArcGISPro.CallArcGISProTool(toolName, toolParams); + + // Serialize the result back to a JSON string + return result; + } } \ No newline at end of file diff --git a/host/CallMcp.cs b/host/CallMcp.cs index a98488d..0e48789 100644 --- a/host/CallMcp.cs +++ b/host/CallMcp.cs @@ -18,8 +18,8 @@ namespace LinkToolAddin.host log.Info("通过反射调用内部MCP工具"); var jsonRpcEntity = JsonConvert.DeserializeObject(jsonRpcString); - Type type = Type.GetType("LinkToolAddin.client."+jsonRpcEntity.Method.Split('.')[0]); - MethodInfo method = type.GetMethod(jsonRpcEntity.Method.Split('.')[1],BindingFlags.Public | BindingFlags.Static); + Type type = Type.GetType("LinkToolAddin.client.tool"+jsonRpcEntity.Method.Split(':')[0]); + MethodInfo method = type.GetMethod(jsonRpcEntity.Method.Split(':')[1],BindingFlags.Public | BindingFlags.Static); var task = method.Invoke(null, new object[] { jsonRpcEntity.Params }) as Task; JsonRpcResultEntity result = await task; return JsonConvert.SerializeObject(result); diff --git a/host/Gateway.cs b/host/Gateway.cs index 2b209b1..847fcd3 100644 --- a/host/Gateway.cs +++ b/host/Gateway.cs @@ -1,7 +1,13 @@ using System; using System.Collections.Generic; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Reflection; +using System.Text; using System.Text.RegularExpressions; using System.Threading; +using System.Threading.Tasks; using System.Xml.Linq; using LinkToolAddin.client; using LinkToolAddin.client.prompt; @@ -10,13 +16,22 @@ using LinkToolAddin.host.llm.entity; using LinkToolAddin.host.mcp; using LinkToolAddin.host.prompt; using LinkToolAddin.message; +using LinkToolAddin.server; +using LinkToolAddin.ui.dockpane; +using log4net; +using Microsoft.Extensions.AI; +using ModelContextProtocol.Client; using ModelContextProtocol.Protocol.Types; using Newtonsoft.Json; +using Newtonsoft.Json.Schema; +using Newtonsoft.Json.Schema.Generation; +using Tool = LinkToolAddin.host.mcp.Tool; namespace LinkToolAddin.host; public class Gateway { + private static ILog log = LogManager.GetLogger(typeof(Gateway)); public static async void SendMessage(string message, string model, string gdbPath, Action callback) { Llm bailian = new Bailian @@ -24,10 +39,12 @@ public class Gateway api_key = "sk-db177155677e438f832860e7f4da6afc" }; List messages = new List(); + string toolInfos = await GetToolInfos(new McpServerList()); + log.Info(SystemPrompt.SysPrompt(gdbPath, toolInfos)); messages.Add(new Message { Role = "system", - Content = SystemPrompt.SysPromptTemplate + Content = SystemPrompt.SysPrompt(gdbPath, toolInfos) }); messages.Add(new Message { @@ -37,7 +54,7 @@ public class Gateway bool goOn = true; string pattern = "[\\s\\S]*?<\\/tool_use>"; string promptPattern = "[\\s\\S]*?<\\/prompt>"; - Dictionary servers = new Dictionary(); + McpServerList mcpServerList = new McpServerList(); while (goOn) { string reponse = await bailian.SendChatAsync(new LlmJsonContent() @@ -62,7 +79,7 @@ public class Gateway Dictionary toolParams = JsonConvert.DeserializeObject>(toolArgs); string serverName = fullToolName.Contains(":") ? fullToolName.Split(':')[0] : fullToolName; string toolName = fullToolName.Contains(":") ? fullToolName.Split(':')[1] : fullToolName; - McpServer mcpServer = servers[serverName]; + McpServer mcpServer = mcpServerList.GetServer(serverName); if (mcpServer is SseMcpServer) { SseMcpServer sseMcpServer = mcpServer as SseMcpServer; @@ -111,6 +128,55 @@ public class Gateway Content = JsonConvert.SerializeObject(toolResponse) }); callback?.Invoke(toolMessageItem); + }else if (mcpServer is InnerMcpServer) + { + Type type = Type.GetType("LinkToolAddin.client.tool."+serverName); + MethodInfo method = type.GetMethod(toolName,BindingFlags.Public | BindingFlags.Static); + var task = method.Invoke(null, toolParams.Values.ToArray()) as Task; + JsonRpcResultEntity innerResult = await task; + if (innerResult is JsonRpcErrorEntity) + { + MessageListItem toolMessageItem = new ToolMessageItem + { + toolName = toolName, + toolParams = toolParams, + type = MessageType.TOOL_MESSAGE, + status = "fail", + content = JsonConvert.SerializeObject(innerResult) + }; + messages.Add(new Message + { + Role = "user", + Content = SystemPrompt.ErrorPromptTemplate + }); + messages.Add(new Message + { + Role = "user", + Content = JsonConvert.SerializeObject(innerResult) + }); + callback?.Invoke(toolMessageItem); + }else if (innerResult is JsonRpcSuccessEntity) + { + MessageListItem toolMessageItem = new ToolMessageItem + { + toolName = toolName, + toolParams = toolParams, + type = MessageType.TOOL_MESSAGE, + status = "success", + content = JsonConvert.SerializeObject(innerResult) + }; + messages.Add(new Message + { + Role = "user", + Content = SystemPrompt.ContinuePromptTemplate + }); + messages.Add(new Message + { + Role = "user", + Content = JsonConvert.SerializeObject(innerResult) + }); + callback?.Invoke(toolMessageItem); + } } } else if (Regex.IsMatch(reponse, promptPattern)) @@ -145,6 +211,115 @@ public class Gateway } } + private static async Task GetToolInfos(McpServerList mcpServerList) + { + StringBuilder toolInfos = new StringBuilder(); + foreach (McpServer mcpServer in mcpServerList.GetAllServers()) + { + if (mcpServer is InnerMcpServer) + { + string serverName = mcpServer.Name; + if (serverName is null) + { + continue; + } + Type type = Type.GetType("LinkToolAddin.client.tool." + serverName); + Type type2 = Type.GetType("LinkToolAddin.client.tool.ArcGisPro"); + MethodInfo[] methods = type.GetMethods(); + foreach (MethodInfo method in methods) + { + if (method.IsPublic && method.IsStatic) + { + string methodName = method.Name; + string methodDescription = method.GetCustomAttribute()?.Description; + string methodParamSchema = GenerateMethodParamSchema(method); + McpToolDefinition toolDefinition = new McpToolDefinition + { + Tool = new Tool + { + Name = methodName, + Description = methodDescription, + Arguments = methodParamSchema + } + }; + toolInfos.AppendLine(JsonConvert.DeserializeXmlNode(JsonConvert.SerializeObject(toolDefinition)).ToString()); + } + } + } + else if(mcpServer is SseMcpServer) + { + SseMcpClient client = new SseMcpClient((mcpServer as SseMcpServer).BaseUrl); + IList tools = await client.GetToolListAsync(); + foreach (McpClientTool tool in tools) + { + string toolName = tool.Name; + string toolDescription = tool.Description; + string toolParamSchema = tool.JsonSchema.ToString(); + McpToolDefinition toolDefinition = new McpToolDefinition + { + Tool = new Tool + { + Name = toolName, + Description = toolDescription, + Arguments = toolParamSchema + } + }; + toolInfos.AppendLine(JsonConvert.DeserializeXNode(JsonConvert.SerializeObject(toolDefinition)).ToString()); + } + }else if (mcpServer is StdioMcpServer) + { + StdioMcpClient client = new StdioMcpClient((mcpServer as StdioMcpServer).Command, (mcpServer as StdioMcpServer).Args); + IList tools = await client.GetToolListAsync(); + foreach (McpClientTool tool in tools) + { + string toolName = tool.Name; + string toolDescription = tool.Description; + string toolParamSchema = tool.JsonSchema.ToString(); + McpToolDefinition toolDefinition = new McpToolDefinition + { + Tool = new Tool + { + Name = toolName, + Description = toolDescription, + Arguments = toolParamSchema + } + }; + toolInfos.AppendLine(JsonConvert.DeserializeXNode(JsonConvert.SerializeObject(toolDefinition)).ToString()); + } + } + } + return toolInfos.ToString(); + } + + private static string GenerateMethodParamSchema(MethodInfo method) + { + var generator = new JSchemaGenerator + { + // 启用属性注解处理 + DefaultRequired = Required.DisallowNull, + SchemaReferenceHandling = SchemaReferenceHandling.None + }; + + var paramSchema = new JSchema { Type = JSchemaType.Object }; + + foreach (ParameterInfo param in method.GetParameters()) + { + // 生成参数类型的基础Schema + JSchema typeSchema = generator.Generate(param.ParameterType); + + // 添加Description描述 + var descriptionAttr = param.GetCustomAttribute(); + if (descriptionAttr != null) + { + typeSchema.Description = descriptionAttr.Description; // 网页6的Description特性处理 + } + + paramSchema.Properties.Add(param.Name, typeSchema); + } + + return paramSchema.ToString(); + } + public static async void TestChatMessage(string message, string model, string gdbPath, Action callback) { @@ -178,7 +353,7 @@ public class Gateway callback?.Invoke(toolListItem); } - public static async void TestWOrkflow(string message, string model, string gdbPath, Action callback) + public static async void TestWorkflow(string message, string model, string gdbPath, Action callback) { Thread.Sleep(2000); MessageListItem chatListItem = new ChatMessageItem diff --git a/host/McpServerList.cs b/host/McpServerList.cs index 48f66c4..52dffd7 100644 --- a/host/McpServerList.cs +++ b/host/McpServerList.cs @@ -1,6 +1,54 @@ -namespace LinkToolAddin.host; +using System.Collections.Generic; +using LinkToolAddin.host.mcp; + +namespace LinkToolAddin.host; public class McpServerList { + private Dictionary servers = new Dictionary(); + + public McpServerList() + { + servers.Add("gaode",new SseMcpServer + { + Name = "gaode", + Type = "sse", + Description = "高德地图API", + IsActive = true, + BaseUrl = "https://mcp.amap.com/sse?key=ed418512c94ade8f83d42c37b77d2bb2", + Headers = new Dictionary() + { + {"Content-Type","application/json"} + } + }); + servers.Add("arcgis", new InnerMcpServer + { + Name = "ArcGisPro", + Type = "inner", + Description = "可以调用arcgis的地理处理工具或执行python代码等", + IsActive = true + }); + } + public McpServer GetServer(string name) + { + if (servers.ContainsKey(name)) + { + return servers[name]; + } + else + { + return null; + } + } + + public List GetAllServers() + { + List serverList = new List(); + foreach (var server in servers) + { + serverList.Add(server.Value); + } + return serverList; + } } \ No newline at end of file diff --git a/host/mcp/InnerMcpServer.cs b/host/mcp/InnerMcpServer.cs index ccf5884..2b06319 100644 --- a/host/mcp/InnerMcpServer.cs +++ b/host/mcp/InnerMcpServer.cs @@ -1,6 +1,24 @@ -namespace LinkToolAddin.host.mcp; - -public class InnerMcpServer +namespace LinkToolAddin.host.mcp { - + using System; + using System.Collections.Generic; + + using System.Globalization; + using Newtonsoft.Json; + using Newtonsoft.Json.Converters; + + public partial class InnerMcpServer : McpServer + { + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("type")] + public string Type { get; set; } + + [JsonProperty("description")] + public string Description { get; set; } + + [JsonProperty("isActive")] + public bool IsActive { get; set; } + } } \ No newline at end of file diff --git a/host/mcp/McpToolDefinition.cs b/host/mcp/McpToolDefinition.cs index 3bd9adc..6e79610 100644 --- a/host/mcp/McpToolDefinition.cs +++ b/host/mcp/McpToolDefinition.cs @@ -1,6 +1,27 @@ -namespace LinkToolAddin.host.mcp; - -public class McpDefinition +namespace LinkToolAddin.host.mcp { - + using System; + using System.Collections.Generic; + + using System.Globalization; + using Newtonsoft.Json; + using Newtonsoft.Json.Converters; + + public partial class McpToolDefinition + { + [JsonProperty("tool")] + public Tool Tool { get; set; } + } + + public partial class Tool + { + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("description")] + public string Description { get; set; } + + [JsonProperty("arguments")] + public string Arguments { get; set; } + } } \ No newline at end of file diff --git a/host/prompt/SystemPrompt.cs b/host/prompt/SystemPrompt.cs index bcd3768..186439b 100644 --- a/host/prompt/SystemPrompt.cs +++ b/host/prompt/SystemPrompt.cs @@ -8,4 +8,12 @@ public class SystemPrompt public static string ErrorPromptTemplate = "执行上一个工具的时候出现以下错误,请根据报错信息重试"; + public static string SysPrompt(string gdbPath, string toolInfos) + { + string sysPrompt = SysPromptTemplate; + sysPrompt = sysPrompt.Replace("{{gdbPath}}", gdbPath); + sysPrompt = sysPrompt.Replace("{{toolInfos}}", toolInfos); + return sysPrompt; + } + } \ No newline at end of file diff --git a/server/CallArcGISPro.cs b/server/CallArcGISPro.cs index 39f5ef0..049fd3f 100644 --- a/server/CallArcGISPro.cs +++ b/server/CallArcGISPro.cs @@ -10,14 +10,29 @@ namespace LinkToolAddin.server; public class CallArcGISPro { private static readonly log4net.ILog log = log4net.LogManager.GetLogger(typeof(CallArcGISPro)); + public async static Task CallArcGISProTool(string toolName, List toolParams) { var results = await Geoprocessing.ExecuteToolAsync(toolName, toolParams); - log.Info($"CallArcGISProTool: {toolName} | {toolParams}"); - return new JsonRpcSuccessEntity() + JsonRpcResultEntity jsonRpcResultEntity; + if (results.ErrorCode == 0) { - Id = 1, - Result = results.ToString() - }; + jsonRpcResultEntity = new JsonRpcErrorEntity() + { + Error = new Error() + { + Code = results.ErrorCode, + Message = results.ErrorMessages.ToString() + } + }; + } + else + { + jsonRpcResultEntity = new JsonRpcSuccessEntity + { + Result = results.Messages.ToString() + }; + } + return jsonRpcResultEntity; } } \ No newline at end of file diff --git a/server/JsonRpcErrorEntity.cs b/server/JsonRpcErrorEntity.cs index 46381de..ad0d795 100644 --- a/server/JsonRpcErrorEntity.cs +++ b/server/JsonRpcErrorEntity.cs @@ -2,10 +2,10 @@ namespace LinkToolAddin.server { using Newtonsoft.Json; - public partial class JsonRpcErrorEntity + public partial class JsonRpcErrorEntity : JsonRpcResultEntity { [JsonProperty("jsonrpc")] - public string Jsonrpc { get; set; } + public string Jsonrpc { get; set; } = "2.0"; [JsonProperty("error")] public Error Error { get; set; } diff --git a/ui/dockpane/TestDockpane.xaml.cs b/ui/dockpane/TestDockpane.xaml.cs index 6f33bb6..802464a 100644 --- a/ui/dockpane/TestDockpane.xaml.cs +++ b/ui/dockpane/TestDockpane.xaml.cs @@ -146,7 +146,7 @@ namespace LinkToolAddin.ui.dockpane private void TestWorkflow_OnClick(object sender, RoutedEventArgs e) { - Gateway.SendMessage("你好","qwen-max","test.gdb",ShowMessage); + Gateway.SendMessage("你有什么工具","qwen-max","test.gdb",ShowMessage); } public void ShowMessage(MessageListItem msg) From d9b98df57fc07e36078228a365b35ff69b517d3f Mon Sep 17 00:00:00 2001 From: PeterZhong Date: Mon, 19 May 2025 21:32:20 +0800 Subject: [PATCH 3/5] =?UTF-8?q?=E5=AE=8C=E6=95=B4=E6=8E=A5=E5=85=A5MCP?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=E8=B0=83=E7=94=A8=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- LinkToolAddin.csproj | 1 + Properties/launchSettings.json | 2 +- host/Gateway.cs | 53 +++++++++++++++++++--------- host/prompt/SystemPrompt.cs | 2 +- ui/dockpane/TestDockpane.xaml | 19 +++++++--- ui/dockpane/TestDockpane.xaml.cs | 18 +++++++++- ui/dockpane/TestDockpaneViewModel.cs | 2 +- 7 files changed, 72 insertions(+), 25 deletions(-) diff --git a/LinkToolAddin.csproj b/LinkToolAddin.csproj index c53da32..a4143cc 100644 --- a/LinkToolAddin.csproj +++ b/LinkToolAddin.csproj @@ -98,6 +98,7 @@ + diff --git a/Properties/launchSettings.json b/Properties/launchSettings.json index e98eaad..5a2c881 100644 --- a/Properties/launchSettings.json +++ b/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "LinkToolAddin": { "commandName": "Executable", - "executablePath": "C:\\Users\\PeterZhong\\AppData\\Local\\Programs\\ArcGIS\\Pro\\bin\\ArcGISPro.exe", + "executablePath": "C:\\Program Files\\ArcGIS\\Pro\\bin\\ArcGISPro.exe", "applicationUrl": "https://localhost:5001", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" diff --git a/host/Gateway.cs b/host/Gateway.cs index 847fcd3..2380d4f 100644 --- a/host/Gateway.cs +++ b/host/Gateway.cs @@ -8,7 +8,9 @@ using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; +using System.Xml; using System.Xml.Linq; +using ArcGIS.Desktop.Framework.Dialogs; using LinkToolAddin.client; using LinkToolAddin.client.prompt; using LinkToolAddin.host.llm; @@ -23,6 +25,7 @@ using Microsoft.Extensions.AI; using ModelContextProtocol.Client; using ModelContextProtocol.Protocol.Types; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using Newtonsoft.Json.Schema; using Newtonsoft.Json.Schema.Generation; using Tool = LinkToolAddin.host.mcp.Tool; @@ -52,8 +55,8 @@ public class Gateway Content = message }); bool goOn = true; - string pattern = "[\\s\\S]*?<\\/tool_use>"; - string promptPattern = "[\\s\\S]*?<\\/prompt>"; + string pattern = "^[\\s\\S]*?<\\/tool_use>$"; + string promptPattern = "^[\\s\\S]*?<\\/prompt>$"; McpServerList mcpServerList = new McpServerList(); while (goOn) { @@ -65,6 +68,7 @@ public class Gateway TopP = 1, MaxTokens = 1000, }); + log.Info(reponse); messages.Add(new Message { Role = "assistant", @@ -213,18 +217,20 @@ public class Gateway private static async Task GetToolInfos(McpServerList mcpServerList) { + int loop = 0; StringBuilder toolInfos = new StringBuilder(); foreach (McpServer mcpServer in mcpServerList.GetAllServers()) { + loop++; + if (loop > 3) + { + MessageBox.Show("达到最大循环次数", "退出循环"); + break; + } if (mcpServer is InnerMcpServer) { - string serverName = mcpServer.Name; - if (serverName is null) - { - continue; - } - Type type = Type.GetType("LinkToolAddin.client.tool." + serverName); - Type type2 = Type.GetType("LinkToolAddin.client.tool.ArcGisPro"); + InnerMcpServer innerMcpServer = (InnerMcpServer)mcpServer; + Type type = Type.GetType("LinkToolAddin.client.tool." + innerMcpServer.Name); MethodInfo[] methods = type.GetMethods(); foreach (MethodInfo method in methods) { @@ -237,12 +243,14 @@ public class Gateway { Tool = new Tool { - Name = methodName, + Name = innerMcpServer.Name + ":" + methodName, Description = methodDescription, Arguments = methodParamSchema } }; - toolInfos.AppendLine(JsonConvert.DeserializeXmlNode(JsonConvert.SerializeObject(toolDefinition)).ToString()); + XNode node = JsonConvert.DeserializeXNode(JsonConvert.SerializeObject(toolDefinition)); + toolInfos.AppendLine(node.ToString()); + toolInfos.AppendLine(); } } } @@ -252,7 +260,7 @@ public class Gateway IList tools = await client.GetToolListAsync(); foreach (McpClientTool tool in tools) { - string toolName = tool.Name; + string toolName = (mcpServer as SseMcpServer).Name + ":" + tool.Name; string toolDescription = tool.Description; string toolParamSchema = tool.JsonSchema.ToString(); McpToolDefinition toolDefinition = new McpToolDefinition @@ -265,6 +273,7 @@ public class Gateway } }; toolInfos.AppendLine(JsonConvert.DeserializeXNode(JsonConvert.SerializeObject(toolDefinition)).ToString()); + toolInfos.AppendLine(); } }else if (mcpServer is StdioMcpServer) { @@ -272,7 +281,7 @@ public class Gateway IList tools = await client.GetToolListAsync(); foreach (McpClientTool tool in tools) { - string toolName = tool.Name; + string toolName = (mcpServer as StdioMcpServer).Name + ":" + tool.Name;; string toolDescription = tool.Description; string toolParamSchema = tool.JsonSchema.ToString(); McpToolDefinition toolDefinition = new McpToolDefinition @@ -281,16 +290,25 @@ public class Gateway { Name = toolName, Description = toolDescription, - Arguments = toolParamSchema + Arguments = CompressJson(toolParamSchema) } }; toolInfos.AppendLine(JsonConvert.DeserializeXNode(JsonConvert.SerializeObject(toolDefinition)).ToString()); + toolInfos.AppendLine(); } } } return toolInfos.ToString(); } + public static string CompressJson(string json) + { + // 解析JSON并自动去除无关空白 + var token = JToken.Parse(json); + // 序列化为无格式紧凑字符串 + return token.ToString(Newtonsoft.Json.Formatting.None); + } + private static string GenerateMethodParamSchema(MethodInfo method) { var generator = new JSchemaGenerator @@ -316,8 +334,11 @@ public class Gateway paramSchema.Properties.Add(param.Name, typeSchema); } - - return paramSchema.ToString(); + var settings = new JsonSerializerSettings { + Formatting = Newtonsoft.Json.Formatting.None, // 关键设置:禁用缩进和换行 + NullValueHandling = NullValueHandling.Ignore // 可选:忽略空值 + }; + return JsonConvert.SerializeObject(paramSchema, settings);; } public static async void TestChatMessage(string message, string model, string gdbPath, diff --git a/host/prompt/SystemPrompt.cs b/host/prompt/SystemPrompt.cs index 186439b..48938d5 100644 --- a/host/prompt/SystemPrompt.cs +++ b/host/prompt/SystemPrompt.cs @@ -2,7 +2,7 @@ public class SystemPrompt { - public static string SysPromptTemplate = "现在你是一个精通ArcGIS Pro的专家,请以此身份回答用户的问题。"; + public static string SysPromptTemplate = "现在你是一个精通ArcGIS Pro的专家,请以此身份回答用户的问题。你有以下工具可以调用{{toolInfos}},用户的数据库路径是{{gdbPath}}。MCP工具调用的格式要求示例:\n search\n {\\\"query\\\": \\\"上海 人口\\\"}\n"; public static string ContinuePromptTemplate = "上一个工具执行的结果如下,请据此继续执行"; diff --git a/ui/dockpane/TestDockpane.xaml b/ui/dockpane/TestDockpane.xaml index 4782989..e550084 100644 --- a/ui/dockpane/TestDockpane.xaml +++ b/ui/dockpane/TestDockpane.xaml @@ -18,11 +18,20 @@ - - + + + - - - + + + + + + + + + + + \ No newline at end of file diff --git a/ui/dockpane/TestDockpane.xaml.cs b/ui/dockpane/TestDockpane.xaml.cs index 802464a..d67e5c9 100644 --- a/ui/dockpane/TestDockpane.xaml.cs +++ b/ui/dockpane/TestDockpane.xaml.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using System.Windows; using System.Windows.Controls; using LinkToolAddin.client; @@ -146,12 +147,27 @@ namespace LinkToolAddin.ui.dockpane private void TestWorkflow_OnClick(object sender, RoutedEventArgs e) { - Gateway.SendMessage("你有什么工具","qwen-max","test.gdb",ShowMessage); + // Gateway.SendMessage("你有什么工具可以调用的?","qwen-max","test.gdb",ShowMessage); + Gateway.TestWorkflow("dafdfdgdagdgui","","",AddReply); } public void ShowMessage(MessageListItem msg) { log.Info(msg.content); } + + private void PromptTestButton_OnClick(object sender, RoutedEventArgs e) + { + string userPrompt = PromptTestTextBox.Text; + Gateway.SendMessage(userPrompt,"qwen-max","C:/Project/test.gdb",AddReply); + } + + public void AddReply(MessageListItem msg) + { + string content = msg.content; + log.Info(content); + string originContent = ReplyTextBox.Text; + ReplyTextBox.Text = originContent + content; + } } } diff --git a/ui/dockpane/TestDockpaneViewModel.cs b/ui/dockpane/TestDockpaneViewModel.cs index 0661d9c..daf9d4c 100644 --- a/ui/dockpane/TestDockpaneViewModel.cs +++ b/ui/dockpane/TestDockpaneViewModel.cs @@ -41,7 +41,7 @@ namespace LinkToolAddin.ui.dockpane /// /// Text shown near the top of the DockPane. /// - private string _heading = "My DockPane"; + private string _heading = "Test Dockpane"; public string Heading { get => _heading; From b6397e5db3a609659edf31b69cf81761f7caffd9 Mon Sep 17 00:00:00 2001 From: PeterZhong Date: Fri, 23 May 2025 19:24:58 +0800 Subject: [PATCH 4/5] =?UTF-8?q?=E5=8A=A8=E6=80=81=E8=B0=83=E7=94=A8Prompt?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/prompt/DynamicPrompt.cs | 13 +- client/prompt/PromptTemplates.cs | 5 + client/tool/ArcGisPro.cs | 60 +++++- common/HttpRequest.cs | 105 ++++++++++ host/Gateway.cs | 258 +++++++++++++++++++++++- host/McpServerList.cs | 2 +- host/llm/Bailian.cs | 14 +- host/llm/Llm.cs | 2 +- host/llm/entity/stream/LlmStreamChat.cs | 54 +++++ host/mcp/McpPromptDefinition.cs | 27 +++ host/prompt/SystemPrompt.cs | 4 +- ui/dockpane/TestDockpane.xaml | 2 + ui/dockpane/TestDockpane.xaml.cs | 64 +++++- 13 files changed, 595 insertions(+), 15 deletions(-) create mode 100644 host/llm/entity/stream/LlmStreamChat.cs create mode 100644 host/mcp/McpPromptDefinition.cs diff --git a/client/prompt/DynamicPrompt.cs b/client/prompt/DynamicPrompt.cs index 2a342e7..52d695c 100644 --- a/client/prompt/DynamicPrompt.cs +++ b/client/prompt/DynamicPrompt.cs @@ -4,10 +4,14 @@ namespace LinkToolAddin.client.prompt; public class DynamicPrompt { - public static string GetPrompt(string name,Dictionary args) + public static string GetPrompt(string name,Dictionary args = null) { PromptTemplates promptTemplate = new PromptTemplates(); string template = promptTemplate.GetPrompt(name); + if (args == null) + { + return template; + } foreach (KeyValuePair pair in args) { string replaceKey = "{{"+pair.Key+"}}"; @@ -15,4 +19,11 @@ public class DynamicPrompt } return template; } + + public static Dictionary GetAllPrompts() + { + PromptTemplates promptTemplate = new PromptTemplates(); + Dictionary template = promptTemplate.GetPromptsDict(); + return template; + } } \ No newline at end of file diff --git a/client/prompt/PromptTemplates.cs b/client/prompt/PromptTemplates.cs index 3992482..9ef4ed3 100644 --- a/client/prompt/PromptTemplates.cs +++ b/client/prompt/PromptTemplates.cs @@ -17,4 +17,9 @@ public class PromptTemplates { return prompts[name]; } + + public Dictionary GetPromptsDict() + { + return prompts; + } } \ No newline at end of file diff --git a/client/tool/ArcGisPro.cs b/client/tool/ArcGisPro.cs index c94f2e3..2808200 100644 --- a/client/tool/ArcGisPro.cs +++ b/client/tool/ArcGisPro.cs @@ -1,6 +1,11 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.ComponentModel; using System.Threading.Tasks; +using ArcGIS.Core.Data; +using ArcGIS.Core.Data.Raster; +using ArcGIS.Core.Geometry; +using ArcGIS.Desktop.Framework.Threading.Tasks; using LinkToolAddin.server; using ModelContextProtocol.Server; using Newtonsoft.Json; @@ -9,7 +14,7 @@ namespace LinkToolAddin.client.tool; public class ArcGisPro { - [McpServerTool, Description("ArcGIS Pro Tool")] + [McpServerTool, Description("可以通过调用ArcGIS Pro的地理处理工具实现一些数据处理功能。")] public static async Task ArcGisProTool(string toolName, List toolParams) { // Call the ArcGIS Pro method and get the result @@ -18,4 +23,55 @@ public class ArcGisPro // Serialize the result back to a JSON string return result; } + + [McpServerTool, Description("查看指定数据的属性,包括坐标系、范围、数据类型等")] + public static async Task DataProperty(string datasetPath,string dataName) + { + using Geodatabase gdb = new Geodatabase(new FileGeodatabaseConnectionPath(new Uri(datasetPath))); + FeatureClass featureClass = gdb.OpenDataset(dataName); + FeatureClassDefinition featureClassDefinition = featureClass.GetDefinition(); + JsonRpcResultEntity result = new JsonRpcSuccessEntity() + { + Id = 1, + Result = JsonConvert.SerializeObject(featureClassDefinition) + }; + return result; + } + + [McpServerTool, Description("列出gdb数据库中的所有数据名称")] + public static async Task ListData(string gdbPath) + { + var datasets = new List(); + await QueuedTask.Run(() => + { + using (Geodatabase gdb = new Geodatabase(new FileGeodatabaseConnectionPath(new Uri(gdbPath)))) + { + // 获取所有要素类(Feature Classes) + var featureClasses = gdb.GetDefinitions(); + foreach (var fc in featureClasses) + datasets.Add($"要素类: {fc.GetName()}"); + + // 获取所有表格(Tables) + var tables = gdb.GetDefinitions(); + foreach (var table in tables) + datasets.Add($"表格: {table.GetName()}"); + + // 获取所有要素数据集(Feature Datasets) + var featureDatasets = gdb.GetDefinitions(); + foreach (var fd in featureDatasets) + datasets.Add($"要素数据集: {fd.GetName()}"); + + // 获取所有栅格数据集(Raster Datasets) + var rasterDatasets = gdb.GetDefinitions(); + foreach (var raster in rasterDatasets) + datasets.Add($"栅格数据: {raster.GetName()}"); + } + }); + JsonRpcResultEntity result = new JsonRpcSuccessEntity() + { + Id = 1, + Result = JsonConvert.SerializeObject(datasets) + }; + return result; + } } \ No newline at end of file diff --git a/common/HttpRequest.cs b/common/HttpRequest.cs index a38807c..16c9383 100644 --- a/common/HttpRequest.cs +++ b/common/HttpRequest.cs @@ -1,8 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; using System.Net.Http; using System.Net.Http.Headers; using System.Text; using System.Threading.Tasks; using LinkToolAddin.host.llm.entity; +using LinkToolAddin.host.llm.entity.stream; using Newtonsoft.Json; namespace LinkToolAddin.common; @@ -18,4 +23,104 @@ public class HttpRequest var responseBody = await response.Content.ReadAsStringAsync(); return responseBody; } + + public static async IAsyncEnumerable SendStreamPostRequestAsync(string url, string jsonContent, string apiKey) + { + using var client = new HttpClient(); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey); + + // 发送 POST 请求并获取响应流 + var response = await client.PostAsync(url,new StringContent(jsonContent, Encoding.UTF8, "application/json")); + + // 验证响应状态 + response.EnsureSuccessStatusCode(); + + // 获取响应流 + using var stream = await response.Content.ReadAsStreamAsync(); + using var reader = new StreamReader(stream); + + // 流式读取 + string line; + while ((line = await reader.ReadLineAsync()) != null) + { + string content = ProcessPartialResponse(line); + yield return content; + } + } + + private static string ProcessPartialResponse(string rawData) + { + try + { + var lines = rawData.Split(new[] { "data: " }, StringSplitOptions.RemoveEmptyEntries); + foreach (var line in lines) + { + var trimmedLine = line.Trim(); + if (!string.IsNullOrEmpty(trimmedLine)) + { + var result = JsonConvert.DeserializeObject(trimmedLine); + return result.Choices[0].Delta.Content; + } + } + } + catch { /* 处理解析异常 */ } + return null; + } + + public static async IAsyncEnumerable PostWithStreamingResponseAsync( + string url, + string body, + string apiKey, + string contentType = "application/json", + Action configureHeaders = null) + { + using (var client = new HttpClient()) + { + // 设置超时时间为30分钟 + client.Timeout = TimeSpan.FromMinutes(30); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey); + using (var request = new HttpRequestMessage(HttpMethod.Post, url)) + { + // 设置请求头和Body + Console.WriteLine("开始请求..."); + configureHeaders?.Invoke(request); + request.Content = new StringContent(body, Encoding.UTF8, contentType); + + // 发送请求并立即开始读取响应流 + using (var response = await client.SendAsync( + request, + HttpCompletionOption.ResponseHeadersRead)) + { + response.EnsureSuccessStatusCode(); + + // 获取响应流 + using (var stream = await response.Content.ReadAsStreamAsync()) + using (var reader = new StreamReader(stream)) + { + string line; + + StringBuilder incompleteJsonBuffer = new StringBuilder(); + + // 流式读取并输出到控制台 + while ((line = await reader.ReadLineAsync()) != null) + { + foreach (var chunk in line.Split(new[] { "data: " }, StringSplitOptions.RemoveEmptyEntries)) + { + LlmStreamChat dataObj = null; + try + { + dataObj = JsonConvert.DeserializeObject(chunk); + }catch{/*process exception*/} + + if (dataObj is not null) + { + yield return dataObj.Choices[0].Delta.Content; + } + } + } + } + } + } + } + } } \ No newline at end of file diff --git a/host/Gateway.cs b/host/Gateway.cs index 2380d4f..15e128d 100644 --- a/host/Gateway.cs +++ b/host/Gateway.cs @@ -11,6 +11,7 @@ using System.Threading.Tasks; using System.Xml; using System.Xml.Linq; using ArcGIS.Desktop.Framework.Dialogs; +using ArcGIS.Desktop.Internal.Mapping.Locate; using LinkToolAddin.client; using LinkToolAddin.client.prompt; using LinkToolAddin.host.llm; @@ -68,6 +69,7 @@ public class Gateway TopP = 1, MaxTokens = 1000, }); + long timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); log.Info(reponse); messages.Add(new Message { @@ -95,7 +97,8 @@ public class Gateway toolParams = toolParams, type = MessageType.TOOL_MESSAGE, status = toolResponse.IsError ? "fail" : "success", - content = toolResponse.Content.ToString() + content = JsonConvert.SerializeObject(toolResponse), + id = timestamp.ToString() }; messages.Add(new Message { @@ -119,7 +122,8 @@ public class Gateway toolParams = toolParams, type = MessageType.TOOL_MESSAGE, status = toolResponse.IsError ? "fail" : "success", - content = toolResponse.Content.ToString() + content = JsonConvert.SerializeObject(toolResponse), + id = timestamp.ToString() }; messages.Add(new Message { @@ -204,7 +208,8 @@ public class Gateway { content = reponse, role = "assistant", - type = MessageType.CHAT_MESSAGE + type = MessageType.CHAT_MESSAGE, + id = timestamp.ToString() }; callback?.Invoke(chatMessageListItem); } @@ -214,7 +219,235 @@ public class Gateway } } } - + + public static async void SendMessageStream(string message, string model, string gdbPath, Action callback) + { + Llm bailian = new Bailian + { + api_key = "sk-db177155677e438f832860e7f4da6afc" + }; + List messages = new List(); + string toolInfos = await GetToolInfos(new McpServerList()); + log.Info(SystemPrompt.SysPrompt(gdbPath, toolInfos)); + messages.Add(new Message + { + Role = "system", + Content = SystemPrompt.SysPrompt(gdbPath, toolInfos) + }); + messages.Add(new Message + { + Role = "user", + Content = message + }); + bool goOn = true; + string toolPattern = "^[\\s\\S]*?<\\/tool_use>$"; + string promptPattern = "^[\\s\\S]*?<\\/prompt>$"; + McpServerList mcpServerList = new McpServerList(); + while (goOn) + { + LlmJsonContent jsonContent = new LlmJsonContent() + { + Model = model, + Messages = messages, + Temperature = 0.7, + TopP = 1, + MaxTokens = 1000, + }; + long timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + string messageContent = ""; + await foreach(var chunk in bailian.SendChatStreamAsync(jsonContent)) + { + if (chunk == "[DONE]") + { + goOn = false; + }else if (chunk.StartsWith("")) + { + if (Regex.IsMatch(chunk, toolPattern)) + { + //返回工具卡片 + XElement toolUse = XElement.Parse(chunk); + string fullToolName = toolUse.Element("name")?.Value; + string toolArgs = toolUse.Element("arguments")?.Value; + Dictionary toolParams = JsonConvert.DeserializeObject>(toolArgs); + string serverName = fullToolName.Contains(":") ? fullToolName.Split(':')[0] : fullToolName; + string toolName = fullToolName.Contains(":") ? fullToolName.Split(':')[1] : fullToolName; + McpServer mcpServer = mcpServerList.GetServer(serverName); + if (mcpServer is SseMcpServer) + { + SseMcpServer sseMcpServer = mcpServer as SseMcpServer; + SseMcpClient client = new SseMcpClient(sseMcpServer.BaseUrl); + CallToolResponse toolResponse = await client.CallToolAsync(toolName,toolParams); + MessageListItem toolMessageItem = new ToolMessageItem + { + toolName = toolName, + toolParams = toolParams, + type = MessageType.TOOL_MESSAGE, + status = toolResponse.IsError ? "fail" : "success", + content = JsonConvert.SerializeObject(toolResponse), + id = timestamp.ToString() + }; + messages.Add(new Message + { + Role = "user", + Content = toolResponse.IsError ? SystemPrompt.ErrorPromptTemplate : SystemPrompt.ContinuePromptTemplate + }); + messages.Add(new Message + { + Role = "user", + Content = JsonConvert.SerializeObject(toolResponse) + }); + callback?.Invoke(toolMessageItem); + }else if (mcpServer is StdioMcpServer) + { + StdioMcpServer stdioMcpServer = mcpServer as StdioMcpServer; + StdioMcpClient client = new StdioMcpClient(stdioMcpServer.Command, stdioMcpServer.Args); + CallToolResponse toolResponse = await client.CallToolAsync(toolName,toolParams); + MessageListItem toolMessageItem = new ToolMessageItem + { + toolName = toolName, + toolParams = toolParams, + type = MessageType.TOOL_MESSAGE, + status = toolResponse.IsError ? "fail" : "success", + content = JsonConvert.SerializeObject(toolResponse), + id = timestamp.ToString() + }; + messages.Add(new Message + { + Role = "user", + Content = toolResponse.IsError ? SystemPrompt.ErrorPromptTemplate : SystemPrompt.ContinuePromptTemplate + }); + messages.Add(new Message + { + Role = "user", + Content = JsonConvert.SerializeObject(toolResponse) + }); + callback?.Invoke(toolMessageItem); + }else if (mcpServer is InnerMcpServer) + { + Type type = Type.GetType("LinkToolAddin.client.tool."+serverName); + MethodInfo method = type.GetMethod(toolName,BindingFlags.Public | BindingFlags.Static); + var task = method.Invoke(null, toolParams.Values.ToArray()) as Task; + JsonRpcResultEntity innerResult = await task; + if (innerResult is JsonRpcErrorEntity) + { + MessageListItem toolMessageItem = new ToolMessageItem + { + toolName = toolName, + toolParams = toolParams, + type = MessageType.TOOL_MESSAGE, + status = "fail", + content = JsonConvert.SerializeObject(innerResult), + id = timestamp.ToString() + }; + messages.Add(new Message + { + Role = "user", + Content = SystemPrompt.ErrorPromptTemplate + }); + messages.Add(new Message + { + Role = "user", + Content = JsonConvert.SerializeObject(innerResult) + }); + callback?.Invoke(toolMessageItem); + }else if (innerResult is JsonRpcSuccessEntity) + { + MessageListItem toolMessageItem = new ToolMessageItem + { + toolName = toolName, + toolParams = toolParams, + type = MessageType.TOOL_MESSAGE, + status = "success", + content = JsonConvert.SerializeObject(innerResult), + id = timestamp.ToString() + }; + messages.Add(new Message + { + Role = "user", + Content = SystemPrompt.ContinuePromptTemplate + }); + messages.Add(new Message + { + Role = "user", + Content = JsonConvert.SerializeObject(innerResult) + }); + callback?.Invoke(toolMessageItem); + } + } + } + else + { + MessageListItem toolMessageItem = new ToolMessageItem + { + toolName = "", + toolParams = new Dictionary(), + type = MessageType.TOOL_MESSAGE, + status = "loading", + content = "正在生成工具调用参数", + id = timestamp.ToString() + }; + callback?.Invoke(toolMessageItem); + continue; + } + }else if (chunk.StartsWith("")) + { + if (Regex.IsMatch(chunk, promptPattern)) + { + XElement promptUse = XElement.Parse(chunk); + string promptKey = promptUse.Element("name")?.Value; + string promptContent = DynamicPrompt.GetPrompt(promptKey,null); + messages.Add(new Message + { + Role = "user", + Content = JsonConvert.SerializeObject(promptContent) + }); + MessageListItem toolMessageItem = new ToolMessageItem + { + toolName = "调用提示词", + toolParams = null, + type = MessageType.TOOL_MESSAGE, + status = "success", + content = promptKey, + id = timestamp.ToString() + }; + callback?.Invoke(toolMessageItem); + } + else + { + MessageListItem toolMessageItem = new ToolMessageItem + { + toolName = "调用提示词", + toolParams = null, + type = MessageType.TOOL_MESSAGE, + status = "loading", + content = "正在调用提示词", + id = timestamp.ToString() + }; + callback?.Invoke(toolMessageItem); + } + } + else + { + //普通流式消息卡片 + MessageListItem chatMessageListItem = new ChatMessageItem() + { + content = chunk, + role = "assistant", + type = MessageType.CHAT_MESSAGE, + id = timestamp.ToString() + }; + messageContent = chunk; + callback?.Invoke(chatMessageListItem); + } + } + messages.Add(new Message + { + Role = "assistant", + Content = messageContent + }); + } + } + private static async Task GetToolInfos(McpServerList mcpServerList) { int loop = 0; @@ -225,7 +458,7 @@ public class Gateway if (loop > 3) { MessageBox.Show("达到最大循环次数", "退出循环"); - break; + break; } if (mcpServer is InnerMcpServer) { @@ -298,6 +531,21 @@ public class Gateway } } } + + Dictionary prompts = DynamicPrompt.GetAllPrompts(); + foreach (KeyValuePair prompt in prompts) + { + McpPromptDefinition promptDefinition = new McpPromptDefinition + { + Prompt = new LinkToolAddin.host.mcp.Prompt + { + Name = prompt.Key + } + }; + XNode node = JsonConvert.DeserializeXNode(JsonConvert.SerializeObject(promptDefinition)); + toolInfos.AppendLine(node.ToString()); + toolInfos.AppendLine(); + } return toolInfos.ToString(); } diff --git a/host/McpServerList.cs b/host/McpServerList.cs index 52dffd7..2c03a41 100644 --- a/host/McpServerList.cs +++ b/host/McpServerList.cs @@ -21,7 +21,7 @@ public class McpServerList {"Content-Type","application/json"} } }); - servers.Add("arcgis", new InnerMcpServer + servers.Add("ArcGisPro", new InnerMcpServer { Name = "ArcGisPro", Type = "inner", diff --git a/host/llm/Bailian.cs b/host/llm/Bailian.cs index bd444c7..b43eed0 100644 --- a/host/llm/Bailian.cs +++ b/host/llm/Bailian.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Net.Http; using System.Net.Http.Headers; @@ -17,9 +18,18 @@ public class Bailian : Llm public string max_tokens { get; set; } public string app_id { get; set; } public string api_key { get; set; } - public IAsyncEnumerable SendChatStreamAsync(string message) + public async IAsyncEnumerable SendChatStreamAsync(LlmJsonContent jsonContent) { - throw new System.NotImplementedException(); + jsonContent.Stream = true; + StringBuilder builder = new StringBuilder(); + await foreach (var chunk in HttpRequest.PostWithStreamingResponseAsync( + "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions", + JsonConvert.SerializeObject(jsonContent), + api_key)) + { + builder.Append(chunk); + yield return builder.ToString(); + } } public IAsyncEnumerable SendApplicationStreamAsync(string message) diff --git a/host/llm/Llm.cs b/host/llm/Llm.cs index 62a11f4..1807b9d 100644 --- a/host/llm/Llm.cs +++ b/host/llm/Llm.cs @@ -11,7 +11,7 @@ public interface Llm public string top_p { get; set; } public string max_tokens { get; set; } - public IAsyncEnumerable SendChatStreamAsync(string message); + public IAsyncEnumerable SendChatStreamAsync(LlmJsonContent jsonContent); public IAsyncEnumerable SendApplicationStreamAsync(string message); public Task SendChatAsync(LlmJsonContent jsonContent); public Task SendApplicationAsync(CommonInput commonInput); diff --git a/host/llm/entity/stream/LlmStreamChat.cs b/host/llm/entity/stream/LlmStreamChat.cs new file mode 100644 index 0000000..6991982 --- /dev/null +++ b/host/llm/entity/stream/LlmStreamChat.cs @@ -0,0 +1,54 @@ +namespace LinkToolAddin.host.llm.entity.stream +{ + using System; + using System.Collections.Generic; + + using System.Globalization; + using Newtonsoft.Json; + using Newtonsoft.Json.Converters; + + public partial class LlmStreamChat + { + [JsonProperty("choices")] + public Choice[] Choices { get; set; } + + [JsonProperty("object")] + public string Object { get; set; } + + [JsonProperty("usage")] + public object Usage { get; set; } + + [JsonProperty("created")] + public long Created { get; set; } + + [JsonProperty("system_fingerprint")] + public object SystemFingerprint { get; set; } + + [JsonProperty("model")] + public string Model { get; set; } + + [JsonProperty("id")] + public string Id { get; set; } + } + + public partial class Choice + { + [JsonProperty("finish_reason")] + public string FinishReason { get; set; } + + [JsonProperty("delta")] + public Delta Delta { get; set; } + + [JsonProperty("index")] + public long Index { get; set; } + + [JsonProperty("logprobs")] + public object Logprobs { get; set; } + } + + public partial class Delta + { + [JsonProperty("content")] + public string Content { get; set; } + } +} \ No newline at end of file diff --git a/host/mcp/McpPromptDefinition.cs b/host/mcp/McpPromptDefinition.cs new file mode 100644 index 0000000..83ee903 --- /dev/null +++ b/host/mcp/McpPromptDefinition.cs @@ -0,0 +1,27 @@ +namespace LinkToolAddin.host.mcp +{ + using System; + using System.Collections.Generic; + + using System.Globalization; + using Newtonsoft.Json; + using Newtonsoft.Json.Converters; + + public partial class McpPromptDefinition + { + [JsonProperty("prompt")] + public Prompt Prompt { get; set; } + } + + public partial class Prompt + { + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("description")] + public string Description { get; set; } + + [JsonProperty("arguments")] + public string Arguments { get; set; } + } +} \ No newline at end of file diff --git a/host/prompt/SystemPrompt.cs b/host/prompt/SystemPrompt.cs index 48938d5..0dde1f7 100644 --- a/host/prompt/SystemPrompt.cs +++ b/host/prompt/SystemPrompt.cs @@ -2,9 +2,9 @@ public class SystemPrompt { - public static string SysPromptTemplate = "现在你是一个精通ArcGIS Pro的专家,请以此身份回答用户的问题。你有以下工具可以调用{{toolInfos}},用户的数据库路径是{{gdbPath}}。MCP工具调用的格式要求示例:\n search\n {\\\"query\\\": \\\"上海 人口\\\"}\n"; + public static string SysPromptTemplate = "现在你是一个精通ArcGIS Pro的专家,请以此身份回答用户的问题。你有以下工具可以调用{{toolInfos}},用户的数据库路径是{{gdbPath}}。MCP工具调用的格式要求示例,必须用标签表示工具调用:\n search\n {\\\"query\\\": \\\"上海 人口\\\"}\n。你每次调用请求都必须放在单独的一条消息中,不附带任何的文字说明,不带markdown格式。如需文字说明,请另外放在一次单独的消息中。\n当你认为已解决用户最初提出的问题时,请输出单独的一条消息,内容为[DONE],不附带任何其它文字说明,程序识别到后会退出循环。\n此外,你还可以通过调用用户提示词,从而使你更好地理解和完成用户的任务。"; - public static string ContinuePromptTemplate = "上一个工具执行的结果如下,请据此继续执行"; + public static string ContinuePromptTemplate = "工具执行的结果如下,根据以上结果决定继续执行工具或是根据结果回答问题。如果不再需要额外说明和额外的操作,请回答单独的一条内容为[DONE]的消息。工具已经成功调用,请勿重复执行上一个工具"; public static string ErrorPromptTemplate = "执行上一个工具的时候出现以下错误,请根据报错信息重试"; diff --git a/ui/dockpane/TestDockpane.xaml b/ui/dockpane/TestDockpane.xaml index e550084..fd7d8c8 100644 --- a/ui/dockpane/TestDockpane.xaml +++ b/ui/dockpane/TestDockpane.xaml @@ -21,6 +21,7 @@ + @@ -33,5 +34,6 @@ + \ No newline at end of file diff --git a/ui/dockpane/TestDockpane.xaml.cs b/ui/dockpane/TestDockpane.xaml.cs index d67e5c9..abf8b4a 100644 --- a/ui/dockpane/TestDockpane.xaml.cs +++ b/ui/dockpane/TestDockpane.xaml.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using System.Text; using System.Windows; using System.Windows.Controls; using LinkToolAddin.client; @@ -27,6 +28,9 @@ namespace LinkToolAddin.ui.dockpane { private static ILog log = LogManager.GetLogger(typeof(TestDockpaneView)); + private List idList = new List(); + private Dictionary messageDict = new Dictionary(); + public TestDockpaneView() { InitLogger(); @@ -139,6 +143,35 @@ namespace LinkToolAddin.ui.dockpane }); log.Info(reponse); } + + private async void Request_Bailian_Stream_Test() + { + LlmJsonContent jsonContent = new LlmJsonContent() + { + Model = "qwen-max", + Messages = new List() + { + new Message() + { + Role = "user", + Content = "给我写一篇1000字的高考议论文" + } + }, + Temperature = 0.7, + TopP = 1, + MaxTokens = 1000, + Stream = true + }; + Llm bailian = new Bailian + { + api_key = "sk-db177155677e438f832860e7f4da6afc", + app_id = "6a77c5a68de64f469b79fcdcde9d5001", + }; + await foreach (var chunk in bailian.SendChatStreamAsync(jsonContent)) + { + log.Info(chunk); + } + } private void TestButton_OnClick(object sender, RoutedEventArgs e) { @@ -159,7 +192,31 @@ namespace LinkToolAddin.ui.dockpane private void PromptTestButton_OnClick(object sender, RoutedEventArgs e) { string userPrompt = PromptTestTextBox.Text; - Gateway.SendMessage(userPrompt,"qwen-max","C:/Project/test.gdb",AddReply); + // Gateway.SendMessage(userPrompt,"qwen-max","C:/Project/test.gdb",AddReply); + Gateway.SendMessageStream(userPrompt,"qwen-max","D:\\01_Project\\20250305_LinkTool\\20250420_AiDemoProject\\20250420_AiDemoProject.gdb",AddReplyStream); + } + + public void AddReplyStream(MessageListItem msg) + { + string id = msg.id; + if (idList.Contains(id)) + { + messageDict[id] = msg; + } + else + { + idList.Add(id); + messageDict.Add(msg.id, msg); + } + ReplyTextBox.Clear(); + StringBuilder builder = new StringBuilder(); + foreach (KeyValuePair pair in messageDict) + { + MessageListItem msgItem = pair.Value; + builder.AppendLine(msgItem.content); + ReplyTextBox.Text = builder.ToString(); + ReplyTextBox.ScrollToEnd(); + } } public void AddReply(MessageListItem msg) @@ -169,5 +226,10 @@ namespace LinkToolAddin.ui.dockpane string originContent = ReplyTextBox.Text; ReplyTextBox.Text = originContent + content; } + + private void TestStream_OnClick(object sender, RoutedEventArgs e) + { + Request_Bailian_Stream_Test(); + } } } From 3b1f65b3ba3adade01d94e7704a45f2ea5727d44 Mon Sep 17 00:00:00 2001 From: PeterZhong Date: Fri, 23 May 2025 20:47:36 +0800 Subject: [PATCH 5/5] =?UTF-8?q?=E8=87=AA=E4=B8=BB=E5=AE=9E=E7=8E=B0JSON=20?= =?UTF-8?q?Schema=E7=94=9F=E6=88=90=EF=BC=8C=E6=8E=A5=E5=85=A5=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E6=9F=A5=E7=9C=8B=E5=B7=A5=E5=85=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/tool/ArcGisPro.cs | 46 +++++++++-- client/tool/KnowledgeBase.cs | 25 ++++++ common/JsonSchemaGenerator.cs | 150 ++++++++++++++++++++++++++++++++++ host/Gateway.cs | 3 +- 4 files changed, 215 insertions(+), 9 deletions(-) create mode 100644 client/tool/KnowledgeBase.cs create mode 100644 common/JsonSchemaGenerator.cs diff --git a/client/tool/ArcGisPro.cs b/client/tool/ArcGisPro.cs index 2808200..da2e604 100644 --- a/client/tool/ArcGisPro.cs +++ b/client/tool/ArcGisPro.cs @@ -24,17 +24,47 @@ public class ArcGisPro return result; } - [McpServerTool, Description("查看指定数据的属性,包括坐标系、范围、数据类型等")] + [McpServerTool, Description("查看指定数据的坐标系、范围、几何类型、是否有Z坐标和M坐标,获取字段列表等")] public static async Task DataProperty(string datasetPath,string dataName) { - using Geodatabase gdb = new Geodatabase(new FileGeodatabaseConnectionPath(new Uri(datasetPath))); - FeatureClass featureClass = gdb.OpenDataset(dataName); - FeatureClassDefinition featureClassDefinition = featureClass.GetDefinition(); - JsonRpcResultEntity result = new JsonRpcSuccessEntity() + JsonRpcResultEntity result = new JsonRpcResultEntity(); + await QueuedTask.Run(() => { - Id = 1, - Result = JsonConvert.SerializeObject(featureClassDefinition) - }; + try + { + using Geodatabase gdb = new Geodatabase(new FileGeodatabaseConnectionPath(new Uri(datasetPath))); + FeatureClass featureClass = gdb.OpenDataset(dataName); + FeatureClassDefinition featureClassDefinition = featureClass.GetDefinition(); + SpatialReference spatialReference = featureClassDefinition.GetSpatialReference(); + GeometryType geometryType = featureClassDefinition.GetShapeType(); + result = new JsonRpcSuccessEntity() + { + Id = 1, + Result = JsonConvert.SerializeObject(new Dictionary() + { + {"spatialReference", spatialReference.Name+"(WKID:"+spatialReference.Wkid+")"}, + {"dataName", dataName}, + {"geometryType", geometryType.ToString()}, + {"hasZValue", featureClassDefinition.HasZ()}, + {"hasMValue", featureClassDefinition.HasM()}, + {"fields",featureClassDefinition.GetFields()} + }) + }; + return result; + } + catch (Exception ex) + { + result = new JsonRpcErrorEntity() + { + Error = new Error() + { + Message = ex.Message + }, + Id = 1 + }; + return result; + } + }); return result; } diff --git a/client/tool/KnowledgeBase.cs b/client/tool/KnowledgeBase.cs new file mode 100644 index 0000000..9d2b162 --- /dev/null +++ b/client/tool/KnowledgeBase.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.ComponentModel; +using System.Threading.Tasks; +using LinkToolAddin.host.llm.entity; +using LinkToolAddin.resource; +using LinkToolAddin.server; +using ModelContextProtocol.Server; +using Newtonsoft.Json; + +namespace LinkToolAddin.client.tool; + +public class KnowledgeBase +{ + [McpServerTool, Description("可以查询ArcGIS Pro的帮助文档获取关于地理处理工具使用参数的说明")] + public static async Task QueryArcgisHelpDoc(string query) + { + DocDb docDb = new DocDb("sk-db177155677e438f832860e7f4da6afc", DocDb.KnowledgeBase.ArcGISProHelpDoc); + KnowledgeResult knowledgeResult = await docDb.Retrieve(query); + JsonRpcResultEntity result = new JsonRpcSuccessEntity() + { + Result = JsonConvert.SerializeObject(knowledgeResult.ChunkList), + }; + return result; + } +} \ No newline at end of file diff --git a/common/JsonSchemaGenerator.cs b/common/JsonSchemaGenerator.cs new file mode 100644 index 0000000..ff799c4 --- /dev/null +++ b/common/JsonSchemaGenerator.cs @@ -0,0 +1,150 @@ +using System.Collections.ObjectModel; + +namespace LinkToolAddin.common; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text.Json; + +public static class JsonSchemaGenerator +{ + public static string GenerateJsonSchema(MethodInfo methodInfo) + { + var parameters = methodInfo.GetParameters(); + var properties = new Dictionary(); + var required = new List(); + + foreach (var param in parameters) + { + var paramName = param.Name ?? throw new InvalidOperationException("参数没有名称。"); + var paramSchema = GenerateSchemaForType(param.ParameterType); + properties[paramName] = paramSchema; + + if (!param.IsOptional) + { + required.Add(paramName); + } + } + + var schemaRoot = new Dictionary + { + { "$schema", "http://json-schema.org/draft-07/schema#" }, + { "type", "object" }, + { "properties", properties } + }; + + if (required.Count > 0) + { + schemaRoot["required"] = required; + } + + var options = new JsonSerializerOptions { WriteIndented = true }; + return JsonSerializer.Serialize(schemaRoot, options); + } + + private static object GenerateSchemaForType(Type type) + { + // 处理可空类型 + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) + { + var underlyingType = Nullable.GetUnderlyingType(type); + return new[] { GenerateSchemaForType(underlyingType), "null" }; + } + + // 处理集合类型(数组或IEnumerable) + if (IsCollectionType(type, out Type elementType)) + { + return new Dictionary + { + { "type", "array" }, + { "items", GenerateSchemaForType(elementType) } + }; + } + + // 处理基本类型(int, string, bool, etc.) + if (IsPrimitiveType(type)) + { + string jsonType = MapClrTypeToJsonType(type); + var schema = new Dictionary { { "type", jsonType } }; + + if (type == typeof(DateTime)) + schema["format"] = "date-time"; + else if (type == typeof(Guid)) + schema["format"] = "uuid"; + + return schema; + } + + // 处理复杂类型(类、结构体) + if (type.IsClass || type.IsValueType) + { + var props = new Dictionary(); + foreach (var prop in type.GetProperties(BindingFlags.Public | BindingFlags.Instance)) + { + props[prop.Name] = GenerateSchemaForType(prop.PropertyType); + } + + return new Dictionary + { + { "type", "object" }, + { "properties", props } + }; + } + + // 默认情况 + return new Dictionary { { "type", "any" } }; + } + + private static bool IsCollectionType(Type type, out Type elementType) + { + if (type == typeof(string)) + { + elementType = null; + return false; + } + + if (type.IsArray) + { + elementType = type.GetElementType(); + return true; + } + + if (type.IsGenericType) + { + var genericTypeDef = type.GetGenericTypeDefinition(); + if (genericTypeDef == typeof(IEnumerable<>) || + genericTypeDef == typeof(List<>) || + genericTypeDef == typeof(Collection<>)) + { + elementType = type.GetGenericArguments()[0]; + return true; + } + } + + elementType = null; + return false; + } + + private static bool IsPrimitiveType(Type type) + { + return type.IsPrimitive || type == typeof(string) || type == typeof(decimal) || type == typeof(DateTime) || type == typeof(Guid); + } + + private static string MapClrTypeToJsonType(Type type) + { + if (type == typeof(int) || type == typeof(short) || type == typeof(long) || + type == typeof(uint) || type == typeof(ushort) || type == typeof(ulong)) + return "integer"; + if (type == typeof(float) || type == typeof(double) || type == typeof(decimal)) + return "number"; + if (type == typeof(bool)) + return "boolean"; + if (type == typeof(string)) + return "string"; + if (type == typeof(DateTime) || type == typeof(Guid)) + return "string"; + return "any"; + } +} \ No newline at end of file diff --git a/host/Gateway.cs b/host/Gateway.cs index 15e128d..fca37cc 100644 --- a/host/Gateway.cs +++ b/host/Gateway.cs @@ -30,6 +30,7 @@ using Newtonsoft.Json.Linq; using Newtonsoft.Json.Schema; using Newtonsoft.Json.Schema.Generation; using Tool = LinkToolAddin.host.mcp.Tool; +using LinkToolAddin.common; namespace LinkToolAddin.host; @@ -471,7 +472,7 @@ public class Gateway { string methodName = method.Name; string methodDescription = method.GetCustomAttribute()?.Description; - string methodParamSchema = GenerateMethodParamSchema(method); + string methodParamSchema = LinkToolAddin.common.JsonSchemaGenerator.GenerateJsonSchema(method); McpToolDefinition toolDefinition = new McpToolDefinition { Tool = new Tool