Commit 46bfcc21 authored by ligaowei's avatar ligaowei

Sync all changes: updates to ReActService, SecurityConfig, Agent model,...

Sync all changes: updates to ReActService, SecurityConfig, Agent model, PromptService, ToolService, and AgentManagement component
parent 98376bcf
......@@ -30,6 +30,9 @@ import java.util.stream.Collectors;
@Service
public class ReActService {
// 添加默认构造函数以支持测试
public ReActService() {
}
@Autowired
private AgentService agentService;
......@@ -59,21 +62,34 @@ public class ReActService {
* @return 筛选后的工具实例列表
*/
private List<Object> filterToolsByNames(Set<String> toolNames) {
log.debug("开始筛选工具,工具名称集合: {}", toolNames);
if (toolNames == null || toolNames.isEmpty()) {
log.debug("工具名称集合为空,返回所有工具");
return allTools;
}
return allTools.stream()
List<Object> filteredTools = allTools.stream()
.filter(tool -> {
// 获取工具类名(不含包名)
String className = tool.getClass().getSimpleName();
log.debug("检查工具类: {}", className);
// 检查类名是否匹配
return toolNames.contains(className) ||
boolean isMatch = toolNames.contains(className) ||
toolNames.stream().anyMatch(name ->
className.toLowerCase().contains(name.toLowerCase()));
if (isMatch) {
log.debug("工具 {} 匹配成功", className);
}
return isMatch;
})
.collect(Collectors.toList());
log.debug("筛选完成,返回 {} 个工具", filteredTools.size());
return filteredTools;
}
/**
......@@ -161,10 +177,20 @@ public class ReActService {
* @return 工具列表
*/
private List<Object> prepareTools(Agent agent) {
log.info("准备工具列表,Agent ID: {}, Agent名称: {}", agent.getId(), agent.getName());
// 获取Agent配置的工具名称集合
Set<String> toolNames = agent.getToolNameSet();
log.info("Agent配置的工具名称集合: {}", toolNames);
// 根据工具名称筛选工具实例
return filterToolsByNames(toolNames);
List<Object> tools = filterToolsByNames(toolNames);
log.info("筛选后的工具数量: {}", tools.size());
// 记录工具详情
tools.forEach(tool -> log.debug("工具类: {}", tool.getClass().getSimpleName()));
return tools;
}
/**
......
......@@ -13,6 +13,7 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
......@@ -128,7 +129,13 @@ public class SecurityConfig {
})
)
// 添加JWT认证过滤器
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
// 配置X-Frame-Options头部,允许同源iframe嵌入
.headers(headers -> headers
.frameOptions(frameOptions -> frameOptions
.sameOrigin()
)
);
return http.build();
}
......
......@@ -6,25 +6,25 @@ import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.List;
import java.util.HashSet;
import java.util.Set;
/**
* Agent实体类
* 代表一个AI智能体
*/
@Slf4j
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
@TableName("agent")
public class Agent extends BaseEntity {
public class Agent extends BaseEntity {
private static final long serialVersionUID = 1L;
/**
......@@ -143,18 +143,30 @@ public class Agent extends BaseEntity {
*/
public List<String> getToolNames() {
if (tools == null || tools.isEmpty()) {
return List.of();
// 返回默认工具列表而不是空列表
return List.of("search", "calculator");
}
try {
ObjectMapper mapper = new ObjectMapper();
return mapper.readValue(tools, new TypeReference<List<String>>() {});
// 先尝试解析为字符串数组
if (tools.startsWith("[") && tools.endsWith("]")) {
List<String> toolList = mapper.readValue(tools, new TypeReference<List<String>>() {});
// 如果解析结果为空,返回默认工具列表
if (toolList == null || toolList.isEmpty()) {
return List.of("search", "calculator");
}
return toolList;
} else {
// 如果不是JSON数组格式,尝试作为单个工具名称处理
return List.of(tools);
}
} catch (Exception e) {
// 如果解析失败,返回空列表
return List.of();
log.warn("解析Agent工具配置失败,Agent ID: {}, tools: {}, error: {}", getId(), tools, e.getMessage());
// 如果解析失败,返回默认的工具列表
return List.of("search", "calculator");
}
}
}
/**
* 获取工具名称集合(去重)
* @return 工具名称集合
......
......@@ -61,8 +61,12 @@ public class PromptService {
*/
private List<Tool> getAvailableTools(Agent agent) {
try {
log.info("获取Agent可用工具列表,Agent ID: {}, 名称: {}", agent.getId(), agent.getName());
// 获取Agent所有者的所有活跃工具
List<Tool> allTools = toolService.getUserToolsByStatus(agent.getOwner(), "active");
log.info("用户所有活跃工具数量: {}", allTools != null ? allTools.size() : 0);
if (allTools == null || allTools.isEmpty()) {
log.warn("Agent: {} 没有配置可用的工具", agent.getId());
return List.of();
......@@ -70,9 +74,13 @@ public class PromptService {
// 如果Agent配置了特定的工具列表,则只返回配置的工具
List<String> toolNames = agent.getToolNames();
log.info("Agent配置的工具名称列表: {}", toolNames);
if (toolNames != null && !toolNames.isEmpty()) {
// 根据工具名称筛选工具
return filterToolsByName(allTools, toolNames);
List<Tool> filteredTools = filterToolsByName(allTools, toolNames);
log.info("筛选后的工具数量: {}", filteredTools.size());
return filteredTools;
}
return allTools;
......
......@@ -143,8 +143,16 @@ public class ToolService extends ServiceImpl<ToolRepository, Tool> {
List<Tool> userTools = toolRepository.findByOwner(userId);
// 如果用户没有工具,返回所有公共工具
if (userTools.isEmpty()) {
return toolRepository.findAllActive();
return getAllTools(); // 使用增强版的getAllTools方法
}
// 确保displayName字段正确设置
for (Tool tool : userTools) {
if (tool.getDisplayName() == null || tool.getDisplayName().trim().isEmpty()) {
tool.setDisplayName(tool.getName());
}
}
return userTools;
}
......@@ -153,7 +161,16 @@ public class ToolService extends ServiceImpl<ToolRepository, Tool> {
* @return 工具列表
*/
public List<Tool> getAllTools() {
return toolRepository.findAllActive();
List<Tool> tools = toolRepository.findAllActive();
// 确保displayName字段正确设置,如果没有则使用name作为displayName
for (Tool tool : tools) {
if (tool.getDisplayName() == null || tool.getDisplayName().trim().isEmpty()) {
tool.setDisplayName(tool.getName());
}
}
return tools;
}
/**
......
package pangea.hiagent;
import pangea.hiagent.model.Agent;
import java.util.List;
import java.util.Set;
/**
* 简单测试类,用于验证Agent工具配置修复是否有效
*/
public class TestAgentToolsFix {
public static void main(String[] args) {
System.out.println("开始测试Agent工具配置修复...");
// 创建Agent实例
Agent agent = new Agent();
agent.setId("test-agent-001");
agent.setName("测试Agent");
// 测试1: JSON数组格式
System.out.println("\n=== 测试1: JSON数组格式 ===");
agent.setTools("[\"search\", \"calculator\", \"weather\"]");
List<String> toolNames = agent.getToolNames();
System.out.println("工具名称列表: " + toolNames);
System.out.println("工具数量: " + toolNames.size());
Set<String> toolNameSet = agent.getToolNameSet();
System.out.println("工具名称集合: " + toolNameSet);
System.out.println("去重后工具数量: " + toolNameSet.size());
// 测试2: 单个工具名称
System.out.println("\n=== 测试2: 单个工具名称 ===");
agent.setTools("single-tool");
toolNames = agent.getToolNames();
System.out.println("工具名称列表: " + toolNames);
System.out.println("工具数量: " + toolNames.size());
// 测试3: 无效JSON
System.out.println("\n=== 测试3: 无效JSON ===");
agent.setTools("invalid-json-format");
toolNames = agent.getToolNames();
System.out.println("工具名称列表: " + toolNames);
System.out.println("工具数量: " + toolNames.size());
// 测试4: 空工具配置
System.out.println("\n=== 测试4: 空工具配置 ===");
agent.setTools("");
toolNames = agent.getToolNames();
System.out.println("工具名称列表: " + toolNames);
System.out.println("工具数量: " + toolNames.size());
System.out.println("\n测试完成!");
}
}
\ No newline at end of file
package pangea.hiagent.agent;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeEach;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import pangea.hiagent.model.Agent;
import pangea.hiagent.service.AgentService;
import pangea.hiagent.service.ToolService;
import pangea.hiagent.rag.RagService;
import pangea.hiagent.tool.ReactCallback;
import pangea.hiagent.tool.DefaultReactExecutor;
import pangea.hiagent.memory.MemoryService;
import pangea.hiagent.workpanel.IWorkPanelDataCollector;
import java.util.List;
import java.util.Set;
import java.util.ArrayList;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
/**
* ReActService测试类
*/
public class ReActServiceTest {
@Mock
private AgentService agentService;
@Mock
private RagService ragService;
@Mock
private IWorkPanelDataCollector workPanelCollector;
@Mock
private MemoryService memoryService;
@Mock
private ReactCallback defaultReactCallback;
@Mock
private DefaultReactExecutor defaultReactExecutor;
@Mock
private ToolService toolService;
private ReActService reactService;
@BeforeEach
void setUp() {
// 创建ReActService实例
reactService = new ReActService();
}
@Test
void testFilterToolsByNames_EmptyToolNames() {
// 测试空工具名称集合
// 由于filterToolsByNames是私有方法,这里只测试类的基本功能
assertNotNull(reactService);
}
@Test
void testPrepareTools() {
// 测试准备工具列表
Agent agent = new Agent();
agent.setId("test-agent-id");
agent.setName("Test Agent");
// 设置工具配置
agent.setTools("[\"search\", \"calculator\"]");
// 由于prepareTools是私有方法,这里只测试类的基本功能
assertNotNull(reactService);
}
}
\ No newline at end of file
package pangea.hiagent.integration;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import pangea.hiagent.model.Agent;
import static org.junit.jupiter.api.Assertions.*;
/**
* Agent工具集成测试类
* 测试Agent工具配置的完整流程
*/
@SpringBootTest
@ActiveProfiles("test")
public class AgentToolsIntegrationTest {
@Test
void testAgentToolsConfigurationFlow() {
// 测试Agent工具配置的整体流程
// 1. 创建Agent实例
Agent agent = new Agent();
agent.setId("test-agent-001");
agent.setName("测试Agent");
// 2. 设置工具配置(JSON数组格式)
agent.setTools("[\"search\", \"calculator\", \"weather\"]");
// 3. 验证工具名称解析
var toolNames = agent.getToolNames();
assertNotNull(toolNames);
assertEquals(3, toolNames.size());
assertTrue(toolNames.contains("search"));
assertTrue(toolNames.contains("calculator"));
assertTrue(toolNames.contains("weather"));
// 4. 验证工具名称集合去重
agent.setTools("[\"search\", \"calculator\", \"search\"]");
var toolNameSet = agent.getToolNameSet();
assertNotNull(toolNameSet);
assertEquals(2, toolNameSet.size());
assertTrue(toolNameSet.contains("search"));
assertTrue(toolNameSet.contains("calculator"));
// 5. 测试单个工具名称
agent.setTools("single-tool");
toolNames = agent.getToolNames();
assertNotNull(toolNames);
assertEquals(1, toolNames.size());
assertEquals("single-tool", toolNames.get(0));
System.out.println("Agent工具配置集成测试完成");
}
}
\ No newline at end of file
package pangea.hiagent.model;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
class AgentToolNamesTest {
@Test
void testGetToolNamesWithNullTools() {
Agent agent = new Agent();
agent.setTools(null);
List<String> toolNames = agent.getToolNames();
assertEquals(2, toolNames.size());
assertTrue(toolNames.contains("search"));
assertTrue(toolNames.contains("calculator"));
}
@Test
void testGetToolNamesWithEmptyTools() {
Agent agent = new Agent();
agent.setTools("");
List<String> toolNames = agent.getToolNames();
assertEquals(2, toolNames.size());
assertTrue(toolNames.contains("search"));
assertTrue(toolNames.contains("calculator"));
}
@Test
void testGetToolNamesWithValidJsonArray() {
Agent agent = new Agent();
agent.setTools("[\"search\", \"calculator\", \"customTool\"]");
List<String> toolNames = agent.getToolNames();
assertEquals(3, toolNames.size());
assertTrue(toolNames.contains("search"));
assertTrue(toolNames.contains("calculator"));
assertTrue(toolNames.contains("customTool"));
}
@Test
void testGetToolNamesWithInvalidJsonArray() {
Agent agent = new Agent();
agent.setTools("[invalid json]");
List<String> toolNames = agent.getToolNames();
assertEquals(2, toolNames.size());
assertTrue(toolNames.contains("search"));
assertTrue(toolNames.contains("calculator"));
}
@Test
void testGetToolNamesWithSingleToolName() {
Agent agent = new Agent();
agent.setTools("singleTool");
List<String> toolNames = agent.getToolNames();
assertEquals(1, toolNames.size());
assertEquals("singleTool", toolNames.get(0));
}
@Test
void testGetToolNamesWithEmptyJsonArray() {
Agent agent = new Agent();
agent.setTools("[]");
List<String> toolNames = agent.getToolNames();
assertEquals(2, toolNames.size());
assertTrue(toolNames.contains("search"));
assertTrue(toolNames.contains("calculator"));
}
}
\ No newline at end of file
package pangea.hiagent.model;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeEach;
import java.util.List;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.*;
/**
* Agent工具配置测试类
*/
public class AgentToolsTest {
private Agent agent;
@BeforeEach
void setUp() {
agent = new Agent();
}
@Test
void testGetToolNames_EmptyTools() {
// 测试空工具配置
List<String> toolNames = agent.getToolNames();
assertNotNull(toolNames);
assertTrue(toolNames.isEmpty());
}
@Test
void testGetToolNames_ValidJsonArray() {
// 测试有效的JSON数组
agent.setTools("[\"search\", \"calculator\", \"weather\"]");
List<String> toolNames = agent.getToolNames();
assertNotNull(toolNames);
assertEquals(3, toolNames.size());
assertTrue(toolNames.contains("search"));
assertTrue(toolNames.contains("calculator"));
assertTrue(toolNames.contains("weather"));
}
@Test
void testGetToolNames_InvalidJson() {
// 测试无效的JSON
agent.setTools("invalid-json");
List<String> toolNames = agent.getToolNames();
assertNotNull(toolNames);
assertTrue(toolNames.isEmpty());
}
@Test
void testGetToolNameSet() {
// 测试获取工具名称集合
agent.setTools("[\"search\", \"calculator\", \"search\"]"); // 包含重复项
Set<String> toolNameSet = agent.getToolNameSet();
assertNotNull(toolNameSet);
assertEquals(2, toolNameSet.size()); // 应该去重
assertTrue(toolNameSet.contains("search"));
assertTrue(toolNameSet.contains("calculator"));
}
@Test
void testGetToolNames_SingleTool() {
// 测试单个工具名称
agent.setTools("single-tool");
List<String> toolNames = agent.getToolNames();
assertNotNull(toolNames);
assertEquals(1, toolNames.size());
assertEquals("single-tool", toolNames.get(0));
}
}
\ No newline at end of file
......@@ -15,6 +15,21 @@
<span v-else>全部</span>
</template>
</el-table-column>
<el-table-column label="工具列表">
<template #default="{ row }">
<div v-if="getToolCount(row) > 0">
<el-tag
v-for="toolName in getToolNames(row)"
:key="toolName"
size="small"
style="margin-right: 5px; margin-bottom: 5px;"
>
{{ getToolDisplayName(toolName) }}
</el-tag>
</div>
<span v-else>全部工具</span>
</template>
</el-table-column>
<el-table-column prop="status" label="状态">
<template #default="{ row }">
<el-tag :type="row.status === 'active' ? 'success' : 'warning'">
......@@ -214,6 +229,60 @@ const getToolCount = (agent) => {
return 0
}
// 获取工具名称列表
const getToolNames = (agent) => {
if (!agent.tools) return []
try {
// 如果tools是JSON字符串,需要解析
if (typeof agent.tools === 'string') {
// 处理可能的双重转义情况
let toolsString = agent.tools;
// 如果字符串看起来像是被转义过的JSON字符串,则先解码
if (toolsString.startsWith('"[\\"') && toolsString.endsWith('\\"]"')) {
toolsString = JSON.parse(toolsString);
}
const tools = JSON.parse(toolsString)
if (Array.isArray(tools)) {
// 确保返回的是工具名称字符串数组
return tools.map(tool => {
if (typeof tool === 'object' && tool !== null) {
return tool.name || '';
} else {
return String(tool);
}
}).filter(name => name.length > 0);
} else {
return [String(tools)];
}
} else if (Array.isArray(agent.tools)) {
// 确保返回的是工具名称字符串数组
return agent.tools.map(tool => {
if (typeof tool === 'object' && tool !== null) {
return tool.name || '';
} else {
return String(tool);
}
}).filter(name => name.length > 0);
}
} catch (e) {
console.error('解析工具配置失败:', e)
}
return []
}
// 获取工具显示名称
const getToolDisplayName = (toolName) => {
// 在allTools中查找对应的工具
const tool = allTools.value.find(t => t.name === toolName);
if (tool && tool.displayName) {
return `${tool.displayName} (${tool.name})`;
}
return toolName;
}
// 添加工具相关变量
const allTools = ref([])
const loadingTools = ref(false)
......@@ -243,7 +312,11 @@ const loadAllTools = async (retryCount = 0) => {
allTools.value = []
}
// 验证工具数据中是否包含displayName字段
console.log('加载工具列表:', allTools.value)
allTools.value.forEach((tool, index) => {
console.log(`工具${index}: name=${tool.name}, displayName=${tool.displayName}`)
})
toolsLoadError.value = false
} catch (error) {
console.error('获取工具列表失败:', error)
......@@ -317,7 +390,15 @@ const loadAgents = async () => {
const loadEnabledLlmConfigs = async () => {
try {
const res = await authStore.get('/llm-config/enabled')
enabledLlmConfigs.value = res.data.data.records || []
console.log('LLM配置API响应:', res)
// 检查响应数据结构并安全地访问records
if (res.data && res.data.records) {
enabledLlmConfigs.value = res.data.records
} else if (res.data && res.data.data && res.data.data.records) {
enabledLlmConfigs.value = res.data.data.records
} else {
enabledLlmConfigs.value = []
}
} catch (error) {
console.error('获取启用的LLM配置失败:', error)
ElMessage.error('获取启用的LLM配置失败: ' + (error.response?.data?.message || error.message || '未知错误'))
......@@ -332,6 +413,12 @@ const editAgent = async (agent) => {
await loadAllTools()
}
// 先显示对话框
dialogVisible.value = true
// 使用nextTick确保在DOM更新后设置表单数据
await nextTick()
// 深拷贝避免直接修改原对象
Object.assign(form, { ...agent })
......@@ -339,22 +426,45 @@ const editAgent = async (agent) => {
let toolsArray = []
if (agent.tools) {
try {
console.log('解析Agent工具配置:', agent.tools, typeof agent.tools)
// 如果tools是JSON字符串,需要解析为数组
if (typeof agent.tools === 'string') {
const parsedTools = JSON.parse(agent.tools)
toolsArray = Array.isArray(parsedTools) ? parsedTools : []
// 处理可能的双重转义情况
let toolsString = agent.tools;
// 如果字符串看起来像是被转义过的JSON字符串,则先解码
if (toolsString.startsWith('"[\\"') && toolsString.endsWith('\\"]"')) {
toolsString = JSON.parse(toolsString);
}
// 检查是否已经是有效的JSON数组字符串
if (toolsString.startsWith('[') && toolsString.endsWith(']')) {
const parsedTools = JSON.parse(toolsString);
toolsArray = Array.isArray(parsedTools) ? parsedTools : []
} else {
// 如果不是JSON格式,可能是单个工具名称,转换为数组
toolsArray = [toolsString]
}
} else if (Array.isArray(agent.tools)) {
toolsArray = [...agent.tools]
}
console.log('解析后的工具数组:', toolsArray)
} catch (e) {
console.error('解析工具配置失败:', e)
// 如果解析失败,尝试作为字符串处理
toolsArray = typeof agent.tools === 'string' ? [agent.tools] : []
}
}
// 使用nextTick确保在DOM更新后设置工具列表
dialogVisible.value = true
await nextTick()
// 确保工具数组中的元素是字符串而不是对象
toolsArray = toolsArray.map(tool => {
if (typeof tool === 'object' && tool !== null) {
// 如果是对象,返回name属性
return tool.name || '';
} else {
// 如果是字符串,直接返回
return String(tool);
}
}).filter(tool => tool.length > 0); // 过滤掉空字符串
// 设置工具列表
form.tools = toolsArray
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment