Compare commits

...

58 Commits
ui ... master

Author SHA1 Message Date
108ee40ffe Merge pull request 'v0.1.4版本' (#2) from host into master
Reviewed-on: #2
2025-06-15 14:42:58 +00:00
7dc73e66eb Merge branch 'prompt' into host 2025-06-15 22:38:59 +08:00
441d8a0759 提示词调整参数问题 2025-06-15 22:36:39 +08:00
776cebd48e 优化模型超参数 2025-06-12 11:17:09 +08:00
3f3ecadec9 1. 修复工具调用前不查知识库问题
2. 完善MCP报错机制
3. 增加ArcGIS Pro查看属性表工具
2025-06-11 00:50:46 +08:00
200db4b10a 合并后提交 2025-06-08 17:10:31 +08:00
09d1b28b5b 提示词 2025-06-08 17:08:16 +08:00
86b800c477 更新对话框版本信息 2025-06-08 16:17:20 +08:00
6a1fe1fa3f 补充程序基本信息 2025-06-08 16:16:26 +08:00
544bc8cd40 解决MCP服务出错影响主程序的问题 2025-06-08 16:13:57 +08:00
9b600174e8 修复回答状态显示异常问题 2025-06-08 15:48:41 +08:00
d0b6671cce 完善ArcGIS工具报错信息 2025-06-08 15:24:55 +08:00
660ba2ad53 增强异常捕获能力,减少闪退 2025-06-08 15:12:48 +08:00
c61938383b 修复Prompt卡片不显示问题 2025-06-08 14:27:26 +08:00
edab972b3e 增加deepseek系列更多模型及自定义模型 2025-06-08 11:53:31 +08:00
ff7e6ad732 合并后提交 2025-06-08 02:09:00 +08:00
d790defcfe prompt 2025-06-08 02:06:41 +08:00
9e17dd7bd6 提示词提及选择与导出的冲突 2025-06-08 02:02:42 +08:00
936c388a62 实现模型选择器功能 2025-06-08 02:01:58 +08:00
b88d405ab8 合并后提交 2025-06-08 00:21:39 +08:00
db488111bb 提示词 2025-06-08 00:12:10 +08:00
56d0e0083c 恢复知识库MCP 2025-06-07 23:19:36 +08:00
718718ef11 Revert "1. 解决提示词卡片显示不出的问题"
This reverts commit 54b78d0b5c.
2025-06-07 23:19:06 +08:00
54b78d0b5c 1. 解决提示词卡片显示不出的问题
2. 强制在第一次调用ArcGIS Pro工具前先查知识库
3. 解决当前回答状态显示错误的问题
2025-06-07 23:17:29 +08:00
e2fd3b376c 执行工具前有文字说明,调用prompt的意识 2025-06-05 00:04:42 +08:00
ec46fe4aad 合并后提交 2025-06-04 21:14:15 +08:00
3b9005381a 优化适配提示词卡片 2025-06-04 16:14:43 +08:00
35054ee7c0 前端UI推理、工具、消息的基础实现 2025-06-03 22:41:03 +08:00
b18c35b7f7 输出纯文本 2025-06-03 22:27:49 +08:00
52148f6936 实现了基础的工具、消息UI界面 2025-06-03 18:35:01 +08:00
c04cc42d94 提示词泛化,错误编码查询 2025-06-02 11:45:35 +08:00
f2bff487b7 Merge branch 'host' into prompt 2025-06-02 11:19:06 +08:00
691f54c73f 内嵌XML规则,明确调用动态Prompt和知识库,简要回答避免画蛇添足 2025-06-02 11:17:24 +08:00
e3dc23de07 解决Prompt找不到和测试输出乱序问题 2025-06-01 23:47:43 +08:00
f4262d6aef 合并后提交 2025-06-01 19:01:45 +08:00
c16557129c 修改系统提示词(没什么大改动) 2025-06-01 18:57:44 +08:00
a15f2c6f53 初步接入Python执行的MCP 2025-06-01 16:44:36 +08:00
d58510e29c UI线程分离解决卡顿问题 2025-06-01 16:04:38 +08:00
7a3516f855 将系统提示词独立为文件 2025-06-01 15:37:56 +08:00
56ed264e22 更新文档 2025-05-31 23:35:42 +08:00
b06e1825df 添加本地文件、必应搜索、网址读取等第三方MCP 2025-05-31 23:31:28 +08:00
23c60b9c34 兼容推理模型,迁移至qwen3开源版 2025-05-31 21:39:03 +08:00
68619f01f8 工具执行错误消息合并为单条,集中模型注意力 2025-05-31 20:55:43 +08:00
d2b18958c1 动态提示词支持参数,格式与Tool对齐 2025-05-31 20:51:13 +08:00
6f384920a3 完善对话历史解决ArcGIS Pro工具返回为空的问题 2025-05-31 17:41:19 +08:00
602fd94fc0 修复重复调用工具的问题,优化流程结束逻辑 2025-05-31 17:33:33 +08:00
af43d0d774 实现了文本内嵌XML调用的识别 2025-05-31 16:20:38 +08:00
9fe13fa719 1. 修复ArcGIS Pro工具范围结果为空的问题
2. 识别单条消息末尾的[DONE]
2025-05-31 11:52:41 +08:00
ea021135a3 合并host分支 2025-05-29 21:01:07 +08:00
9c6a45a58c 系统提示词格式优化 2025-05-28 13:28:46 +08:00
a4aec62065 提示词 2025-05-26 10:08:37 +08:00
43ea7dd06a 添加停止对话按钮,增加文本框高度 2025-05-26 00:07:37 +08:00
271f42dd71 接入知识库查询MCP 2025-05-26 00:07:22 +08:00
4bbc743ef2 修复ArcGIS Pro工具调用传参类型错误问题 2025-05-26 00:07:06 +08:00
c2c65e5a35 1. 配合程序调试,适当修改系统提示词
2. 合并Continue Prompt与工具运行结果
2025-05-26 00:06:32 +08:00
619a30906c 系统提示词:只能输出格式规定的;增加prompt的格式;继续与错误会有执行结果分析;结束之后单独发送一个[DONE];增加用户的提示词
仍存在问题:流式输出反复调用相同的工具,markdown格式
2025-05-24 15:40:09 +08:00
675efd6b33 初步system提示词 2025-05-23 17:53:54 +08:00
dc29a12a12 环境配置 2025-05-19 22:09:21 +08:00
43 changed files with 1837 additions and 352 deletions

View File

@ -6,6 +6,13 @@
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<NoWarn>CA1416</NoWarn> <NoWarn>CA1416</NoWarn>
<TargetFramework>net8.0-windows</TargetFramework> <TargetFramework>net8.0-windows</TargetFramework>
<Version>0.1.3</Version>
<Title>LinkToolAddin</Title>
<Authors>LinkTool团队</Authors>
<Description>LinkTool以大模型赋能让您只需一两句话便能完成复杂的空间分析与时空大数据处理任务。</Description>
<Copyright>华南农业大学</Copyright>
<PackageReleaseNotes>校AI大赛提交版本</PackageReleaseNotes>
<Company>华南农业大学</Company>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<None Remove="Config.daml" /> <None Remove="Config.daml" />
@ -97,10 +104,6 @@
<Private>False</Private> <Private>False</Private>
</Reference> </Reference>
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="doc\" />
<Folder Include="resource\" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Esri.ArcGISPro.Extensions30" Version="3.4.1.55405" /> <PackageReference Include="Esri.ArcGISPro.Extensions30" Version="3.4.1.55405" />
<PackageReference Include="log4net" Version="3.1.0" /> <PackageReference Include="log4net" Version="3.1.0" />
@ -108,6 +111,42 @@
<PackageReference Include="ModelContextProtocol.AspNetCore" Version="0.1.0-preview.13" /> <PackageReference Include="ModelContextProtocol.AspNetCore" Version="0.1.0-preview.13" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Newtonsoft.Json.Schema" Version="4.0.2-beta2" /> <PackageReference Include="Newtonsoft.Json.Schema" Version="4.0.2-beta2" />
</ItemGroup>
<ItemGroup>
<None Remove="resource\SystemPrompt.txt" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="resource\prompt\SystemPrompt.txt">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</EmbeddedResource>
<None Remove="resource\prompt\ContinuePrompt.txt" />
<EmbeddedResource Include="resource\prompt\ContinuePrompt.txt">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</EmbeddedResource>
<None Remove="resource\prompt\ErrorPrompt.txt" />
<EmbeddedResource Include="resource\prompt\ErrorPrompt.txt">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</EmbeddedResource>
<None Remove="resource\img\linktool.png" />
<EmbeddedResource Include="resource\img\linktool.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</EmbeddedResource>
<None Remove="resource\img\user.png" />
<EmbeddedResource Include="resource\img\user.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</EmbeddedResource>
<None Remove="resource\img\tool.png" />
<EmbeddedResource Include="resource\img\tool.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</EmbeddedResource>
<None Remove="resource\img\unfold.png" />
<EmbeddedResource Include="resource\img\unfold.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</EmbeddedResource>
<None Remove="resource\img\fold.png" />
<EmbeddedResource Include="resource\img\fold.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</EmbeddedResource>
</ItemGroup> </ItemGroup>
<Import Project="C:\Program Files\ArcGIS\Pro\bin\Esri.ProApp.SDK.Desktop.targets" Condition="Exists('C:\Program Files\ArcGIS\Pro\bin\Esri.ProApp.SDK.Desktop.targets') AND !Exists('Esri.ArcGISPro.Extensions.targets')" /> <Import Project="C:\Program Files\ArcGIS\Pro\bin\Esri.ProApp.SDK.Desktop.targets" Condition="Exists('C:\Program Files\ArcGIS\Pro\bin\Esri.ProApp.SDK.Desktop.targets') AND !Exists('Esri.ArcGISPro.Extensions.targets')" />
</Project> </Project>

View File

@ -1,12 +1,12 @@
{ {
"profiles": { "profiles": {
"LinkToolAddin": { "LinkToolAddin": {
"commandName": "Executable", "commandName": "Executable",
"executablePath": "C:\\Program Files\\ArcGIS\\Pro\\bin\\ArcGISPro.exe", "executablePath": "C:\\Users\\86158\\AppData\\Local\\Programs\\ArcGIS\\Pro\\bin\\ArcGISPro.exe",
"applicationUrl": "https://localhost:5001", "applicationUrl": "https://localhost:5001",
"environmentVariables": { "environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development" "ASPNETCORE_ENVIRONMENT": "Development"
} }
} }
} }
} }

View File

@ -31,6 +31,7 @@ git clone xxx.git
5. 部分情况下可能还需要替换LinkToolAddin.csproj中的所有相关安装路径 5. 部分情况下可能还需要替换LinkToolAddin.csproj中的所有相关安装路径
6. 点击运行进行测试看是否能正常打开ArcGIS Pro和LinkTool插件 6. 点击运行进行测试看是否能正常打开ArcGIS Pro和LinkTool插件
7. 确认无误建议commit到本地的master分支以备后续使用但请勿推送至远端 7. 确认无误建议commit到本地的master分支以备后续使用但请勿推送至远端
8. 修改McpServerList里面filesystem的白名单目录
#### 创建分支 #### 创建分支

View File

@ -51,9 +51,18 @@ public class StdioMcpClient : McpClient
public async Task<IList<McpClientTool>> GetToolListAsync() public async Task<IList<McpClientTool>> GetToolListAsync()
{ {
IMcpClient client = await McpClientFactory.CreateAsync(new StdioClientTransport(transportOptions)); try
IList<McpClientTool> tools = await client.ListToolsAsync(); {
return tools; IMcpClient client = await McpClientFactory.CreateAsync(new StdioClientTransport(transportOptions));
IList<McpClientTool> tools = await client.ListToolsAsync();
return tools;
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
return null;
}
} }
public async Task<CallToolResponse> CallToolAsync(string toolName, Dictionary<string, object> parameters = null) public async Task<CallToolResponse> CallToolAsync(string toolName, Dictionary<string, object> parameters = null)

View File

@ -1,29 +1,41 @@
using System.Collections.Generic; using System.Collections.Generic;
using LinkToolAddin.host;
using LinkToolAddin.host.prompt;
namespace LinkToolAddin.client.prompt; namespace LinkToolAddin.client.prompt;
public class DynamicPrompt public class DynamicPrompt
{ {
public static string GetPrompt(string name,Dictionary<string,object> args = null) public static string GetPrompt(string name,Dictionary<string,string> args = null)
{ {
PromptTemplates promptTemplate = new PromptTemplates(); PromptServerList promptServerList = new PromptServerList();
string template = promptTemplate.GetPrompt(name); string template = promptServerList.GetPromptServer(name).Content;
if (args == null) if (args == null)
{ {
return template; return template;
} }
foreach (KeyValuePair<string,object> pair in args) foreach (KeyValuePair<string,string> pair in args)
{ {
string replaceKey = "{{"+pair.Key+"}}"; string replaceKey = "{{"+pair.Key+"}}";
template.Replace(replaceKey, pair.Value.ToString()); template = template.Replace(replaceKey, pair.Value.ToString());
} }
return template; return template;
} }
public static Dictionary<string, string> GetAllPrompts() public static List<UserPrompt> GetAllPrompts()
{ {
PromptTemplates promptTemplate = new PromptTemplates(); PromptServerList promptServerList = new PromptServerList();
Dictionary<string, string> template = promptTemplate.GetPromptsDict(); List<UserPrompt> prompts = new List<UserPrompt>();
return template; Dictionary<string, PromptServer> promptDefinitions = promptServerList.GetPromptsDict();
foreach (KeyValuePair<string, PromptServer> pair in promptDefinitions)
{
prompts.Add(new UserPrompt()
{
Name = pair.Value.Name,
Description = pair.Value.Description,
Arguments = pair.Value.Arguments
});
}
return prompts;
} }
} }

View File

@ -1,4 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using LinkToolAddin.host.prompt;
namespace LinkToolAddin.client.prompt; namespace LinkToolAddin.client.prompt;
@ -8,9 +9,17 @@ public class PromptTemplates
public PromptTemplates() public PromptTemplates()
{ {
prompts.Add("plan", "请根据用户所提问题进行工具规划"); prompts.Add("plan", "请根据用户所提问题进行工具规划,输出格式为'1.工具2.工具如果是ArcGIS Pro的工具根据用户的具体需求和提供的数据类型," +
prompts.Add("param", "根据帮助文档填写工具参数"); "判断并列出所有必要的分析步骤和工具同时严格遵守知识库中“ArcGIS Pro工具调用大全”里“工具调用名称”一列的工具命名规则(例如analysis.Erase)" +
prompts.Add("code", "现在需要生成代码,要求语法正确"); "确保工具名的准确无误,工具与所属工具箱的对应关系正确无误,严格遵循文档规定的格式和大小写。工具的组合顺序优先参考知识库中“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) public string GetPrompt(string name)

View File

@ -1,27 +1,59 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using ArcGIS.Core.Data; using ArcGIS.Core.Data;
using ArcGIS.Core.Data.Raster; using ArcGIS.Core.Data.Raster;
using ArcGIS.Core.Geometry; using ArcGIS.Core.Geometry;
using ArcGIS.Desktop.Core.Geoprocessing;
using ArcGIS.Desktop.Framework.Threading.Tasks; using ArcGIS.Desktop.Framework.Threading.Tasks;
using LinkToolAddin.server; using LinkToolAddin.server;
using LinkToolAddin.ui.dockpane;
using log4net;
using ModelContextProtocol.Server; using ModelContextProtocol.Server;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace LinkToolAddin.client.tool; namespace LinkToolAddin.client.tool;
public class ArcGisPro public class ArcGisPro
{ {
[McpServerTool, Description("可以通过调用ArcGIS Pro的地理处理工具实现一些数据处理功能。")] private static ILog log = LogManager.GetLogger(typeof(ArcGisPro));
[McpServerTool, Description("可以通过调用ArcGIS Pro的地理处理工具实现一些数据处理功能传入参数必须严格遵循ArcGIS Pro调用工具的标准调用名和参数要求知识库。")]
public static async Task<JsonRpcResultEntity> ArcGisProTool(string toolName, List<string> toolParams) public static async Task<JsonRpcResultEntity> ArcGisProTool(string toolName, List<string> toolParams)
{ {
// Call the ArcGIS Pro method and get the result IGPResult results = await Geoprocessing.ExecuteToolAsync(toolName, toolParams,null,null,null,GPExecuteToolFlags.InheritGPOptions|GPExecuteToolFlags.GPThread);
var result = await server.CallArcGISPro.CallArcGISProTool(toolName, toolParams); JsonRpcResultEntity jsonRpcResultEntity;
if (results.IsFailed)
// Serialize the result back to a JSON string {
return result; 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坐标获取字段列表等")] [McpServerTool, Description("查看指定数据的坐标系、范围、几何类型、是否有Z坐标和M坐标获取字段列表等")]
@ -68,7 +100,7 @@ public class ArcGisPro
return result; return result;
} }
[McpServerTool, Description("列出gdb数据库中的所有数据名称")] [McpServerTool, Description("列出gdb数据库中的所有数据名称")]
public static async Task<JsonRpcResultEntity> ListData(string gdbPath) public static async Task<JsonRpcResultEntity> ListData(string gdbPath)
{ {
var datasets = new List<string>(); var datasets = new List<string>();
@ -104,4 +136,88 @@ public class ArcGisPro
}; };
return result; return result;
} }
[McpServerTool, Description("获取要素类的属性表内容可以查看属性表中的至多前20条记录的内容")]
public static async Task<JsonRpcResultEntity> 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<FeatureClass>(dataName);
List<Dictionary<string, string>> 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<List<Dictionary<string, string>>> GetAttributeTableAsync(FeatureClass featureClass,int limit = 5)
{
if (limit > 20)
limit = 20;
return await QueuedTask.Run(() =>
{
var result = new List<Dictionary<string, string>>();
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<string, string>();
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<IGPMessage> messages)
{
StringBuilder messagesStr = new StringBuilder();
foreach (var gpMessage in messages)
{
messagesStr.AppendLine(gpMessage.Text);
}
return messagesStr.ToString();
}
} }

View File

@ -22,4 +22,28 @@ public class KnowledgeBase
}; };
return result; return result;
} }
[McpServerTool, Description("查询ArcGIS Pro调用工具的标准调用名和参数要求知识库")]
public static async Task<JsonRpcResultEntity> 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<JsonRpcResultEntity> 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;
}
} }

View File

@ -67,7 +67,7 @@ public class HttpRequest
return null; return null;
} }
public static async IAsyncEnumerable<string> PostWithStreamingResponseAsync( public static async IAsyncEnumerable<LlmStreamChat> PostWithStreamingResponseAsync(
string url, string url,
string body, string body,
string apiKey, string apiKey,
@ -114,7 +114,7 @@ public class HttpRequest
if (dataObj is not null) if (dataObj is not null)
{ {
yield return dataObj.Choices[0].Delta.Content; yield return dataObj;
} }
} }
} }

47
common/LocalResource.cs Normal file
View File

@ -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;
}
}
}

28
doc/TodoList.md Normal file
View File

@ -0,0 +1,28 @@
# 待办事项
本文档用于记录和跟踪当前阶段待办事项及完成进度
## 核心程序
- [x] 提示词调用完整实现:以面向对象形式取代字典形式,加入参数和描述
- [x] 接入本地文件系统等基础性MCP服务
- [ ] Python代码执行的实现
- [x] 适配qwen3、deepseek等推理模型并迁移
- [x] 工具执行错误消息合并为一条
## 提示词工程
- [x] 将原来的单独XML规则修改为内嵌XML规则
- [x] 系统提示词明确提示调用动态Prompt和知识库
- [x] 错误和继续提示词预留工具消息占位符
- [x] 错误和继续提示词明示
- [ ] 系统提示词泛化增强
- [ ] 针对qwen3适当调整
## 前端交互
- [ ] 三类消息卡片
- [ ] 流式输出根据id匹配修改或新增
- [ ] 添加工作空间、发送、复制粘贴等交互
- [ ] 独立UI线程
- [ ] 工具卡片特殊提示,弹出窗口显示内容

View File

@ -8,12 +8,15 @@ using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows;
using System.Windows.Documents;
using System.Xml; using System.Xml;
using System.Xml.Linq; using System.Xml.Linq;
using ArcGIS.Desktop.Framework.Dialogs; using ArcGIS.Desktop.Internal.Mapping;
using ArcGIS.Desktop.Internal.Mapping.Locate; using ArcGIS.Desktop.Internal.Mapping.Locate;
using LinkToolAddin.client; using LinkToolAddin.client;
using LinkToolAddin.client.prompt; using LinkToolAddin.client.prompt;
using LinkToolAddin.client.tool;
using LinkToolAddin.host.llm; using LinkToolAddin.host.llm;
using LinkToolAddin.host.llm.entity; using LinkToolAddin.host.llm.entity;
using LinkToolAddin.host.mcp; using LinkToolAddin.host.mcp;
@ -31,12 +34,21 @@ using Newtonsoft.Json.Schema;
using Newtonsoft.Json.Schema.Generation; using Newtonsoft.Json.Schema.Generation;
using Tool = LinkToolAddin.host.mcp.Tool; using Tool = LinkToolAddin.host.mcp.Tool;
using LinkToolAddin.common; using LinkToolAddin.common;
using LinkToolAddin.host.llm.entity.stream;
using MessageBox = ArcGIS.Desktop.Framework.Dialogs.MessageBox;
namespace LinkToolAddin.host; namespace LinkToolAddin.host;
public class Gateway public class Gateway
{ {
private static ILog log = LogManager.GetLogger(typeof(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<MessageListItem> callback) public static async void SendMessage(string message, string model, string gdbPath, Action<MessageListItem> callback)
{ {
Llm bailian = new Bailian Llm bailian = new Bailian
@ -66,9 +78,11 @@ public class Gateway
{ {
Model = model, Model = model,
Messages = messages, Messages = messages,
Temperature = 0.7, Temperature = 0.3,
TopP = 1, TopP = 0.6,
MaxTokens = 1000, TopK = 25,
MaxTokens = 1024,
ThinkingBudget = 1024
}); });
long timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); long timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
log.Info(reponse); log.Info(reponse);
@ -104,12 +118,7 @@ public class Gateway
messages.Add(new Message messages.Add(new Message
{ {
Role = "user", Role = "user",
Content = toolResponse.IsError ? SystemPrompt.ErrorPromptTemplate : SystemPrompt.ContinuePromptTemplate Content = toolResponse.IsError ? SystemPrompt.ErrorPrompt(JsonConvert.SerializeObject(toolResponse)) : SystemPrompt.ContinuePrompt(JsonConvert.SerializeObject(toolResponse))
});
messages.Add(new Message
{
Role = "user",
Content = JsonConvert.SerializeObject(toolResponse)
}); });
callback?.Invoke(toolMessageItem); callback?.Invoke(toolMessageItem);
}else if (mcpServer is StdioMcpServer) }else if (mcpServer is StdioMcpServer)
@ -129,12 +138,7 @@ public class Gateway
messages.Add(new Message messages.Add(new Message
{ {
Role = "user", Role = "user",
Content = toolResponse.IsError ? SystemPrompt.ErrorPromptTemplate : SystemPrompt.ContinuePromptTemplate Content = toolResponse.IsError ? SystemPrompt.ErrorPrompt(JsonConvert.SerializeObject(toolResponse)) : SystemPrompt.ContinuePrompt(JsonConvert.SerializeObject(toolResponse))
});
messages.Add(new Message
{
Role = "user",
Content = JsonConvert.SerializeObject(toolResponse)
}); });
callback?.Invoke(toolMessageItem); callback?.Invoke(toolMessageItem);
}else if (mcpServer is InnerMcpServer) }else if (mcpServer is InnerMcpServer)
@ -156,12 +160,7 @@ public class Gateway
messages.Add(new Message messages.Add(new Message
{ {
Role = "user", Role = "user",
Content = SystemPrompt.ErrorPromptTemplate Content = SystemPrompt.ErrorPrompt(JsonConvert.SerializeObject(innerResult))
});
messages.Add(new Message
{
Role = "user",
Content = JsonConvert.SerializeObject(innerResult)
}); });
callback?.Invoke(toolMessageItem); callback?.Invoke(toolMessageItem);
}else if (innerResult is JsonRpcSuccessEntity) }else if (innerResult is JsonRpcSuccessEntity)
@ -177,12 +176,7 @@ public class Gateway
messages.Add(new Message messages.Add(new Message
{ {
Role = "user", Role = "user",
Content = SystemPrompt.ContinuePromptTemplate Content = SystemPrompt.ContinuePrompt(JsonConvert.SerializeObject(innerResult))
});
messages.Add(new Message
{
Role = "user",
Content = JsonConvert.SerializeObject(innerResult)
}); });
callback?.Invoke(toolMessageItem); callback?.Invoke(toolMessageItem);
} }
@ -196,7 +190,7 @@ public class Gateway
Dictionary<string, object> promptParams = JsonConvert.DeserializeObject<Dictionary<string, object>>(promptArgs); Dictionary<string, object> promptParams = JsonConvert.DeserializeObject<Dictionary<string, object>>(promptArgs);
string serverName = fullPromptName.Contains(":") ? fullPromptName.Split(':')[0] : fullPromptName; string serverName = fullPromptName.Contains(":") ? fullPromptName.Split(':')[0] : fullPromptName;
string promptName = fullPromptName.Contains(":") ? fullPromptName.Split(':')[1] : fullPromptName; string promptName = fullPromptName.Contains(":") ? fullPromptName.Split(':')[1] : fullPromptName;
string promptRes = DynamicPrompt.GetPrompt(promptName, promptParams); string promptRes = DynamicPrompt.GetPrompt(promptName, null);
messages.Add(new Message messages.Add(new Message
{ {
Role = "user", Role = "user",
@ -214,13 +208,34 @@ public class Gateway
}; };
callback?.Invoke(chatMessageListItem); callback?.Invoke(chatMessageListItem);
} }
if (reponse == "[DONE]") if (reponse.EndsWith("[DONE]"))
{ {
goOn = false; 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<MessageListItem> callback) public static async void SendMessageStream(string message, string model, string gdbPath, Action<MessageListItem> callback)
{ {
Llm bailian = new Bailian Llm bailian = new Bailian
@ -228,324 +243,569 @@ public class Gateway
api_key = "sk-db177155677e438f832860e7f4da6afc" api_key = "sk-db177155677e438f832860e7f4da6afc"
}; };
List<Message> messages = new List<Message>(); List<Message> messages = new List<Message>();
string toolInfos = await GetToolInfos(new McpServerList()); string toolInfos = "";
log.Info(SystemPrompt.SysPrompt(gdbPath, toolInfos)); try
messages.Add(new Message
{ {
Role = "system", toolInfos = await GetToolInfos(new McpServerList());
Content = SystemPrompt.SysPrompt(gdbPath, toolInfos) messages.Add(new Message
}); {
messages.Add(new Message Role = "system",
Content = SystemPrompt.SysPrompt(gdbPath, toolInfos)
});
messages.Add(new Message
{
Role = "user",
Content = message
});
}catch (Exception ex)
{ {
Role = "user", log.Error(ex);
Content = message MessageBox.Show(ex.Message,"获取MCP列表失败");
}); }
bool goOn = true; goOn = true;
string toolPattern = "^<tool_use>[\\s\\S]*?<\\/tool_use>$"; string toolPattern = "<tool_use>([\\s\\S]*?)<name>([\\s\\S]*?)<\\/name>([\\s\\S]*?)<arguments>([\\s\\S]*?)<\\/arguments>([\\s\\S]*?)<\\/tool_use>";
string promptPattern = "^<prompt>[\\s\\S]*?<\\/prompt>$"; string promptPattern = "<prompt>([\\s\\S]*?)<name>([\\s\\S]*?)<\\/name>([\\s\\S]*?)<arguments>([\\s\\S]*?)<\\/arguments>([\\s\\S]*?)<\\/prompt>";
McpServerList mcpServerList = new McpServerList(); McpServerList mcpServerList = new McpServerList();
PromptServerList promptServerList = new PromptServerList();
int loop = 0;
string messageContent = ""; //一次请求下完整的response
bool queriedKnowledge = false;
bool executedTool = false;
while (goOn) while (goOn)
{ {
loop++;
if (loop > 500)
{
MessageBox.Show("达到最大循环次数", "退出循环");
break;
}
LlmJsonContent jsonContent = new LlmJsonContent() LlmJsonContent jsonContent = new LlmJsonContent()
{ {
Model = model, Model = model,
Messages = messages, Messages = messages,
Temperature = 0.7, Temperature = 0.3,
TopP = 1, TopP = 0.4,
TopK = 7,
MaxTokens = 1000, MaxTokens = 1000,
}; };
long timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); long timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
string messageContent = ""; List<McpToolRequest> mcpToolRequests = new List<McpToolRequest>();
await foreach(var chunk in bailian.SendChatStreamAsync(jsonContent)) List<PromptRequest> promptRequests = new List<PromptRequest>();
var (toolMatched, toolRemaining) = ExtractMatchedPart(messageContent, toolPattern);
var (promptMatched, promptRemaining) = ExtractMatchedPart(messageContent, promptPattern);
if (toolMatched == "" && promptMatched == "" && messageContent != "")
{ {
if (chunk == "[DONE]") //如果本次回复不包含任何工具的调用或提示词的调用,则不再请求
goOn = false;
MessageListItem endMessageListItem1 = new ChatMessageItem
{ {
goOn = false; type = MessageType.END_TAG,
}else if (chunk.StartsWith("<tool_use>")) content = "",
id = (timestamp + 3).ToString(),
role = "assistant"
};
callback?.Invoke(endMessageListItem1);
break;
}
try
{
await foreach(LlmStreamChat llmStreamChat in bailian.SendChatStreamAsync(jsonContent))
{ {
if (Regex.IsMatch(chunk, toolPattern)) if (!goOn)
{ {
//返回工具卡片 MessageListItem endMessageListItem2 = new ChatMessageItem
XElement toolUse = XElement.Parse(chunk);
string fullToolName = toolUse.Element("name")?.Value;
string toolArgs = toolUse.Element("arguments")?.Value;
Dictionary<string, object> toolParams = JsonConvert.DeserializeObject<Dictionary<string, object>>(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; type = MessageType.END_TAG,
SseMcpClient client = new SseMcpClient(sseMcpServer.BaseUrl); content = "",
CallToolResponse toolResponse = await client.CallToolAsync(toolName,toolParams); id = (timestamp+3).ToString(),
MessageListItem toolMessageItem = new ToolMessageItem role = "assistant"
{ };
toolName = toolName, callback?.Invoke(endMessageListItem2);
toolParams = toolParams, break;
type = MessageType.TOOL_MESSAGE, }
status = toolResponse.IsError ? "fail" : "success", try
content = JsonConvert.SerializeObject(toolResponse), {
id = timestamp.ToString() string chunk = llmStreamChat.Choices[0].Delta.Content;
}; MessageListItem reasonMessageListItem = new ChatMessageItem()
messages.Add(new Message
{
Role = "user",
Content = toolResponse.IsError ? SystemPrompt.ErrorPromptTemplate : SystemPrompt.ContinuePromptTemplate
});
messages.Add(new Message
{
Role = "user",
Content = JsonConvert.SerializeObject(toolResponse)
});
callback?.Invoke(toolMessageItem);
}else if (mcpServer is StdioMcpServer)
{ {
StdioMcpServer stdioMcpServer = mcpServer as StdioMcpServer; content = llmStreamChat.Choices[0].Delta.ResoningContent,
StdioMcpClient client = new StdioMcpClient(stdioMcpServer.Command, stdioMcpServer.Args); role = "assistant",
CallToolResponse toolResponse = await client.CallToolAsync(toolName,toolParams); type = MessageType.REASON_MESSAGE,
MessageListItem toolMessageItem = new ToolMessageItem id = (timestamp+2).ToString()
{ };
toolName = toolName, Application.Current.Dispatcher.Invoke(() =>
toolParams = toolParams,
type = MessageType.TOOL_MESSAGE,
status = toolResponse.IsError ? "fail" : "success",
content = JsonConvert.SerializeObject(toolResponse),
id = timestamp.ToString()
};
messages.Add(new Message
{
Role = "user",
Content = toolResponse.IsError ? SystemPrompt.ErrorPromptTemplate : SystemPrompt.ContinuePromptTemplate
});
messages.Add(new Message
{
Role = "user",
Content = JsonConvert.SerializeObject(toolResponse)
});
callback?.Invoke(toolMessageItem);
}else if (mcpServer is InnerMcpServer)
{ {
Type type = Type.GetType("LinkToolAddin.client.tool."+serverName); callback?.Invoke(reasonMessageListItem);
MethodInfo method = type.GetMethod(toolName,BindingFlags.Public | BindingFlags.Static); });
var task = method.Invoke(null, toolParams.Values.ToArray()) as Task<JsonRpcResultEntity>; messageContent = chunk;
JsonRpcResultEntity innerResult = await task; var (matched, remaining) = ExtractMatchedPart(chunk, toolPattern);
if (innerResult is JsonRpcErrorEntity) if (matched == "")
{
var (matchedPrompt, remainingPrompt) = ExtractMatchedPart(chunk, promptPattern);
if (matchedPrompt == "")
{ {
MessageListItem toolMessageItem = new ToolMessageItem //普通消息文本
MessageListItem chatMessageListItem = new ChatMessageItem()
{ {
toolName = toolName, content = remainingPrompt,
toolParams = toolParams, role = "assistant",
type = MessageType.TOOL_MESSAGE, type = MessageType.CHAT_MESSAGE,
status = "fail",
content = JsonConvert.SerializeObject(innerResult),
id = timestamp.ToString() id = timestamp.ToString()
}; };
messages.Add(new Message Application.Current.Dispatcher.Invoke(() =>
{ {
Role = "user", callback?.Invoke(chatMessageListItem);
Content = SystemPrompt.ErrorPromptTemplate
}); });
messages.Add(new Message }
{ else
Role = "user",
Content = JsonConvert.SerializeObject(innerResult)
});
callback?.Invoke(toolMessageItem);
}else if (innerResult is JsonRpcSuccessEntity)
{ {
MessageListItem toolMessageItem = new ToolMessageItem //包含Prompt调用请求的消息
MessageListItem chatMessageListItem = new ChatMessageItem()
{ {
toolName = toolName, content = remainingPrompt,
toolParams = toolParams, role = "assistant",
type = MessageType.TOOL_MESSAGE, type = MessageType.CHAT_MESSAGE,
status = "success",
content = JsonConvert.SerializeObject(innerResult),
id = timestamp.ToString() id = timestamp.ToString()
}; };
messages.Add(new Message callback?.Invoke(chatMessageListItem);
XElement promptUse = XElement.Parse(matchedPrompt);
string promptKey = promptUse.Element("name")?.Value;
string promptArgs = promptUse.Element("arguments")?.Value;
Dictionary<string, string> promptParams = JsonConvert.DeserializeObject<Dictionary<string, string>>(promptArgs);
promptRequests = new List<PromptRequest>();
promptRequests.Add(new PromptRequest()
{ {
Role = "user", PromptName = promptKey,
Content = SystemPrompt.ContinuePromptTemplate PromptArgs = promptParams,
PromptServer = promptServerList.GetPromptServer(promptKey)
}); });
messages.Add(new Message
{
Role = "user",
Content = JsonConvert.SerializeObject(innerResult)
});
callback?.Invoke(toolMessageItem);
} }
} }
} else
else
{
MessageListItem toolMessageItem = new ToolMessageItem
{ {
toolName = "", //包含工具调用请求的消息
toolParams = new Dictionary<string, object>(), XElement toolUse = XElement.Parse(matched);
type = MessageType.TOOL_MESSAGE, string fullToolName = toolUse.Element("name")?.Value;
status = "loading", string toolArgs = toolUse.Element("arguments")?.Value;
content = "正在生成工具调用参数", Dictionary<string, object> toolParams = JsonConvert.DeserializeObject<Dictionary<string, object>>(toolArgs);
id = timestamp.ToString() string serverName = fullToolName.Contains(":") ? fullToolName.Split(':')[0] : fullToolName;
}; string toolName = fullToolName.Contains(":") ? fullToolName.Split(':')[1] : fullToolName;
callback?.Invoke(toolMessageItem); McpServer mcpServer = mcpServerList.GetServer(serverName);
continue; //将工具调用请求添加至列表中待一次回答完整后再统一进行调用
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 mcpToolRequest = new McpToolRequest()
{
McpServer = mcpServer,
ToolName = toolName,
ToolArgs = toolParams,
};
mcpToolRequests.Add(mcpToolRequest);
}
} }
}else if (chunk.StartsWith("<prompt>")) catch (Exception e)
{
if (Regex.IsMatch(chunk, promptPattern))
{ {
XElement promptUse = XElement.Parse(chunk); Console.WriteLine(e);
string promptKey = promptUse.Element("name")?.Value; log.Error(e.Message);
string promptContent = DynamicPrompt.GetPrompt(promptKey,null); }
}
}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<string, object> 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 messages.Add(new Message
{ {
Role = "user", Role = "user",
Content = JsonConvert.SerializeObject(promptContent) Content = toolResponse.IsError
? SystemPrompt.ErrorPrompt(JsonConvert.SerializeObject(toolResponse))
: SystemPrompt.ContinuePrompt(JsonConvert.SerializeObject(toolResponse))
}); });
MessageListItem toolMessageItem = new ToolMessageItem Application.Current.Dispatcher.Invoke(() =>
{ {
toolName = "调用提示词", callback?.Invoke(toolMessageItem1);
toolParams = null, });
type = MessageType.TOOL_MESSAGE,
status = "success",
content = promptKey,
id = timestamp.ToString()
};
callback?.Invoke(toolMessageItem);
} }
else else if (mcpServer is StdioMcpServer)
{ {
MessageListItem toolMessageItem = new ToolMessageItem 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 = toolName,
toolParams = null, toolParams = toolParams,
type = MessageType.TOOL_MESSAGE, type = MessageType.TOOL_MESSAGE,
status = "loading", status = toolResponse.IsError ? "fail" : "success",
content = "正在调用提示词", content = JsonConvert.SerializeObject(toolResponse),
id = timestamp.ToString() result = JsonConvert.SerializeObject(toolResponse),
id = (timestamp + 1).ToString(),
role = "user"
}; };
callback?.Invoke(toolMessageItem); messages.Add(new Message
{
Role = "user",
Content = toolResponse.IsError
? SystemPrompt.ErrorPrompt(JsonConvert.SerializeObject(toolResponse))
: SystemPrompt.ContinuePrompt(JsonConvert.SerializeObject(toolResponse))
});
Application.Current.Dispatcher.Invoke(() =>
{
callback?.Invoke(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<string, object>(){{"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<string, object>(){{"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<string> list = new List<string>();
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>;
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);
});
}
} }
} }
else catch (Exception ex)
{ {
//普通流式消息卡片 Console.WriteLine(ex.Message);
MessageListItem chatMessageListItem = new ChatMessageItem() log.Error(ex.Message);
}
}
/*统一处理本次请求中的Prompt调用需求*/
foreach (PromptRequest promptRequest in promptRequests)
{
try
{
string promptContent = DynamicPrompt.GetPrompt(promptRequest.PromptName, promptRequest.PromptArgs);
messages.Add(new Message
{ {
content = chunk, Role = "user",
role = "assistant", Content = promptContent
type = MessageType.CHAT_MESSAGE, });
id = timestamp.ToString() MessageListItem toolMessageItem = new ToolMessageItem
{
toolName = "调用提示词",
toolParams = new Dictionary<string, object>(),
type = MessageType.TOOL_MESSAGE,
status = "success",
content = "成功调用提示词:"+promptRequest.PromptName,
id = (timestamp+1).ToString(),
result = "成功调用提示词:"+promptRequest.PromptName,
role = "user"
}; };
messageContent = chunk; Application.Current.Dispatcher.Invoke(() =>
callback?.Invoke(chatMessageListItem); {
callback?.Invoke(toolMessageItem);
});
}
catch (Exception e)
{
Console.WriteLine(e);
log.Error(e.Message);
} }
} }
messages.Add(new Message MessageListItem endMessageListItem = new ChatMessageItem
{ {
Role = "assistant", type = MessageType.END_TAG,
Content = messageContent content = "",
}); id = (timestamp+3).ToString(),
role = "assistant"
};
callback?.Invoke(endMessageListItem);
} }
} }
private static async Task<string> GetToolInfos(McpServerList mcpServerList) private static async Task<string> GetToolInfos(McpServerList mcpServerList)
{ {
int loop = 0;
StringBuilder toolInfos = new StringBuilder(); StringBuilder toolInfos = new StringBuilder();
int i = 0;
List<int> failedMcp = new List<int>();
List<string> failedMcpString = new List<string>();
foreach (McpServer mcpServer in mcpServerList.GetAllServers()) foreach (McpServer mcpServer in mcpServerList.GetAllServers())
{ {
loop++; i++;
if (loop > 3) try
{ {
MessageBox.Show("达到最大循环次数", "退出循环"); if (mcpServer is InnerMcpServer)
break;
}
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) InnerMcpServer innerMcpServer = (InnerMcpServer)mcpServer;
Type type = Type.GetType("LinkToolAddin.client.tool." + innerMcpServer.Name);
MethodInfo[] methods = type.GetMethods();
foreach (MethodInfo method in methods)
{ {
string methodName = method.Name; if (method.IsPublic && method.IsStatic)
string methodDescription = method.GetCustomAttribute<DescriptionAttribute>()?.Description; {
string methodParamSchema = LinkToolAddin.common.JsonSchemaGenerator.GenerateJsonSchema(method); string methodName = method.Name;
string methodDescription = method.GetCustomAttribute<DescriptionAttribute>()?.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<McpClientTool> 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 McpToolDefinition toolDefinition = new McpToolDefinition
{ {
Tool = new Tool Tool = new Tool
{ {
Name = innerMcpServer.Name + ":" + methodName, Name = toolName,
Description = methodDescription, Description = toolDescription,
Arguments = methodParamSchema Arguments = toolParamSchema
} }
}; };
XNode node = JsonConvert.DeserializeXNode(JsonConvert.SerializeObject(toolDefinition)); toolInfos.AppendLine(JsonConvert.DeserializeXNode(JsonConvert.SerializeObject(toolDefinition)).ToString());
toolInfos.AppendLine(node.ToString()); toolInfos.AppendLine();
}
}else if (mcpServer is StdioMcpServer)
{
StdioMcpClient client = new StdioMcpClient((mcpServer as StdioMcpServer).Command, (mcpServer as StdioMcpServer).Args);
IList<McpClientTool> 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(); toolInfos.AppendLine();
} }
} }
} }catch (Exception e)
else if(mcpServer is SseMcpServer)
{ {
SseMcpClient client = new SseMcpClient((mcpServer as SseMcpServer).BaseUrl); log.Error(e.Message);
IList<McpClientTool> tools = await client.GetToolListAsync(); failedMcp.Add(i);
foreach (McpClientTool tool in tools) failedMcpString.Add(mcpServerList.GetAllServerNames()[i]);
{
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<McpClientTool> 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();
}
} }
} }
Dictionary<string, string> prompts = DynamicPrompt.GetAllPrompts(); List<UserPrompt> prompts = DynamicPrompt.GetAllPrompts();
foreach (KeyValuePair<string, string> prompt in prompts) foreach (UserPrompt userPrompt in prompts)
{ {
McpPromptDefinition promptDefinition = new McpPromptDefinition try
{ {
Prompt = new LinkToolAddin.host.mcp.Prompt XNode node = JsonConvert.DeserializeXNode(JsonConvert.SerializeObject(new PromptDefinition(){UserPrompt = userPrompt}));
{ toolInfos.AppendLine(node.ToString());
Name = prompt.Key toolInfos.AppendLine();
} }
}; catch (Exception e)
XNode node = JsonConvert.DeserializeXNode(JsonConvert.SerializeObject(promptDefinition)); {
toolInfos.AppendLine(node.ToString()); log.Error(e.Message);
toolInfos.AppendLine(); }
}
if (!failedMcp.IsNullOrEmpty())
{
MessageBox.Show($"读取失败的MCP服务有{JsonConvert.SerializeObject(failedMcpString)}","MCP读取错误");
} }
return toolInfos.ToString(); return toolInfos.ToString();
} }

