diff --git a/LinkToolAddin.csproj b/LinkToolAddin.csproj index a4143cc..35cdbec 100644 --- a/LinkToolAddin.csproj +++ b/LinkToolAddin.csproj @@ -97,10 +97,6 @@ False - - - - @@ -108,6 +104,16 @@ + + + + + + + Always + + + diff --git a/README.md b/README.md index f0d5845..71d45b5 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ git clone xxx.git 5. 部分情况下可能还需要替换LinkToolAddin.csproj中的所有相关安装路径 6. 点击运行进行测试,看是否能正常打开ArcGIS Pro和LinkTool插件 7. 确认无误建议commit到本地的master分支以备后续使用(但请勿推送至远端) +8. 修改McpServerList里面filesystem的白名单目录 #### 创建分支 diff --git a/client/StdioMcpClient.cs b/client/StdioMcpClient.cs index caad4b7..64933ad 100644 --- a/client/StdioMcpClient.cs +++ b/client/StdioMcpClient.cs @@ -51,9 +51,18 @@ public class StdioMcpClient : McpClient public async Task> GetToolListAsync() { - IMcpClient client = await McpClientFactory.CreateAsync(new StdioClientTransport(transportOptions)); - IList tools = await client.ListToolsAsync(); - return tools; + try + { + IMcpClient client = await McpClientFactory.CreateAsync(new StdioClientTransport(transportOptions)); + IList tools = await client.ListToolsAsync(); + return tools; + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + return null; + } + } public async Task CallToolAsync(string toolName, Dictionary parameters = null) diff --git a/client/prompt/DynamicPrompt.cs b/client/prompt/DynamicPrompt.cs index 52d695c..c63c9fa 100644 --- a/client/prompt/DynamicPrompt.cs +++ b/client/prompt/DynamicPrompt.cs @@ -1,29 +1,41 @@ using System.Collections.Generic; +using LinkToolAddin.host; +using LinkToolAddin.host.prompt; namespace LinkToolAddin.client.prompt; public class DynamicPrompt { - public static string GetPrompt(string name,Dictionary args = null) + public static string GetPrompt(string name,Dictionary args = null) { - PromptTemplates promptTemplate = new PromptTemplates(); - string template = promptTemplate.GetPrompt(name); + PromptServerList promptServerList = new PromptServerList(); + string template = promptServerList.GetPromptServer(name).Content; if (args == null) { return template; } - foreach (KeyValuePair pair in args) + foreach (KeyValuePair pair in args) { string replaceKey = "{{"+pair.Key+"}}"; - template.Replace(replaceKey, pair.Value.ToString()); + template = template.Replace(replaceKey, pair.Value.ToString()); } return template; } - public static Dictionary GetAllPrompts() + public static List GetAllPrompts() { - PromptTemplates promptTemplate = new PromptTemplates(); - Dictionary template = promptTemplate.GetPromptsDict(); - return template; + PromptServerList promptServerList = new PromptServerList(); + List prompts = new List(); + Dictionary promptDefinitions = promptServerList.GetPromptsDict(); + foreach (KeyValuePair pair in promptDefinitions) + { + prompts.Add(new UserPrompt() + { + Name = pair.Value.Name, + Description = pair.Value.Description, + Arguments = pair.Value.Arguments + }); + } + return prompts; } } \ No newline at end of file diff --git a/client/prompt/PromptTemplates.cs b/client/prompt/PromptTemplates.cs index 7e0e18f..6f9eaa1 100644 --- a/client/prompt/PromptTemplates.cs +++ b/client/prompt/PromptTemplates.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using LinkToolAddin.host.prompt; namespace LinkToolAddin.client.prompt; diff --git a/client/tool/ArcGisPro.cs b/client/tool/ArcGisPro.cs index 163f253..52994b0 100644 --- a/client/tool/ArcGisPro.cs +++ b/client/tool/ArcGisPro.cs @@ -2,12 +2,16 @@ using System.Collections.Generic; using System.ComponentModel; using System.Linq; +using System.Text; using System.Threading.Tasks; using ArcGIS.Core.Data; using ArcGIS.Core.Data.Raster; using ArcGIS.Core.Geometry; +using ArcGIS.Desktop.Core.Geoprocessing; using ArcGIS.Desktop.Framework.Threading.Tasks; using LinkToolAddin.server; +using LinkToolAddin.ui.dockpane; +using log4net; using ModelContextProtocol.Server; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -16,14 +20,40 @@ namespace LinkToolAddin.client.tool; public class ArcGisPro { - [McpServerTool, Description("可以通过调用ArcGIS Pro的地理处理工具实现一些数据处理功能。")] + private static ILog log = LogManager.GetLogger(typeof(ArcGisPro)); + [McpServerTool, Description("可以通过调用ArcGIS Pro的地理处理工具实现一些数据处理功能,传入参数必须严格遵循ArcGIS Pro调用工具的标准调用名和参数要求知识库。")] 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; + IGPResult results = await Geoprocessing.ExecuteToolAsync(toolName, toolParams); + JsonRpcResultEntity jsonRpcResultEntity; + if (results.IsFailed) + { + log.Error(results.ErrorMessages); + jsonRpcResultEntity = new JsonRpcErrorEntity() + { + Error = new Error() + { + Code = results.ErrorCode, + Message = GetMessagesString(results.ErrorMessages) + } + }; + }else if(results.HasWarnings) + { + log.Warn(results.Messages); + jsonRpcResultEntity = new JsonRpcSuccessEntity + { + Result = GetMessagesString(results.Messages) + }; + } + else + { + log.Info("success gp tool"); + jsonRpcResultEntity = new JsonRpcSuccessEntity + { + Result = GetMessagesString(results.Messages) + }; + } + return jsonRpcResultEntity; } [McpServerTool, Description("查看指定数据的坐标系、范围、几何类型、是否有Z坐标和M坐标,获取字段列表等")] @@ -106,4 +136,14 @@ public class ArcGisPro }; return result; } + + private static string GetMessagesString(IEnumerable messages) + { + StringBuilder messagesStr = new StringBuilder(); + foreach (var gpMessage in messages) + { + messagesStr.AppendLine(gpMessage.Text); + } + return messagesStr.ToString(); + } } \ No newline at end of file diff --git a/common/HttpRequest.cs b/common/HttpRequest.cs index 16c9383..5d42052 100644 --- a/common/HttpRequest.cs +++ b/common/HttpRequest.cs @@ -67,7 +67,7 @@ public class HttpRequest return null; } - public static async IAsyncEnumerable PostWithStreamingResponseAsync( + public static async IAsyncEnumerable PostWithStreamingResponseAsync( string url, string body, string apiKey, @@ -114,7 +114,7 @@ public class HttpRequest if (dataObj is not null) { - yield return dataObj.Choices[0].Delta.Content; + yield return dataObj; } } } diff --git a/common/LocalResource.cs b/common/LocalResource.cs new file mode 100644 index 0000000..fc1e719 --- /dev/null +++ b/common/LocalResource.cs @@ -0,0 +1,31 @@ +using System; +using System.IO; + +namespace LinkToolAddin.common; + +public class LocalResource +{ + public static string ReadFileByResource(string resourceName) + { + var assembly = System.Reflection.Assembly.GetExecutingAssembly(); + + string[] resourceNames = assembly.GetManifestResourceNames(); + foreach (string name in resourceNames) + { + Console.WriteLine(name); + } + + using (Stream stream = assembly.GetManifestResourceStream(resourceName)) + { + if (stream == null) + { + return($"找不到嵌入资源:{resourceName}"); + } + + using (StreamReader reader = new StreamReader(stream)) + { + return reader.ReadToEnd(); + } + } + } +} \ No newline at end of file diff --git a/doc/TodoList.md b/doc/TodoList.md new file mode 100644 index 0000000..65f668c --- /dev/null +++ b/doc/TodoList.md @@ -0,0 +1,28 @@ +# 待办事项 + +本文档用于记录和跟踪当前阶段待办事项及完成进度 + +## 核心程序 + +- [x] 提示词调用完整实现:以面向对象形式取代字典形式,加入参数和描述 +- [x] 接入本地文件系统等基础性MCP服务 +- [ ] Python代码执行的实现 +- [x] 适配qwen3、deepseek等推理模型并迁移 +- [x] 工具执行错误消息合并为一条 + +## 提示词工程 + +- [ ] 将原来的单独XML规则修改为内嵌XML规则 +- [ ] 系统提示词明确提示调用动态Prompt和知识库 +- [ ] 错误和继续提示词预留工具消息占位符 +- [ ] 错误和继续提示词明示 +- [ ] 系统提示词泛化增强 +- [ ] 针对qwen3适当调整 + +## 前端交互 + +- [ ] 三类消息卡片 +- [ ] 流式输出,根据id匹配修改或新增 +- [ ] 添加工作空间、发送、复制粘贴等交互 +- [ ] 独立UI线程 +- [ ] 工具卡片特殊提示,弹出窗口显示内容 \ No newline at end of file diff --git a/host/Gateway.cs b/host/Gateway.cs index 28935f0..2a789c5 100644 --- a/host/Gateway.cs +++ b/host/Gateway.cs @@ -8,10 +8,10 @@ using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; +using System.Windows; using System.Windows.Documents; 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; @@ -32,6 +32,8 @@ using Newtonsoft.Json.Schema; using Newtonsoft.Json.Schema.Generation; using Tool = LinkToolAddin.host.mcp.Tool; using LinkToolAddin.common; +using LinkToolAddin.host.llm.entity.stream; +using MessageBox = ArcGIS.Desktop.Framework.Dialogs.MessageBox; namespace LinkToolAddin.host; @@ -112,12 +114,7 @@ public class Gateway messages.Add(new Message { Role = "user", - Content = toolResponse.IsError ? SystemPrompt.ErrorPromptTemplate : SystemPrompt.ContinuePromptTemplate - }); - messages.Add(new Message - { - Role = "user", - Content = JsonConvert.SerializeObject(toolResponse) + Content = toolResponse.IsError ? SystemPrompt.ErrorPrompt(JsonConvert.SerializeObject(toolResponse)) : SystemPrompt.ContinuePrompt(JsonConvert.SerializeObject(toolResponse)) }); callback?.Invoke(toolMessageItem); }else if (mcpServer is StdioMcpServer) @@ -137,12 +134,7 @@ public class Gateway messages.Add(new Message { Role = "user", - Content = toolResponse.IsError ? SystemPrompt.ErrorPromptTemplate : SystemPrompt.ContinuePromptTemplate - }); - messages.Add(new Message - { - Role = "user", - Content = JsonConvert.SerializeObject(toolResponse) + Content = toolResponse.IsError ? SystemPrompt.ErrorPrompt(JsonConvert.SerializeObject(toolResponse)) : SystemPrompt.ContinuePrompt(JsonConvert.SerializeObject(toolResponse)) }); callback?.Invoke(toolMessageItem); }else if (mcpServer is InnerMcpServer) @@ -164,12 +156,7 @@ public class Gateway messages.Add(new Message { Role = "user", - Content = SystemPrompt.ErrorPromptTemplate - }); - messages.Add(new Message - { - Role = "user", - Content = JsonConvert.SerializeObject(innerResult) + Content = SystemPrompt.ErrorPrompt(JsonConvert.SerializeObject(innerResult)) }); callback?.Invoke(toolMessageItem); }else if (innerResult is JsonRpcSuccessEntity) @@ -185,12 +172,7 @@ public class Gateway messages.Add(new Message { Role = "user", - Content = SystemPrompt.ContinuePromptTemplate - }); - messages.Add(new Message - { - Role = "user", - Content = JsonConvert.SerializeObject(innerResult) + Content = SystemPrompt.ContinuePrompt(JsonConvert.SerializeObject(innerResult)) }); callback?.Invoke(toolMessageItem); } @@ -204,7 +186,7 @@ public class Gateway Dictionary promptParams = JsonConvert.DeserializeObject>(promptArgs); string serverName = fullPromptName.Contains(":") ? fullPromptName.Split(':')[0] : fullPromptName; string promptName = fullPromptName.Contains(":") ? fullPromptName.Split(':')[1] : fullPromptName; - string promptRes = DynamicPrompt.GetPrompt(promptName, promptParams); + string promptRes = DynamicPrompt.GetPrompt(promptName, null); messages.Add(new Message { Role = "user", @@ -222,13 +204,34 @@ public class Gateway }; callback?.Invoke(chatMessageListItem); } - if (reponse == "[DONE]") + if (reponse.EndsWith("[DONE]")) { goOn = false; } } } + private static (string Matched, string Remaining) ExtractMatchedPart(string input, string toolPattern) + { + if (string.IsNullOrEmpty(input) || string.IsNullOrEmpty(toolPattern)) + return (string.Empty, input); + + Regex regex = new Regex(toolPattern); + Match match = regex.Match(input); + + if (!match.Success) + return (string.Empty, input); + + string matched = match.Value; + int startIndex = match.Index; + int length = match.Length; + + // 构造剩余字符串 + string remaining = input.Substring(0, startIndex) + input.Substring(startIndex + length); + + return (matched, remaining); + } + public static async void SendMessageStream(string message, string model, string gdbPath, Action callback) { Llm bailian = new Bailian @@ -237,7 +240,6 @@ public class Gateway }; List messages = new List(); string toolInfos = await GetToolInfos(new McpServerList()); - log.Info(SystemPrompt.SysPrompt(gdbPath, toolInfos)); messages.Add(new Message { Role = "system", @@ -249,10 +251,12 @@ public class Gateway Content = message }); goOn = true; - string toolPattern = "^[\\s\\S]*?<\\/tool_use>$"; - string promptPattern = "^[\\s\\S]*?<\\/prompt>$"; + string toolPattern = "([\\s\\S]*?)([\\s\\S]*?)<\\/name>([\\s\\S]*?)([\\s\\S]*?)<\\/arguments>([\\s\\S]*?)<\\/tool_use>"; + string promptPattern = "([\\s\\S]*?)([\\s\\S]*?)<\\/name>([\\s\\S]*?)([\\s\\S]*?)<\\/arguments>([\\s\\S]*?)<\\/prompt>"; McpServerList mcpServerList = new McpServerList(); + PromptServerList promptServerList = new PromptServerList(); int loop = 0; + string messageContent = ""; //一次请求下完整的response while (goOn) { loop++; @@ -270,220 +274,291 @@ public class Gateway MaxTokens = 1000, }; long timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); - string messageContent = ""; - await foreach(var chunk in bailian.SendChatStreamAsync(jsonContent)) + List mcpToolRequests = new List(); + List promptRequests = new List(); + var (toolMatched, toolRemaining) = ExtractMatchedPart(messageContent, toolPattern); + var (promptMatched, promptRemaining) = ExtractMatchedPart(messageContent, promptPattern); + if (toolMatched == "" && promptMatched == "" && messageContent != "") { - if (chunk == "[DONE]") + //如果本次回复不包含任何工具的调用或提示词的调用,则不再请求 + goOn = false; + break; + } + await foreach(LlmStreamChat llmStreamChat in bailian.SendChatStreamAsync(jsonContent)) + { + if (!goOn) { - goOn = false; - }else if (chunk.StartsWith("")) + break; + } + + try { - if (Regex.IsMatch(chunk, toolPattern)) + string chunk = llmStreamChat.Choices[0].Delta.Content; + MessageListItem reasonMessageListItem = new ChatMessageItem() { - //返回工具卡片 - messages.Add(new Message + content = llmStreamChat.Choices[0].Delta.ResoningContent, + role = "assistant", + type = MessageType.REASON_MESSAGE, + id = (timestamp-1).ToString() + }; + Application.Current.Dispatcher.Invoke(() => + { + callback?.Invoke(reasonMessageListItem); + }); + messageContent = chunk; + var (matched, remaining) = ExtractMatchedPart(chunk, toolPattern); + if (matched == "") + { + var (matchedPrompt, remainingPrompt) = ExtractMatchedPart(chunk, promptPattern); + if (matchedPrompt == "") { - Role = "assistant", - Content = chunk + //普通消息文本 + MessageListItem chatMessageListItem = new ChatMessageItem() + { + content = remainingPrompt, + role = "assistant", + type = MessageType.CHAT_MESSAGE, + id = timestamp.ToString() + }; + Application.Current.Dispatcher.Invoke(() => + { + callback?.Invoke(chatMessageListItem); + }); + } + else + { + //包含Prompt调用请求的消息 + MessageListItem chatMessageListItem = new ChatMessageItem() + { + content = remainingPrompt, + role = "assistant", + type = MessageType.CHAT_MESSAGE, + id = timestamp.ToString() + }; + callback?.Invoke(chatMessageListItem); + XElement promptUse = XElement.Parse(matchedPrompt); + string promptKey = promptUse.Element("name")?.Value; + string promptArgs = promptUse.Element("arguments")?.Value; + Dictionary promptParams = JsonConvert.DeserializeObject>(promptArgs); + promptRequests = new List(); + promptRequests.Add(new PromptRequest() + { + PromptName = promptKey, + PromptArgs = promptParams, + PromptServer = promptServerList.GetPromptServer(promptKey) + }); + } + } + else + { + //包含工具调用请求的消息 + MessageListItem chatMessageListItem = new ChatMessageItem() + { + content = remaining, + role = "assistant", + type = MessageType.CHAT_MESSAGE, + id = timestamp.ToString() + }; + Application.Current.Dispatcher.Invoke(() => + { + callback?.Invoke(chatMessageListItem); }); - XElement toolUse = XElement.Parse(chunk); + XElement toolUse = XElement.Parse(matched); 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) + //将工具调用请求添加至列表中待一次回答完整后再统一进行调用 + mcpToolRequests = new List(); + McpToolRequest mcpToolRequest = new McpToolRequest() { - 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 - Content = toolResponse.IsError ? SystemPrompt.ErrorPromptTemplate : SystemPrompt.ContinuePrompt(JsonConvert.SerializeObject(toolResponse)) - }); - // 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 - Content = toolResponse.IsError ? SystemPrompt.ErrorPromptTemplate : SystemPrompt.ContinuePrompt(JsonConvert.SerializeObject(toolResponse)) - }); - // 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 methodParams = toolParams.Values.ToArray(); - object[] args = new object[methodParams.Length]; - for (int i = 0; i < methodParams.Length; i++) - { - if (methodParams[i].GetType() == typeof(JArray)) - { - List list = new List(); - list = (methodParams[i] as JArray).Select(token => token.ToString()).ToList(); - args[i] = list; - } - else - { - args[i] = methodParams[i]; - } - } - var task = method.Invoke(null, args) 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 - Content = SystemPrompt.ContinuePrompt(JsonConvert.SerializeObject(innerResult)) - }); - // messages.Add(new Message - // { - // Role = "user", - // Content = JsonConvert.SerializeObject(innerResult) - // }); - callback?.Invoke(toolMessageItem); - } - } + McpServer = mcpServer, + ToolName = toolName, + ToolArgs = toolParams, + }; + mcpToolRequests.Add(mcpToolRequest); } - else + } + catch (Exception e) + { + Console.WriteLine(e); + log.Error(e.Message); + } + } + if (messageContent != "") + { + messages.Add(new Message + { + Role = "assistant", + Content = messageContent + }); + } + /*统一处理本次请求中的MCP工具调用需求*/ + foreach (McpToolRequest mcpToolRequest in mcpToolRequests) + { + try + { + McpServer mcpServer = mcpToolRequest.McpServer; + string toolName = mcpToolRequest.ToolName; + Dictionary toolParams = mcpToolRequest.ToolArgs; + 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 = "", - toolParams = new Dictionary(), + toolName = toolName, + toolParams = toolParams, type = MessageType.TOOL_MESSAGE, - status = "loading", - content = "正在生成工具调用参数", - id = timestamp.ToString() + status = toolResponse.IsError ? "fail" : "success", + content = JsonConvert.SerializeObject(toolResponse), + id = (timestamp + 1).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) + Content = toolResponse.IsError + ? SystemPrompt.ErrorPrompt(JsonConvert.SerializeObject(toolResponse)) + : SystemPrompt.ContinuePrompt(JsonConvert.SerializeObject(toolResponse)) }); - MessageListItem toolMessageItem = new ToolMessageItem + Application.Current.Dispatcher.Invoke(() => { - toolName = "调用提示词", - toolParams = null, - type = MessageType.TOOL_MESSAGE, - status = "success", - content = promptKey, - id = timestamp.ToString() - }; - callback?.Invoke(toolMessageItem); + callback?.Invoke(toolMessageItem); + }); } - else + 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 = "调用提示词", - toolParams = null, + toolName = toolName, + toolParams = toolParams, type = MessageType.TOOL_MESSAGE, - status = "loading", - content = "正在调用提示词", - id = timestamp.ToString() + status = toolResponse.IsError ? "fail" : "success", + content = JsonConvert.SerializeObject(toolResponse), + id = (timestamp + 1).ToString() }; - callback?.Invoke(toolMessageItem); + messages.Add(new Message + { + Role = "user", + Content = toolResponse.IsError + ? SystemPrompt.ErrorPrompt(JsonConvert.SerializeObject(toolResponse)) + : SystemPrompt.ContinuePrompt(JsonConvert.SerializeObject(toolResponse)) + }); + Application.Current.Dispatcher.Invoke(() => + { + callback?.Invoke(toolMessageItem); + }); + } + else if (mcpServer is InnerMcpServer) + { + Type type = Type.GetType("LinkToolAddin.client.tool." + (mcpServer as InnerMcpServer).Name); + MethodInfo method = type.GetMethod(toolName, BindingFlags.Public | BindingFlags.Static); + var methodParams = toolParams.Values.ToArray(); + object[] args = new object[methodParams.Length]; + for (int i = 0; i < methodParams.Length; i++) + { + if (methodParams[i].GetType() == typeof(JArray)) + { + List list = new List(); + list = (methodParams[i] as JArray).Select(token => token.ToString()).ToList(); + args[i] = list; + } + else + { + args[i] = methodParams[i]; + } + } + + var task = method.Invoke(null, args) 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 + 1).ToString() + }; + messages.Add(new Message + { + Role = "user", + Content = SystemPrompt.ErrorPrompt(JsonConvert.SerializeObject(toolMessageItem)) + }); + Application.Current.Dispatcher.Invoke(() => + { + 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 + 1).ToString() + }; + messages.Add(new Message + { + Role = "user", + Content = SystemPrompt.ContinuePrompt(JsonConvert.SerializeObject(toolMessageItem)) + }); + Application.Current.Dispatcher.Invoke(() => + { + callback?.Invoke(toolMessageItem); + }); + } } } - else + catch (Exception ex) { - //普通流式消息卡片 - MessageListItem chatMessageListItem = new ChatMessageItem() + Console.WriteLine(ex.Message); + log.Error(ex.Message); + } + + } + /*统一处理本次请求中的Prompt调用需求*/ + foreach (PromptRequest promptRequest in promptRequests) + { + try + { + string promptContent = DynamicPrompt.GetPrompt(promptRequest.PromptName, promptRequest.PromptArgs); + messages.Add(new Message { - content = chunk, - role = "assistant", - type = MessageType.CHAT_MESSAGE, - id = timestamp.ToString() + Role = "user", + Content = promptContent + }); + MessageListItem toolMessageItem = new ToolMessageItem + { + toolName = "调用提示词", + toolParams = null, + type = MessageType.TOOL_MESSAGE, + status = "success", + content = "成功调用提示词:"+promptRequest.PromptName, + id = (timestamp+1).ToString() }; - messageContent = chunk; - callback?.Invoke(chatMessageListItem); + Application.Current.Dispatcher.Invoke(() => + { + callback?.Invoke(toolMessageItem); + }); + } + catch (Exception e) + { + Console.WriteLine(e); + log.Error(e.Message); } } - messages.Add(new Message - { - Role = "assistant", - Content = messageContent - }); } } @@ -492,6 +567,7 @@ public class Gateway StringBuilder toolInfos = new StringBuilder(); foreach (McpServer mcpServer in mcpServerList.GetAllServers()) { + log.Info($"正在列出{mcpServer.Name}中的工具"); if (mcpServer is InnerMcpServer) { InnerMcpServer innerMcpServer = (InnerMcpServer)mcpServer; @@ -564,17 +640,10 @@ public class Gateway } } - Dictionary prompts = DynamicPrompt.GetAllPrompts(); - foreach (KeyValuePair prompt in prompts) + List prompts = DynamicPrompt.GetAllPrompts(); + foreach (UserPrompt userPrompt in prompts) { - McpPromptDefinition promptDefinition = new McpPromptDefinition - { - Prompt = new LinkToolAddin.host.mcp.Prompt - { - Name = prompt.Key - } - }; - XNode node = JsonConvert.DeserializeXNode(JsonConvert.SerializeObject(promptDefinition)); + XNode node = JsonConvert.DeserializeXNode(JsonConvert.SerializeObject(new PromptDefinition(){UserPrompt = userPrompt})); toolInfos.AppendLine(node.ToString()); toolInfos.AppendLine(); } diff --git a/host/McpServerList.cs b/host/McpServerList.cs index 68f83b4..ca7415d 100644 --- a/host/McpServerList.cs +++ b/host/McpServerList.cs @@ -35,6 +35,53 @@ public class McpServerList Description = "可以调用进行查询知识库,获取相关参考信息。", IsActive = true }); + servers.Add("filesystem", new StdioMcpServer() + { + Name = "filesystem", + Type = "stdio", + Command = "npx", + Args = new List() + { + "-y", + "@modelcontextprotocol/server-filesystem", + "D:\\01_Project\\20250305_LinkTool\\20250420_AiDemoProject\\TestData" + } + }); + servers.Add("fetch", new StdioMcpServer() + { + Name = "fetch", + Type = "stdio", + Command = "uvx", + Args = new List() + { + "mcp-server-fetch" + } + }); + servers.Add("bing-search", new StdioMcpServer() + { + Name = "bing-search", + Type = "stdio", + Command = "npx", + Args = new List() + { + "bing-cn-mcp" + } + }); + servers.Add("mcp-python-interpreter", new StdioMcpServer() + { + Name = "mcp-python-interpreter", + Type = "stdio", + Command = "uvx", + Args = new List() + { + "--native-tls", + "mcp-python-interpreter", + "--dir", + "D:\\01_Project\\20250305_LinkTool\\20250420_AiDemoProject\\TestData", + "--python-path", + "C:\\Program Files\\ArcGIS\\Pro\\bin\\Python\\envs\\custom\\python.exe" + } + }); } public McpServer GetServer(string name) diff --git a/host/PromptServerList.cs b/host/PromptServerList.cs new file mode 100644 index 0000000..e4a5cd5 --- /dev/null +++ b/host/PromptServerList.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using LinkToolAddin.host.prompt; + +namespace LinkToolAddin.host; + +public class PromptServerList +{ + Dictionary promptServers = new Dictionary(); + + public PromptServerList() + { + promptServers.Add("plan", new PromptServer("plan", + "根据用户描述的问题推断出需要使用的ArcGIS Pro工具调用名称列表", + "请根据用户所提问题进行工具规划,输出格式为'1.工具2.工具’,如果是ArcGIS Pro的工具,根据用户的具体需求和提供的数据类型," + + "判断并列出所有必要的分析步骤和工具,同时严格遵守知识库中“ArcGIS Pro工具调用大全”里“工具调用名称”一列的工具命名规则(例如:analysis.Erase)," + + "确保工具名的准确无误,工具与所属工具箱的对应关系正确无误,严格遵循文档规定的格式和大小写。工具的组合顺序优先参考知识库中“ArcGIS Pro的帮助文档”。" + + "有一些与分析无关的数据能够进行排除,在选择工具时不受其干扰。")); + promptServers.Add("param", new PromptServer("param", + "填写ArcGIS Pro工具调用参数,生成规范的可执行的工具调用请求", + "根据帮助文档填写工具参数,请你根据“所需调用工具”,参照知识库“ArcGIS Pro工具调用大全”里工具所需的参数顺序进行陈列。" + + "列出所需调用工具的名称及其按照“ArcGIS Pro工具调用大全”里“的该工具所需的参数顺序陈列对应的参数。如果跳过了可选参数需要用空字符表示。" + + "不能更改所需调用工具的名称。例如:arcpy.analysis.Buffer,不能只写成Buffer。确保格式、参数的完整性和准确性,避免任何遗漏或错误。" + + "特别注意,所有参数均应视为字符串类型,即使它们可能代表数字或文件路径。例如问题为:使用地理处理中的\"擦除分析\"工具(Erase),将圆形要素(circle.shp)与方形要素(square.shp)进行空间叠加运算。" + + "输出: \"in_features\":\"circle.shp\",\r\n \"erase_features\":\"sqaure.shp\",\r\n \"out_feature_class\":\"res.shp\",\r\n \"cluster_tolerance\":\"1\"")); + promptServers.Add("code", new PromptServer("code", + "生成可运行的arcpy代码", + "根据你在多种编程语言、框架、设计模式和最佳实践方面拥有的广泛知识。现在需要根据用户需求生成高质量的代码,并确保语法正确。" + + "编写Arcpy代码时必须符合ArcGIS官方文档要求。参考官方文档的方法参数,确保编写正确。")); + } + + public Dictionary GetPromptsDict() + { + return promptServers; + } + + public PromptServer GetPromptServer(string key) + { + return promptServers[key]; + } +} \ No newline at end of file diff --git a/host/llm/Bailian.cs b/host/llm/Bailian.cs index b43eed0..6d7d273 100644 --- a/host/llm/Bailian.cs +++ b/host/llm/Bailian.cs @@ -6,6 +6,7 @@ using System.Text; using System.Threading.Tasks; using LinkToolAddin.common; using LinkToolAddin.host.llm.entity; +using LinkToolAddin.host.llm.entity.stream; using Newtonsoft.Json; namespace LinkToolAddin.host.llm; @@ -18,17 +19,22 @@ public class Bailian : Llm public string max_tokens { get; set; } public string app_id { get; set; } public string api_key { get; set; } - public async IAsyncEnumerable SendChatStreamAsync(LlmJsonContent jsonContent) + public async IAsyncEnumerable SendChatStreamAsync(LlmJsonContent jsonContent) { jsonContent.Stream = true; - StringBuilder builder = new StringBuilder(); - await foreach (var chunk in HttpRequest.PostWithStreamingResponseAsync( + StringBuilder contentBuilder = new StringBuilder(); + StringBuilder reasonBuilder = new StringBuilder(); + await foreach (LlmStreamChat 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(); + contentBuilder.Append(chunk.Choices[0].Delta.Content); + reasonBuilder.Append(chunk.Choices[0].Delta.ResoningContent); + LlmStreamChat fullChunk = chunk; + fullChunk.Choices[0].Delta.Content = contentBuilder.ToString(); + fullChunk.Choices[0].Delta.ResoningContent = reasonBuilder.ToString(); + yield return fullChunk; } } diff --git a/host/llm/Llm.cs b/host/llm/Llm.cs index 1807b9d..bb3ba53 100644 --- a/host/llm/Llm.cs +++ b/host/llm/Llm.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using LinkToolAddin.host.llm.entity; +using LinkToolAddin.host.llm.entity.stream; namespace LinkToolAddin.host.llm; @@ -11,7 +12,7 @@ public interface Llm public string top_p { get; set; } public string max_tokens { get; set; } - public IAsyncEnumerable SendChatStreamAsync(LlmJsonContent jsonContent); + 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/LlmJsonContent.cs b/host/llm/entity/LlmJsonContent.cs index 5b53edf..c38382f 100644 --- a/host/llm/entity/LlmJsonContent.cs +++ b/host/llm/entity/LlmJsonContent.cs @@ -25,6 +25,15 @@ namespace LinkToolAddin.host.llm.entity [JsonProperty("top_k")] public int TopK { get; set; } = 40; + + [JsonProperty("enable_thinking")] + public bool EnableThinking { get; set; } = true; + + [JsonProperty("thinking_budget")] + public long ThinkingBudget { get; set; } = 1200; + + [JsonProperty("incremental_output")] + public bool IncrementalOutput { get; set; } = true; } public partial class Message diff --git a/host/llm/entity/stream/LlmStreamChat.cs b/host/llm/entity/stream/LlmStreamChat.cs index 6991982..6813e7e 100644 --- a/host/llm/entity/stream/LlmStreamChat.cs +++ b/host/llm/entity/stream/LlmStreamChat.cs @@ -50,5 +50,7 @@ { [JsonProperty("content")] public string Content { get; set; } + [JsonProperty("reasoning_content")] + public string ResoningContent { get; set; } } } \ No newline at end of file diff --git a/host/mcp/McpToolRequest.cs b/host/mcp/McpToolRequest.cs new file mode 100644 index 0000000..cdbfab0 --- /dev/null +++ b/host/mcp/McpToolRequest.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace LinkToolAddin.host.mcp; + +public class McpToolRequest +{ + public McpServer McpServer { get; set; } + public string ToolName { get; set; } + public Dictionary ToolArgs { get; set; } +} \ No newline at end of file diff --git a/host/prompt/PromptDefinition.cs b/host/prompt/PromptDefinition.cs new file mode 100644 index 0000000..59f93fd --- /dev/null +++ b/host/prompt/PromptDefinition.cs @@ -0,0 +1,9 @@ +using Newtonsoft.Json; + +namespace LinkToolAddin.host.prompt; + +public class PromptDefinition +{ + [JsonProperty("prompt")] + public UserPrompt UserPrompt { get; set; } +} \ No newline at end of file diff --git a/host/prompt/PromptRequest.cs b/host/prompt/PromptRequest.cs new file mode 100644 index 0000000..ffbb966 --- /dev/null +++ b/host/prompt/PromptRequest.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace LinkToolAddin.host.prompt; + +public class PromptRequest +{ + public PromptServer PromptServer { get; set; } + public string PromptName { get; set; } + public Dictionary PromptArgs { get; set; } +} \ No newline at end of file diff --git a/host/prompt/PromptServer.cs b/host/prompt/PromptServer.cs new file mode 100644 index 0000000..709c4a7 --- /dev/null +++ b/host/prompt/PromptServer.cs @@ -0,0 +1,32 @@ +using System.Text; +using System.Text.RegularExpressions; + +namespace LinkToolAddin.host.prompt; + +public class PromptServer +{ + public string Name { get; set; } + public string Description { get; set; } + public string Arguments { get; set; } + public string Content { get; set; } + + public PromptServer(string name, string description, string content) + { + Name = name; + Description = description; + Content = content; + string pattern = "{{([\\s\\S]*?)}}"; + var matches = Regex.Matches(content, pattern); + StringBuilder args = new StringBuilder(); + foreach (var match in matches) + { + string variableName = match.ToString().Replace("{{","").Replace("}}",""); + string arg = "{ \"" + variableName + "\" : \"type\" : \" string \" }"; + args.Append(arg); + } + if (args.ToString() != string.Empty) + { + Arguments = "{\"type\":\"object\",\"properties\":" + args.ToString() + "}"; + } + } +} \ No newline at end of file diff --git a/host/prompt/SystemPrompt.cs b/host/prompt/SystemPrompt.cs index 9f9481d..2295d68 100644 --- a/host/prompt/SystemPrompt.cs +++ b/host/prompt/SystemPrompt.cs @@ -1,37 +1,36 @@ -namespace LinkToolAddin.host.prompt; +using LinkToolAddin.common; + +namespace LinkToolAddin.host.prompt; public class SystemPrompt { - public static string SysPromptTemplate = - "现在你是一个精通地理信息分析和ArcGIS Pro软件的专家,请以此身份回答用户的问题。" + - "指令:您可以使用一组工具来回答用户的问题。还可以通过调用用户提示词,从而使你更好地理解和完成用户的任务。整个流程结束之后单独输出一条内容为'[DONE]'的消息,前后不要有任何说明文字。" + - "调用工具要求:如果要调用工具,每次消息只能使用一个工具,用户的回复中将包含该工具的调用结果。您需要通过逐步使用工具来完成给定任务,每次工具调用需基于前一次的结果。成功调用工具之后应该马上调用下一个工具。你还可以通过的方式来调用用户提示词,你能更好地理解和解决用户的问题。" + - "工具调用背景:你有以下工具可以调用{{toolInfos}},用户的数据库路径是{{gdbPath}}。" + - "输出风格:每次仅调用一个工具,必须基于前序工具的输出结果进行下一步操作。工具调用使用 XML 风格的标签进行格式化以单独信息格式输出,前后没有文字信息。" + - "如果需要进行文字说明,请只有文字内容,以普通段落形式呈现没有其他格式。" + - "工具调用格式:工具名称包含在一对标签内,每个参数也需用对应的标签包裹。结构如下:\n {tool_name}\n {json_arguments}\n。" + - "工具名称:需与所使用工具的精确名称一致。" + - "参数:应为包含工具所需参数的 JSON 对象。例如:\\n gaode:maps_geo\\n {\\\"address\\\":\\\"广州市政府, 广州市\\\", \\\"city\\\":\\\"广州\\\"}\\n" + - "你必须严格遵守以下输出规则:" + - "1.XML工具调用格式必须在下一条单独输出,如果前面有文字将永远无法调用工具。" + - //"只有单独一条的调用请求文本程序才能识别并调用,从而将结果反馈给你。" + - "2.文字说明如果紧跟着工具调用的XML将暴露程序,XML会在下一条单独输出,因此文字描述后面一定不能输出工具调用的XML格式。" + - "3.用户时间宝贵,不得重复调用上一次已成功执行的工具调用,除非有新的参数或上下文变化。" + - "4.如果目前工作已经完成或无法知道用户其他需求时,一定要单独输出一条内容为'[DONE]'的消息表示工具调用结束。不要在文本的末尾。" + - "5.不得在同一消息中混合说明文字与工具调用。" + - "结果:用户将以以下格式返回工具调用结果:\n {tool_name}\n {result}\n。应为字符串类型,可以表示文件或其他输出类型。" + - "工具调用示例:MCP工具调用的格式要求示例:以下是使用虚拟工具的示例:\\n gaode:maps_geo\\n {\\\"address\\\":\\\"广州市政府, 广州市\\\", \\\"city\\\":\\\"广州\\\"}\\n"; - //"现在你是一个精通地理信息分析和ArcGIS Pro软件的专家,请以此身份回答用户的问题。\r\n\r\n指令:\r\n你需要使用一组工具来回答用户的问题。也可以通过 标签调用用户提示词,以帮助你更好地理解任务。所有操作完成后,在最后一条输出中包含 '[DONE]',但不要单独发送这条消息。\r\n\r\n调用工具要求:\r\n1. 每次只能调用一个工具,并等待用户的反馈结果。\r\n2. 所有工具调用都必须基于前一步的结果进行推理和决策。\r\n3. 工具调用必须以 XML 格式输出,并且必须作为**独立的一条消息输出**,前后不得有任何解释性文字或说明内容。\r\n4. 如果需要提供解释、说明或引导信息,请先单独输出一段普通文本,**其中不能包含任何 XML 标签或格式**。\r\n5. 不得重复调用已经成功执行过的工具,除非有新的参数或上下文需要重新调用。\r\n\r\n工具调用背景:\r\n你有以下工具可以调用:{{toolInfos}} \r\n用户的数据库路径是:{{gdbPath}}\r\n\r\n输出风格:\r\n- 所有工具调用都必须使用标准 XML 格式输出,并且每条消息只包含一次调用。\r\n- 文字说明必须单独输出,且为普通段落格式,不含任何 XML 或 Markdown 标记。\r\n- 输出顺序应为:【可选的文字说明】→【必选的工具调用】,或者仅输出工具调用。\r\n\r\n工具调用格式示例:\r\n\r\n {tool_name}\r\n {json_arguments}\r\n\r\n\r\n例如:\r\n\r\n gaode:maps_geo\r\n {\"address\":\"广州市政府, 广州市\", \"city\":\"广州\"}\r\n\r\n\r\n注意事项:\r\n1. 工具名称必须与实际工具完全一致。\r\n2. 参数必须为合法 JSON 格式,且符合工具要求。\r\n3. 用户时间宝贵,请高效调用工具,尽快完成任务。\r\n4. 最终完成时应在最后一次响应中包含 [DONE],而不是单独输出。\r\n\r\n工具调用结果返回格式:\r\n用户将以如下格式返回工具调用结果:\r\n\r\n {tool_name}\r\n {result}\r\n\r\n\r\n例如:\r\n\r\n ArcGIS_Pro:GP\r\n {\"output_file\": \"source.shp\"}\r\n\r\n\r\n请始终遵循此格式以确保工具调用被正确解析和执行。"; - - public static string ContinuePromptTemplate = "这是上述工具调用的结果。{{toolResult}}\n请根据以下执行结果,清晰解释执行结果并执行下一步操作。" + - "执行下一步工具的要求:1. 解析工具输出结果2. 调用下一个工具时确保参数继承前序输出。请据此继续执行"; - - public static string ErrorPromptTemplate = "执行上一个工具的时候出现以下错误,需按以下流程处理:1. 错误解析:分析错误类型及具体原因(见下方错误信息),根据错误信息选择修复方案,```" + - "2. 修复方案:(1)根据错误类型生成对应的工具重试策略(2)参数调整:明确需要修改的参数及修改方式。3. 工具重试:使用调整后的参数重新调用工具。错误信息如下,请根据报错信息重试,4.如果没有具体的错误描述信息请检查输出文件是否已经存在,若已经存在默认执行成功"; + // public static string SysPromptTemplate = + // "现在你是一个精通地理信息分析和ArcGIS Pro软件的专家,请以此身份回答用户的问题。" + + // "指令:您可以使用一组工具来回答用户的问题。还可以通过调用用户提示词,从而使你更好地理解和完成用户的任务。" + + // "调用工具要求:如果要调用工具,每次消息只能使用一个工具,用户的回复中将包含该工具的调用结果。您需要通过逐步使用工具来完成给定任务,每次工具调用需基于前一次的结果。成功调用工具之后应该马上调用下一个工具。你还可以通过的方式来调用用户提示词,你能更好地理解和解决用户的问题。" + + // "工具调用背景:你有以下工具可以调用{{toolInfos}},用户的数据库路径是{{gdbPath}}。" + + // "输出风格:每次仅调用一个工具,必须基于前序工具的输出结果进行下一步操作。工具调用使用 XML 风格的标签进行格式化以单独信息格式输出,前后没有文字信息。" + + // "如果需要进行文字说明,请只有文字内容,以普通段落形式呈现没有其他格式。" + + // "工具调用格式:工具名称包含在一对标签内,每个参数也需用对应的标签包裹。结构如下:\n {tool_name}\n {json_arguments}\n。" + + // "工具名称:需与所使用工具的精确名称一致。" + + // "参数:应为包含工具所需参数的 JSON 对象。例如:\\n gaode:maps_geo\\n {\\\"address\\\":\\\"广州市政府, 广州市\\\", \\\"city\\\":\\\"广州\\\"}\\n" + + // "你必须严格遵守以下输出规则:" + + // "3.用户时间宝贵,不得重复调用上一次已成功执行的工具调用,除非有新的参数或上下文变化。" + + // "结果:用户将以以下格式返回工具调用结果:\n {tool_name}\n {result}\n。应为字符串类型,可以表示文件或其他输出类型。" + + // "工具调用示例:MCP工具调用的格式要求示例:以下是使用虚拟工具的示例:\\n gaode:maps_geo\\n {\\\"address\\\":\\\"广州市政府, 广州市\\\", \\\"city\\\":\\\"广州\\\"}\\n"+ + // "特别地:需要调用ArcGIS Pro工具前必须先查询帮助文档、标准调用名和参数后再进行调用"; + // //"现在你是一个精通地理信息分析和ArcGIS Pro软件的专家,请以此身份回答用户的问题。\r\n\r\n指令:\r\n你需要使用一组工具来回答用户的问题。也可以通过 标签调用用户提示词,以帮助你更好地理解任务。所有操作完成后,在最后一条输出中包含 '[DONE]',但不要单独发送这条消息。\r\n\r\n调用工具要求:\r\n1. 每次只能调用一个工具,并等待用户的反馈结果。\r\n2. 所有工具调用都必须基于前一步的结果进行推理和决策。\r\n3. 工具调用必须以 XML 格式输出,并且必须作为**独立的一条消息输出**,前后不得有任何解释性文字或说明内容。\r\n4. 如果需要提供解释、说明或引导信息,请先单独输出一段普通文本,**其中不能包含任何 XML 标签或格式**。\r\n5. 不得重复调用已经成功执行过的工具,除非有新的参数或上下文需要重新调用。\r\n\r\n工具调用背景:\r\n你有以下工具可以调用:{{toolInfos}} \r\n用户的数据库路径是:{{gdbPath}}\r\n\r\n输出风格:\r\n- 所有工具调用都必须使用标准 XML 格式输出,并且每条消息只包含一次调用。\r\n- 文字说明必须单独输出,且为普通段落格式,不含任何 XML 或 Markdown 标记。\r\n- 输出顺序应为:【可选的文字说明】→【必选的工具调用】,或者仅输出工具调用。\r\n\r\n工具调用格式示例:\r\n\r\n {tool_name}\r\n {json_arguments}\r\n\r\n\r\n例如:\r\n\r\n gaode:maps_geo\r\n {\"address\":\"广州市政府, 广州市\", \"city\":\"广州\"}\r\n\r\n\r\n注意事项:\r\n1. 工具名称必须与实际工具完全一致。\r\n2. 参数必须为合法 JSON 格式,且符合工具要求。\r\n3. 用户时间宝贵,请高效调用工具,尽快完成任务。\r\n4. 最终完成时应在最后一次响应中包含 [DONE],而不是单独输出。\r\n\r\n工具调用结果返回格式:\r\n用户将以如下格式返回工具调用结果:\r\n\r\n {tool_name}\r\n {result}\r\n\r\n\r\n例如:\r\n\r\n ArcGIS_Pro:GP\r\n {\"output_file\": \"source.shp\"}\r\n\r\n\r\n请始终遵循此格式以确保工具调用被正确解析和执行。"; + // + // public static string ContinuePromptTemplate = "这是上述工具调用的结果。{{toolResult}}\n请根据以下执行结果,清晰解释执行结果并执行下一步操作。" + + // "执行下一步工具的要求:1. 解析工具输出结果2. 调用下一个工具时确保参数继承前序输出。请据此继续执行"; + // + // public static string ErrorPromptTemplate = "执行上一个工具的时候出现以下错误\n{{toolResult}}\n,需按以下流程处理:1. 错误解析:分析错误类型及具体原因(见下方错误信息),根据错误信息选择修复方案,```" + + // "2. 修复方案:(1)根据错误类型生成对应的工具重试策略(2)参数调整:明确需要修改的参数及修改方式。3. 工具重试:使用调整后的参数重新调用工具。错误信息如下,请根据报错信息重试,4.如果没有具体的错误描述信息请检查输出文件是否已经存在,若已经存在默认执行成功"+ + // "5.查询可能相关的知识库和帮助文档,纠正调用参数中存在的错误"; public static string SysPrompt(string gdbPath, string toolInfos) { - string sysPrompt = SysPromptTemplate; + string sysPrompt = LocalResource.ReadFileByResource("LinkToolAddin.resource.prompt.SystemPrompt.txt");; sysPrompt = sysPrompt.Replace("{{gdbPath}}", gdbPath); sysPrompt = sysPrompt.Replace("{{toolInfos}}", toolInfos); return sysPrompt; @@ -39,8 +38,15 @@ public class SystemPrompt public static string ContinuePrompt(string toolResult) { - string continuePrompt = ContinuePromptTemplate; + string continuePrompt = LocalResource.ReadFileByResource("LinkToolAddin.resource.prompt.ContinuePrompt.txt"); continuePrompt = continuePrompt.Replace("{{toolResult}}", toolResult); return continuePrompt; } + + public static string ErrorPrompt(string toolResult) + { + string errPrompt = LocalResource.ReadFileByResource("LinkToolAddin.resource.prompt.ErrorPrompt.txt"); + errPrompt = errPrompt.Replace("{{toolResult}}", toolResult); + return errPrompt; + } } \ No newline at end of file diff --git a/host/prompt/UserPrompt.cs b/host/prompt/UserPrompt.cs index 128de5b..00c1cd8 100644 --- a/host/prompt/UserPrompt.cs +++ b/host/prompt/UserPrompt.cs @@ -1,6 +1,13 @@ -namespace LinkToolAddin.host.prompt; +using Newtonsoft.Json; + +namespace LinkToolAddin.host.prompt; public class UserPrompt { - + [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/resource/prompt/ContinuePrompt.txt b/resource/prompt/ContinuePrompt.txt new file mode 100644 index 0000000..deb62c7 --- /dev/null +++ b/resource/prompt/ContinuePrompt.txt @@ -0,0 +1,11 @@ +这是上述工具调用的结果。 + +{{toolResult}} + +请根据以下执行结果,清晰解释执行结果并执行下一步操作。 + +执行下一步工具的要求: +1. 解析工具输出结果 +2. 调用下一个工具时确保参数继承前序输出。 + +请据此继续执行 \ No newline at end of file diff --git a/resource/prompt/ErrorPrompt.txt b/resource/prompt/ErrorPrompt.txt new file mode 100644 index 0000000..3257463 --- /dev/null +++ b/resource/prompt/ErrorPrompt.txt @@ -0,0 +1,12 @@ +执行上一个工具的时候出现以下错误 + +{{toolResult}} + +需按以下流程处理: +1. 错误解析:分析错误类型及具体原因(见下方错误信息),根据错误信息选择修复方案, +2. 修复方案: +(1)根据错误类型生成对应的工具重试策略 +(2)参数调整:明确需要修改的参数及修改方式。 +3. 工具重试:使用调整后的参数重新调用工具。错误信息如下,请根据报错信息重试, +4.如果没有具体的错误描述信息请检查输出文件是否已经存在,若已经存在默认执行成功 +5.查询可能相关的知识库和帮助文档,纠正调用参数中存在的错误 \ No newline at end of file diff --git a/resource/prompt/SystemPrompt.txt b/resource/prompt/SystemPrompt.txt new file mode 100644 index 0000000..a248f9f --- /dev/null +++ b/resource/prompt/SystemPrompt.txt @@ -0,0 +1,40 @@ +现在你是一个精通地理信息分析和ArcGIS Pro软件的专家,请以此身份回答用户的问题。 + +指令:您可以使用一组工具来回答用户的问题。还可以通过调用用户提示词,从而使你更好地理解和完成用户的任务。 + +调用工具要求:如果要调用工具,每次消息只能使用一个工具,用户的回复中将包含该工具的调用结果。您需要通过逐步使用工具来完成给定任务,每次工具调用需基于前一次的结果。成功调用工具之后应该马上调用下一个工具。你还可以通过的方式来调用用户提示词,你能更好地理解和解决用户的问题。工具调用背景:你有以下工具可以调用{{toolInfos}},用户的数据库路径是{{gdbPath}}。 + +输出风格:每次仅调用一个工具,必须基于前序工具的输出结果进行下一步操作。工具调用使用 XML 风格的标签进行格式化以单独信息格式输出,前后没有文字信息。如果需要进行文字说明,请只有文字内容,以普通段落形式呈现没有其他格式。 + +工具调用格式:工具名称包含在一对标签内,每个参数也需用对应的标签包裹。结构如下: + + + {tool_name} + {json_arguments} +。 + +工具名称:需与所使用工具的精确名称一致。 +参数:应为包含工具所需参数的 JSON 对象。 +例如: + + gaode:maps_geo + {\\\"address\\\":\\\"广州市政府, 广州市\\\", \\\"city\\\":\\\"广州\\\"} + + +你必须严格遵守以下输出规则:用户时间宝贵,不得重复调用上一次已成功执行的工具调用,除非有新的参数或上下文变化。 + +结果:用户将以以下格式返回工具调用结果: + + {tool_name} + {result} +。 + +应为字符串类型,可以表示文件或其他输出类型。 + +工具调用示例:MCP工具调用的格式要求示例:以下是使用虚拟工具的示例: + + gaode:maps_geo + {\\\"address\\\":\\\"广州市政府, 广州市\\\", \\\"city\\\":\\\"广州\\\"} + + +特别地:需要调用ArcGIS Pro工具前必须先查询帮助文档、标准调用名和参数后再进行调用 \ No newline at end of file diff --git a/server/JsonRpcResultEntity.cs b/server/JsonRpcResultEntity.cs index 5d80803..f97442a 100644 --- a/server/JsonRpcResultEntity.cs +++ b/server/JsonRpcResultEntity.cs @@ -7,7 +7,7 @@ namespace LinkToolAddin.server public partial class JsonRpcResultEntity { [JsonProperty("jsonrpc")] - public string Jsonrpc { get; set; } + public string Jsonrpc { get; set; } = "2.0"; [JsonProperty("id")] public long Id { get; set; } diff --git a/ui/dockpane/TestDockpane.xaml b/ui/dockpane/TestDockpane.xaml index 53c1501..6d4ae00 100644 --- a/ui/dockpane/TestDockpane.xaml +++ b/ui/dockpane/TestDockpane.xaml @@ -24,12 +24,13 @@ + - + @@ -39,5 +40,6 @@ + \ No newline at end of file diff --git a/ui/dockpane/TestDockpane.xaml.cs b/ui/dockpane/TestDockpane.xaml.cs index 0286d87..267b7ea 100644 --- a/ui/dockpane/TestDockpane.xaml.cs +++ b/ui/dockpane/TestDockpane.xaml.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Reflection; @@ -8,6 +9,7 @@ using System.Windows; using System.Windows.Controls; using System.Xml.Linq; using LinkToolAddin.client; +using LinkToolAddin.common; using LinkToolAddin.host; using LinkToolAddin.host.llm; using LinkToolAddin.host.llm.entity; @@ -23,6 +25,7 @@ using log4net.Layout; using ModelContextProtocol.Client; using ModelContextProtocol.Protocol.Types; using Newtonsoft.Json; +using MessageBox = ArcGIS.Desktop.Framework.Dialogs.MessageBox; namespace LinkToolAddin.ui.dockpane @@ -35,7 +38,7 @@ namespace LinkToolAddin.ui.dockpane private static ILog log = LogManager.GetLogger(typeof(TestDockpaneView)); private List idList = new List(); - private Dictionary messageDict = new Dictionary(); + private ConcurrentDictionary messageDict = new ConcurrentDictionary(); public TestDockpaneView() { @@ -195,14 +198,26 @@ namespace LinkToolAddin.ui.dockpane log.Info(msg.content); } - private void PromptTestButton_OnClick(object sender, RoutedEventArgs e) + private async void PromptTestButton_OnClick(object sender, RoutedEventArgs e) { string userPrompt = PromptTestTextBox.Text; - // Gateway.SendMessage(userPrompt,"qwen-max","C:/Project/test.gdb",AddReply); - Gateway.SendMessageStream(userPrompt,"qwen-max", "F:\\secondsemester\\linktool\\test\\linktooltest\\linktooltest.gdb", AddReplyStream); + // model可选值:qwen3-235b-a22b,qwen-max,deepseek-r1 + try + { + await Task.Run(() => Gateway.SendMessageStream(userPrompt,"qwen3-235b-a22b", "D:\\01_Project\\20250305_LinkTool\\20250420_AiDemoProject\\20250420_AiDemoProject.gdb", AddReplyStream)); + } + catch (Exception exception) + { + log.Error(exception.Message); + } } - public void AddReplyStream(MessageListItem msg) + public async void AddReplyStream(MessageListItem msg) + { + await Task.Run(() => ProcessReplyStream(msg)); + } + + private void ProcessReplyStream(MessageListItem msg) { string id = msg.id; if (idList.Contains(id)) @@ -212,16 +227,34 @@ namespace LinkToolAddin.ui.dockpane else { idList.Add(id); - messageDict.Add(msg.id, msg); + messageDict.TryAdd(msg.id, msg); } - ReplyTextBox.Clear(); - StringBuilder builder = new StringBuilder(); - foreach (KeyValuePair pair in messageDict) + Application.Current.Dispatcher.Invoke(() => { - MessageListItem msgItem = pair.Value; - builder.AppendLine(msgItem.content); - ReplyTextBox.Text = builder.ToString(); - ReplyTextBox.ScrollToEnd(); + ReplyTextBox.Clear(); + }); + try + { + StringBuilder builder = new StringBuilder(); + foreach (KeyValuePair pair in messageDict) + { + MessageListItem msgItem = pair.Value; + string content = msgItem.content; + if (msgItem.type == MessageType.REASON_MESSAGE) + { + content = "" + content + ""; + } + builder.AppendLine(content); + builder.AppendLine(); + } + Application.Current.Dispatcher.Invoke(() => + { + ReplyTextBox.Text = builder.ToString(); + ReplyTextBox.ScrollToEnd(); + }); + }catch (Exception exception) + { + log.Error(exception.Message); } } @@ -245,6 +278,7 @@ namespace LinkToolAddin.ui.dockpane private async void TestArcGisTool_OnClick(object sender, RoutedEventArgs e) { + Type type1 = Type.GetType("LinkToolAddin.client.tool.ArcGisPro"); string xmlStr = "\nArcGisPro:ArcGisProTool\n{\"toolName\": \"Buffer\", \"toolParams\": [\"D:\\\\01_Project\\\\20250305_LinkTool\\\\20250420_AiDemoProject\\\\20250420_AiDemoProject.gdb\\\\LandUse_2005_Copy\", \"D:\\\\01_Project\\\\20250305_LinkTool\\\\20250420_AiDemoProject\\\\20250420_AiDemoProject.gdb\\\\LandUse_2005_Buffer30m\", \"30 Meters\", \"NONE\", \"ROUND\", \"ALL\"]}\n"; XElement toolUse = XElement.Parse(xmlStr); @@ -274,5 +308,11 @@ namespace LinkToolAddin.ui.dockpane AddReply(toolMessageItem); } } + + private void TestResource_OnClick(object sender, RoutedEventArgs e) + { + string content = LocalResource.ReadFileByResource("LinkToolAddin.resource.SystemPrompt.txt"); + MessageBox.Show(content); + } } } diff --git a/ui/message/MessageListItem.cs b/ui/message/MessageListItem.cs index 1c58cbd..9b97709 100644 --- a/ui/message/MessageListItem.cs +++ b/ui/message/MessageListItem.cs @@ -4,6 +4,7 @@ public enum MessageType { TOOL_MESSAGE, CHAT_MESSAGE, + REASON_MESSAGE, } public interface MessageListItem