diff --git a/LinkToolAddin.csproj b/LinkToolAddin.csproj index 0f3f990..d764cc6 100644 --- a/LinkToolAddin.csproj +++ b/LinkToolAddin.csproj @@ -120,6 +120,18 @@ Always + + + Always + + + + Always + + + + Always + diff --git a/common/LocalResource.cs b/common/LocalResource.cs index fc1e719..4f4e53a 100644 --- a/common/LocalResource.cs +++ b/common/LocalResource.cs @@ -1,5 +1,7 @@ using System; using System.IO; +using System.Windows.Controls; +using System.Windows.Media.Imaging; namespace LinkToolAddin.common; @@ -8,12 +10,6 @@ public class LocalResource public static string ReadFileByResource(string resourceName) { var assembly = System.Reflection.Assembly.GetExecutingAssembly(); - - string[] resourceNames = assembly.GetManifestResourceNames(); - foreach (string name in resourceNames) - { - Console.WriteLine(name); - } using (Stream stream = assembly.GetManifestResourceStream(resourceName)) { @@ -28,4 +24,24 @@ public class LocalResource } } } + + public static BitmapImage ReadImageByResource(string resourceName) + { + var assembly = System.Reflection.Assembly.GetExecutingAssembly(); + using (Stream stream = assembly.GetManifestResourceStream(resourceName)) + { + if (stream == null) + { + Console.WriteLine("资源未找到,请检查资源名称是否正确。"); + return null; + } + + // 如果是 WPF,可以转换为 BitmapImage + BitmapImage bitmap = new BitmapImage(); + bitmap.BeginInit(); + bitmap.StreamSource = stream; + bitmap.EndInit(); + return bitmap; + } + } } \ No newline at end of file diff --git a/host/Gateway.cs b/host/Gateway.cs index 2a789c5..63ef2b2 100644 --- a/host/Gateway.cs +++ b/host/Gateway.cs @@ -328,14 +328,17 @@ public class Gateway else { //包含Prompt调用请求的消息 - MessageListItem chatMessageListItem = new ChatMessageItem() + if (remainingPrompt != "") { - content = remainingPrompt, - role = "assistant", - type = MessageType.CHAT_MESSAGE, - id = timestamp.ToString() - }; - callback?.Invoke(chatMessageListItem); + MessageListItem chatMessageListItem = new ChatMessageItem() + { + content = remainingPrompt, + role = "assistant", + type = MessageType.CHAT_MESSAGE, + id = timestamp.ToString() + }; + callback?.Invoke(chatMessageListItem); + } XElement promptUse = XElement.Parse(matchedPrompt); string promptKey = promptUse.Element("name")?.Value; string promptArgs = promptUse.Element("arguments")?.Value; @@ -352,17 +355,20 @@ public class Gateway else { //包含工具调用请求的消息 - MessageListItem chatMessageListItem = new ChatMessageItem() + if (remaining != "") { - content = remaining, - role = "assistant", - type = MessageType.CHAT_MESSAGE, - id = timestamp.ToString() - }; - Application.Current.Dispatcher.Invoke(() => - { - callback?.Invoke(chatMessageListItem); - }); + MessageListItem chatMessageListItem = new ChatMessageItem() + { + content = remaining, + role = "assistant", + type = MessageType.CHAT_MESSAGE, + id = timestamp.ToString() + }; + Application.Current.Dispatcher.Invoke(() => + { + callback?.Invoke(chatMessageListItem); + }); + } XElement toolUse = XElement.Parse(matched); string fullToolName = toolUse.Element("name")?.Value; string toolArgs = toolUse.Element("arguments")?.Value; @@ -415,7 +421,8 @@ public class Gateway type = MessageType.TOOL_MESSAGE, status = toolResponse.IsError ? "fail" : "success", content = JsonConvert.SerializeObject(toolResponse), - id = (timestamp + 1).ToString() + id = (timestamp + 1).ToString(), + role = "user" }; messages.Add(new Message { @@ -441,7 +448,8 @@ public class Gateway type = MessageType.TOOL_MESSAGE, status = toolResponse.IsError ? "fail" : "success", content = JsonConvert.SerializeObject(toolResponse), - id = (timestamp + 1).ToString() + id = (timestamp + 1).ToString(), + role = "user" }; messages.Add(new Message { @@ -486,7 +494,8 @@ public class Gateway type = MessageType.TOOL_MESSAGE, status = "fail", content = JsonConvert.SerializeObject(innerResult), - id = (timestamp + 1).ToString() + id = (timestamp + 1).ToString(), + role = "user" }; messages.Add(new Message { @@ -507,7 +516,8 @@ public class Gateway type = MessageType.TOOL_MESSAGE, status = "success", content = JsonConvert.SerializeObject(innerResult), - id = (timestamp + 1).ToString() + id = (timestamp + 1).ToString(), + role = "user" }; messages.Add(new Message { diff --git a/host/McpServerList.cs b/host/McpServerList.cs index ca7415d..b452abc 100644 --- a/host/McpServerList.cs +++ b/host/McpServerList.cs @@ -35,53 +35,53 @@ public class McpServerList Description = "可以调用进行查询知识库,获取相关参考信息。", IsActive = true }); - servers.Add("filesystem", new StdioMcpServer() - { - Name = "filesystem", - Type = "stdio", - Command = "npx", - Args = new List() - { - "-y", - "@modelcontextprotocol/server-filesystem", - "D:\\01_Project\\20250305_LinkTool\\20250420_AiDemoProject\\TestData" - } - }); - servers.Add("fetch", new StdioMcpServer() - { - Name = "fetch", - Type = "stdio", - Command = "uvx", - Args = new List() - { - "mcp-server-fetch" - } - }); - servers.Add("bing-search", new StdioMcpServer() - { - Name = "bing-search", - Type = "stdio", - Command = "npx", - Args = new List() - { - "bing-cn-mcp" - } - }); - servers.Add("mcp-python-interpreter", new StdioMcpServer() - { - Name = "mcp-python-interpreter", - Type = "stdio", - Command = "uvx", - Args = new List() - { - "--native-tls", - "mcp-python-interpreter", - "--dir", - "D:\\01_Project\\20250305_LinkTool\\20250420_AiDemoProject\\TestData", - "--python-path", - "C:\\Program Files\\ArcGIS\\Pro\\bin\\Python\\envs\\custom\\python.exe" - } - }); + // servers.Add("filesystem", new StdioMcpServer() + // { + // Name = "filesystem", + // Type = "stdio", + // Command = "npx", + // Args = new List() + // { + // "-y", + // "@modelcontextprotocol/server-filesystem", + // "D:\\01_Project\\20250305_LinkTool\\20250420_AiDemoProject\\TestData" + // } + // }); + // servers.Add("fetch", new StdioMcpServer() + // { + // Name = "fetch", + // Type = "stdio", + // Command = "uvx", + // Args = new List() + // { + // "mcp-server-fetch" + // } + // }); + // servers.Add("bing-search", new StdioMcpServer() + // { + // Name = "bing-search", + // Type = "stdio", + // Command = "npx", + // Args = new List() + // { + // "bing-cn-mcp" + // } + // }); + // servers.Add("mcp-python-interpreter", new StdioMcpServer() + // { + // Name = "mcp-python-interpreter", + // Type = "stdio", + // Command = "uvx", + // Args = new List() + // { + // "--native-tls", + // "mcp-python-interpreter", + // "--dir", + // "D:\\01_Project\\20250305_LinkTool\\20250420_AiDemoProject\\TestData", + // "--python-path", + // "C:\\Program Files\\ArcGIS\\Pro\\bin\\Python\\envs\\custom\\python.exe" + // } + // }); } public McpServer GetServer(string name) diff --git a/resource/img/linktool.png b/resource/img/linktool.png new file mode 100644 index 0000000..37264c2 Binary files /dev/null and b/resource/img/linktool.png differ diff --git a/resource/img/tool.png b/resource/img/tool.png new file mode 100644 index 0000000..09bee43 Binary files /dev/null and b/resource/img/tool.png differ diff --git a/resource/img/user.png b/resource/img/user.png new file mode 100644 index 0000000..a992685 Binary files /dev/null and b/resource/img/user.png differ diff --git a/ui/dockpane/DialogDockpane.xaml b/ui/dockpane/DialogDockpane.xaml index 84e7de1..fafe132 100644 --- a/ui/dockpane/DialogDockpane.xaml +++ b/ui/dockpane/DialogDockpane.xaml @@ -5,9 +5,10 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:ui="clr-namespace:LinkToolAddin.ui.dockpane" xmlns:extensions="clr-namespace:ArcGIS.Desktop.Extensions;assembly=ArcGIS.Desktop.Extensions" - xmlns:controls="clr-namespace:ArcGIS.Desktop.Framework.Controls;assembly=ArcGIS.Desktop.Framework" + xmlns:controls="clr-namespace:ArcGIS.Desktop.Framework.Controls;assembly=ArcGIS.Desktop.Framework" + xmlns:system="clr-namespace:System;assembly=System.Runtime" mc:Ignorable="d" - d:DesignHeight="300" d:DesignWidth="300" + d:DesignHeight="650" d:DesignWidth="300" d:DataContext="{Binding Path=ui.DialogDockpaneViewModel}"> @@ -16,13 +17,27 @@ - + - + + + + - - - + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ui/dockpane/DialogDockpane.xaml.cs b/ui/dockpane/DialogDockpane.xaml.cs index aa2ea7f..1148b88 100644 --- a/ui/dockpane/DialogDockpane.xaml.cs +++ b/ui/dockpane/DialogDockpane.xaml.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.ComponentModel; using System.IO; using System.Net.Http; @@ -12,10 +14,15 @@ using Microsoft.Extensions.Logging; using System.Text.Json; using System.Threading; using System.Windows.Documents; +using System.Windows.Media; +using ArcGIS.Desktop.Core; using ArcGIS.Desktop.Core.Geoprocessing; using LinkToolAddin.client; +using LinkToolAddin.common; +using LinkToolAddin.host; using LinkToolAddin.host.llm; using LinkToolAddin.host.llm.entity; +using LinkToolAddin.message; using LinkToolAddin.resource; using LinkToolAddin.server; using log4net; @@ -29,17 +36,26 @@ using Newtonsoft.Json; namespace LinkToolAddin.ui.dockpane { + public class ItemModel + { + public string Content { get; set; } + } /// /// Interaction logic for DialogDockpaneView.xaml /// public partial class DialogDockpaneView : UserControl { private static ILog log = LogManager.GetLogger(typeof(DialogDockpaneView)); - + + private List idList = new List(); + private ConcurrentDictionary messageDict = new ConcurrentDictionary(); + private ConcurrentDictionary borderItemsDict = new ConcurrentDictionary(); + public DialogDockpaneView() { InitLogger(); InitializeComponent(); + DataContext = this; } private async void TestServer_OnClick(object sender, RoutedEventArgs e) @@ -81,5 +97,190 @@ namespace LinkToolAddin.ui.dockpane log.Info("Info 日志(控制台和文件可见)"); log.Error("Error 日志(严重问题)"); } + + private void SendButton_OnClick(object sender, RoutedEventArgs e) + { + string question = QuestionTextbox.Text; + string defaultGdbPath = Project.Current.DefaultGeodatabasePath; + string gdbPath = @""; + long timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + MessageListItem userMsg = new ChatMessageItem() + { + content = question, + role = "user", + type = MessageType.CHAT_MESSAGE, + id = timestamp.ToString() + }; + Border userMsgBoder = GetUserChatBorder(userMsg); + idList.Add(timestamp.ToString()); + messageDict[timestamp.ToString()] = userMsg; + QuestionTextbox.Text = ""; + borderItemsDict[timestamp.ToString()] = userMsgBoder; + ChatHistoryStackPanel.Children.Add(userMsgBoder); + Gateway.SendMessageStream(question,"qwen-max",defaultGdbPath,NewMessage_Recall); + } + + public void NewMessage_Recall(MessageListItem msg) + { + string msgId = msg.id; + log.Info(msg.content); + if (!idList.Contains(msgId)) + { + //不存在该消息,需添加到ListView中 + if (msg.content == "") + { + 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); + }else if (msg.type == MessageType.CHAT_MESSAGE) + { + Border border = GetUserChatBorder(msg); + borderItemsDict[msgId] = border; + ChatHistoryStackPanel.Children.Add(border); + } + } + else + { + Border border = GetAiChatBorder(msg); + borderItemsDict[msgId] = border; + ChatHistoryStackPanel.Children.Add(border); + } + } + else + { + //已有该消息,只需要修改内容 + messageDict[msgId] = msg; + 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; + textBlock.Text = (msg as ToolMessageItem).toolName; + }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; + } + } + else + { + Border borderItem = borderItemsDict[msgId]; + Grid grid = borderItem.Child as Grid; + TextBox textBox = grid.Children[1] as TextBox; + textBox.Text = msg.content; + } + } + } + + 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.Background = Brushes.Transparent; + textBox.Text = msg.content; + textBox.TextWrapping = TextWrapping.Wrap; + border.Child = grid; + return border; + } + + 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(); + grid.Children.Add(icon); + grid.Children.Add(textBox); + Grid.SetColumn(icon, 1); + Grid.SetColumn(textBox, 0); + textBox.Background = Brushes.Transparent; + 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.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)}); + TextBlock textBlock = new TextBlock(); + textBlock.Text = (msg as ToolMessageItem).toolName; + textBlock.TextWrapping = TextWrapping.Wrap; + grid.Children.Add(icon); + grid.Children.Add(textBlock); + Grid.SetColumn(icon, 0); + Grid.SetColumn(textBlock, 1); + border.Child = grid; + return border; + } + + private void TestButton_OnClick(object sender, RoutedEventArgs e) + { + long timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + MessageListItem toolMessageItem = new ToolMessageItem + { + toolName = "toolName", + toolParams = new Dictionary(), + type = MessageType.TOOL_MESSAGE, + status = "success", + content = "JsonConvert.SerializeObject(toolResponse)", + id = (timestamp + 1).ToString(), + role = "user" + }; + NewMessage_Recall(toolMessageItem); + } } } diff --git a/ui/dockpane/DialogDockpaneViewModel.cs b/ui/dockpane/DialogDockpaneViewModel.cs index 1d7a4e5..42258fc 100644 --- a/ui/dockpane/DialogDockpaneViewModel.cs +++ b/ui/dockpane/DialogDockpaneViewModel.cs @@ -14,6 +14,7 @@ using ArcGIS.Desktop.Layouts; using ArcGIS.Desktop.Mapping; using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -27,8 +28,14 @@ namespace LinkToolAddin.ui.dockpane internal class DialogDockpaneViewModel : DockPane { private const string _dockPaneID = "DialogDockpane"; + + public ObservableCollection Items { get; set; } - protected DialogDockpaneViewModel() { } + protected DialogDockpaneViewModel() + { + Items = new ObservableCollection(); + Items.Add(new ItemModel(){Content = "adfdfdafdfs"}); + } /// /// Show the DockPane.