View File

@ -28,6 +28,61 @@ public class McpServerList
Description = "可以调用arcgis的地理处理工具或执行python代码等", Description = "可以调用arcgis的地理处理工具或执行python代码等",
IsActive = true 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<string>()
// {
// "-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<string>()
// {
// "mcp-server-fetch"
// }
//});
//servers.Add("bing-search", new StdioMcpServer()
//{
// Name = "bing-search",
// Type = "stdio",
// Command = "npx",
// Args = new List<string>()
// {
// "bing-cn-mcp"
// }
//});
//servers.Add("mcp-python-interpreter", new StdioMcpServer()
//{
// Name = "mcp-python-interpreter",
// Type = "stdio",
// Command = "uvx",
// Args = new List<string>()
// {
// "--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) public McpServer GetServer(string name)
@ -51,4 +106,14 @@ public class McpServerList
} }
return serverList; return serverList;
} }
public List<string> GetAllServerNames()
{
List<string> serverList = new List<string>();
foreach (var server in servers)
{
serverList.Add(server.Key);
}
return serverList;
}
} }

48
host/PromptServerList.cs Normal file
View File

@ -0,0 +1,48 @@
using System.Collections.Generic;
using LinkToolAddin.host.prompt;
namespace LinkToolAddin.host;
public class PromptServerList
{
Dictionary<string, PromptServer> promptServers = new Dictionary<string, PromptServer>();
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<string, PromptServer> GetPromptsDict()
{
return promptServers;
}
public PromptServer GetPromptServer(string key)
{
return promptServers[key];
}
}

