diff --git a/Config.daml b/Config.daml index 9147f6f..18a04f2 100644 --- a/Config.daml +++ b/Config.daml @@ -23,9 +23,10 @@ - + + + + diff --git a/LinkToolAddin.csproj b/LinkToolAddin.csproj index 818ba95..26846fa 100644 --- a/LinkToolAddin.csproj +++ b/LinkToolAddin.csproj @@ -1,11 +1,18 @@  - net8.0-windows true win-x64 false true CA1416 + net8.0-windows + 0.1.3 + LinkToolAddin + LinkTool团队 + LinkTool以大模型赋能让您只需一两句话便能完成复杂的空间分析与时空大数据处理任务。 + 华南农业大学 + 校AI大赛提交版本 + 华南农业大学 @@ -22,85 +29,124 @@ - C:\Users\PeterZhong\AppData\Local\Programs\ArcGIS\Pro\bin\ArcGIS.Desktop.Framework.dll + C:\Program Files\ArcGIS\Pro\bin\ArcGIS.Desktop.Framework.dll False False - C:\Users\PeterZhong\AppData\Local\Programs\ArcGIS\Pro\bin\ArcGIS.Core.dll + C:\Program Files\ArcGIS\Pro\bin\ArcGIS.Core.dll False False - C:\Users\PeterZhong\AppData\Local\Programs\ArcGIS\Pro\bin\Extensions\Core\ArcGIS.Desktop.Core.dll + C:\Program Files\ArcGIS\Pro\bin\Extensions\Core\ArcGIS.Desktop.Core.dll False False - C:\Users\PeterZhong\AppData\Local\Programs\ArcGIS\Pro\bin\Extensions\Mapping\ArcGIS.Desktop.Mapping.dll + C:\Program Files\ArcGIS\Pro\bin\Extensions\Mapping\ArcGIS.Desktop.Mapping.dll False False - C:\Users\PeterZhong\AppData\Local\Programs\ArcGIS\Pro\bin\Extensions\Catalog\ArcGIS.Desktop.Catalog.dll + C:\Program Files\ArcGIS\Pro\bin\Extensions\Catalog\ArcGIS.Desktop.Catalog.dll False False - C:\Users\PeterZhong\AppData\Local\Programs\ArcGIS\Pro\bin\Extensions\Editing\ArcGIS.Desktop.Editing.dll + C:\Program Files\ArcGIS\Pro\bin\Extensions\Editing\ArcGIS.Desktop.Editing.dll False False - C:\Users\PeterZhong\AppData\Local\Programs\ArcGIS\Pro\bin\Extensions\DesktopExtensions\ArcGIS.Desktop.Extensions.dll + C:\Program Files\ArcGIS\Pro\bin\Extensions\DesktopExtensions\ArcGIS.Desktop.Extensions.dll False False - C:\Users\PeterZhong\AppData\Local\Programs\ArcGIS\Pro\bin\Extensions\GeoProcessing\ArcGIS.Desktop.GeoProcessing.dll + C:\Program Files\ArcGIS\Pro\bin\Extensions\GeoProcessing\ArcGIS.Desktop.GeoProcessing.dll False False - C:\Users\PeterZhong\AppData\Local\Programs\ArcGIS\Pro\bin\Extensions\Layout\ArcGIS.Desktop.Layouts.dll + C:\Program Files\ArcGIS\Pro\bin\Extensions\Layout\ArcGIS.Desktop.Layouts.dll False False - C:\Users\PeterZhong\AppData\Local\Programs\ArcGIS\Pro\bin\Extensions\KnowledgeGraph\ArcGIS.Desktop.KnowledgeGraph.dll + C:\Program Files\ArcGIS\Pro\bin\Extensions\KnowledgeGraph\ArcGIS.Desktop.KnowledgeGraph.dll False False - C:\Users\PeterZhong\AppData\Local\Programs\ArcGIS\Pro\bin\ArcGIS.Desktop.Shared.Wpf.dll + C:\Program Files\ArcGIS\Pro\bin\ArcGIS.Desktop.Shared.Wpf.dll False False - C:\Users\PeterZhong\AppData\Local\Programs\ArcGIS\Pro\bin\ArcGIS.Desktop.Ribbon.Wpf.dll + C:\Program Files\ArcGIS\Pro\bin\ArcGIS.Desktop.Ribbon.Wpf.dll False False - C:\Users\PeterZhong\AppData\Local\Programs\ArcGIS\Pro\bin\ArcGIS.Desktop.DataGrid.Contrib.Wpf.dll + C:\Program Files\ArcGIS\Pro\bin\ArcGIS.Desktop.DataGrid.Contrib.Wpf.dll False False - C:\Users\PeterZhong\AppData\Local\Programs\ArcGIS\Pro\bin\ArcGIS.Desktop.Resources.dll + C:\Program Files\ArcGIS\Pro\bin\ArcGIS.Desktop.Resources.dll False False - C:\Users\PeterZhong\AppData\Local\Programs\ArcGIS\Pro\bin\ESRI.ArcGIS.ItemIndex.dll + C:\Program Files\ArcGIS\Pro\bin\ESRI.ArcGIS.ItemIndex.dll False False - - - + + + + + + - + + + + + + Always + + + + Always + + + + Always + + + + Always + + + + Always + + + + Always + + + + Always + + + + Always + + + diff --git a/Properties/launchSettings.json b/Properties/launchSettings.json index f8e1bfb..165b000 100644 --- a/Properties/launchSettings.json +++ b/Properties/launchSettings.json @@ -1,8 +1,12 @@ { "profiles": { "LinkToolAddin": { - "commandName": "Executable", - "executablePath": "C:\\Users\\PeterZhong\\AppData\\Local\\Programs\\ArcGIS\\Pro\\bin\\ArcGISPro.exe" + "commandName": "Executable", + "executablePath": "C:\\Users\\86158\\AppData\\Local\\Programs\\ArcGIS\\Pro\\bin\\ArcGISPro.exe", + "applicationUrl": "https://localhost:5001", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } } } } \ No newline at end of file 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/CallArcGISPro.cs b/client/CallArcGISPro.cs new file mode 100644 index 0000000..a3c00e4 --- /dev/null +++ b/client/CallArcGISPro.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using LinkToolAddin.server; +using Newtonsoft.Json; + +namespace LinkToolAddin.client; + +public class CallArcGISPro +{ + public async static Task CallArcGISProTool(Dictionary parameters) + { + // Call the ArcGIS Pro method and get the result + var result = await server.CallArcGISPro.CallArcGISProTool(parameters["toolName"], JsonConvert.DeserializeObject>(parameters["toolParams"])); + + // Serialize the result back to a JSON string + return JsonConvert.SerializeObject(result); + } +} \ No newline at end of file diff --git a/client/McpClient.cs b/client/McpClient.cs new file mode 100644 index 0000000..c03734b --- /dev/null +++ b/client/McpClient.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using ModelContextProtocol.Client; +using ModelContextProtocol.Protocol.Types; + +namespace LinkToolAddin.client; + +public interface McpClient +{ + public Task> GetToolListAsync(); + + public Task CallToolAsync(string toolName, Dictionary parameters = null); +} \ No newline at end of file diff --git a/client/SseMcpClient.cs b/client/SseMcpClient.cs new file mode 100644 index 0000000..6af5c55 --- /dev/null +++ b/client/SseMcpClient.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using ModelContextProtocol.Client; +using ModelContextProtocol.Protocol.Transport; +using ModelContextProtocol.Protocol.Types; +using Newtonsoft.Json; + +namespace LinkToolAddin.client; + +public class SseMcpClient : McpClient +{ + private SseClientTransportOptions options; + private IClientTransport transport; + public SseMcpClient(string url) + { + options = new SseClientTransportOptions + { + Endpoint = new Uri(url), + }; + transport = new SseClientTransport(options); + } + public async Task> GetToolListAsync() + { + // 创建 MCP Client + IMcpClient client = await McpClientFactory.CreateAsync(transport); + var tools = await client.ListToolsAsync(); + return tools; + } + + public async Task CallToolAsync(string toolName,Dictionary parameters = null) + { + IMcpClient client = await McpClientFactory.CreateAsync(transport); + CallToolResponse result = await client.CallToolAsync(toolName,parameters); + return result; + } +} \ No newline at end of file diff --git a/client/StdioMcpClient.cs b/client/StdioMcpClient.cs new file mode 100644 index 0000000..64933ad --- /dev/null +++ b/client/StdioMcpClient.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Windows.Documents; +using ModelContextProtocol.Protocol.Types; +using Newtonsoft.Json; + +namespace LinkToolAddin.client; + +using ModelContextProtocol.Client; +using ModelContextProtocol.Protocol.Transport; + +public class StdioMcpClient : McpClient +{ + private List arguments; + private StdioClientTransportOptions transportOptions; + + public StdioMcpClient(string command,List arguments) + { + this.arguments = arguments; + transportOptions = new StdioClientTransportOptions() + { + Command = command, + Arguments = this.arguments + }; + } + + public static async Task StdioMcpTest() + { + List arguments = new List(); + arguments.Add("mcp-server-time"); + arguments.Add("--local-timezone=America/New_York"); + StdioClientTransportOptions transportOptions = new StdioClientTransportOptions() + { + Command = "uvx", // 运行服务器的命令 + Arguments = arguments + }; + var client = await McpClientFactory.CreateAsync(new StdioClientTransport(transportOptions)); + var tools = await client.ListToolsAsync(); + Console.WriteLine("Available Tools:"); + foreach (var tool in tools) + { + Console.WriteLine($"- {tool.Name}"); + } + + var result = await client.CallToolAsync("get_current_time", + new Dictionary { { "timezone", "America/New_York" } }); + Console.WriteLine(JsonConvert.SerializeObject(result)); + return tools[0].Name; + } + + public async Task> GetToolListAsync() + { + 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) + { + IMcpClient client = await McpClientFactory.CreateAsync(new StdioClientTransport(transportOptions)); + CallToolResponse result = await client.CallToolAsync(toolName,parameters); + return result; + } +} \ No newline at end of file diff --git a/client/prompt/DynamicPrompt.cs b/client/prompt/DynamicPrompt.cs new file mode 100644 index 0000000..c63c9fa --- /dev/null +++ b/client/prompt/DynamicPrompt.cs @@ -0,0 +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) + { + PromptServerList promptServerList = new PromptServerList(); + string template = promptServerList.GetPromptServer(name).Content; + if (args == null) + { + return template; + } + foreach (KeyValuePair pair in args) + { + string replaceKey = "{{"+pair.Key+"}}"; + template = template.Replace(replaceKey, pair.Value.ToString()); + } + return template; + } + + public static List GetAllPrompts() + { + 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 new file mode 100644 index 0000000..6f9eaa1 --- /dev/null +++ b/client/prompt/PromptTemplates.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using LinkToolAddin.host.prompt; + +namespace LinkToolAddin.client.prompt; + +public class PromptTemplates +{ + private Dictionary prompts = new Dictionary(); + + public PromptTemplates() + { + prompts.Add("plan", "请根据用户所提问题进行工具规划,输出格式为'1.工具2.工具’,如果是ArcGIS Pro的工具,根据用户的具体需求和提供的数据类型," + + "判断并列出所有必要的分析步骤和工具,同时严格遵守知识库中“ArcGIS Pro工具调用大全”里“工具调用名称”一列的工具命名规则(例如:analysis.Erase)," + + "确保工具名的准确无误,工具与所属工具箱的对应关系正确无误,严格遵循文档规定的格式和大小写。工具的组合顺序优先参考知识库中“ArcGIS Pro的帮助文档”。" + + "有一些与分析无关的数据能够进行排除,在选择工具时不受其干扰。"); + prompts.Add("param", "根据帮助文档填写工具参数,请你根据“所需调用工具”,参照知识库“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\""); + prompts.Add("code", "根据你在多种编程语言、框架、设计模式和最佳实践方面拥有的广泛知识。现在需要根据用户需求生成高质量的代码,并确保语法正确。" + + "编写Arcpy代码时必须符合ArcGIS官方文档要求。参考官方文档的方法参数,确保编写正确。"); + } + + public string GetPrompt(string name) + { + 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 new file mode 100644 index 0000000..8e2ce0a --- /dev/null +++ b/client/tool/ArcGisPro.cs @@ -0,0 +1,223 @@ +using System; +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; + +namespace LinkToolAddin.client.tool; + +public class ArcGisPro +{ + private static ILog log = LogManager.GetLogger(typeof(ArcGisPro)); + [McpServerTool, Description("可以通过调用ArcGIS Pro的地理处理工具实现一些数据处理功能,传入参数必须严格遵循ArcGIS Pro调用工具的标准调用名和参数要求知识库。")] + public static async Task ArcGisProTool(string toolName, List toolParams) + { + IGPResult results = await Geoprocessing.ExecuteToolAsync(toolName, toolParams,null,null,null,GPExecuteToolFlags.InheritGPOptions|GPExecuteToolFlags.GPThread); + JsonRpcResultEntity jsonRpcResultEntity; + if (results.IsFailed) + { + log.Error(results.ErrorMessages); + jsonRpcResultEntity = new JsonRpcErrorEntity() + { + Error = new Error() + { + Code = results.ErrorCode.ToString(), + Message = GetMessagesString(results.ErrorMessages)+"\n"+GetMessagesString(results.Messages) + } + }; + }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坐标,获取字段列表等")] + public static async Task DataProperty(string datasetPath,string dataName) + { + JsonRpcResultEntity result = new JsonRpcResultEntity(); + await QueuedTask.Run(() => + { + 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; + } + + [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; + } + + [McpServerTool, Description("获取要素类的属性表内容,可以查看属性表中的至多前20条记录的内容")] + public static async Task GetFeatureDatasetAttributeTable(string datasetPath, string dataName, string rowsLimit) + { + JsonRpcResultEntity result = new JsonRpcResultEntity(); + await QueuedTask.Run(async () => + { + try + { + using Geodatabase gdb = new Geodatabase(new FileGeodatabaseConnectionPath(new Uri(datasetPath))); + FeatureClass featureClass = gdb.OpenDataset(dataName); + List> attributeTable = await GetAttributeTableAsync(featureClass,Convert.ToInt32(rowsLimit)); + result = new JsonRpcSuccessEntity() + { + Id = 1, + Result = JsonConvert.SerializeObject(attributeTable) + }; + return result; + }catch (Exception ex) + { + result = new JsonRpcErrorEntity() + { + Error = new Error() + { + Message = ex.Message, + Code = "500" + } + }; + return result; + } + }); + return result; + } + + private static async Task>> GetAttributeTableAsync(FeatureClass featureClass,int limit = 5) + { + if (limit > 20) + limit = 20; + return await QueuedTask.Run(() => + { + var result = new List>(); + + using (var cursor = featureClass.Search()) + { + int i = 0; + while (cursor.MoveNext()) + { + i++; + if (i >= limit && limit != -1) + break; + var feature = cursor.Current as Feature; + if (feature == null) + continue; + + var record = new Dictionary(); + + foreach (var field in featureClass.GetDefinition().GetFields()) + { + var value = feature[field.Name]; + + // 处理 DBNull 值 + if (value is DBNull) + value = null; + + record[field.Name] = value.ToString(); + } + + result.Add(record); + } + } + + 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/client/tool/KnowledgeBase.cs b/client/tool/KnowledgeBase.cs new file mode 100644 index 0000000..38fe949 --- /dev/null +++ b/client/tool/KnowledgeBase.cs @@ -0,0 +1,49 @@ +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; + } + + [McpServerTool, Description("查询ArcGIS Pro调用工具的标准调用名和参数要求知识库")] + public static async Task QueryArcgisToolDoc(string query) + { + DocDb docDb = new DocDb("sk-db177155677e438f832860e7f4da6afc", DocDb.KnowledgeBase.ArcGISProToolDoc); + KnowledgeResult knowledgeResult = await docDb.Retrieve(query); + JsonRpcResultEntity result = new JsonRpcSuccessEntity() + { + Result = JsonConvert.SerializeObject(knowledgeResult.ChunkList), + }; + return result; + } + + [McpServerTool, Description("查询使用ArcGIS Pro进行任务规划和解决实际问题的案例知识库")] + public static async Task QueryArcgisExampleDoc(string query) + { + DocDb docDb = new DocDb("sk-db177155677e438f832860e7f4da6afc", DocDb.KnowledgeBase.ArcGISProApplicantExample); + 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/HttpRequest.cs b/common/HttpRequest.cs new file mode 100644 index 0000000..5d42052 --- /dev/null +++ b/common/HttpRequest.cs @@ -0,0 +1,126 @@ +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; + +public class HttpRequest +{ + public static async Task SendPostRequestAsync(string url, string jsonContent, string apiKey) + { + using var httpClient = new HttpClient(); + httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey); + var response = await httpClient.PostAsync(url, new StringContent(jsonContent, Encoding.UTF8, "application/json")); + response.EnsureSuccessStatusCode(); + 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; + } + } + } + } + } + } + } + } +} \ 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/common/LocalResource.cs b/common/LocalResource.cs new file mode 100644 index 0000000..4f4e53a --- /dev/null +++ b/common/LocalResource.cs @@ -0,0 +1,47 @@ +using System; +using System.IO; +using System.Windows.Controls; +using System.Windows.Media.Imaging; + +namespace LinkToolAddin.common; + +public class LocalResource +{ + public static string ReadFileByResource(string resourceName) + { + var assembly = System.Reflection.Assembly.GetExecutingAssembly(); + + using (Stream stream = assembly.GetManifestResourceStream(resourceName)) + { + if (stream == null) + { + return($"找不到嵌入资源:{resourceName}"); + } + + using (StreamReader reader = new StreamReader(stream)) + { + return reader.ReadToEnd(); + } + } + } + + public static BitmapImage ReadImageByResource(string resourceName) + { + var assembly = System.Reflection.Assembly.GetExecutingAssembly(); + using (Stream stream = assembly.GetManifestResourceStream(resourceName)) + { + if (stream == null) + { + Console.WriteLine("资源未找到,请检查资源名称是否正确。"); + return null; + } + + // 如果是 WPF,可以转换为 BitmapImage + BitmapImage bitmap = new BitmapImage(); + bitmap.BeginInit(); + bitmap.StreamSource = stream; + bitmap.EndInit(); + return bitmap; + } + } +} \ No newline at end of file diff --git a/doc/TodoList.md b/doc/TodoList.md new file mode 100644 index 0000000..f674f27 --- /dev/null +++ b/doc/TodoList.md @@ -0,0 +1,28 @@ +# 待办事项 + +本文档用于记录和跟踪当前阶段待办事项及完成进度 + +## 核心程序 + +- [x] 提示词调用完整实现:以面向对象形式取代字典形式,加入参数和描述 +- [x] 接入本地文件系统等基础性MCP服务 +- [ ] Python代码执行的实现 +- [x] 适配qwen3、deepseek等推理模型并迁移 +- [x] 工具执行错误消息合并为一条 + +## 提示词工程 + +- [x] 将原来的单独XML规则修改为内嵌XML规则 +- [x] 系统提示词明确提示调用动态Prompt和知识库 +- [x] 错误和继续提示词预留工具消息占位符 +- [x] 错误和继续提示词明示 +- [ ] 系统提示词泛化增强 +- [ ] 针对qwen3适当调整 + +## 前端交互 + +- [ ] 三类消息卡片 +- [ ] 流式输出,根据id匹配修改或新增 +- [ ] 添加工作空间、发送、复制粘贴等交互 +- [ ] 独立UI线程 +- [ ] 工具卡片特殊提示,弹出窗口显示内容 \ No newline at end of file diff --git a/host/CallMcp.cs b/host/CallMcp.cs new file mode 100644 index 0000000..0e48789 --- /dev/null +++ b/host/CallMcp.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using LinkToolAddin.server; +using log4net; +using Newtonsoft.Json; + +namespace LinkToolAddin.host +{ + public class CallMcp + { + private static readonly ILog log = LogManager.GetLogger(typeof(CallMcp)); + public static async Task CallInnerMcpTool(string jsonRpcString) + { + log.Info("通过反射调用内部MCP工具"); + var jsonRpcEntity = JsonConvert.DeserializeObject(jsonRpcString); + + 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); + } + } +} \ No newline at end of file diff --git a/host/Gateway.cs b/host/Gateway.cs new file mode 100644 index 0000000..ad85e57 --- /dev/null +++ b/host/Gateway.cs @@ -0,0 +1,915 @@ +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.Windows; +using System.Windows.Documents; +using System.Xml; +using System.Xml.Linq; +using ArcGIS.Desktop.Internal.Mapping; +using ArcGIS.Desktop.Internal.Mapping.Locate; +using LinkToolAddin.client; +using LinkToolAddin.client.prompt; +using LinkToolAddin.client.tool; +using LinkToolAddin.host.llm; +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.Linq; +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; + +public class Gateway +{ + private static ILog log = LogManager.GetLogger(typeof(Gateway)); + private static bool goOn = true; + + public static void StopConversation() + { + goOn = false; + } + + public static async void SendMessage(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 pattern = "^[\\s\\S]*?<\\/tool_use>$"; + string promptPattern = "^[\\s\\S]*?<\\/prompt>$"; + McpServerList mcpServerList = new McpServerList(); + while (goOn) + { + string reponse = await bailian.SendChatAsync(new LlmJsonContent() + { + Model = model, + Messages = messages, + Temperature = 0.3, + TopP = 0.6, + TopK = 25, + MaxTokens = 1024, + ThinkingBudget = 1024 + }); + long timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + log.Info(reponse); + messages.Add(new Message + { + Role = "assistant", + Content = reponse + }); + if (Regex.IsMatch(reponse, pattern)) + { + //工具类型的消息 + XElement toolUse = XElement.Parse(reponse); + 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.ErrorPrompt(JsonConvert.SerializeObject(toolResponse)) : SystemPrompt.ContinuePrompt(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.ErrorPrompt(JsonConvert.SerializeObject(toolResponse)) : SystemPrompt.ContinuePrompt(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.ErrorPrompt(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.ContinuePrompt(JsonConvert.SerializeObject(innerResult)) + }); + callback?.Invoke(toolMessageItem); + } + } + } + else if (Regex.IsMatch(reponse, promptPattern)) + { + XElement prompt = XElement.Parse(reponse); + string fullPromptName = prompt.Element("name")?.Value; + string promptArgs = prompt.Element("arguments")?.Value; + 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, null); + messages.Add(new Message + { + Role = "user", + Content = promptRes + }); + } + else + { + MessageListItem chatMessageListItem = new ChatMessageItem() + { + content = reponse, + role = "assistant", + type = MessageType.CHAT_MESSAGE, + id = timestamp.ToString() + }; + callback?.Invoke(chatMessageListItem); + } + 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 + { + api_key = "sk-db177155677e438f832860e7f4da6afc" + }; + List messages = new List(); + string toolInfos = ""; + try + { + toolInfos = await GetToolInfos(new McpServerList()); + messages.Add(new Message + { + Role = "system", + Content = SystemPrompt.SysPrompt(gdbPath, toolInfos) + }); + messages.Add(new Message + { + Role = "user", + Content = message + }); + }catch (Exception ex) + { + log.Error(ex); + MessageBox.Show(ex.Message,"获取MCP列表失败"); + } + goOn = true; + 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 + bool queriedKnowledge = false; + bool executedTool = false; + while (goOn) + { + loop++; + if (loop > 500) + { + MessageBox.Show("达到最大循环次数", "退出循环"); + break; + } + LlmJsonContent jsonContent = new LlmJsonContent() + { + Model = model, + Messages = messages, + Temperature = 0.3, + TopP = 0.4, + TopK = 7, + MaxTokens = 1000, + }; + long timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + List mcpToolRequests = new List(); + List promptRequests = new List(); + var (toolMatched, toolRemaining) = ExtractMatchedPart(messageContent, toolPattern); + var (promptMatched, promptRemaining) = ExtractMatchedPart(messageContent, promptPattern); + if (toolMatched == "" && promptMatched == "" && messageContent != "") + { + //如果本次回复不包含任何工具的调用或提示词的调用,则不再请求 + goOn = false; + MessageListItem endMessageListItem1 = new ChatMessageItem + { + type = MessageType.END_TAG, + content = "", + id = (timestamp + 3).ToString(), + role = "assistant" + }; + callback?.Invoke(endMessageListItem1); + break; + } + + try + { + await foreach(LlmStreamChat llmStreamChat in bailian.SendChatStreamAsync(jsonContent)) + { + if (!goOn) + { + MessageListItem endMessageListItem2 = new ChatMessageItem + { + type = MessageType.END_TAG, + content = "", + id = (timestamp+3).ToString(), + role = "assistant" + }; + callback?.Invoke(endMessageListItem2); + break; + } + try + { + string chunk = llmStreamChat.Choices[0].Delta.Content; + MessageListItem reasonMessageListItem = new ChatMessageItem() + { + content = llmStreamChat.Choices[0].Delta.ResoningContent, + role = "assistant", + type = MessageType.REASON_MESSAGE, + id = (timestamp+2).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 == "") + { + //普通消息文本 + 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 + { + //包含工具调用请求的消息 + 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); + //将工具调用请求添加至列表中待一次回答完整后再统一进行调用 + MessageListItem chatMessageListItem = new ChatMessageItem() + { + content = remaining, + role = "assistant", + type = MessageType.CHAT_MESSAGE, + id = timestamp.ToString() + }; + Application.Current.Dispatcher.Invoke(() => + { + callback?.Invoke(chatMessageListItem); + }); + if (!executedTool) + { + MessageListItem toolMessageListItem = new ToolMessageItem() + { + content = toolName, + result = "工具运行中" + toolName, + toolName = toolName, + toolParams = toolParams, + role = "user", + type = MessageType.TOOL_MESSAGE, + id = (timestamp+1).ToString(), + status = "loading" + }; + Application.Current.Dispatcher.Invoke(() => + { + callback?.Invoke(toolMessageListItem); + }); + } + mcpToolRequests = new List(); + McpToolRequest mcpToolRequest = new McpToolRequest() + { + McpServer = mcpServer, + ToolName = toolName, + ToolArgs = toolParams, + }; + mcpToolRequests.Add(mcpToolRequest); + } + } + catch (Exception e) + { + Console.WriteLine(e); + log.Error(e.Message); + } + } + }catch (Exception e) + { + log.Error(e.Message); + MessageBox.Show(e.Message, "请求大模型出错"); + } + if (messageContent != "") + { + messages.Add(new Message + { + Role = "assistant", + Content = messageContent + }); + } + /*统一处理本次请求中的MCP工具调用需求*/ + foreach (McpToolRequest mcpToolRequest in mcpToolRequests) + { + executedTool = true; + 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 toolMessageItem1 = new ToolMessageItem + { + toolName = toolName, + toolParams = toolParams, + type = MessageType.TOOL_MESSAGE, + status = toolResponse.IsError ? "fail" : "success", + content = JsonConvert.SerializeObject(toolResponse), + result = JsonConvert.SerializeObject(toolResponse), + id = (timestamp + 1).ToString(), + role = "user" + }; + 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(toolMessageItem1); + }); + } + 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 toolMessageItem1 = new ToolMessageItem + { + toolName = toolName, + toolParams = toolParams, + type = MessageType.TOOL_MESSAGE, + status = toolResponse.IsError ? "fail" : "success", + content = JsonConvert.SerializeObject(toolResponse), + result = JsonConvert.SerializeObject(toolResponse), + id = (timestamp + 1).ToString(), + role = "user" + }; + 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(toolMessageItem1); + }); + } + else if (mcpServer is InnerMcpServer) + { + InnerMcpServer innerMcpServer = mcpServer as InnerMcpServer; + string mcpServerName = innerMcpServer.Name; + if (toolName == "ArcGisProTool" && queriedKnowledge == false) + { + JsonRpcResultEntity knowledgeResult = await KnowledgeBase.QueryArcgisToolDoc(toolParams["toolName"].ToString()); + if (knowledgeResult is JsonRpcSuccessEntity) + { + JsonRpcSuccessEntity knowledgeSuccessResult = knowledgeResult as JsonRpcSuccessEntity; + MessageListItem toolMessageItem = new ToolMessageItem + { + toolName = "QueryArcgisToolDoc", + toolParams = new Dictionary(){{"query",toolParams["toolName"].ToString()}}, + type = MessageType.TOOL_MESSAGE, + status = "success", + content = JsonConvert.SerializeObject(knowledgeSuccessResult.Result), + result = JsonConvert.SerializeObject(knowledgeSuccessResult.Result), + id = (timestamp + 4).ToString(), + role = "user" + }; + messages.Add(new Message + { + Role = "user", + Content = SystemPrompt.ToolContinuePrompt(JsonConvert.SerializeObject(knowledgeSuccessResult)) + }); + Application.Current.Dispatcher.Invoke(() => + { + callback?.Invoke(toolMessageItem); + }); + queriedKnowledge = true; + } + else + { + JsonRpcErrorEntity knowledgeErrorResult = knowledgeResult as JsonRpcErrorEntity; + MessageListItem toolMessageItem = new ToolMessageItem + { + toolName = "QueryArcgisToolDoc", + toolParams = new Dictionary(){{"query",toolParams["toolName"].ToString()}}, + type = MessageType.TOOL_MESSAGE, + status = "fail", + content = JsonConvert.SerializeObject(knowledgeErrorResult.Error), + result = JsonConvert.SerializeObject(knowledgeErrorResult.Error), + id = (timestamp + 4).ToString(), + role = "user" + }; + messages.Add(new Message + { + Role = "user", + Content = SystemPrompt.ErrorPrompt(JsonConvert.SerializeObject(knowledgeErrorResult)) + }); + Application.Current.Dispatcher.Invoke(() => + { + callback?.Invoke(toolMessageItem); + }); + } + continue; + } + 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; + string displayToolName = toolName; + if (displayToolName == "ArcGisProTool") + { + displayToolName = "【GP】"+toolParams["toolName"].ToString(); + } + queriedKnowledge = false; + if (innerResult is JsonRpcErrorEntity) + { + MessageListItem toolMessageItem1 = new ToolMessageItem + { + toolName = displayToolName, + toolParams = toolParams, + type = MessageType.TOOL_MESSAGE, + status = "fail", + content = JsonConvert.SerializeObject(innerResult), + result = JsonConvert.SerializeObject(innerResult), + id = (timestamp + 1).ToString(), + role = "user" + }; + messages.Add(new Message + { + Role = "user", + Content = SystemPrompt.ErrorPrompt(JsonConvert.SerializeObject(innerResult)) + }); + Application.Current.Dispatcher.Invoke(() => + { + callback?.Invoke(toolMessageItem1); + }); + } + else if (innerResult is JsonRpcSuccessEntity) + { + MessageListItem toolMessageItem1 = new ToolMessageItem + { + toolName = displayToolName, + toolParams = toolParams, + type = MessageType.TOOL_MESSAGE, + status = "success", + content = JsonConvert.SerializeObject(innerResult), + result = JsonConvert.SerializeObject(innerResult), + id = (timestamp + 1).ToString(), + role = "user" + }; + messages.Add(new Message + { + Role = "user", + Content = SystemPrompt.ContinuePrompt(JsonConvert.SerializeObject(innerResult)) + }); + Application.Current.Dispatcher.Invoke(() => + { + callback?.Invoke(toolMessageItem1); + }); + } + } + } + catch (Exception ex) + { + 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 + { + Role = "user", + Content = promptContent + }); + MessageListItem toolMessageItem = new ToolMessageItem + { + toolName = "调用提示词", + toolParams = new Dictionary(), + type = MessageType.TOOL_MESSAGE, + status = "success", + content = "成功调用提示词:"+promptRequest.PromptName, + id = (timestamp+1).ToString(), + result = "成功调用提示词:"+promptRequest.PromptName, + role = "user" + }; + Application.Current.Dispatcher.Invoke(() => + { + callback?.Invoke(toolMessageItem); + }); + } + catch (Exception e) + { + Console.WriteLine(e); + log.Error(e.Message); + } + } + MessageListItem endMessageListItem = new ChatMessageItem + { + type = MessageType.END_TAG, + content = "", + id = (timestamp+3).ToString(), + role = "assistant" + }; + callback?.Invoke(endMessageListItem); + } + } + + private static async Task GetToolInfos(McpServerList mcpServerList) + { + StringBuilder toolInfos = new StringBuilder(); + int i = 0; + List failedMcp = new List(); + List failedMcpString = new List(); + foreach (McpServer mcpServer in mcpServerList.GetAllServers()) + { + i++; + try + { + if (mcpServer is InnerMcpServer) + { + InnerMcpServer innerMcpServer = (InnerMcpServer)mcpServer; + Type type = Type.GetType("LinkToolAddin.client.tool." + innerMcpServer.Name); + 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 = LinkToolAddin.common.JsonSchemaGenerator.GenerateJsonSchema(method); + McpToolDefinition toolDefinition = new McpToolDefinition + { + Tool = new Tool + { + Name = innerMcpServer.Name + ":" + methodName, + Description = methodDescription, + Arguments = methodParamSchema + } + }; + XNode node = JsonConvert.DeserializeXNode(JsonConvert.SerializeObject(toolDefinition)); + toolInfos.AppendLine(node.ToString()); + toolInfos.AppendLine(); + } + } + } + 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 = (mcpServer as SseMcpServer).Name + ":" + 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()); + toolInfos.AppendLine(); + } + }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 = (mcpServer as StdioMcpServer).Name + ":" + tool.Name;; + string toolDescription = tool.Description; + string toolParamSchema = tool.JsonSchema.ToString(); + McpToolDefinition toolDefinition = new McpToolDefinition + { + Tool = new Tool + { + Name = toolName, + Description = toolDescription, + Arguments = CompressJson(toolParamSchema) + } + }; + toolInfos.AppendLine(JsonConvert.DeserializeXNode(JsonConvert.SerializeObject(toolDefinition)).ToString()); + toolInfos.AppendLine(); + } + } + }catch (Exception e) + { + log.Error(e.Message); + failedMcp.Add(i); + failedMcpString.Add(mcpServerList.GetAllServerNames()[i]); + } + } + + List prompts = DynamicPrompt.GetAllPrompts(); + foreach (UserPrompt userPrompt in prompts) + { + try + { + XNode node = JsonConvert.DeserializeXNode(JsonConvert.SerializeObject(new PromptDefinition(){UserPrompt = userPrompt})); + toolInfos.AppendLine(node.ToString()); + toolInfos.AppendLine(); + } + catch (Exception e) + { + log.Error(e.Message); + } + } + + if (!failedMcp.IsNullOrEmpty()) + { + MessageBox.Show($"读取失败的MCP服务有:{JsonConvert.SerializeObject(failedMcpString)}","MCP读取错误"); + } + 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 + { + // 启用属性注解处理 + 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); + } + 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, + Action callback) + { + MessageListItem chatListItem = new ChatMessageItem + { + content = message, + role = "assistant", + type = MessageType.CHAT_MESSAGE, + id = "testmsg12345" + }; + callback?.Invoke(chatListItem); + } + + public static async void TestToolMessage(string message, string model, string gdbPath, Action callback) + { + MessageListItem toolListItem = new ToolMessageItem + { + content = message, + type = MessageType.TOOL_MESSAGE, + toolName = "arcgis_pro.executeTool", + toolParams = new Dictionary + { + {"gp_name","analysis.Buffer"}, + {"gp_params","[\"C:\\test.gdb\\river\",\"30 Meters\"]"} + }, + id = "testtool123456", + status = "success", + role = "user", + result = "成功创建缓冲区" + }; + callback?.Invoke(toolListItem); + } + + public static async void TestWorkflow(string message, string model, string gdbPath, Action callback) + { + Thread.Sleep(2000); + MessageListItem chatListItem = new ChatMessageItem + { + content = message, + role = "assistant", + type = MessageType.CHAT_MESSAGE, + id = "testid12345" + }; + callback?.Invoke(chatListItem); + Thread.Sleep(1500); + MessageListItem toolListItem = new ToolMessageItem + { + content = message, + type = MessageType.TOOL_MESSAGE, + toolName = "arcgis_pro.executeTool", + toolParams = new Dictionary + { + {"gp_name","analysis.Buffer"}, + {"gp_params","[\"C:\\test.gdb\\river\",\"30 Meters\"]"} + }, + id = "testtool123456", + status = "success", + role = "user", + result = "成功创建缓冲区" + }; + callback?.Invoke(toolListItem); + } +} \ No newline at end of file diff --git a/host/McpServerList.cs b/host/McpServerList.cs new file mode 100644 index 0000000..3393414 --- /dev/null +++ b/host/McpServerList.cs @@ -0,0 +1,119 @@ +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("ArcGisPro", new InnerMcpServer + { + Name = "ArcGisPro", + Type = "inner", + Description = "可以调用arcgis的地理处理工具或执行python代码等", + IsActive = true + }); + servers.Add("KnowledgeBase", new InnerMcpServer + { + Name = "KnowledgeBase", + Type = "inner", + Description = "可以调用进行查询知识库,获取相关参考信息。" , + // "有地理信息的相关案例步骤参考以及Arcgis Pro的工具详细信息", + IsActive = true + }); + //servers.Add("filesystem", new StdioMcpServer() + //{ + // Name = "filesystem", + // Type = "stdio", + // Command = "npx", + // Args = new List() + // { + // "-y", + // "@modelcontextprotocol/server-filesystem", + // "F:\\secondsemester\\linktool\\test\\LinkTool0607\\LinkTool0607.gdb" + // } + //}); + //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", + // "F:\\secondsemester\\linktool\\test\\LinkTool0607\\LinkTool0607.gdb", + // "--python-path", + // "C:\\Program Files\\ArcGIS\\Pro\\bin\\Python\\envs\\custom\\python.exe" + // } + //}); + } + + 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; + } + + public List GetAllServerNames() + { + List serverList = new List(); + foreach (var server in servers) + { + serverList.Add(server.Key); + } + return serverList; + } +} \ No newline at end of file diff --git a/host/PromptServerList.cs b/host/PromptServerList.cs new file mode 100644 index 0000000..5a69be1 --- /dev/null +++ b/host/PromptServerList.cs @@ -0,0 +1,48 @@ +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的帮助文档”。" + + // "有一些与分析无关的数据能够进行排除,在选择工具时不受其干扰。" + "你对ArcGIS Pro的工具箱及其功能了如指掌,能够根据用户的具体需求和提供的数据类型,迅速判断并列出所有必要的分析步骤和工具,并随之调用知识库工具确认任务规划中的调用名是否正确,以及确认其对应参数" + + "同时严格遵守知识库中“ArcGIS Pro工具调用大全”里“工具调用名称”一列的工具命名规则(例如:analysis.Erase),确保工具名的准确无误。" + + "工具的组合顺序优先参考知识库中“ArcGIS Pro的帮助文档”。有一些与分析无关的数据能够进行排除,在选择工具时不受其干扰。" + + "你的任务是,基于用户提出的地理分析需求和所提供的数据集,逐一识别并列出完成整个分析流程所需的全部ArcGIS Pro工具," + + "确保每个工具的名称完全匹配“ArcGIS Pro工具调用大全”里“工具调用名称”一列的记录,且工具与所属工具箱的对应关系正确无误。" + + "请直接输出工具调用名称,格式为“工具调用名称”,无需额外解释或说明,工具名称只参考“ArcGIS Pro工具调用大全”,不要受其他文档的干扰。" + + + "你的回复应简洁明了,仅包含所需工具的列表,输出格式为序号分隔的工具调用名以及调用知识库工具工具的XML格式,主要目的是纠正规划中错误的调用名和了解工具参数便于下一步的工具调用,因为调用名和参数对于工具执行非常重要,一旦出错则可能导致运行失败。严格遵循文档规定的格式和大小写,确保信息的准确性和专业性。")); + promptServers.Add("param", new PromptServer("param", + "填写ArcGIS Pro工具调用参数,生成规范的可执行的工具调用请求", + "根据知识库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/ToolRequest.cs b/host/ToolRequest.cs new file mode 100644 index 0000000..944026a --- /dev/null +++ b/host/ToolRequest.cs @@ -0,0 +1,6 @@ +namespace LinkToolAddin.host; + +public class ToolRequest +{ + +} \ No newline at end of file diff --git a/host/llm/Bailian.cs b/host/llm/Bailian.cs new file mode 100644 index 0000000..6d7d273 --- /dev/null +++ b/host/llm/Bailian.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +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; + +public class Bailian : Llm +{ + public string model { get; set; } = "qwen-max"; + public string temperature { get; set; } + public string top_p { get; set; } + public string max_tokens { get; set; } + public string app_id { get; set; } + public string api_key { get; set; } + public async IAsyncEnumerable SendChatStreamAsync(LlmJsonContent jsonContent) + { + jsonContent.Stream = true; + 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)) + { + 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; + } + } + + public IAsyncEnumerable SendApplicationStreamAsync(string message) + { + throw new System.NotImplementedException(); + } + + public async Task SendChatAsync(LlmJsonContent jsonContent) + { + string url = "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions"; + string responseBody = await HttpRequest.SendPostRequestAsync(url, JsonConvert.SerializeObject(jsonContent), api_key); + LlmChat result = JsonConvert.DeserializeObject(responseBody); + return result.Choices[0].Message.Content; + } + + public async Task SendApplicationAsync(CommonInput commonInput) + { + string url = $"https://dashscope.aliyuncs.com/api/v1/apps/{app_id}/completion"; + string responseBody = await HttpRequest.SendPostRequestAsync(url, JsonConvert.SerializeObject(commonInput), api_key); + ApplicationOutput result = JsonConvert.DeserializeObject(responseBody); + return responseBody; + } +} \ No newline at end of file diff --git a/host/llm/Llm.cs b/host/llm/Llm.cs index 95e11c4..bb3ba53 100644 --- a/host/llm/Llm.cs +++ b/host/llm/Llm.cs @@ -1,4 +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; @@ -9,6 +12,8 @@ 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); } \ No newline at end of file diff --git a/host/llm/entity/ApplicationOutput.cs b/host/llm/entity/ApplicationOutput.cs new file mode 100644 index 0000000..94405fa --- /dev/null +++ b/host/llm/entity/ApplicationOutput.cs @@ -0,0 +1,51 @@ +namespace LinkToolAddin.host.llm.entity +{ + using System; + using System.Collections.Generic; + + using System.Globalization; + using Newtonsoft.Json; + using Newtonsoft.Json.Converters; + + public partial class ApplicationOutput + { + [JsonProperty("output")] + public Output Output { get; set; } + + [JsonProperty("usage")] + public Usage Usage { get; set; } + + [JsonProperty("request_id")] + public Guid RequestId { get; set; } + } + + public partial class Output + { + [JsonProperty("finish_reason")] + public string FinishReason { get; set; } + + [JsonProperty("session_id")] + public string SessionId { get; set; } + + [JsonProperty("text")] + public string Text { get; set; } + } + + public partial class Usage + { + [JsonProperty("models")] + public List Models { get; set; } + } + + public partial class Model + { + [JsonProperty("output_tokens")] + public long OutputTokens { get; set; } + + [JsonProperty("model_id")] + public string ModelId { get; set; } + + [JsonProperty("input_tokens")] + public long InputTokens { get; set; } + } +} \ No newline at end of file diff --git a/host/llm/entity/CommonInput.cs b/host/llm/entity/CommonInput.cs new file mode 100644 index 0000000..6d1da54 --- /dev/null +++ b/host/llm/entity/CommonInput.cs @@ -0,0 +1,31 @@ +namespace LinkToolAddin.host.llm.entity +{ + using System; + using System.Collections.Generic; + + using System.Globalization; + using Newtonsoft.Json; + using Newtonsoft.Json.Converters; + + public partial class CommonInput + { + [JsonProperty("input")] + public Input Input { get; set; } + + [JsonProperty("parameters")] + public Debug Parameters { get; set; } + + [JsonProperty("debug")] + public Debug Debug { get; set; } + } + + public partial class Debug + { + } + + public partial class Input + { + [JsonProperty("prompt")] + public string Prompt { get; set; } + } +} \ No newline at end of file diff --git a/host/llm/entity/KnowldgeResult.cs b/host/llm/entity/KnowldgeResult.cs new file mode 100644 index 0000000..0937e30 --- /dev/null +++ b/host/llm/entity/KnowldgeResult.cs @@ -0,0 +1,42 @@ +namespace LinkToolAddin.host.llm.entity +{ + using System; + using System.Collections.Generic; + + using System.Globalization; + using Newtonsoft.Json; + using Newtonsoft.Json.Converters; + + public partial class KnowledgeResult + { + [JsonProperty("rewriteQuery")] + public string RewriteQuery { get; set; } + + [JsonProperty("chunkList")] + public List ChunkList { get; set; } + } + + public partial class ChunkList + { + [JsonProperty("score")] + public double Score { get; set; } + + [JsonProperty("imagesUrl")] + public List ImagesUrl { get; set; } + + [JsonProperty("documentName")] + public string DocumentName { get; set; } + + [JsonProperty("titcontent", NullValueHandling = NullValueHandling.Ignore)] + public string Titcontent { get; set; } + + [JsonProperty("title", NullValueHandling = NullValueHandling.Ignore)] + public string Title { get; set; } + + [JsonProperty("content", NullValueHandling = NullValueHandling.Ignore)] + public string Content { get; set; } + + [JsonProperty("conten_score_with_weight", NullValueHandling = NullValueHandling.Ignore)] + public string ContenScoreWithWeight { get; set; } + } +} diff --git a/host/llm/entity/LlmChat.cs b/host/llm/entity/LlmChat.cs new file mode 100644 index 0000000..d354865 --- /dev/null +++ b/host/llm/entity/LlmChat.cs @@ -0,0 +1,69 @@ +namespace LinkToolAddin.host.llm.entity +{ + using System; + using System.Collections.Generic; + + using System.Globalization; + using Newtonsoft.Json; + using Newtonsoft.Json.Converters; + + public partial class LlmChat + { + [JsonProperty("choices")] + public List Choices { get; set; } + + [JsonProperty("object")] + public string Object { get; set; } + + [JsonProperty("usage")] + public Usage 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("message")] + public Message Message { get; set; } + + [JsonProperty("finish_reason")] + public string FinishReason { get; set; } + + [JsonProperty("index")] + public long Index { get; set; } + + [JsonProperty("logprobs")] + public object Logprobs { get; set; } + } + + public partial class Usage + { + [JsonProperty("prompt_tokens")] + public long PromptTokens { get; set; } + + [JsonProperty("completion_tokens")] + public long CompletionTokens { get; set; } + + [JsonProperty("total_tokens")] + public long TotalTokens { get; set; } + + [JsonProperty("prompt_tokens_details")] + public PromptTokensDetails PromptTokensDetails { get; set; } + } + + public partial class PromptTokensDetails + { + [JsonProperty("cached_tokens")] + public long CachedTokens { get; set; } + } +} \ No newline at end of file diff --git a/host/llm/entity/LlmJsonContent.cs b/host/llm/entity/LlmJsonContent.cs new file mode 100644 index 0000000..c38382f --- /dev/null +++ b/host/llm/entity/LlmJsonContent.cs @@ -0,0 +1,47 @@ +namespace LinkToolAddin.host.llm.entity +{ + using System.Collections.Generic; + using Newtonsoft.Json; + + public partial class LlmJsonContent + { + [JsonProperty("model")] + public string Model { get; set; } + + [JsonProperty("messages")] + public List Messages { get; set; } + + [JsonProperty("stream")] + public bool Stream { get; set; } = false; + + [JsonProperty("temperature")] + public double Temperature { get; set; } = 0.7; + + [JsonProperty("top_p")] + public double TopP { get; set; } = 1.0; + + [JsonProperty("max_tokens")] + public int MaxTokens { get; set; } = 2048; + + [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 + { + [JsonProperty("role")] + public string Role { get; set; } + + [JsonProperty("content")] + public string Content { get; set; } + } +} \ No newline at end of file diff --git a/host/llm/entity/stream/LlmStreamChat.cs b/host/llm/entity/stream/LlmStreamChat.cs new file mode 100644 index 0000000..6813e7e --- /dev/null +++ b/host/llm/entity/stream/LlmStreamChat.cs @@ -0,0 +1,56 @@ +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; } + [JsonProperty("reasoning_content")] + public string ResoningContent { get; set; } + } +} \ No newline at end of file diff --git a/host/mcp/InnerMcpServer.cs b/host/mcp/InnerMcpServer.cs new file mode 100644 index 0000000..2b06319 --- /dev/null +++ b/host/mcp/InnerMcpServer.cs @@ -0,0 +1,24 @@ +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/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/mcp/McpServer.cs b/host/mcp/McpServer.cs new file mode 100644 index 0000000..616798c --- /dev/null +++ b/host/mcp/McpServer.cs @@ -0,0 +1,24 @@ +namespace LinkToolAddin.host.mcp +{ + using System; + using System.Collections.Generic; + + using System.Globalization; + using Newtonsoft.Json; + using Newtonsoft.Json.Converters; + + public partial class 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 new file mode 100644 index 0000000..6e79610 --- /dev/null +++ b/host/mcp/McpToolDefinition.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 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/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/mcp/SseMcpServer.cs b/host/mcp/SseMcpServer.cs new file mode 100644 index 0000000..cd2525e --- /dev/null +++ b/host/mcp/SseMcpServer.cs @@ -0,0 +1,30 @@ +namespace LinkToolAddin.host.mcp +{ + using System; + using System.Collections.Generic; + + using System.Globalization; + using Newtonsoft.Json; + using Newtonsoft.Json.Converters; + + public partial class SseMcpServer : 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; } + + [JsonProperty("baseUrl")] + public string BaseUrl { get; set; } + + [JsonProperty("headers")] + public Dictionary Headers { get; set; } + } +} \ No newline at end of file diff --git a/host/mcp/StdioMcpServer.cs b/host/mcp/StdioMcpServer.cs new file mode 100644 index 0000000..a70ce40 --- /dev/null +++ b/host/mcp/StdioMcpServer.cs @@ -0,0 +1,30 @@ +namespace LinkToolAddin.host.mcp +{ + using System; + using System.Collections.Generic; + + using System.Globalization; + using Newtonsoft.Json; + using Newtonsoft.Json.Converters; + + public partial class StdioMcpServer : McpServer + { + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("type")] + public string Type { get; set; } + + [JsonProperty("isActive")] + public bool IsActive { get; set; } + + [JsonProperty("registryUrl")] + public string RegistryUrl { get; set; } + + [JsonProperty("command")] + public string Command { get; set; } + + [JsonProperty("args")] + public List Args { 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 new file mode 100644 index 0000000..b6db27c --- /dev/null +++ b/host/prompt/SystemPrompt.cs @@ -0,0 +1,59 @@ +using LinkToolAddin.common; + +namespace LinkToolAddin.host.prompt; + +public class SystemPrompt +{ + // 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 = LocalResource.ReadFileByResource("LinkToolAddin.resource.prompt.SystemPrompt.txt");; + sysPrompt = sysPrompt.Replace("{{gdbPath}}", gdbPath); + sysPrompt = sysPrompt.Replace("{{toolInfos}}", toolInfos); + return sysPrompt; + } + + public static string ContinuePrompt(string toolResult) + { + 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; + } + + public static string ToolContinuePrompt(string toolResult) + { + string continuePrompt = "根据你需要调用的工具查询到如下帮助文档内容,请严格按照帮助文档中的参数和名称要求再次生成准确的工具调用请求以确保工具调用成功。\n{{toolResult}}"; + continuePrompt = continuePrompt.Replace("{{toolResult}}", toolResult); + return continuePrompt; + } +} \ No newline at end of file diff --git a/host/prompt/UserPrompt.cs b/host/prompt/UserPrompt.cs new file mode 100644 index 0000000..00c1cd8 --- /dev/null +++ b/host/prompt/UserPrompt.cs @@ -0,0 +1,13 @@ +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/DocDb.cs b/resource/DocDb.cs new file mode 100644 index 0000000..f4937fe --- /dev/null +++ b/resource/DocDb.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using LinkToolAddin.host.llm; +using LinkToolAddin.host.llm.entity; +using Newtonsoft.Json; + +namespace LinkToolAddin.resource; + +public class DocDb +{ + public string appId {get;set;} + public string apiKey {get;set;} + + public DocDb(string apiKey,KnowledgeBase knowledgeBaseEnum) + { + this.apiKey = apiKey; + appId = knowledgeBase[knowledgeBaseEnum]; + } + + public enum KnowledgeBase + { + ArcGISProHelpDoc, + ArcGISProToolDoc, + ArcGISProApplicantExample + } + + public Dictionary knowledgeBase = new Dictionary + { + {KnowledgeBase.ArcGISProHelpDoc,"6a77c5a68de64f469b79fcdcde9d5001"}, + {KnowledgeBase.ArcGISProToolDoc,"080f8925318247ea822a9e12db5cb5cd"}, + {KnowledgeBase.ArcGISProApplicantExample,"eef60f7c879b4e8597138c261578d2a5"} + }; + + public async Task Retrieve(string query) + { + Llm bailian = new Bailian + { + api_key = apiKey, + app_id = appId, + }; + var commonInput = new CommonInput + { + Input = new Input + { + Prompt = query + }, + Parameters = new Debug(), + Debug = new Debug() + }; + string responseBody = await bailian.SendApplicationAsync(commonInput); + ApplicationOutput result = JsonConvert.DeserializeObject(responseBody); + KnowledgeResult knowledgeResult = JsonConvert.DeserializeObject(result.Output.Text); + return knowledgeResult; + } +} \ No newline at end of file diff --git a/resource/GpToolDb.cs b/resource/GpToolDb.cs new file mode 100644 index 0000000..3706c8d --- /dev/null +++ b/resource/GpToolDb.cs @@ -0,0 +1,9 @@ +namespace LinkToolAddin.resource; + +public class GpToolDb +{ + public string toolName { get; set; } + public string toolDescription { get; set; } + public string exeName { get; set; } + public string toolParma { get; set; } +} \ No newline at end of file diff --git a/resource/PromptDb.cs b/resource/PromptDb.cs new file mode 100644 index 0000000..6b6e6d3 --- /dev/null +++ b/resource/PromptDb.cs @@ -0,0 +1,8 @@ +namespace LinkToolAddin.resource; + +public class PromptDb +{ + public string promptName { get; set; } + public string promptDescription { get; set; } + public string promptContent { get; set; } +} \ No newline at end of file diff --git a/resource/img/fold.png b/resource/img/fold.png new file mode 100644 index 0000000..2690b19 Binary files /dev/null and b/resource/img/fold.png differ diff --git a/resource/img/linktool.png b/resource/img/linktool.png new file mode 100644 index 0000000..37264c2 Binary files /dev/null and b/resource/img/linktool.png differ diff --git a/resource/img/tool.png b/resource/img/tool.png new file mode 100644 index 0000000..09bee43 Binary files /dev/null and b/resource/img/tool.png differ diff --git a/resource/img/unfold.png b/resource/img/unfold.png new file mode 100644 index 0000000..749e87e Binary files /dev/null and b/resource/img/unfold.png differ diff --git a/resource/img/user.png b/resource/img/user.png new file mode 100644 index 0000000..a992685 Binary files /dev/null and b/resource/img/user.png differ diff --git a/resource/prompt/ContinuePrompt.txt b/resource/prompt/ContinuePrompt.txt new file mode 100644 index 0000000..44b1763 --- /dev/null +++ b/resource/prompt/ContinuePrompt.txt @@ -0,0 +1,12 @@ +这是上述工具调用的结果。 + +{{toolResult}} + +请根据以下执行结果,清晰解释执行结果并执行下一步操作。 + +执行下一步工具的要求: +1. 解析工具输出结果 +2. 调用下一个工具时确保参数继承前序输出,如果已经完成用户需求则不需继续执行工具。 +3. 执行Arcgis Pro工具时可以调用知识库和提示词使任务完成得更出色! + +请据此继续执行 \ No newline at end of file diff --git a/resource/prompt/ErrorPrompt.txt b/resource/prompt/ErrorPrompt.txt new file mode 100644 index 0000000..07acb1d --- /dev/null +++ b/resource/prompt/ErrorPrompt.txt @@ -0,0 +1,14 @@ +执行上一个工具的时候出现以下错误 + +{{toolResult}} + +需按以下流程处理: +1. 错误解析:分析错误类型及具体原因(见错误信息),根据错误信息选择修复方案。 +2. 修复方案: +(1)如果是ArcGisPro工具,第一时间查询知识库的工具参数是否有误,多次执行失败极有可能是工具名或者参数错误,尤其注重调用工具或者提示词修正 +(2)根据错误类型生成对应的工具重试策略。 +(3)参数和调用名调整:及时调用知识库和用户提示词,确保工具名和参数的完全正确。明确需要修改的工具名、参数及修改方式。 +(4)可以选择调用工具规划提示词优化工具执行流程或者调用知识库工具优化工具调用名和工具参数。 +3. 工具重试:使用调整后的参数重新调用工具。请根据报错信息重试。 +4.如果没有具体的错误描述信息请检查输出文件是否已经存在,若已经存在默认执行成功 +5.出现错误编码号时要明晰编码在知识库中有相应解析,可以通过调用知识库解析错误原因 diff --git a/resource/prompt/SystemPrompt.txt b/resource/prompt/SystemPrompt.txt new file mode 100644 index 0000000..bccbc96 --- /dev/null +++ b/resource/prompt/SystemPrompt.txt @@ -0,0 +1,53 @@ +现在你是一个精通地理信息分析和ArcGIS Pro软件的专家,请以此身份回答用户的问题。 +指令:您可以使用一组工具加文字说明来回答用户的问题。完成了用户的需求即可,不用猜测用户下一步还想做什么。计划使用ArcGisPro工具前,你可以通过{}格式调用用户提示词,或者调用知识库工具,从而使你更好地理解和完成用户的任务。除此之外,你具有搜索网页、编写代码、管理文件系统的能力,合理运用这些工具完成用户的需求。 +调用工具要求:如果要调用工具,每次消息只能使用一个工具,用户的回复中将包含该工具的调用结果。你还可以通过方式来调用用户提示词,或者调用知识库工具,你能更好地理解和解决用户的问题。您需要通过逐步使用工具或提示词来完成给定任务,每次调用需基于前一次的结果。成功调用工具或提示词之后应该马上调用下一个工具或提示词。 +工具调用背景:你有以下工具可以调用{{toolInfos}},用户的数据库路径是{{gdbPath}}。 +输出风格:在工具调用前描述每一步将要做什么,简洁有力,每次仅调用一个工具,基于前序工具的输出结果进行下一步操作,如果已经完成用户需求则不需继续执行工具。话语开头不要回复好的。工具调用使用 XML 风格的标签输出。 +工具调用的XML格式一定一定要完全正确,不能有错漏,格式如下: + + {tool_name} + {json_arguments} + + +工具名称:需与所使用工具的精确名称一致。 +参数:应为包含工具所需参数的 JSON 对象。 +例如: +调用工具示例: +“将执行高德的兴趣点工具确认广州市政府的位置 + + gaode:maps_geo + {\\\"address\\\":\\\"广州市政府, 广州市\\\", \\\"city\\\":\\\"广州\\\"} + +” +调用用户提示词示例: +“ +先进行ArcGisPro工具任务规划 + + plan + {} + + + +” +结果示例:用户将以以下格式返回工具调用结果: + + {tool_name} + {result} + + +工具调用示例:MCP工具调用的格式要求示例:以下是使用虚拟工具的示例: + + gaode:maps_geo + {\\\"address\\\":\\\"广州市政府, 广州市\\\", \\\"city\\\":\\\"广州\\\"} + + +你必须严格遵守以下每一条规则: +1.用户时间宝贵,一旦确认工具成功调用之后,不得重复调用上一次已成功执行的工具,除非有新的参数或上下文变化。 +2.调用“ArcGisPro:ArcGisProTool”工具,name一定要严格按照知识库的调用名,例如"analysis.Union","management.Clip"等。输入与输出数据一定需要加上路径信息,输出数据到默认数据库中。`in_features`参数通过分号(`;`)拼接多个输入要素类路径,参数列表中有"{}"代表选填,其他必须要填写。 +3.参数一定都为字符串类型,可以表示文件或其他输出类型。 +4.一次只能调用一个工具,逐步调用!不要调用多个,一旦消息中没有工具或提示词调用信息即视为任务完成。因此工具或提示词调用必须连续完成。 +5.只响应用户目前的需求即可,不要过度猜测用户的需求,如果有下一步的工具建议只输出文本即可,如果输出XML会执行大量无用的工具。 + +特别注意: +1.ArcGIS Pro中不能通过先SelectByAttribute选择后再执行ExportFeatures导出指定的部分,正确的做法是直接用ExportFeatures传入where_clause导出指定部分的数据。 +2.可以先参考知识库中的案例辅助工具的规划 diff --git a/server/ArcGISProMcpServer.cs b/server/ArcGISProMcpServer.cs new file mode 100644 index 0000000..7aa94f7 --- /dev/null +++ b/server/ArcGISProMcpServer.cs @@ -0,0 +1,37 @@ +using System; +using System.ComponentModel; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using ModelContextProtocol.Server; + +namespace LinkToolAddin.server; + +public class ArcGISProMcpServer +{ + public static async void TestMcpServer() + { + var builder = WebApplication.CreateBuilder(); + builder.Logging.AddConsole(options => + { + options.LogToStandardErrorThreshold = LogLevel.Trace; + });; + builder.Services.AddMcpServer().WithHttpTransport().WithToolsFromAssembly(); + var app = builder.Build(); + app.MapMcp("/sse"); + app.MapGet("/", () => "MCP Server is running!"); + Console.WriteLine("About to start server..."); + await app.RunAsync(); + } +} + +[McpServerToolType] +public static class EchoTool +{ + [McpServerTool, Description("Echoes the message back to the client.")] + public static string Echo(string message) + { + Console.WriteLine($"received message: {message}"); + return $"hello {message}"; + } +} \ No newline at end of file diff --git a/server/CallArcGISPro.cs b/server/CallArcGISPro.cs new file mode 100644 index 0000000..016b399 --- /dev/null +++ b/server/CallArcGISPro.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using ArcGIS.Desktop.Core.Geoprocessing; +using ArcGIS.Desktop.Framework.Dialogs; +using ArcGIS.Desktop.Framework.Threading.Tasks; +using Newtonsoft.Json; + +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); + JsonRpcResultEntity jsonRpcResultEntity; + if (results.ErrorCode == 0) + { + jsonRpcResultEntity = new JsonRpcErrorEntity() + { + Error = new Error() + { + Code = results.ErrorCode.ToString(), + Message = JsonConvert.SerializeObject(results.ErrorMessages) + } + }; + } + else + { + jsonRpcResultEntity = new JsonRpcSuccessEntity + { + Result = JsonConvert.SerializeObject(results.Messages) + }; + } + return jsonRpcResultEntity; + } +} \ No newline at end of file diff --git a/server/JsonRpcEntity.cs b/server/JsonRpcEntity.cs new file mode 100644 index 0000000..01a91e3 --- /dev/null +++ b/server/JsonRpcEntity.cs @@ -0,0 +1,25 @@ +using System.Text.Json.Serialization; +using Newtonsoft.Json; + +namespace LinkToolAddin.server +{ + using System; + using System.Collections.Generic; + + using System.Globalization; + + public partial class JsonRpcEntity + { + [JsonProperty("jsonrpc")] + public string Jsonrpc { get; set; } + + [JsonProperty("method")] + public string Method { get; set; } + + [JsonProperty("params")] + public Dictionary Params { get; set; } + + [JsonProperty("id")] + public long Id { get; set; } + } +} \ No newline at end of file diff --git a/server/JsonRpcErrorEntity.cs b/server/JsonRpcErrorEntity.cs new file mode 100644 index 0000000..ca57640 --- /dev/null +++ b/server/JsonRpcErrorEntity.cs @@ -0,0 +1,25 @@ +namespace LinkToolAddin.server +{ + using Newtonsoft.Json; + + public partial class JsonRpcErrorEntity : JsonRpcResultEntity + { + [JsonProperty("jsonrpc")] + public string Jsonrpc { get; set; } = "2.0"; + + [JsonProperty("error")] + public Error Error { get; set; } + + [JsonProperty("id")] + public long Id { get; set; } + } + + public partial class Error + { + [JsonProperty("code")] + public string Code { get; set; } + + [JsonProperty("message")] + public string Message { get; set; } + } +} \ No newline at end of file diff --git a/server/JsonRpcResultEntity.cs b/server/JsonRpcResultEntity.cs new file mode 100644 index 0000000..f97442a --- /dev/null +++ b/server/JsonRpcResultEntity.cs @@ -0,0 +1,15 @@ +using Newtonsoft.Json; + +namespace LinkToolAddin.server +{ + using Newtonsoft.Json; + + public partial class JsonRpcResultEntity + { + [JsonProperty("jsonrpc")] + public string Jsonrpc { get; set; } = "2.0"; + + [JsonProperty("id")] + public long Id { get; set; } + } +} \ No newline at end of file diff --git a/server/JsonRpcSuccessEntity.cs b/server/JsonRpcSuccessEntity.cs new file mode 100644 index 0000000..a37985f --- /dev/null +++ b/server/JsonRpcSuccessEntity.cs @@ -0,0 +1,16 @@ +namespace LinkToolAddin.server +{ + using Newtonsoft.Json; + + public partial class JsonRpcSuccessEntity : JsonRpcResultEntity + { + [JsonProperty("jsonrpc")] + public string Jsonrpc { get; set; } + + [JsonProperty("result")] + public string Result { get; set; } + + [JsonProperty("id")] + public long Id { get; set; } + } +} \ No newline at end of file diff --git a/ui/VersionButton.cs b/ui/VersionButton.cs index 62e157d..bec6767 100644 --- a/ui/VersionButton.cs +++ b/ui/VersionButton.cs @@ -22,7 +22,7 @@ namespace LinkToolAddin { internal class VersionButton : Button { - private string version = "0.1.0"; + private string version = "0.1.4"; protected override void OnClick() { MessageBox.Show($"当前LinkTool版本为{version}", "版本信息"); diff --git a/ui/dockpane/DialogDockpane.xaml b/ui/dockpane/DialogDockpane.xaml index 3b569d7..b62fab7 100644 --- a/ui/dockpane/DialogDockpane.xaml +++ b/ui/dockpane/DialogDockpane.xaml @@ -5,9 +5,10 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:ui="clr-namespace:LinkToolAddin.ui.dockpane" xmlns:extensions="clr-namespace:ArcGIS.Desktop.Extensions;assembly=ArcGIS.Desktop.Extensions" - xmlns:controls="clr-namespace:ArcGIS.Desktop.Framework.Controls;assembly=ArcGIS.Desktop.Framework" + xmlns:controls="clr-namespace:ArcGIS.Desktop.Framework.Controls;assembly=ArcGIS.Desktop.Framework" + xmlns:system="clr-namespace:System;assembly=System.Runtime" mc:Ignorable="d" - d:DesignHeight="300" d:DesignWidth="300" + d:DesignHeight="650" d:DesignWidth="300" d:DataContext="{Binding Path=ui.DialogDockpaneViewModel}"> @@ -16,19 +17,52 @@ - + - + + + + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ui/dockpane/DialogDockpane.xaml.cs b/ui/dockpane/DialogDockpane.xaml.cs index 4a594fd..68b55b0 100644 --- a/ui/dockpane/DialogDockpane.xaml.cs +++ b/ui/dockpane/DialogDockpane.xaml.cs @@ -1,29 +1,557 @@ -using System; +using System; +using System.Collections.Concurrent; using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.IO; +using System.Net.Http; +using System.Net.Http.Headers; using System.Windows; using System.Windows.Controls; -using System.Windows.Data; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System.Text.Json; +using System.Threading; using System.Windows.Documents; -using System.Windows.Input; using System.Windows.Media; -using System.Windows.Media.Imaging; -using System.Windows.Navigation; -using System.Windows.Shapes; - +using ArcGIS.Desktop.Core; +using ArcGIS.Desktop.Core.Geoprocessing; +using LinkToolAddin.client; +using LinkToolAddin.common; +using LinkToolAddin.host; +using LinkToolAddin.host.llm; +using LinkToolAddin.host.llm.entity; +using LinkToolAddin.message; +using LinkToolAddin.resource; +using LinkToolAddin.server; +using log4net; +using log4net.Appender; +using log4net.Config; +using log4net.Layout; +using ModelContextProtocol.Client; +using ModelContextProtocol.Protocol.Types; +using ModelContextProtocol.Server; +using Newtonsoft.Json; namespace LinkToolAddin.ui.dockpane { + public class ItemModel + { + public string Content { get; set; } + } /// /// Interaction logic for DialogDockpaneView.xaml /// public partial class DialogDockpaneView : UserControl { + private static ILog log = LogManager.GetLogger(typeof(DialogDockpaneView)); + + private List idList = new List(); + private ConcurrentDictionary messageDict = new ConcurrentDictionary(); + private ConcurrentDictionary borderItemsDict = new ConcurrentDictionary(); + public DialogDockpaneView() { + InitLogger(); InitializeComponent(); + DataContext = this; + } + + private async void TestServer_OnClick(object sender, RoutedEventArgs e) + { + log.Info("TestServer Clicked"); + } + + protected void InitLogger() + { + // 1. 创建控制台输出器(Appender) + var consoleAppender = new ConsoleAppender + { + Layout = new PatternLayout("%date [%thread] %-5level %logger - %message%newline"), + Threshold = log4net.Core.Level.Info // 仅输出 Info 及以上级别 + }; + consoleAppender.ActivateOptions(); // 激活配置 + + // 2. 创建文件滚动输出器(按大小滚动) + var fileAppender = new RollingFileAppender + { + File = Path.Combine("Logs", "D:\\linktool_app.log"), // 日志文件路径 + AppendToFile = true, // 追加模式 + RollingStyle = RollingFileAppender.RollingMode.Size, // 按文件大小滚动 + MaxSizeRollBackups = 10, // 保留 10 个历史文件 + MaximumFileSize = "1MB", // 单个文件最大 1MB + StaticLogFileName = true, // 固定文件名(否则自动追加序号) + Layout = new PatternLayout("%date [%thread] %-5level %logger - %message%newline"), + Threshold = log4net.Core.Level.Info // 仅输出 Info 及以上级别 + }; + fileAppender.ActivateOptions(); // 激活配置 + + // 3. 直接通过 BasicConfigurator 注册 Appender + BasicConfigurator.Configure(consoleAppender, fileAppender); + + log = LogManager.GetLogger(typeof(DialogDockpaneView)); + + // 测试日志输出 + log.Debug("Debug 日志(控制台可见)"); + log.Info("Info 日志(控制台和文件可见)"); + log.Error("Error 日志(严重问题)"); + } + + private void SendButton_OnClick(object sender, RoutedEventArgs e) + { + string question = QuestionTextbox.Text; + string defaultGdbPath = Project.Current.DefaultGeodatabasePath; + string gdbPath = @""; + long timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + MessageListItem userMsg = new ChatMessageItem() + { + content = question, + role = "user", + type = MessageType.CHAT_MESSAGE, + id = timestamp.ToString() + }; + Border userMsgBoder = GetUserChatBorder(userMsg); + idList.Add(timestamp.ToString()); + messageDict[timestamp.ToString()] = userMsg; + QuestionTextbox.Text = ""; + borderItemsDict[timestamp.ToString()] = userMsgBoder; + ChatHistoryStackPanel.Children.Add(userMsgBoder); + string model = (ModelComboBox.SelectedItem as ComboBoxItem).Content.ToString() is null ? "qwen3-235b-a22b" : (ModelComboBox.SelectedItem as ComboBoxItem).Content.ToString(); + if (model == "自定义") + { + model = ShowInputBox("自定义模型", "请输入模型名称:", ""); + } + ScrollViewer.ScrollToBottom(); + Gateway.SendMessageStream(question,model,defaultGdbPath,NewMessage_Recall); + } + + private string ShowInputBox(string title, string message, string defaultValue = "") + { + // 创建一个自定义的输入对话框 + var dialog = new System.Windows.Window + { + Title = title, + Width = 300, + Height = 150, + WindowStyle = WindowStyle.ToolWindow, + ResizeMode = ResizeMode.NoResize, + Topmost = true, + WindowStartupLocation = WindowStartupLocation.CenterOwner + }; + + // 设置对话框内容 + var stackPanel = new StackPanel { Margin = new Thickness(10) }; + stackPanel.Children.Add(new TextBlock { Text = message, Margin = new Thickness(0, 0, 0, 5) }); + + var textBox = new TextBox { Text = defaultValue, Margin = new Thickness(0, 0, 0, 10) }; + stackPanel.Children.Add(textBox); + + var buttonPanel = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Right }; + + var okButton = new Button { Content = "确定", Width = 75, Margin = new Thickness(0, 0, 5, 0) }; + okButton.Click += (sender, e) => dialog.Close(); + + var cancelButton = new Button { Content = "取消", Width = 75 }; + cancelButton.Click += (sender, e) => { textBox.Text = null; dialog.Close(); }; + + buttonPanel.Children.Add(okButton); + buttonPanel.Children.Add(cancelButton); + stackPanel.Children.Add(buttonPanel); + + dialog.Content = stackPanel; + + // 显示对话框并获取结果 + dialog.ShowDialog(); + return textBox.Text; + } + + public void NewMessage_Recall(MessageListItem msg) + { + string msgId = msg.id; + log.Info(msg.content); + double verticalOffset = ScrollViewer.VerticalOffset; + double viewportHeight = ScrollViewer.ViewportHeight; + double contentHeight = ChatHistoryStackPanel.ActualHeight; + double tolerance = 24; + if (!idList.Contains(msgId)) + { + //不存在该消息,需添加到ListView中 + if (msg.content == "") + { + if (msg.type == MessageType.END_TAG) + { + StatusTextBlock.Text = ""; + } + return; + } + idList.Add(msgId); + messageDict[msgId] = msg; + if (msg.role == "user") + { + if (msg.type == MessageType.TOOL_MESSAGE) + { + Border border = GetToolChatBorder(msg); + borderItemsDict[msgId] = border; + ChatHistoryStackPanel.Children.Add(border); + StatusTextBlock.Text = "正在执行工具"; + }else if (msg.type == MessageType.CHAT_MESSAGE) + { + Border border = GetUserChatBorder(msg); + borderItemsDict[msgId] = border; + ChatHistoryStackPanel.Children.Add(border); + StatusTextBlock.Text = "正在读取用户输入"; + } + } + else if(msg.role == "assistant") + { + if (msg.type == MessageType.REASON_MESSAGE) + { + Border border = GetAiReasonBorder(msg); + borderItemsDict[msgId] = border; + ChatHistoryStackPanel.Children.Add(border); + StatusTextBlock.Text = "深度思考中"; + }else if (msg.type == MessageType.CHAT_MESSAGE) + { + Border border = GetAiChatBorder(msg); + borderItemsDict[msgId] = border; + ChatHistoryStackPanel.Children.Add(border); + StatusTextBlock.Text = "回答生成中"; + } + } + else if (msg.role == "system") + { + if (msg.type == MessageType.WARNING) + { + + } + } + } + else + { + //已有该消息,只需要修改内容 + messageDict[msgId] = msg; + if (msg.content == "") + { + if (msg.type == MessageType.END_TAG) + { + StatusTextBlock.Text = ""; + } + ChatHistoryStackPanel.Children.Remove(borderItemsDict[msgId]); + borderItemsDict.TryRemove(msgId, out Border border); + messageDict.TryRemove(msgId, out MessageListItem tempMsg); + idList.Remove(msgId); + } + if (msg.role == "user") + { + if (msg.type == MessageType.TOOL_MESSAGE) + { + Border borderItem = borderItemsDict[msgId]; + Grid grid = borderItem.Child as Grid; + TextBlock textBlock = grid.Children[1] as TextBlock; + ToolMessageItem msgItem = msg as ToolMessageItem; + textBlock.Text = msgItem.toolName + " | " + msgItem.status; + Button resButton = grid.Children[3] as Button; + resButton.Tag = msg; + StatusTextBlock.Text = "正在执行工具"; + }else if (msg.type == MessageType.CHAT_MESSAGE) + { + Border borderItem = borderItemsDict[msgId]; + Grid grid = borderItem.Child as Grid; + TextBox textBox = grid.Children[1] as TextBox; + textBox.Text = msg.content; + StatusTextBlock.Text = "正在读取用户输入"; + } + } + else + { + if (msg.type == MessageType.REASON_MESSAGE) + { + Border borderItem = borderItemsDict[msgId]; + Grid grid = borderItem.Child as Grid; + TextBox textBox = grid.Children[0] as TextBox; + textBox.Text = msg.content; + StatusTextBlock.Text = "深度思考中"; + }else if (msg.type == MessageType.CHAT_MESSAGE) + { + Border borderItem = borderItemsDict[msgId]; + Grid grid = borderItem.Child as Grid; + TextBox textBox = grid.Children[1] as TextBox; + textBox.Text = msg.content; + StatusTextBlock.Text = "回答生成中"; + } + } + } + if (Math.Abs(verticalOffset + viewportHeight - contentHeight) < tolerance) + { + ScrollViewer.ScrollToBottom(); + } + } + + private Border GetAiChatBorder(MessageListItem msg) + { + Border border = new Border(); + border.Margin = new Thickness(8, 12, 8, 12); + border.BorderThickness = new Thickness(0); + // border.Background = Brushes.DarkSeaGreen; + Grid grid = new Grid(); + Image icon = new Image() + { + Source = LocalResource.ReadImageByResource("LinkToolAddin.resource.img.linktool.png"), + Stretch = Stretch.Fill, + HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Top + }; + grid.ColumnDefinitions.Add(new ColumnDefinition(){Width = new GridLength(24, GridUnitType.Pixel)}); + grid.ColumnDefinitions.Add(new ColumnDefinition(){Width = new GridLength(1, GridUnitType.Star)}); + TextBox textBox = new TextBox(); + grid.Children.Add(icon); + grid.Children.Add(textBox); + Grid.SetColumn(icon, 0); + Grid.SetColumn(textBox, 1); + textBox.IsReadOnly = true; + textBox.BorderThickness = new Thickness(0); + textBox.Background = Brushes.Transparent; + textBox.Text = msg.content; + textBox.TextWrapping = TextWrapping.Wrap; + border.Child = grid; + return border; + } + + private Border GetAiReasonBorder(MessageListItem msg) + { + Border border = new Border(); + border.Margin = new Thickness(8, 12, 8, 12); + border.BorderThickness = new Thickness(0); + // border.Background = Brushes.DarkSeaGreen; + Grid grid = new Grid(); + Image icon = new Image() + { + Source = LocalResource.ReadImageByResource("LinkToolAddin.resource.img.linktool.png"), + Stretch = Stretch.Fill, + HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Top + }; + Image fold = new Image() + { + Source = LocalResource.ReadImageByResource("LinkToolAddin.resource.img.fold.png"), + Stretch = Stretch.Fill, + HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Top + }; + Image unfold = new Image() + { + Source = LocalResource.ReadImageByResource("LinkToolAddin.resource.img.unfold.png"), + Stretch = Stretch.Fill, + HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Top + }; + grid.ColumnDefinitions.Add(new ColumnDefinition(){Width = new GridLength(24, GridUnitType.Pixel)}); + grid.ColumnDefinitions.Add(new ColumnDefinition(){Width = new GridLength(1, GridUnitType.Star)}); + grid.ColumnDefinitions.Add(new ColumnDefinition(){Width = new GridLength(16, GridUnitType.Pixel)}); + TextBox textBox = new TextBox(); + // grid.Children.Add(icon); + grid.Children.Add(textBox); + // Grid.SetColumn(icon, 0); + Grid.SetColumn(textBox, 1); + textBox.IsReadOnly = true; + textBox.Foreground = Brushes.Gray; + textBox.BorderThickness = new Thickness(2,0,0,0); + textBox.BorderBrush = Brushes.Gray; + textBox.Background = Brushes.Transparent; + textBox.Text = msg.content; + textBox.TextWrapping = TextWrapping.Wrap; + Button button = new Button(); + StackPanel panel = new StackPanel(); + panel.Orientation = Orientation.Horizontal; + panel.Children.Add(fold); + button.Content = panel; + button.BorderThickness = new Thickness(0); + button.Background = Brushes.Transparent; + button.Width = 16; + button.Height = 16; + button.Padding = new Thickness(0); + button.HorizontalAlignment = HorizontalAlignment.Center; + button.VerticalAlignment = VerticalAlignment.Top; + button.Tag = "fold"; + button.Click += FoldButton_OnClick; + Grid.SetColumn(button, 2); + grid.Children.Add(button); + border.Child = grid; + return border; + } + + private void FoldButton_OnClick(object sender, RoutedEventArgs e) + { + Button button = sender as Button; + string tag = button.Tag.ToString(); + if (tag == "fold") + { + button.Tag = "unfold"; + Grid grid = button.Parent as Grid; + TextBox textBox = grid.Children[0] as TextBox; + textBox.Visibility = Visibility.Collapsed; + StackPanel stackPanel = button.Content as StackPanel; + Image unfold = new Image() + { + Source = LocalResource.ReadImageByResource("LinkToolAddin.resource.img.unfold.png"), + Stretch = Stretch.Fill, + HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Top + }; + stackPanel.Children.Clear(); + stackPanel.Children.Add(unfold); + } + else + { + button.Tag = "fold"; + Image fold = new Image() + { + Source = LocalResource.ReadImageByResource("LinkToolAddin.resource.img.fold.png"), + Stretch = Stretch.Fill, + HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Top + }; + Grid grid = button.Parent as Grid; + TextBox textBox = grid.Children[0] as TextBox; + textBox.Visibility = Visibility.Visible; + StackPanel stackPanel = button.Content as StackPanel; + stackPanel.Children.Clear(); + stackPanel.Children.Add(fold); + } + + } + + private Border GetUserChatBorder(MessageListItem msg) + { + Border border = new Border(); + border.Margin = new Thickness(8, 12, 8, 12); + border.BorderThickness = new Thickness(0); + // border.Background = Brushes.DarkSeaGreen; + Grid grid = new Grid(); + Image icon = new Image() + { + Source = LocalResource.ReadImageByResource("LinkToolAddin.resource.img.user.png"), + Stretch = Stretch.Fill, + HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Top + }; + grid.ColumnDefinitions.Add(new ColumnDefinition(){Width = new GridLength(1, GridUnitType.Star)}); + grid.ColumnDefinitions.Add(new ColumnDefinition(){Width = new GridLength(24, GridUnitType.Pixel)}); + TextBox textBox = new TextBox(); + textBox.HorizontalAlignment = HorizontalAlignment.Right; + grid.Children.Add(icon); + grid.Children.Add(textBox); + Grid.SetColumn(icon, 1); + Grid.SetColumn(textBox, 0); + textBox.Background = Brushes.Transparent; + textBox.IsReadOnly = true; + textBox.BorderThickness = new Thickness(0); + textBox.Text = msg.content; + textBox.TextWrapping = TextWrapping.Wrap; + border.Child = grid; + return border; + } + + private Border GetToolChatBorder(MessageListItem msg) + { + Border border = new Border(); + border.Margin = new Thickness(24); + border.Padding = new Thickness(8); + border.BorderThickness = new Thickness(1); + border.BorderBrush = Brushes.Gray; + border.CornerRadius = new CornerRadius(3); + // border.Background = Brushes.DarkSeaGreen; + Grid grid = new Grid(); + Image icon = new Image() + { + Source = LocalResource.ReadImageByResource("LinkToolAddin.resource.img.tool.png"), + Stretch = Stretch.Fill, + HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center + }; + icon.Margin = new Thickness(0, 0, 8, 0); + grid.ColumnDefinitions.Add(new ColumnDefinition(){Width = new GridLength(24, GridUnitType.Pixel)}); + grid.ColumnDefinitions.Add(new ColumnDefinition(){Width = new GridLength(1, GridUnitType.Star)}); + grid.ColumnDefinitions.Add(new ColumnDefinition(){Width = new GridLength(36, GridUnitType.Pixel)}); + grid.ColumnDefinitions.Add(new ColumnDefinition(){Width = new GridLength(36, GridUnitType.Pixel)}); + TextBlock textBlock = new TextBlock(); + ToolMessageItem toolMsg = msg as ToolMessageItem; + textBlock.Text = toolMsg.toolName + " | " + toolMsg.status; + textBlock.TextWrapping = TextWrapping.Wrap; + grid.Children.Add(icon); + grid.Children.Add(textBlock); + Grid.SetColumn(icon, 0); + Grid.SetColumn(textBlock, 1); + Button argsButton = new Button(); + argsButton.Content = "参数"; + argsButton.Tag = msg as ToolMessageItem; + argsButton.Click += ToolArgsButton_OnClick; + border.Child = grid; + Button resButton = new Button(); + resButton.Content = "结果"; + resButton.Tag = msg as ToolMessageItem; + resButton.Click += ToolResButton_OnClick; + grid.Children.Add(argsButton); + Grid.SetColumn(argsButton, 2); + grid.Children.Add(resButton); + Grid.SetColumn(resButton, 3); + return border; + } + + private void ToolArgsButton_OnClick(object sender, RoutedEventArgs e) + { + Button button = sender as Button; + ToolMessageItem toolItem = button.Tag as ToolMessageItem; + ArcGIS.Desktop.Framework.Dialogs.MessageBox.Show(JsonConvert.SerializeObject(toolItem.toolParams),toolItem.toolName+"工具参数"); + } + + private void ToolResButton_OnClick(object sender, RoutedEventArgs e) + { + Button button = sender as Button; + ToolMessageItem toolItem = button.Tag as ToolMessageItem; + ArcGIS.Desktop.Framework.Dialogs.MessageBox.Show(toolItem.result,toolItem.toolName+"运行结果"); + } + + private void TestButton_OnClick(object sender, RoutedEventArgs e) + { + long timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + MessageListItem toolMessageItem = new ToolMessageItem + { + toolName = "toolName", + toolParams = new Dictionary(), + type = MessageType.TOOL_MESSAGE, + status = "success", + content = "JsonConvert.SerializeObject(toolResponse)", + id = (timestamp + 1).ToString(), + role = "user" + }; + NewMessage_Recall(toolMessageItem); + } + + private void ClearButton_OnClick(object sender, RoutedEventArgs e) + { + idList.Clear(); + messageDict.Clear(); + borderItemsDict.Clear(); + ChatHistoryStackPanel.Children.Clear(); + QuestionTextbox.Clear(); + StatusTextBlock.Text = ""; + } + + private void TopButton_OnClick(object sender, RoutedEventArgs e) + { + ScrollViewer.ScrollToTop(); + } + + private void BottomButton_OnClick(object sender, RoutedEventArgs e) + { + ScrollViewer.ScrollToBottom(); + } + + private void StopButton_OnClick(object sender, RoutedEventArgs e) + { + Gateway.StopConversation(); + StatusTextBlock.Text = ""; + } + + private void ModelComboBox_OnSelectionChanged(object sender, SelectionChangedEventArgs e) + { + log.Info("ModelComboBox_OnSelectionChanged"); + string model = ModelComboBox.SelectedValue.ToString(); + log.Info(model); } } } diff --git a/ui/dockpane/DialogDockpaneViewModel.cs b/ui/dockpane/DialogDockpaneViewModel.cs index 5871201..42258fc 100644 --- a/ui/dockpane/DialogDockpaneViewModel.cs +++ b/ui/dockpane/DialogDockpaneViewModel.cs @@ -14,17 +14,28 @@ using ArcGIS.Desktop.Layouts; using ArcGIS.Desktop.Mapping; using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; using System.Text; using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Console; namespace LinkToolAddin.ui.dockpane { internal class DialogDockpaneViewModel : DockPane { private const string _dockPaneID = "DialogDockpane"; + + public ObservableCollection Items { get; set; } - protected DialogDockpaneViewModel() { } + protected DialogDockpaneViewModel() + { + Items = new ObservableCollection(); + Items.Add(new ItemModel(){Content = "adfdfdafdfs"}); + } /// /// Show the DockPane. @@ -56,6 +67,7 @@ namespace LinkToolAddin.ui.dockpane { protected override void OnClick() { + DialogDockpaneViewModel.Show(); } } diff --git a/ui/dockpane/TestDockpane.xaml b/ui/dockpane/TestDockpane.xaml new file mode 100644 index 0000000..0306e36 --- /dev/null +++ b/ui/dockpane/TestDockpane.xaml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ui/dockpane/TestDockpane.xaml.cs b/ui/dockpane/TestDockpane.xaml.cs new file mode 100644 index 0000000..5985146 --- /dev/null +++ b/ui/dockpane/TestDockpane.xaml.cs @@ -0,0 +1,324 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Xml.Linq; +using LinkToolAddin.client; +using LinkToolAddin.client.tool; +using LinkToolAddin.common; +using LinkToolAddin.host; +using LinkToolAddin.host.llm; +using LinkToolAddin.host.llm.entity; +using LinkToolAddin.host.mcp; +using LinkToolAddin.host.prompt; +using LinkToolAddin.message; +using LinkToolAddin.resource; +using LinkToolAddin.server; +using log4net; +using log4net.Appender; +using log4net.Config; +using log4net.Layout; +using ModelContextProtocol.Client; +using ModelContextProtocol.Protocol.Types; +using Newtonsoft.Json; +using MessageBox = ArcGIS.Desktop.Framework.Dialogs.MessageBox; + + +namespace LinkToolAddin.ui.dockpane +{ + /// + /// Interaction logic for TestDockpaneView.xaml + /// + public partial class TestDockpaneView : UserControl + { + private static ILog log = LogManager.GetLogger(typeof(TestDockpaneView)); + + private List idList = new List(); + private ConcurrentDictionary messageDict = new ConcurrentDictionary(); + + public TestDockpaneView() + { + InitLogger(); + InitializeComponent(); + } + + protected void InitLogger() + { + // 1. 创建控制台输出器(Appender) + var consoleAppender = new ConsoleAppender + { + Layout = new PatternLayout("%date [%thread] %-5level %logger - %message%newline"), + Threshold = log4net.Core.Level.Info // 仅输出 Info 及以上级别 + }; + consoleAppender.ActivateOptions(); // 激活配置 + + // 2. 创建文件滚动输出器(按大小滚动) + var fileAppender = new RollingFileAppender + { + File = System.IO.Path.Combine("Logs", "linktool_app.log"), // 日志文件路径 + AppendToFile = true, // 追加模式 + RollingStyle = RollingFileAppender.RollingMode.Size, // 按文件大小滚动 + MaxSizeRollBackups = 10, // 保留 10 个历史文件 + MaximumFileSize = "1MB", // 单个文件最大 1MB + StaticLogFileName = true, // 固定文件名(否则自动追加序号) + Layout = new PatternLayout("%date [%thread] %-5level %logger - %message%newline"), + Threshold = log4net.Core.Level.Info // 仅输出 Info 及以上级别 + }; + fileAppender.ActivateOptions(); // 激活配置 + + // 3. 直接通过 BasicConfigurator 注册 Appender + BasicConfigurator.Configure(consoleAppender, fileAppender); + + log = LogManager.GetLogger(typeof(DialogDockpaneView)); + + // 测试日志输出 + log.Debug("Debug 日志(控制台可见)"); + log.Info("Info 日志(控制台和文件可见)"); + log.Error("Error 日志(严重问题)"); + } + + public void CallBack(string str,object obj) + { + log.Info($"CallBack {str}"); + } + + private async void TestServer_OnClick(object sender, RoutedEventArgs e) + { + log.Info("TestServer Clicked"); + ArcGISProMcpServer.TestMcpServer(); + } + + private async void StdioMcp_test() + { + List args = new List(); + args.Add("mcp-server-time"); + args.Add("--local-timezone=America/New_York"); + McpClient stdioMcpClient = new StdioMcpClient("uvx",args); + IList tools = await stdioMcpClient.GetToolListAsync(); + foreach (McpClientTool tool in tools) + { + log.Info(tool.JsonSchema.ToString()); + } + CallToolResponse response = await stdioMcpClient.CallToolAsync("get_current_time", + new Dictionary { { "timezone", "America/New_York" } }); + log.Info(JsonConvert.SerializeObject(response)); + } + + private async void SseMcp_test() + { + SseMcpClient client = new SseMcpClient("https://mcp.amap.com/sse?key=ed418512c94ade8f83d42c37b77d2bb2"); + IList tools = await client.GetToolListAsync(); + foreach (McpClientTool tool in tools) + { + log.Info(tool.JsonSchema.ToString()); + } + } + + private async void Retrieve_Test() + { + log.Info("TestServer Clicked"); + // string jsonRpcString = @"{""jsonrpc"":""2.0"",""method"":""CallArcGISPro.CallArcGISProTool"",""params"":{""toolName"":""analysis.Buffer"",""toolParams"":""[\""D:/01_Development/02_ArcGIS_Pro_Project/20250319_GisAi/Test.gdb/河流\"",\""D:/01_Development/02_ArcGIS_Pro_Project/20250319_GisAi/Test.gdb/河流buffer\"",\""100\""]""},""id"":1}"; + DocDb docDb = new DocDb("sk-db177155677e438f832860e7f4da6afc", DocDb.KnowledgeBase.ArcGISProHelpDoc); + string query = "缓冲区"; + KnowledgeResult knowledgeResult = await docDb.Retrieve(query); + log.Info(JsonConvert.SerializeObject(knowledgeResult.ChunkList)); + } + + private async void Request_Bailian_Test() + { + Llm bailian = new Bailian + { + api_key = "sk-db177155677e438f832860e7f4da6afc", + app_id = "6a77c5a68de64f469b79fcdcde9d5001", + }; + string reponse = await bailian.SendChatAsync(new LlmJsonContent() + { + Model = "qwen-max", + Messages = new List() + { + new Message() + { + Role = "user", + Content = "你是谁" + } + }, + Temperature = 0.7, + TopP = 1, + MaxTokens = 1000, + }); + 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) + { + throw new System.NotImplementedException(); + } + + private void TestWorkflow_OnClick(object sender, RoutedEventArgs e) + { + // Gateway.SendMessage("你有什么工具可以调用的?","qwen-max","test.gdb",ShowMessage); + Gateway.TestWorkflow("dafdfdgdagdgui","","",AddReply); + } + + public void ShowMessage(MessageListItem msg) + { + log.Info(msg.content); + } + + private async void PromptTestButton_OnClick(object sender, RoutedEventArgs e) + { + string userPrompt = PromptTestTextBox.Text; + // model可选值:qwen3-235b-a22b,qwen-max,deepseek-r1 + try + { + await Task.Run(() => Gateway.SendMessageStream(userPrompt,"qwen3-235b-a22b", "F:\\secondsemester\\linktool\\test\\LinkTool0604\\LinkTool0604.gdb", AddReplyStream)); + } + catch (Exception exception) + { + log.Error(exception.Message); + } + } + + public async void AddReplyStream(MessageListItem msg) + { + await Task.Run(() => ProcessReplyStream(msg)); + } + + private void ProcessReplyStream(MessageListItem msg) + { + string id = msg.id; + if (idList.Contains(id)) + { + messageDict[id] = msg; + } + else + { + idList.Add(id); + messageDict.TryAdd(msg.id, msg); + } + try + { + StringBuilder builder = new StringBuilder(); + foreach (string idStr in idList) + { + MessageListItem msgItem = messageDict[idStr]; + string content = msgItem.content; + if (msgItem.type == MessageType.REASON_MESSAGE) + { + content = "" + content + ""; + } + builder.AppendLine(content); + builder.AppendLine(); + } + Application.Current.Dispatcher.Invoke(() => + { + ReplyTextBox.Clear(); + ReplyTextBox.Text = builder.ToString(); + ReplyTextBox.ScrollToEnd(); + }); + }catch (Exception exception) + { + log.Error(exception.Message); + } + } + + public void AddReply(MessageListItem msg) + { + string content = msg.content; + log.Info(content); + string originContent = ReplyTextBox.Text; + ReplyTextBox.Text = originContent + content; + } + + private void TestStream_OnClick(object sender, RoutedEventArgs e) + { + Request_Bailian_Stream_Test(); + } + + private void StopConversation_OnClick(object sender, RoutedEventArgs e) + { + Gateway.StopConversation(); + } + + 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); + 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; + McpServerList mcpServerList = new McpServerList(); + McpServer mcpServer = mcpServerList.GetServer(serverName); + if (mcpServer is InnerMcpServer) + { + Type type = Type.GetType("LinkToolAddin.client.tool." + serverName); + var toolParamsValues = toolParams.Values.ToArray(); + MethodInfo method = type.GetMethod(toolName, BindingFlags.Public | BindingFlags.Static); + var task = method.Invoke(null, toolParams.Values.ToArray()) as Task; + JsonRpcResultEntity innerResult = await task; + MessageListItem toolMessageItem = new ToolMessageItem + { + toolName = toolName, + toolParams = toolParams, + type = MessageType.TOOL_MESSAGE, + status = "fail", + content = JsonConvert.SerializeObject(innerResult), + id = "1test" + }; + AddReply(toolMessageItem); + } + } + + private void TestResource_OnClick(object sender, RoutedEventArgs e) + { + string content = LocalResource.ReadFileByResource("LinkToolAddin.resource.SystemPrompt.txt"); + MessageBox.Show(content); + } + + private async void TestAttrTable_OnClick(object sender, RoutedEventArgs e) + { + JsonRpcResultEntity result = await ArcGisPro.GetFeatureDatasetAttributeTable( + "D:\\01_Project\\20250305_LinkTool\\20250420_AiDemoProject\\20250420_AiDemoProject.gdb", + "LandUse_2005_Copy", "30"); + log.Info("finish"); + } + } +} diff --git a/ui/dockpane/TestDockpaneViewModel.cs b/ui/dockpane/TestDockpaneViewModel.cs new file mode 100644 index 0000000..daf9d4c --- /dev/null +++ b/ui/dockpane/TestDockpaneViewModel.cs @@ -0,0 +1,62 @@ +using ArcGIS.Core.CIM; +using ArcGIS.Core.Data; +using ArcGIS.Core.Geometry; +using ArcGIS.Desktop.Catalog; +using ArcGIS.Desktop.Core; +using ArcGIS.Desktop.Editing; +using ArcGIS.Desktop.Extensions; +using ArcGIS.Desktop.Framework; +using ArcGIS.Desktop.Framework.Contracts; +using ArcGIS.Desktop.Framework.Dialogs; +using ArcGIS.Desktop.Framework.Threading.Tasks; +using ArcGIS.Desktop.KnowledgeGraph; +using ArcGIS.Desktop.Layouts; +using ArcGIS.Desktop.Mapping; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LinkToolAddin.ui.dockpane +{ + internal class TestDockpaneViewModel : DockPane + { + private const string _dockPaneID = "LinkToolAddin_ui_dockpane_TestDockpane"; + + protected TestDockpaneViewModel() { } + + /// + /// Show the DockPane. + /// + internal static void Show() + { + DockPane pane = FrameworkApplication.DockPaneManager.Find(_dockPaneID); + if (pane == null) + return; + + pane.Activate(); + } + + /// + /// Text shown near the top of the DockPane. + /// + private string _heading = "Test Dockpane"; + public string Heading + { + get => _heading; + set => SetProperty(ref _heading, value); + } + } + + /// + /// Button implementation to show the DockPane. + /// + internal class TestDockpane_ShowButton : Button + { + protected override void OnClick() + { + TestDockpaneViewModel.Show(); + } + } +} diff --git a/ui/message/ChatMessageItem.cs b/ui/message/ChatMessageItem.cs new file mode 100644 index 0000000..2c35847 --- /dev/null +++ b/ui/message/ChatMessageItem.cs @@ -0,0 +1,9 @@ +namespace LinkToolAddin.message; + +public class ChatMessageItem : MessageListItem +{ + public string id { get; set; } + public string role { get; set; } + public string content { get; set; } + public MessageType type { get; set; } +} \ No newline at end of file diff --git a/ui/message/MessageListItem.cs b/ui/message/MessageListItem.cs index 0de3b34..31610a5 100644 --- a/ui/message/MessageListItem.cs +++ b/ui/message/MessageListItem.cs @@ -1,7 +1,19 @@ namespace LinkToolAddin.message; +public enum MessageType +{ + TOOL_MESSAGE, + CHAT_MESSAGE, + REASON_MESSAGE, + END_TAG, + WARNING, + ERROR +} + public interface MessageListItem { + string id { get; set; } string role { get; set; } string content { get; set; } + MessageType type { get; set; } } \ No newline at end of file diff --git a/ui/message/ToolMessageItem.cs b/ui/message/ToolMessageItem.cs new file mode 100644 index 0000000..211a4db --- /dev/null +++ b/ui/message/ToolMessageItem.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; + +namespace LinkToolAddin.message; + +public class ToolMessageItem : MessageListItem +{ + public string id { get; set; } + public string role { get; set; } + public string content { get; set; } + public string toolName { get; set; } + public Dictionary toolParams { get; set; } + public MessageType type { get; set; } + public string status { get; set; } + public string result { get; set; } +} \ No newline at end of file