实现了基础的工具、消息UI界面

This commit is contained in:
PeterZhong 2025-06-03 18:35:01 +08:00
parent e3dc23de07
commit 52148f6936
10 changed files with 344 additions and 83 deletions

View File

@ -120,6 +120,18 @@
<EmbeddedResource Include="resource\prompt\ErrorPrompt.txt"> <EmbeddedResource Include="resource\prompt\ErrorPrompt.txt">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</EmbeddedResource> </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>
</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,5 +1,7 @@
using System; using System;
using System.IO; using System.IO;
using System.Windows.Controls;
using System.Windows.Media.Imaging;
namespace LinkToolAddin.common; namespace LinkToolAddin.common;
@ -9,12 +11,6 @@ public class LocalResource
{ {
var assembly = System.Reflection.Assembly.GetExecutingAssembly(); var assembly = System.Reflection.Assembly.GetExecutingAssembly();
string[] resourceNames = assembly.GetManifestResourceNames();
foreach (string name in resourceNames)
{
Console.WriteLine(name);
}
using (Stream stream = assembly.GetManifestResourceStream(resourceName)) using (Stream stream = assembly.GetManifestResourceStream(resourceName))
{ {
if (stream == null) if (stream == null)
@ -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;
}
}
} }

View File

@ -328,14 +328,17 @@ public class Gateway
else else
{ {
//包含Prompt调用请求的消息 //包含Prompt调用请求的消息
MessageListItem chatMessageListItem = new ChatMessageItem() if (remainingPrompt != "")
{ {
content = remainingPrompt, MessageListItem chatMessageListItem = new ChatMessageItem()
role = "assistant", {
type = MessageType.CHAT_MESSAGE, content = remainingPrompt,
id = timestamp.ToString() role = "assistant",
}; type = MessageType.CHAT_MESSAGE,
callback?.Invoke(chatMessageListItem); id = timestamp.ToString()
};
callback?.Invoke(chatMessageListItem);
}
XElement promptUse = XElement.Parse(matchedPrompt); XElement promptUse = XElement.Parse(matchedPrompt);
string promptKey = promptUse.Element("name")?.Value; string promptKey = promptUse.Element("name")?.Value;
string promptArgs = promptUse.Element("arguments")?.Value; string promptArgs = promptUse.Element("arguments")?.Value;
@ -352,17 +355,20 @@ public class Gateway
else else
{ {
//包含工具调用请求的消息 //包含工具调用请求的消息
MessageListItem chatMessageListItem = new ChatMessageItem() if (remaining != "")
{ {
content = remaining, MessageListItem chatMessageListItem = new ChatMessageItem()
role = "assistant", {
type = MessageType.CHAT_MESSAGE, content = remaining,
id = timestamp.ToString() role = "assistant",
}; type = MessageType.CHAT_MESSAGE,
Application.Current.Dispatcher.Invoke(() => id = timestamp.ToString()
{ };
callback?.Invoke(chatMessageListItem); Application.Current.Dispatcher.Invoke(() =>
}); {
callback?.Invoke(chatMessageListItem);
});
}
XElement toolUse = XElement.Parse(matched); XElement toolUse = XElement.Parse(matched);
string fullToolName = toolUse.Element("name")?.Value; string fullToolName = toolUse.Element("name")?.Value;
string toolArgs = toolUse.Element("arguments")?.Value; string toolArgs = toolUse.Element("arguments")?.Value;
@ -415,7 +421,8 @@ public class Gateway
type = MessageType.TOOL_MESSAGE, type = MessageType.TOOL_MESSAGE,
status = toolResponse.IsError ? "fail" : "success", status = toolResponse.IsError ? "fail" : "success",
content = JsonConvert.SerializeObject(toolResponse), content = JsonConvert.SerializeObject(toolResponse),
id = (timestamp + 1).ToString() id = (timestamp + 1).ToString(),
role = "user"
}; };
messages.Add(new Message messages.Add(new Message
{ {
@ -441,7 +448,8 @@ public class Gateway
type = MessageType.TOOL_MESSAGE, type = MessageType.TOOL_MESSAGE,
status = toolResponse.IsError ? "fail" : "success", status = toolResponse.IsError ? "fail" : "success",
content = JsonConvert.SerializeObject(toolResponse), content = JsonConvert.SerializeObject(toolResponse),
id = (timestamp + 1).ToString() id = (timestamp + 1).ToString(),
role = "user"
}; };
messages.Add(new Message messages.Add(new Message
{ {
@ -486,7 +494,8 @@ public class Gateway
type = MessageType.TOOL_MESSAGE, type = MessageType.TOOL_MESSAGE,
status = "fail", status = "fail",
content = JsonConvert.SerializeObject(innerResult), content = JsonConvert.SerializeObject(innerResult),
id = (timestamp + 1).ToString() id = (timestamp + 1).ToString(),
role = "user"
}; };
messages.Add(new Message messages.Add(new Message
{ {
@ -507,7 +516,8 @@ public class Gateway
type = MessageType.TOOL_MESSAGE, type = MessageType.TOOL_MESSAGE,
status = "success", status = "success",
content = JsonConvert.SerializeObject(innerResult), content = JsonConvert.SerializeObject(innerResult),
id = (timestamp + 1).ToString() id = (timestamp + 1).ToString(),
role = "user"
}; };
messages.Add(new Message messages.Add(new Message
{ {

View File

@ -35,53 +35,53 @@ public class McpServerList
Description = "可以调用进行查询知识库,获取相关参考信息。", Description = "可以调用进行查询知识库,获取相关参考信息。",
IsActive = true IsActive = true
}); });
servers.Add("filesystem", new StdioMcpServer() // servers.Add("filesystem", new StdioMcpServer()
{ // {
Name = "filesystem", // Name = "filesystem",
Type = "stdio", // Type = "stdio",
Command = "npx", // Command = "npx",
Args = new List<string>() // Args = new List<string>()
{ // {
"-y", // "-y",
"@modelcontextprotocol/server-filesystem", // "@modelcontextprotocol/server-filesystem",
"D:\\01_Project\\20250305_LinkTool\\20250420_AiDemoProject\\TestData" // "D:\\01_Project\\20250305_LinkTool\\20250420_AiDemoProject\\TestData"
} // }
}); // });
servers.Add("fetch", new StdioMcpServer() // servers.Add("fetch", new StdioMcpServer()
{ // {
Name = "fetch", // Name = "fetch",
Type = "stdio", // Type = "stdio",
Command = "uvx", // Command = "uvx",
Args = new List<string>() // Args = new List<string>()
{ // {
"mcp-server-fetch" // "mcp-server-fetch"
} // }
}); // });
servers.Add("bing-search", new StdioMcpServer() // servers.Add("bing-search", new StdioMcpServer()
{ // {
Name = "bing-search", // Name = "bing-search",
Type = "stdio", // Type = "stdio",
Command = "npx", // Command = "npx",
Args = new List<string>() // Args = new List<string>()
{ // {
"bing-cn-mcp" // "bing-cn-mcp"
} // }
}); // });
servers.Add("mcp-python-interpreter", new StdioMcpServer() // servers.Add("mcp-python-interpreter", new StdioMcpServer()
{ // {
Name = "mcp-python-interpreter", // Name = "mcp-python-interpreter",
Type = "stdio", // Type = "stdio",
Command = "uvx", // Command = "uvx",
Args = new List<string>() // Args = new List<string>()
{ // {
"--native-tls", // "--native-tls",
"mcp-python-interpreter", // "mcp-python-interpreter",
"--dir", // "--dir",
"D:\\01_Project\\20250305_LinkTool\\20250420_AiDemoProject\\TestData", // "D:\\01_Project\\20250305_LinkTool\\20250420_AiDemoProject\\TestData",
"--python-path", // "--python-path",
"C:\\Program Files\\ArcGIS\\Pro\\bin\\Python\\envs\\custom\\python.exe" // "C:\\Program Files\\ArcGIS\\Pro\\bin\\Python\\envs\\custom\\python.exe"
} // }
}); // });
} }
public McpServer GetServer(string name) public McpServer GetServer(string name)

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/user.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 784 B

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,27 @@
</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"> <Button Grid.Row="0" Content="Test" Name="TestButton" Click="TestButton_OnClick"></Button>
<Button Content="Test Server" Name="TestServer" Click="TestServer_OnClick"></Button> <!-- <TextBlock Grid.Row="0" HorizontalAlignment="Center" VerticalAlignment="Center" Text="LinkTool"/> -->
</DockPanel> <ScrollViewer Grid.Row="1" HorizontalContentAlignment="Stretch" VerticalAlignment="Top">
<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="这是用户的问题"></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,190 @@ 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);
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<string,object>(),
type = MessageType.TOOL_MESSAGE,
status = "success",
content = "JsonConvert.SerializeObject(toolResponse)",
id = (timestamp + 1).ToString(),
role = "user"
};
NewMessage_Recall(toolMessageItem);
}
} }
} }

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.