View File

@ -6,6 +6,7 @@ using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using LinkToolAddin.common; using LinkToolAddin.common;
using LinkToolAddin.host.llm.entity; using LinkToolAddin.host.llm.entity;
using LinkToolAddin.host.llm.entity.stream;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace LinkToolAddin.host.llm; namespace LinkToolAddin.host.llm;
@ -18,17 +19,22 @@ public class Bailian : Llm
public string max_tokens { get; set; } public string max_tokens { get; set; }
public string app_id { get; set; } public string app_id { get; set; }
public string api_key { get; set; } public string api_key { get; set; }
public async IAsyncEnumerable<string> SendChatStreamAsync(LlmJsonContent jsonContent) public async IAsyncEnumerable<LlmStreamChat> SendChatStreamAsync(LlmJsonContent jsonContent)
{ {
jsonContent.Stream = true; jsonContent.Stream = true;
StringBuilder builder = new StringBuilder(); StringBuilder contentBuilder = new StringBuilder();
await foreach (var chunk in HttpRequest.PostWithStreamingResponseAsync( StringBuilder reasonBuilder = new StringBuilder();
await foreach (LlmStreamChat chunk in HttpRequest.PostWithStreamingResponseAsync(
"https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions", "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions",
JsonConvert.SerializeObject(jsonContent), JsonConvert.SerializeObject(jsonContent),
api_key)) api_key))
{ {
builder.Append(chunk); contentBuilder.Append(chunk.Choices[0].Delta.Content);
yield return builder.ToString(); 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;
} }
} }

View File

@ -1,6 +1,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using LinkToolAddin.host.llm.entity; using LinkToolAddin.host.llm.entity;
using LinkToolAddin.host.llm.entity.stream;
namespace LinkToolAddin.host.llm; namespace LinkToolAddin.host.llm;
@ -11,7 +12,7 @@ public interface Llm
public string top_p { get; set; } public string top_p { get; set; }
public string max_tokens { get; set; } public string max_tokens { get; set; }
public IAsyncEnumerable<string> SendChatStreamAsync(LlmJsonContent jsonContent); public IAsyncEnumerable<LlmStreamChat> SendChatStreamAsync(LlmJsonContent jsonContent);
public IAsyncEnumerable<string> SendApplicationStreamAsync(string message); public IAsyncEnumerable<string> SendApplicationStreamAsync(string message);
public Task<string> SendChatAsync(LlmJsonContent jsonContent); public Task<string> SendChatAsync(LlmJsonContent jsonContent);
public Task<string> SendApplicationAsync(CommonInput commonInput); public Task<string> SendApplicationAsync(CommonInput commonInput);

View File

@ -25,6 +25,15 @@ namespace LinkToolAddin.host.llm.entity
[JsonProperty("top_k")] [JsonProperty("top_k")]
public int TopK { get; set; } = 40; 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 public partial class Message

View File

@ -50,5 +50,7 @@
{ {
[JsonProperty("content")] [JsonProperty("content")]
public string Content { get; set; } public string Content { get; set; }
[JsonProperty("reasoning_content")]
public string ResoningContent { get; set; }
} }
} }

View File

@ -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<string, object> ToolArgs { get; set; }
}

View File

@ -0,0 +1,9 @@
using Newtonsoft.Json;
namespace LinkToolAddin.host.prompt;
public class PromptDefinition
{
[JsonProperty("prompt")]
public UserPrompt UserPrompt { get; set; }
}

View File

@ -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<string, string> PromptArgs { get; set; }
}

View File

@ -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() + "}";
}
}
}

View File

@ -1,19 +1,59 @@
namespace LinkToolAddin.host.prompt; using LinkToolAddin.common;
namespace LinkToolAddin.host.prompt;
public class SystemPrompt public class SystemPrompt
{ {
public static string SysPromptTemplate = "现在你是一个精通ArcGIS Pro的专家请以此身份回答用户的问题。你有以下工具可以调用{{toolInfos}},用户的数据库路径是{{gdbPath}}。MCP工具调用的格式要求示例必须用<tool_use>标签表示工具调用:<tool_use>\n <name>search</name>\n <arguments>{\\\"query\\\": \\\"上海 人口\\\"}</arguments>\n</tool_use>。你每次调用请求都必须放在单独的一条消息中不附带任何的文字说明不带markdown格式。如需文字说明请另外放在一次单独的消息中。\n当你认为已解决用户最初提出的问题时请输出单独的一条消息内容为[DONE],不附带任何其它文字说明,程序识别到后会退出循环。\n此外你还可以通过<prompt></prompt>调用用户提示词,从而使你更好地理解和完成用户的任务。"; // public static string SysPromptTemplate =
// "现在你是一个精通地理信息分析和ArcGIS Pro软件的专家请以此身份回答用户的问题。" +
public static string ContinuePromptTemplate = "工具执行的结果如下,根据以上结果决定继续执行工具或是根据结果回答问题。如果不再需要额外说明和额外的操作,请回答单独的一条内容为[DONE]的消息。工具已经成功调用,请勿重复执行上一个工具"; // "指令:您可以使用一组工具来回答用户的问题。还可以通过<prompt></prompt>调用用户提示词,从而使你更好地理解和完成用户的任务。" +
// "调用工具要求:如果要调用工具,每次消息只能使用一个工具,用户的回复中将包含该工具的调用结果。您需要通过逐步使用工具来完成给定任务,每次工具调用需基于前一次的结果。成功调用工具之后应该马上调用下一个工具。你还可以通过<prompt></prompt>的方式来调用用户提示词,你能更好地理解和解决用户的问题。" +
public static string ErrorPromptTemplate = "执行上一个工具的时候出现以下错误,请根据报错信息重试"; // "工具调用背景:你有以下工具可以调用{{toolInfos}},用户的数据库路径是{{gdbPath}}。" +
// "输出风格:每次仅调用一个工具,必须基于前序工具的输出结果进行下一步操作。工具调用使用 XML 风格的标签进行格式化以单独信息格式输出,前后没有文字信息。" +
// "如果需要进行文字说明,请只有文字内容,以普通段落形式呈现没有其他格式。" +
// "工具调用格式:工具名称包含在一对标签内,每个参数也需用对应的标签包裹。结构如下:<tool_use>\n <name>{tool_name}</name>\n <arguments>{json_arguments}</arguments>\n</tool_use>。" +
// "工具名称:需与所使用工具的精确名称一致。" +
// "参数:应为包含工具所需参数的 JSON 对象。例如:<tool_use>\\n <name>gaode:maps_geo</name>\\n <arguments>{\\\"address\\\":\\\"广州市政府, 广州市\\\", \\\"city\\\":\\\"广州\\\"}</arguments>\\n</tool_use>" +
// "你必须严格遵守以下输出规则:" +
// "3.用户时间宝贵,不得重复调用上一次已成功执行的工具调用,除非有新的参数或上下文变化。" +
// "结果:用户将以以下格式返回工具调用结果:<tool_use_result>\n <name>{tool_name}</name>\n <result>{result}</result>\n</tool_use_result>。应为字符串类型,可以表示文件或其他输出类型。" +
// "工具调用示例:MCP工具调用的格式要求示例以下是使用虚拟工具的示例<tool_use>\\n <name>gaode:maps_geo</name>\\n <arguments>{\\\"address\\\":\\\"广州市政府, 广州市\\\", \\\"city\\\":\\\"广州\\\"}</arguments>\\n</tool_use>"+
// "特别地需要调用ArcGIS Pro工具前必须先查询帮助文档、标准调用名和参数后再进行调用";
// //"现在你是一个精通地理信息分析和ArcGIS Pro软件的专家请以此身份回答用户的问题。\r\n\r\n指令\r\n你需要使用一组工具来回答用户的问题。也可以通过 <prompt> 标签调用用户提示词,以帮助你更好地理解任务。所有操作完成后,在最后一条输出中包含 '[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<tool_use>\r\n <name>{tool_name}</name>\r\n <arguments>{json_arguments}</arguments>\r\n</tool_use>\r\n\r\n例如\r\n<tool_use>\r\n <name>gaode:maps_geo</name>\r\n <arguments>{\"address\":\"广州市政府, 广州市\", \"city\":\"广州\"}</arguments>\r\n</tool_use>\r\n\r\n注意事项\r\n1. 工具名称必须与实际工具完全一致。\r\n2. 参数必须为合法 JSON 格式,且符合工具要求。\r\n3. 用户时间宝贵,请高效调用工具,尽快完成任务。\r\n4. 最终完成时应在最后一次响应中包含 [DONE],而不是单独输出。\r\n\r\n工具调用结果返回格式\r\n用户将以如下格式返回工具调用结果\r\n<tool_use_result>\r\n <name>{tool_name}</name>\r\n <result>{result}</result>\r\n</tool_use_result>\r\n\r\n例如\r\n<tool_use_result>\r\n <name>ArcGIS_Pro:GP</name>\r\n <result>{\"output_file\": \"source.shp\"}</result>\r\n</tool_use_result>\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) public static string SysPrompt(string gdbPath, string toolInfos)
{ {
string sysPrompt = SysPromptTemplate; string sysPrompt = LocalResource.ReadFileByResource("LinkToolAddin.resource.prompt.SystemPrompt.txt");;
sysPrompt = sysPrompt.Replace("{{gdbPath}}", gdbPath); sysPrompt = sysPrompt.Replace("{{gdbPath}}", gdbPath);
sysPrompt = sysPrompt.Replace("{{toolInfos}}", toolInfos); sysPrompt = sysPrompt.Replace("{{toolInfos}}", toolInfos);
return sysPrompt; 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;
}
} }

View File

@ -1,6 +1,13 @@
namespace LinkToolAddin.host.prompt; using Newtonsoft.Json;
namespace LinkToolAddin.host.prompt;
public class UserPrompt public class UserPrompt
{ {
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("description")]
public string Description { get; set; }
[JsonProperty("arguments")]
public string Arguments { get; set; }
} }

View File

@ -21,13 +21,14 @@ public class DocDb
{ {
ArcGISProHelpDoc, ArcGISProHelpDoc,
ArcGISProToolDoc, ArcGISProToolDoc,
TaskPlanningDoc,
ArcGISProApplicantExample ArcGISProApplicantExample
} }
public Dictionary<KnowledgeBase, string> knowledgeBase = new Dictionary<KnowledgeBase, string> public Dictionary<KnowledgeBase, string> knowledgeBase = new Dictionary<KnowledgeBase, string>
{ {
{KnowledgeBase.ArcGISProHelpDoc,"6a77c5a68de64f469b79fcdcde9d5001"} {KnowledgeBase.ArcGISProHelpDoc,"6a77c5a68de64f469b79fcdcde9d5001"},
{KnowledgeBase.ArcGISProToolDoc,"080f8925318247ea822a9e12db5cb5cd"},
{KnowledgeBase.ArcGISProApplicantExample,"eef60f7c879b4e8597138c261578d2a5"}
}; };
public async Task<KnowledgeResult> Retrieve(string query) public async Task<KnowledgeResult> Retrieve(string query)

BIN
resource/img/fold.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 B

BIN
resource/img/linktool.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 931 B

BIN
resource/img/tool.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
resource/img/unfold.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 962 B

BIN
resource/img/user.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 784 B

View File

@ -0,0 +1,12 @@
这是上述工具调用的结果。
{{toolResult}}
请根据以下执行结果,清晰解释执行结果并执行下一步操作。
执行下一步工具的要求:
1. 解析工具输出结果
2. 调用下一个工具时确保参数继承前序输出,如果已经完成用户需求则不需继续执行工具。
3. 执行Arcgis Pro工具时可以调用知识库和提示词使任务完成得更出色
请据此继续执行

View File

@ -0,0 +1,14 @@
执行上一个工具的时候出现以下错误
{{toolResult}}
需按以下流程处理:
1. 错误解析:分析错误类型及具体原因(见错误信息),根据错误信息选择修复方案。
2. 修复方案:
(1)如果是ArcGisPro工具第一时间查询知识库的工具参数是否有误多次执行失败极有可能是工具名或者参数错误尤其注重调用工具或者提示词修正
(2)根据错误类型生成对应的工具重试策略。
(3)参数和调用名调整:及时调用知识库和用户提示词,确保工具名和参数的完全正确。明确需要修改的工具名、参数及修改方式。
(4)可以选择调用工具规划提示词优化工具执行流程或者调用知识库工具优化工具调用名和工具参数。
3. 工具重试:使用调整后的参数重新调用工具。请根据报错信息重试。
4.如果没有具体的错误描述信息请检查输出文件是否已经存在,若已经存在默认执行成功
5.出现错误编码号时要明晰编码在知识库中有相应解析,可以通过调用知识库解析错误原因

View File

@ -0,0 +1,53 @@
现在你是一个精通地理信息分析和ArcGIS Pro软件的专家请以此身份回答用户的问题。
指令您可以使用一组工具加文字说明来回答用户的问题。完成了用户的需求即可不用猜测用户下一步还想做什么。计划使用ArcGisPro工具前你可以通过<prompt><name></name><arguments>{}</arguments></prompt>格式调用用户提示词,或者调用知识库工具,从而使你更好地理解和完成用户的任务。除此之外,你具有搜索网页、编写代码、管理文件系统的能力,合理运用这些工具完成用户的需求。
调用工具要求:如果要调用工具,每次消息只能使用一个工具,用户的回复中将包含该工具的调用结果。你还可以通过<prompt></prompt>方式来调用用户提示词,或者调用知识库工具,你能更好地理解和解决用户的问题。您需要通过逐步使用工具或提示词来完成给定任务,每次调用需基于前一次的结果。成功调用工具或提示词之后应该马上调用下一个工具或提示词。
工具调用背景:你有以下工具可以调用{{toolInfos}},用户的数据库路径是{{gdbPath}}。
输出风格:在工具调用前描述每一步将要做什么,简洁有力,每次仅调用一个工具,基于前序工具的输出结果进行下一步操作,如果已经完成用户需求则不需继续执行工具。话语开头不要回复好的。工具调用使用 XML 风格的标签输出。
工具调用的XML格式一定一定要完全正确不能有错漏格式如下
<tool_use>
<name>{tool_name}</name>
<arguments>{json_arguments}</arguments>
</tool_use>
工具名称:需与所使用工具的精确名称一致。
参数:应为包含工具所需参数的 JSON 对象。
例如:
调用工具示例:
“将执行高德的兴趣点工具确认广州市政府的位置
<tool_use>
<name>gaode:maps_geo</name>
<arguments>{\\\"address\\\":\\\"广州市政府, 广州市\\\", \\\"city\\\":\\\"广州\\\"}</arguments>
</tool_use>
调用用户提示词示例:
先进行ArcGisPro工具任务规划
<prompt>
<name>plan</name>
<arguments>{}</arguments>
</prompt>
结果示例:用户将以以下格式返回工具调用结果:
<tool_use_result>
<name>{tool_name}</name>
<result>{result}</result>
</tool_use_result>
工具调用示例:MCP工具调用的格式要求示例以下是使用虚拟工具的示例
<tool_use>
<name>gaode:maps_geo</name>
<arguments>{\\\"address\\\":\\\"广州市政府, 广州市\\\", \\\"city\\\":\\\"广州\\\"}</arguments>
</tool_use>
你必须严格遵守以下每一条规则:
1.用户时间宝贵,一旦确认工具成功调用之后,不得重复调用上一次已成功执行的工具,除非有新的参数或上下文变化。
2.调用“ArcGisPro:ArcGisProTool”工具name一定要严格按照知识库的调用名例如"analysis.Union","management.Clip"等。输入与输出数据一定需要加上路径信息,输出数据到默认数据库中。`in_features`参数通过分号(`;`)拼接多个输入要素类路径,参数列表中有"{}"代表选填,其他必须要填写。
3.参数一定都为字符串类型,可以表示文件或其他输出类型。
4.一次只能调用一个工具,逐步调用!不要调用多个,一旦消息中没有工具或提示词调用信息即视为任务完成。因此工具或提示词调用必须连续完成。
5.只响应用户目前的需求即可不要过度猜测用户的需求如果有下一步的工具建议只输出文本即可如果输出XML会执行大量无用的工具。
特别注意:
1.ArcGIS Pro中不能通过先SelectByAttribute选择后再执行ExportFeatures导出指定的部分正确的做法是直接用ExportFeatures传入where_clause导出指定部分的数据。
2.可以先参考知识库中的案例辅助工具的规划

View File

@ -4,6 +4,7 @@ using System.Threading.Tasks;
using ArcGIS.Desktop.Core.Geoprocessing; using ArcGIS.Desktop.Core.Geoprocessing;
using ArcGIS.Desktop.Framework.Dialogs; using ArcGIS.Desktop.Framework.Dialogs;
using ArcGIS.Desktop.Framework.Threading.Tasks; using ArcGIS.Desktop.Framework.Threading.Tasks;
using Newtonsoft.Json;
namespace LinkToolAddin.server; namespace LinkToolAddin.server;
@ -21,8 +22,8 @@ public class CallArcGISPro
{ {
Error = new Error() Error = new Error()
{ {
Code = results.ErrorCode, Code = results.ErrorCode.ToString(),
Message = results.ErrorMessages.ToString() Message = JsonConvert.SerializeObject(results.ErrorMessages)
} }
}; };
} }
@ -30,7 +31,7 @@ public class CallArcGISPro
{ {
jsonRpcResultEntity = new JsonRpcSuccessEntity jsonRpcResultEntity = new JsonRpcSuccessEntity
{ {
Result = results.Messages.ToString() Result = JsonConvert.SerializeObject(results.Messages)
}; };
} }
return jsonRpcResultEntity; return jsonRpcResultEntity;

View File

@ -17,7 +17,7 @@ namespace LinkToolAddin.server
public partial class Error public partial class Error
{ {
[JsonProperty("code")] [JsonProperty("code")]
public long Code { get; set; } public string Code { get; set; }
[JsonProperty("message")] [JsonProperty("message")]
public string Message { get; set; } public string Message { get; set; }

View File

@ -7,7 +7,7 @@ namespace LinkToolAddin.server
public partial class JsonRpcResultEntity public partial class JsonRpcResultEntity
{ {
[JsonProperty("jsonrpc")] [JsonProperty("jsonrpc")]
public string Jsonrpc { get; set; } public string Jsonrpc { get; set; } = "2.0";
[JsonProperty("id")] [JsonProperty("id")]
public long Id { get; set; } public long Id { get; set; }

View File

@ -22,7 +22,7 @@ namespace LinkToolAddin
{ {
internal class VersionButton : Button internal class VersionButton : Button
{ {
private string version = "0.1.0"; private string version = "0.1.4";
protected override void OnClick() protected override void OnClick()
{ {
MessageBox.Show($"当前LinkTool版本为{version}", "版本信息"); MessageBox.Show($"当前LinkTool版本为{version}", "版本信息");

View File

@ -6,8 +6,9 @@
xmlns:ui="clr-namespace:LinkToolAddin.ui.dockpane" xmlns:ui="clr-namespace:LinkToolAddin.ui.dockpane"
xmlns:extensions="clr-namespace:ArcGIS.Desktop.Extensions;assembly=ArcGIS.Desktop.Extensions" 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" mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300" d:DesignHeight="650" d:DesignWidth="300"
d:DataContext="{Binding Path=ui.DialogDockpaneViewModel}"> d:DataContext="{Binding Path=ui.DialogDockpaneViewModel}">
<UserControl.Resources> <UserControl.Resources>
<ResourceDictionary> <ResourceDictionary>
@ -16,13 +17,52 @@
</ResourceDictionary.MergedDictionaries> </ResourceDictionary.MergedDictionaries>
</ResourceDictionary> </ResourceDictionary>
</UserControl.Resources> </UserControl.Resources>
<Grid> <Grid Margin="8">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto"/> <RowDefinition Height="24"/>
<RowDefinition Height="*"/> <RowDefinition Height="*"/>
<RowDefinition Height="24"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="24"/>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<DockPanel Grid.Row="0" LastChildFill="true" KeyboardNavigation.TabNavigation="Local" Height="30"> <Grid Grid.Row="0" HorizontalAlignment="Center">
<Button Content="Test Server" Name="TestServer" Click="TestServer_OnClick"></Button> <Grid.ColumnDefinitions>
</DockPanel> <ColumnDefinition Width="36"/>
<ColumnDefinition Width="36"/>
<ColumnDefinition Width="36"/>
<ColumnDefinition Width="36"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Button Grid.Column="0" Content="清除" Name="ClearButton" Click="ClearButton_OnClick"></Button>
<Button Grid.Column="1" Content="顶部" Name="TopButton" Click="TopButton_OnClick"></Button>
<Button Grid.Column="2" Content="底部" Name="BottomButton" Click="BottomButton_OnClick"></Button>
<Button Grid.Column="3" Content="停止" Name="StopButton" Click="StopButton_OnClick"></Button>
<ComboBox Grid.Column="4" Name="ModelComboBox" SelectionChanged="ModelComboBox_OnSelectionChanged">
<ComboBoxItem Content="qwen3-235b-a22b" IsSelected="True"/>
<ComboBoxItem Content="qwen3-32b" />
<ComboBoxItem Content="qwq-32b" />
<ComboBoxItem Content="qwen-max-latest" />
<ComboBoxItem Content="deepseek-r1" />
<ComboBoxItem Content="deepseek-r1-0528" />
<ComboBoxItem Content="deepseek-r1-distill-qwen-32b" />
<ComboBoxItem Content="deepseek-r1-distill-llama-70b" />
<ComboBoxItem Content="deepseek-v3" />
<ComboBoxItem Content="自定义" />
</ComboBox>
</Grid>
<!-- <Button Grid.Row="0" Content="Test" Name="TestButton" Click="TestButton_OnClick"></Button> -->
<!-- <TextBlock Grid.Row="0" HorizontalAlignment="Center" VerticalAlignment="Center" Text="LinkTool"/> -->
<ScrollViewer Grid.Row="1" HorizontalContentAlignment="Stretch" VerticalAlignment="Top" Name="ScrollViewer">
<StackPanel Name="ChatHistoryStackPanel"></StackPanel>
</ScrollViewer>
<TextBlock Grid.Row="2" VerticalAlignment="Center" Name="StatusTextBlock" Text=""/>
<Grid Grid.Row="3">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"></ColumnDefinition>
<ColumnDefinition Width="48"></ColumnDefinition>
</Grid.ColumnDefinitions>
<TextBox Grid.Column="0" Name="QuestionTextbox" Text="这是用户的问题" TextWrapping="Wrap"></TextBox>
<Button Grid.Column="1" Name="SendButton" Content="发送" Click="SendButton_OnClick"></Button>
</Grid>
</Grid> </Grid>
</UserControl> </UserControl>

View File

@ -1,5 +1,7 @@
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel; using System.ComponentModel;
using System.IO; using System.IO;
using System.Net.Http; using System.Net.Http;
@ -12,10 +14,15 @@ using Microsoft.Extensions.Logging;
using System.Text.Json; using System.Text.Json;
using System.Threading; using System.Threading;
using System.Windows.Documents; using System.Windows.Documents;
using System.Windows.Media;
using ArcGIS.Desktop.Core;
using ArcGIS.Desktop.Core.Geoprocessing; using ArcGIS.Desktop.Core.Geoprocessing;
using LinkToolAddin.client; using LinkToolAddin.client;
using LinkToolAddin.common;
using LinkToolAddin.host;
using LinkToolAddin.host.llm; using LinkToolAddin.host.llm;
using LinkToolAddin.host.llm.entity; using LinkToolAddin.host.llm.entity;
using LinkToolAddin.message;
using LinkToolAddin.resource; using LinkToolAddin.resource;
using LinkToolAddin.server; using LinkToolAddin.server;
using log4net; using log4net;
@ -29,6 +36,10 @@ using Newtonsoft.Json;
namespace LinkToolAddin.ui.dockpane namespace LinkToolAddin.ui.dockpane
{ {
public class ItemModel
{
public string Content { get; set; }
}
/// <summary> /// <summary>
/// Interaction logic for DialogDockpaneView.xaml /// Interaction logic for DialogDockpaneView.xaml
/// </summary> /// </summary>
@ -36,10 +47,15 @@ namespace LinkToolAddin.ui.dockpane
{ {
private static ILog log = LogManager.GetLogger(typeof(DialogDockpaneView)); private static ILog log = LogManager.GetLogger(typeof(DialogDockpaneView));
private List<string> idList = new List<string>();
private ConcurrentDictionary<string,MessageListItem> messageDict = new ConcurrentDictionary<string,MessageListItem>();
private ConcurrentDictionary<string,Border> borderItemsDict = new ConcurrentDictionary<string,Border>();
public DialogDockpaneView() public DialogDockpaneView()
{ {
InitLogger(); InitLogger();
InitializeComponent(); InitializeComponent();
DataContext = this;
} }
private async void TestServer_OnClick(object sender, RoutedEventArgs e) private async void TestServer_OnClick(object sender, RoutedEventArgs e)
@ -81,5 +97,461 @@ namespace LinkToolAddin.ui.dockpane
log.Info("Info 日志(控制台和文件可见)"); log.Info("Info 日志(控制台和文件可见)");
log.Error("Error 日志(严重问题)"); 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<string,object>(),
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);
}
} }
} }

View File

@ -14,6 +14,7 @@ using ArcGIS.Desktop.Layouts;
using ArcGIS.Desktop.Mapping; using ArcGIS.Desktop.Mapping;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -28,7 +29,13 @@ namespace LinkToolAddin.ui.dockpane
{ {
private const string _dockPaneID = "DialogDockpane"; private const string _dockPaneID = "DialogDockpane";
protected DialogDockpaneViewModel() { } public ObservableCollection<ItemModel> Items { get; set; }
protected DialogDockpaneViewModel()
{
Items = new ObservableCollection<ItemModel>();
Items.Add(new ItemModel(){Content = "adfdfdafdfs"});
}
/// <summary> /// <summary>
/// Show the DockPane. /// Show the DockPane.

View File

@ -20,7 +20,11 @@
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="24"/> <RowDefinition Height="24"/>
<RowDefinition Height="24"/> <RowDefinition Height="24"/>
<RowDefinition Height="240"/> <RowDefinition Height="480"/>
<RowDefinition Height="24"/>
<RowDefinition Height="24"/>
<RowDefinition Height="24"/>
<RowDefinition Height="24"/>
<RowDefinition Height="24"/> <RowDefinition Height="24"/>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Button Grid.Row="0" Content="Test Workflow" Name="TestServer" Click="TestWorkflow_OnClick"></Button> <Button Grid.Row="0" Content="Test Workflow" Name="TestServer" Click="TestWorkflow_OnClick"></Button>
@ -31,9 +35,13 @@
<ColumnDefinition Width="Auto"/> <ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<TextBox Grid.Row="0" Grid.Column="0" ToolTip="输入测试提示词" Name="PromptTestTextBox"></TextBox> <TextBox Grid.Row="0" Grid.Column="0" ToolTip="输入测试提示词" Name="PromptTestTextBox"></TextBox>
<Button Grid.Row="0" Grid.Column="1" Content="测试" Name="PromptTestButton" Click="PromptTestButton_OnClick"></Button> <Button Grid.Row="0" Grid.Column="1" Content="发送" Name="PromptTestButton" Click="PromptTestButton_OnClick"></Button>
</Grid> </Grid>
<TextBox Grid.Row="2" ToolTip="大模型回复" Name="ReplyTextBox" TextWrapping="Wrap"></TextBox> <TextBox Grid.Row="2" ToolTip="大模型回复" Name="ReplyTextBox" TextWrapping="Wrap"></TextBox>
<Button Grid.Row="3" Content="Test Stream" Name="TestStream" Click="TestStream_OnClick"></Button> <Button Grid.Row="3" Content="停止回答" Name="StopConversation" Click="StopConversation_OnClick"></Button>
<Button Grid.Row="4" Content="Test Stream" Name="TestStream" Click="TestStream_OnClick"></Button>
<Button Grid.Row="5" Content="Test Arcgis Tool" Name="TestArcGisTool" Click="TestArcGisTool_OnClick"></Button>
<Button Grid.Row="6" Content="Test Resource" Name="TestResource" Click="TestResource_OnClick"></Button>
<Button Grid.Row="7" Content="测试获取属性表" Name="TestAttrTable" Click="TestAttrTable_OnClick"></Button>
</Grid> </Grid>
</UserControl> </UserControl>

View File

@ -1,12 +1,21 @@
using System.Collections.Generic; using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reflection;
using System.Text; using System.Text;
using System.Threading.Tasks;
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Xml.Linq;
using LinkToolAddin.client; using LinkToolAddin.client;
using LinkToolAddin.client.tool;
using LinkToolAddin.common;
using LinkToolAddin.host; using LinkToolAddin.host;
using LinkToolAddin.host.llm; using LinkToolAddin.host.llm;
using LinkToolAddin.host.llm.entity; using LinkToolAddin.host.llm.entity;
using LinkToolAddin.host.mcp;
using LinkToolAddin.host.prompt;
using LinkToolAddin.message; using LinkToolAddin.message;
using LinkToolAddin.resource; using LinkToolAddin.resource;
using LinkToolAddin.server; using LinkToolAddin.server;
@ -17,6 +26,7 @@ using log4net.Layout;
using ModelContextProtocol.Client; using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol.Types; using ModelContextProtocol.Protocol.Types;
using Newtonsoft.Json; using Newtonsoft.Json;
using MessageBox = ArcGIS.Desktop.Framework.Dialogs.MessageBox;
namespace LinkToolAddin.ui.dockpane namespace LinkToolAddin.ui.dockpane
@ -29,7 +39,7 @@ namespace LinkToolAddin.ui.dockpane
private static ILog log = LogManager.GetLogger(typeof(TestDockpaneView)); private static ILog log = LogManager.GetLogger(typeof(TestDockpaneView));
private List<string> idList = new List<string>(); private List<string> idList = new List<string>();
private Dictionary<string,MessageListItem> messageDict = new Dictionary<string,MessageListItem>(); private ConcurrentDictionary<string,MessageListItem> messageDict = new ConcurrentDictionary<string,MessageListItem>();
public TestDockpaneView() public TestDockpaneView()
{ {
@ -189,14 +199,26 @@ namespace LinkToolAddin.ui.dockpane
log.Info(msg.content); log.Info(msg.content);
} }
private void PromptTestButton_OnClick(object sender, RoutedEventArgs e) private async void PromptTestButton_OnClick(object sender, RoutedEventArgs e)
{ {
string userPrompt = PromptTestTextBox.Text; string userPrompt = PromptTestTextBox.Text;
// Gateway.SendMessage(userPrompt,"qwen-max","C:/Project/test.gdb",AddReply); // model可选值qwen3-235b-a22b,qwen-max,deepseek-r1
Gateway.SendMessageStream(userPrompt,"qwen-max","D:\\01_Project\\20250305_LinkTool\\20250420_AiDemoProject\\20250420_AiDemoProject.gdb",AddReplyStream); 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 void AddReplyStream(MessageListItem msg) public async void AddReplyStream(MessageListItem msg)
{
await Task.Run(() => ProcessReplyStream(msg));
}
private void ProcessReplyStream(MessageListItem msg)
{ {
string id = msg.id; string id = msg.id;
if (idList.Contains(id)) if (idList.Contains(id))
@ -206,16 +228,31 @@ namespace LinkToolAddin.ui.dockpane
else else
{ {
idList.Add(id); idList.Add(id);
messageDict.Add(msg.id, msg); messageDict.TryAdd(msg.id, msg);
} }
ReplyTextBox.Clear(); try
StringBuilder builder = new StringBuilder();
foreach (KeyValuePair<string,MessageListItem> pair in messageDict)
{ {
MessageListItem msgItem = pair.Value; StringBuilder builder = new StringBuilder();
builder.AppendLine(msgItem.content); foreach (string idStr in idList)
ReplyTextBox.Text = builder.ToString(); {
ReplyTextBox.ScrollToEnd(); MessageListItem msgItem = messageDict[idStr];
string content = msgItem.content;
if (msgItem.type == MessageType.REASON_MESSAGE)
{
content = "<think>" + content + "</think>";
}
builder.AppendLine(content);
builder.AppendLine();
}
Application.Current.Dispatcher.Invoke(() =>
{
ReplyTextBox.Clear();
ReplyTextBox.Text = builder.ToString();
ReplyTextBox.ScrollToEnd();
});
}catch (Exception exception)
{
log.Error(exception.Message);
} }
} }
@ -231,5 +268,57 @@ namespace LinkToolAddin.ui.dockpane
{ {
Request_Bailian_Stream_Test(); 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 =
"<tool_use>\n<name>ArcGisPro:ArcGisProTool</name>\n<arguments>{\"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\"]}</arguments>\n</tool_use>";
XElement toolUse = XElement.Parse(xmlStr);
string fullToolName = toolUse.Element("name")?.Value;
string toolArgs = toolUse.Element("arguments")?.Value;
Dictionary<string, object> toolParams = JsonConvert.DeserializeObject<Dictionary<string, object>>(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>;
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");
}
} }
} }

View File

@ -4,6 +4,10 @@ public enum MessageType
{ {
TOOL_MESSAGE, TOOL_MESSAGE,
CHAT_MESSAGE, CHAT_MESSAGE,
REASON_MESSAGE,
END_TAG,
WARNING,
ERROR
} }
public interface MessageListItem public interface MessageListItem