feat:【ai 大模型】增加 MCP Server Boot Starter 示例
This commit is contained in:
@@ -187,6 +187,12 @@
|
|||||||
</exclusions>
|
</exclusions>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.ai</groupId>
|
||||||
|
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
|
||||||
|
<version>${spring-ai.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- TinyFlow:AI 工作流 -->
|
<!-- TinyFlow:AI 工作流 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>dev.tinyflow</groupId>
|
<groupId>dev.tinyflow</groupId>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
package cn.iocoder.yudao.module.ai.dal.dataobject.model;
|
package cn.iocoder.yudao.module.ai.dal.dataobject.model;
|
||||||
|
|
||||||
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
|
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
|
||||||
import cn.iocoder.yudao.module.ai.service.model.tool.DirectoryListToolFunction;
|
import cn.iocoder.yudao.module.ai.tool.function.DirectoryListToolFunction;
|
||||||
import cn.iocoder.yudao.module.ai.service.model.tool.WeatherQueryToolFunction;
|
import cn.iocoder.yudao.module.ai.tool.function.WeatherQueryToolFunction;
|
||||||
import com.baomidou.mybatisplus.annotation.KeySequence;
|
import com.baomidou.mybatisplus.annotation.KeySequence;
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import cn.iocoder.yudao.module.ai.framework.ai.core.model.suno.api.SunoApi;
|
|||||||
import cn.iocoder.yudao.module.ai.framework.ai.core.model.xinghuo.XingHuoChatModel;
|
import cn.iocoder.yudao.module.ai.framework.ai.core.model.xinghuo.XingHuoChatModel;
|
||||||
import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchClient;
|
import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchClient;
|
||||||
import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.bocha.AiBoChaWebSearchClient;
|
import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.bocha.AiBoChaWebSearchClient;
|
||||||
|
import cn.iocoder.yudao.module.ai.tool.method.PersonService;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.ai.deepseek.DeepSeekChatModel;
|
import org.springframework.ai.deepseek.DeepSeekChatModel;
|
||||||
import org.springframework.ai.deepseek.DeepSeekChatOptions;
|
import org.springframework.ai.deepseek.DeepSeekChatOptions;
|
||||||
@@ -25,8 +26,10 @@ import org.springframework.ai.model.tool.ToolCallingManager;
|
|||||||
import org.springframework.ai.openai.OpenAiChatModel;
|
import org.springframework.ai.openai.OpenAiChatModel;
|
||||||
import org.springframework.ai.openai.OpenAiChatOptions;
|
import org.springframework.ai.openai.OpenAiChatOptions;
|
||||||
import org.springframework.ai.openai.api.OpenAiApi;
|
import org.springframework.ai.openai.api.OpenAiApi;
|
||||||
|
import org.springframework.ai.support.ToolCallbacks;
|
||||||
import org.springframework.ai.tokenizer.JTokkitTokenCountEstimator;
|
import org.springframework.ai.tokenizer.JTokkitTokenCountEstimator;
|
||||||
import org.springframework.ai.tokenizer.TokenCountEstimator;
|
import org.springframework.ai.tokenizer.TokenCountEstimator;
|
||||||
|
import org.springframework.ai.tool.ToolCallback;
|
||||||
import org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusServiceClientProperties;
|
import org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusServiceClientProperties;
|
||||||
import org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusVectorStoreProperties;
|
import org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusVectorStoreProperties;
|
||||||
import org.springframework.ai.vectorstore.qdrant.autoconfigure.QdrantVectorStoreProperties;
|
import org.springframework.ai.vectorstore.qdrant.autoconfigure.QdrantVectorStoreProperties;
|
||||||
@@ -36,6 +39,8 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
|
|||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 芋道 AI 自动配置
|
* 芋道 AI 自动配置
|
||||||
*
|
*
|
||||||
@@ -271,4 +276,14 @@ public class AiAutoConfiguration {
|
|||||||
return new AiBoChaWebSearchClient(yudaoAiProperties.getWebSearch().getApiKey());
|
return new AiBoChaWebSearchClient(yudaoAiProperties.getWebSearch().getApiKey());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== MCP 相关 ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 参考自 <a href="https://docs.spring.io/spring-ai/reference/api/mcp/mcp-client-boot-starter-docs.html">MCP Server Boot Starter</>
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
public List<ToolCallback> toolCallbacks(PersonService personService) {
|
||||||
|
return List.of(ToolCallbacks.from(personService));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package cn.iocoder.yudao.module.ai.framework.security.config;
|
||||||
|
|
||||||
|
import cn.iocoder.yudao.framework.security.config.AuthorizeRequestsCustomizer;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import org.springframework.ai.mcp.server.autoconfigure.McpServerProperties;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI 模块的 Security 配置
|
||||||
|
*/
|
||||||
|
@Configuration(proxyBeanMethods = false, value = "aiSecurityConfiguration")
|
||||||
|
public class SecurityConfiguration {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private McpServerProperties serverProperties;
|
||||||
|
|
||||||
|
@Bean("aiAuthorizeRequestsCustomizer")
|
||||||
|
public AuthorizeRequestsCustomizer authorizeRequestsCustomizer() {
|
||||||
|
return new AuthorizeRequestsCustomizer() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void customize(AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry registry) {
|
||||||
|
// MCP Server
|
||||||
|
registry.requestMatchers(serverProperties.getSseEndpoint()).permitAll();
|
||||||
|
registry.requestMatchers(serverProperties.getSseMessageEndpoint()).permitAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
/**
|
||||||
|
* 占位
|
||||||
|
*/
|
||||||
|
package cn.iocoder.yudao.module.ai.framework.security.core;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package cn.iocoder.yudao.module.ai.service.model.tool;
|
package cn.iocoder.yudao.module.ai.tool.function;
|
||||||
|
|
||||||
import cn.hutool.core.date.LocalDateTimeUtil;
|
import cn.hutool.core.date.LocalDateTimeUtil;
|
||||||
import cn.hutool.core.io.FileUtil;
|
import cn.hutool.core.io.FileUtil;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package cn.iocoder.yudao.module.ai.service.model.tool;
|
package cn.iocoder.yudao.module.ai.tool.function;
|
||||||
|
|
||||||
import cn.iocoder.yudao.module.ai.util.AiUtils;
|
import cn.iocoder.yudao.module.ai.util.AiUtils;
|
||||||
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package cn.iocoder.yudao.module.ai.service.model.tool;
|
package cn.iocoder.yudao.module.ai.tool.function;
|
||||||
|
|
||||||
import cn.hutool.core.date.LocalDateTimeUtil;
|
import cn.hutool.core.date.LocalDateTimeUtil;
|
||||||
import cn.hutool.core.util.RandomUtil;
|
import cn.hutool.core.util.RandomUtil;
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
/**
|
||||||
|
* 参考 <a href="https://docs.spring.io/spring-ai/reference/api/tools.html#_methods_as_tools">Tool Calling —— Methods as Tools</a>
|
||||||
|
*/
|
||||||
|
package cn.iocoder.yudao.module.ai.tool.function;
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package cn.iocoder.yudao.module.ai.tool.method;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 来自 Spring AI 官方文档
|
||||||
|
*
|
||||||
|
* Represents a person with basic information.
|
||||||
|
* This is an immutable record.
|
||||||
|
*/
|
||||||
|
public record Person(
|
||||||
|
int id,
|
||||||
|
String firstName,
|
||||||
|
String lastName,
|
||||||
|
String email,
|
||||||
|
String sex,
|
||||||
|
String ipAddress,
|
||||||
|
String jobTitle,
|
||||||
|
int age
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package cn.iocoder.yudao.module.ai.tool.method;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 来自 Spring AI 官方文档
|
||||||
|
*
|
||||||
|
* Service interface for managing Person data.
|
||||||
|
* Defines the contract for CRUD operations and search/filter functionalities.
|
||||||
|
*/
|
||||||
|
public interface PersonService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new Person record.
|
||||||
|
* Assigns a unique ID to the person and stores it.
|
||||||
|
*
|
||||||
|
* @param personData The data for the new person (ID field is ignored). Must not be null.
|
||||||
|
* @return The created Person record, including the generated ID.
|
||||||
|
*/
|
||||||
|
Person createPerson(Person personData);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a Person by their unique ID.
|
||||||
|
*
|
||||||
|
* @param id The ID of the person to retrieve.
|
||||||
|
* @return An Optional containing the found Person, or an empty Optional if not found.
|
||||||
|
*/
|
||||||
|
Optional<Person> getPersonById(int id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves all Person records currently stored.
|
||||||
|
*
|
||||||
|
* @return An unmodifiable List containing all Persons. Returns an empty list if none exist.
|
||||||
|
*/
|
||||||
|
List<Person> getAllPersons();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates an existing Person record identified by ID.
|
||||||
|
* Replaces the existing data with the provided data, keeping the original ID.
|
||||||
|
*
|
||||||
|
* @param id The ID of the person to update.
|
||||||
|
* @param updatedPersonData The new data for the person (ID field is ignored). Must not be null.
|
||||||
|
* @return true if the person was found and updated, false otherwise.
|
||||||
|
*/
|
||||||
|
boolean updatePerson(int id, Person updatedPersonData);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes a Person record identified by ID.
|
||||||
|
*
|
||||||
|
* @param id The ID of the person to delete.
|
||||||
|
* @return true if the person was found and deleted, false otherwise.
|
||||||
|
*/
|
||||||
|
boolean deletePerson(int id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Searches for Persons whose job title contains the given query string (case-insensitive).
|
||||||
|
*
|
||||||
|
* @param jobTitleQuery The string to search for within job titles. Can be null or blank.
|
||||||
|
* @return An unmodifiable List of matching Persons. Returns an empty list if no matches or query is invalid.
|
||||||
|
*/
|
||||||
|
List<Person> searchByJobTitle(String jobTitleQuery);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters Persons by their exact sex (case-insensitive).
|
||||||
|
*
|
||||||
|
* @param sex The sex to filter by (e.g., "Male", "Female"). Can be null or blank.
|
||||||
|
* @return An unmodifiable List of matching Persons. Returns an empty list if no matches or filter is invalid.
|
||||||
|
*/
|
||||||
|
List<Person> filterBySex(String sex);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters Persons by their exact age.
|
||||||
|
*
|
||||||
|
* @param age The age to filter by.
|
||||||
|
* @return An unmodifiable List of matching Persons. Returns an empty list if no matches.
|
||||||
|
*/
|
||||||
|
List<Person> filterByAge(int age);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,336 @@
|
|||||||
|
package cn.iocoder.yudao.module.ai.tool.method;
|
||||||
|
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.ai.tool.annotation.Tool;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 来自 Spring AI 官方文档
|
||||||
|
*
|
||||||
|
* Implementation of the PersonService interface using an in-memory data store.
|
||||||
|
* Manages a collection of Person objects loaded from embedded CSV data.
|
||||||
|
* This class is thread-safe due to the use of ConcurrentHashMap and AtomicInteger.
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@Slf4j
|
||||||
|
public class PersonServiceImpl implements PersonService {
|
||||||
|
|
||||||
|
private final Map<Integer, Person> personStore = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
private AtomicInteger idGenerator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Embedded CSV data for initial population
|
||||||
|
*/
|
||||||
|
private static final String CSV_DATA = """
|
||||||
|
Id,FirstName,LastName,Email,Sex,IpAddress,JobTitle,Age
|
||||||
|
1,Fons,Tollfree,ftollfree0@senate.gov,Male,55.1 Tollfree Lane,Research Associate,31
|
||||||
|
2,Emlynne,Tabourier,etabourier1@networksolutions.com,Female,18 Tabourier Way,Associate Professor,38
|
||||||
|
3,Shae,Johncey,sjohncey2@yellowpages.com,Male,1 Johncey Circle,Structural Analysis Engineer,30
|
||||||
|
4,Sebastien,Bradly,sbradly3@mapquest.com,Male,2 Bradly Hill,Chief Executive Officer,40
|
||||||
|
5,Harriott,Kitteringham,hkitteringham4@typepad.com,Female,3 Kitteringham Drive,VP Sales,47
|
||||||
|
6,Anallise,Parradine,aparradine5@miibeian.gov.cn,Female,4 Parradine Street,Analog Circuit Design manager,44
|
||||||
|
7,Gorden,Kirkbright,gkirkbright6@reuters.com,Male,5 Kirkbright Plaza,Senior Editor,40
|
||||||
|
8,Veradis,Ledwitch,vledwitch7@google.com.au,Female,6 Ledwitch Avenue,Computer Systems Analyst IV,44
|
||||||
|
9,Agnesse,Penhalurick,apenhalurick8@google.it,Female,7 Penhalurick Terrace,Automation Specialist IV,41
|
||||||
|
10,Bibby,Hutable,bhutable9@craigslist.org,Female,8 Hutable Place,Account Representative I,43
|
||||||
|
11,Karoly,Lightoller,klightollera@rakuten.co.jp,Female,9 Lightoller Parkway,Senior Developer,46
|
||||||
|
12,Cristine,Durrad,cdurradb@aol.com,Female,10 Durrad Center,Senior Developer,48
|
||||||
|
13,Aggy,Napier,anapierc@hostgator.com,Female,11 Napier Court,VP Product Management,44
|
||||||
|
14,Prisca,Caddens,pcaddensd@vinaora.com,Female,12 Caddens Alley,Business Systems Development Analyst,41
|
||||||
|
15,Khalil,McKernan,kmckernane@google.fr,Male,13 McKernan Pass,Engineer IV,44
|
||||||
|
16,Lorry,MacTrusty,lmactrustyf@eventbrite.com,Male,14 MacTrusty Junction,Design Engineer,42
|
||||||
|
17,Casandra,Worsell,cworsellg@goo.gl,Female,15 Worsell Point,Systems Administrator IV,45
|
||||||
|
18,Ulrikaumeko,Haveline,uhavelineh@usgs.gov,Female,16 Haveline Trail,Financial Advisor,42
|
||||||
|
19,Shurlocke,Albany,salbanyi@artisteer.com,Male,17 Albany Plaza,Software Test Engineer III,46
|
||||||
|
20,Myrilla,Brimilcombe,mbrimilcombej@accuweather.com,Female,18 Brimilcombe Road,Programmer Analyst I,48
|
||||||
|
21,Carlina,Scimonelli,cscimonellik@va.gov,Female,19 Scimonelli Pass,Help Desk Technician,45
|
||||||
|
22,Tina,Goullee,tgoulleel@miibeian.gov.cn,Female,20 Goullee Crossing,Accountant IV,43
|
||||||
|
23,Adriaens,Storek,astorekm@devhub.com,Female,21 Storek Avenue,Recruiting Manager,40
|
||||||
|
24,Tedra,Giraudot,tgiraudotn@wiley.com,Female,22 Giraudot Terrace,Speech Pathologist,47
|
||||||
|
25,Josiah,Soares,jsoareso@google.nl,Male,23 Soares Street,Tax Accountant,45
|
||||||
|
26,Kayle,Gaukrodge,kgaukrodgep@wikispaces.com,Female,24 Gaukrodge Parkway,Accountant II,43
|
||||||
|
27,Ardys,Chuter,achuterq@ustream.tv,Female,25 Chuter Drive,Engineer IV,41
|
||||||
|
28,Francyne,Baudinet,fbaudinetr@newyorker.com,Female,26 Baudinet Center,VP Accounting,48
|
||||||
|
29,Gerick,Bullan,gbullans@seesaa.net,Male,27 Bullan Way,Senior Financial Analyst,43
|
||||||
|
30,Northrup,Grivori,ngrivorit@unc.edu,Male,28 Grivori Plaza,Systems Administrator I,45
|
||||||
|
31,Town,Duguid,tduguidu@squarespace.com,Male,29 Duguid Pass,Safety Technician IV,46
|
||||||
|
32,Pierette,Kopisch,pkopischv@google.com.br,Female,30 Kopisch Lane,Director of Sales,41
|
||||||
|
33,Jacquenetta,Le Prevost,jleprevostw@netlog.com,Female,31 Le Prevost Trail,Senior Developer,47
|
||||||
|
34,Garvy,Rusted,grustedx@aboutads.info,Male,32 Rusted Junction,Senior Developer,42
|
||||||
|
35,Clarice,Aysh,cayshy@merriam-webster.com,Female,33 Aysh Avenue,VP Quality Control,40
|
||||||
|
36,Tracie,Fedorski,tfedorskiz@bloglines.com,Male,34 Fedorski Terrace,Design Engineer,44
|
||||||
|
37,Noelyn,Matushenko,nmatushenko10@globo.com,Female,35 Matushenko Place,VP Sales,48
|
||||||
|
38,Rudiger,Klaesson,rklaesson11@usnews.com,Male,36 Klaesson Road,Database Administrator IV,43
|
||||||
|
39,Mirella,Syddie,msyddie12@geocities.jp,Female,37 Syddie Circle,Geological Engineer,46
|
||||||
|
40,Donalt,O'Lunny,dolunny13@elpais.com,Male,38 O'Lunny Center,Analog Circuit Design manager,41
|
||||||
|
41,Guntar,Deniskevich,gdeniskevich14@google.com.hk,Male,39 Deniskevich Way,Structural Engineer,47
|
||||||
|
42,Hort,Shufflebotham,hshufflebotham15@about.me,Male,40 Shufflebotham Court,Structural Analysis Engineer,45
|
||||||
|
43,Dominique,Thickett,dthickett16@slashdot.org,Male,41 Thickett Crossing,Safety Technician I,42
|
||||||
|
44,Zebulen,Piscopello,zpiscopello17@umich.edu,Male,42 Piscopello Parkway,Web Developer II,40
|
||||||
|
45,Mellicent,Mac Giany,mmacgiany18@state.tx.us,Female,43 Mac Giany Pass,Assistant Manager,44
|
||||||
|
46,Merle,Bounds,mbounds19@amazon.co.jp,Female,44 Bounds Alley,Systems Administrator III,41
|
||||||
|
47,Madelle,Farbrace,mfarbrace1a@xinhuanet.com,Female,45 Farbrace Terrace,Quality Engineer,48
|
||||||
|
48,Galvin,O'Sheeryne,gosheeryne1b@addtoany.com,Male,46 O'Sheeryne Way,Environmental Specialist,43
|
||||||
|
49,Guillemette,Bootherstone,gbootherstone1c@nationalgeographic.com,Female,47 Bootherstone Plaza,Professor,46
|
||||||
|
50,Letti,Aylmore,laylmore1d@vinaora.com,Female,48 Aylmore Circle,Automation Specialist I,40
|
||||||
|
51,Nonie,Rivalland,nrivalland1e@weather.com,Female,49 Rivalland Avenue,Software Test Engineer IV,45
|
||||||
|
52,Jacquelynn,Halfacre,jhalfacre1f@surveymonkey.com,Female,50 Halfacre Pass,Geologist II,42
|
||||||
|
53,Anderea,MacKibbon,amackibbon1g@weibo.com,Female,51 MacKibbon Parkway,Automation Specialist II,47
|
||||||
|
54,Wash,Klimko,wklimko1h@slashdot.org,Male,52 Klimko Alley,Database Administrator I,40
|
||||||
|
55,Flori,Kynett,fkynett1i@auda.org.au,Female,53 Kynett Trail,Quality Control Specialist,46
|
||||||
|
56,Libbey,Penswick,lpenswick1j@google.co.uk,Female,54 Penswick Point,VP Accounting,43
|
||||||
|
57,Silvanus,Skellorne,sskellorne1k@booking.com,Male,55 Skellorne Drive,Account Executive,48
|
||||||
|
58,Carmine,Mateos,cmateos1l@plala.or.jp,Male,56 Mateos Terrace,Systems Administrator I,41
|
||||||
|
59,Sheffie,Blazewicz,sblazewicz1m@google.com.au,Male,57 Blazewicz Center,VP Sales,44
|
||||||
|
60,Leanor,Worsnop,lworsnop1n@uol.com.br,Female,58 Worsnop Plaza,Systems Administrator III,45
|
||||||
|
61,Caspar,Pamment,cpamment1o@google.co.jp,Male,59 Pamment Court,Senior Financial Analyst,42
|
||||||
|
62,Justinian,Pentycost,jpentycost1p@sciencedaily.com,Male,60 Pentycost Way,Senior Quality Engineer,47
|
||||||
|
63,Gerianne,Jarnell,gjarnell1q@bing.com,Female,61 Jarnell Avenue,Help Desk Operator,40
|
||||||
|
64,Boycie,Zanetto,bzanetto1r@about.com,Male,62 Zanetto Place,Quality Engineer,46
|
||||||
|
65,Camilla,Mac Giany,cmacgiany1s@state.gov,Female,63 Mac Giany Parkway,Senior Cost Accountant,43
|
||||||
|
66,Hadlee,Piscopiello,hpiscopiello1t@artisteer.com,Male,64 Piscopiello Street,Account Representative III,48
|
||||||
|
67,Bobbie,Penvarden,bpenvarden1u@google.cn,Male,65 Penvarden Lane,Help Desk Operator,41
|
||||||
|
68,Ali,Gowlett,agowlett1v@parallels.com,Male,66 Gowlett Pass,VP Marketing,44
|
||||||
|
69,Olivette,Acome,oacome1w@qq.com,Female,67 Acome Hill,VP Product Management,45
|
||||||
|
70,Jehanna,Brotherheed,jbrotherheed1x@google.nl,Female,68 Brotherheed Junction,Database Administrator III,42
|
||||||
|
71,Morgan,Berthomieu,mberthomieu1y@artisteer.com,Male,69 Berthomieu Alley,Systems Administrator II,47
|
||||||
|
72,Linzy,Shilladay,lshilladay1z@icq.com,Female,70 Shilladay Trail,Research Assistant IV,40
|
||||||
|
73,Faydra,Brimner,fbrimner20@mozilla.org,Female,71 Brimner Road,Senior Editor,46
|
||||||
|
74,Gwenore,Oxlee,goxlee21@devhub.com,Female,72 Oxlee Terrace,Systems Administrator II,43
|
||||||
|
75,Evangelin,Beinke,ebeinke22@mozilla.com,Female,73 Beinke Circle,Accountant I,48
|
||||||
|
76,Missy,Cockling,mcockling23@si.edu,Female,74 Cockling Way,Software Engineer I,41
|
||||||
|
77,Suzanne,Klimschak,sklimschak24@etsy.com,Female,75 Klimschak Plaza,Tax Accountant,44
|
||||||
|
78,Candide,Goricke,cgoricke25@weebly.com,Female,76 Goricke Pass,Sales Associate,45
|
||||||
|
79,Gerome,Pinsent,gpinsent26@google.com.au,Male,77 Pinsent Junction,Software Consultant,42
|
||||||
|
80,Lezley,Mac Giany,lmacgiany27@scribd.com,Male,78 Mac Giany Alley,Operator,47
|
||||||
|
81,Tobiah,Durn,tdurn28@state.tx.us,Male,79 Durn Court,VP Sales,40
|
||||||
|
82,Sherlocke,Cockshoot,scockshoot29@yelp.com,Male,80 Cockshoot Street,Senior Financial Analyst,46
|
||||||
|
83,Myrle,Speenden,mspeenden2a@utexas.edu,Female,81 Speenden Center,Senior Developer,43
|
||||||
|
84,Isidore,Gorries,igorries2b@flavors.me,Male,82 Gorries Parkway,Sales Representative,48
|
||||||
|
85,Isac,Kitchingman,ikitchingman2c@businessinsider.com,Male,83 Kitchingman Drive,VP Accounting,41
|
||||||
|
86,Benedetta,Purrier,bpurrier2d@admin.ch,Female,84 Purrier Trail,VP Accounting,44
|
||||||
|
87,Tera,Fitchell,tfitchell2e@fotki.com,Female,85 Fitchell Place,Software Engineer IV,45
|
||||||
|
88,Abbe,Pamment,apamment2f@about.com,Male,86 Pamment Avenue,VP Sales,42
|
||||||
|
89,Jandy,Gommowe,jgommowe2g@angelfire.com,Female,87 Gommowe Road,Financial Analyst,47
|
||||||
|
90,Karena,Fussey,kfussey2h@google.com.au,Female,88 Fussey Point,Assistant Professor,40
|
||||||
|
91,Gaspar,Pammenter,gpammenter2i@google.com.br,Male,89 Pammenter Hill,Help Desk Operator,46
|
||||||
|
92,Stanwood,Mac Giany,smacgiany2j@prlog.org,Male,90 Mac Giany Terrace,Research Associate,43
|
||||||
|
93,Byrom,Beedell,bbeedell2k@google.co.jp,Male,91 Beedell Way,VP Sales,48
|
||||||
|
94,Annabella,Rowbottom,arowbottom2l@google.com.au,Female,92 Rowbottom Plaza,Help Desk Operator,41
|
||||||
|
95,Rodolphe,Debell,rdebell2m@imageshack.us,Male,93 Debell Pass,Design Engineer,44
|
||||||
|
96,Tyne,Gommey,tgommey2n@joomla.org,Female,94 Gommey Junction,VP Marketing,45
|
||||||
|
97,Christoper,Pincked,cpincked2o@icq.com,Male,95 Pincked Alley,Human Resources Manager,42
|
||||||
|
98,Kore,Le Prevost,kleprevost2p@tripadvisor.com,Female,96 Le Prevost Street,VP Quality Control,47
|
||||||
|
99,Ceciley,Petrolli,cpetrolli2q@oaic.gov.au,Female,97 Petrolli Court,Senior Developer,40
|
||||||
|
100,Elspeth,Mac Giany,emacgiany2r@icio.us,Female,98 Mac Giany Parkway,Internal Auditor,46
|
||||||
|
""";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the service after dependency injection by loading data from the CSV string.
|
||||||
|
* Uses @PostConstruct to ensure this runs after the bean is created.
|
||||||
|
*/
|
||||||
|
@PostConstruct
|
||||||
|
private void initializeData() {
|
||||||
|
log.info("Initializing PersonService data store...");
|
||||||
|
int maxId = loadDataFromCsv();
|
||||||
|
idGenerator = new AtomicInteger(maxId);
|
||||||
|
log.info("PersonService initialized with {} records. Next ID: {}", personStore.size(), idGenerator.get() + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the embedded CSV data and populates the in-memory store.
|
||||||
|
* Calculates the maximum ID found in the data to initialize the ID generator.
|
||||||
|
*
|
||||||
|
* @return The maximum ID found in the loaded CSV data.
|
||||||
|
*/
|
||||||
|
private int loadDataFromCsv() {
|
||||||
|
final AtomicInteger currentMaxId = new AtomicInteger(0);
|
||||||
|
// Clear existing data before loading (important for tests or re-initialization scenarios)
|
||||||
|
personStore.clear();
|
||||||
|
try (Stream<String> lines = CSV_DATA.lines().skip(1)) { // Skip header row
|
||||||
|
lines.forEach(line -> {
|
||||||
|
try {
|
||||||
|
// Split carefully, handling potential commas within quoted fields if necessary (simple split here)
|
||||||
|
String[] fields = line.split(",", 8); // Limit split to handle potential commas in job title
|
||||||
|
if (fields.length == 8) {
|
||||||
|
int id = Integer.parseInt(fields[0].trim());
|
||||||
|
String firstName = fields[1].trim();
|
||||||
|
String lastName = fields[2].trim();
|
||||||
|
String email = fields[3].trim();
|
||||||
|
String sex = fields[4].trim();
|
||||||
|
String ipAddress = fields[5].trim();
|
||||||
|
String jobTitle = fields[6].trim();
|
||||||
|
int age = Integer.parseInt(fields[7].trim());
|
||||||
|
|
||||||
|
Person person = new Person(id, firstName, lastName, email, sex, ipAddress, jobTitle, age);
|
||||||
|
personStore.put(id, person);
|
||||||
|
currentMaxId.updateAndGet(max -> Math.max(max, id));
|
||||||
|
} else {
|
||||||
|
log.warn("Skipping malformed CSV line (expected 8 fields, found {}): {}", fields.length, line);
|
||||||
|
}
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
log.warn("Skipping line due to parsing error (ID or Age): {} - Error: {}", line, e.getMessage());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Skipping line due to unexpected error: {} - Error: {}", line, e.getMessage(), e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Fatal error reading embedded CSV data: {}", e.getMessage(), e);
|
||||||
|
// In a real application, might throw a specific initialization exception
|
||||||
|
}
|
||||||
|
return currentMaxId.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Tool(
|
||||||
|
name = "ps_create_person",
|
||||||
|
description = "Create a new person record in the in-memory store."
|
||||||
|
)
|
||||||
|
public Person createPerson(Person personData) {
|
||||||
|
if (personData == null) {
|
||||||
|
throw new IllegalArgumentException("Person data cannot be null");
|
||||||
|
}
|
||||||
|
int newId = idGenerator.incrementAndGet();
|
||||||
|
// Create a new Person record using data from the input, but with the generated ID
|
||||||
|
Person newPerson = new Person(
|
||||||
|
newId,
|
||||||
|
personData.firstName(),
|
||||||
|
personData.lastName(),
|
||||||
|
personData.email(),
|
||||||
|
personData.sex(),
|
||||||
|
personData.ipAddress(),
|
||||||
|
personData.jobTitle(),
|
||||||
|
personData.age()
|
||||||
|
);
|
||||||
|
personStore.put(newId, newPerson);
|
||||||
|
log.debug("Created person: {}", newPerson);
|
||||||
|
return newPerson;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Tool(
|
||||||
|
name = "ps_get_person_by_id",
|
||||||
|
description = "Retrieve a person record by ID from the in-memory store."
|
||||||
|
)
|
||||||
|
public Optional<Person> getPersonById(int id) {
|
||||||
|
Person person = personStore.get(id);
|
||||||
|
log.debug("Retrieved person by ID {}: {}", id, person);
|
||||||
|
return Optional.ofNullable(person);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Tool(
|
||||||
|
name = "ps_get_all_persons",
|
||||||
|
description = "Retrieve all person records from the in-memory store."
|
||||||
|
)
|
||||||
|
public List<Person> getAllPersons() {
|
||||||
|
// Return an unmodifiable view of the values
|
||||||
|
List<Person> allPersons = personStore.values().stream().toList();
|
||||||
|
log.debug("Retrieved all persons (count: {})", allPersons.size());
|
||||||
|
return allPersons;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Tool(
|
||||||
|
name = "ps_update_person",
|
||||||
|
description = "Update an existing person record by ID in the in-memory store."
|
||||||
|
)
|
||||||
|
public boolean updatePerson(int id, Person updatedPersonData) {
|
||||||
|
if (updatedPersonData == null) {
|
||||||
|
throw new IllegalArgumentException("Updated person data cannot be null");
|
||||||
|
}
|
||||||
|
// Use computeIfPresent for atomic update if the key exists
|
||||||
|
Person result = personStore.computeIfPresent(id, (key, existingPerson) ->
|
||||||
|
// Create a new Person record with the original ID but updated data
|
||||||
|
new Person(
|
||||||
|
id, // Keep original ID
|
||||||
|
updatedPersonData.firstName(),
|
||||||
|
updatedPersonData.lastName(),
|
||||||
|
updatedPersonData.email(),
|
||||||
|
updatedPersonData.sex(),
|
||||||
|
updatedPersonData.ipAddress(),
|
||||||
|
updatedPersonData.jobTitle(),
|
||||||
|
updatedPersonData.age()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
boolean updated = result != null;
|
||||||
|
log.debug("Update attempt for ID {}: {}", id, updated ? "Successful" : "Failed (Not Found)");
|
||||||
|
if(updated) log.trace("Updated person data for ID {}: {}", id, result);
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Tool(
|
||||||
|
name = "ps_delete_person",
|
||||||
|
description = "Delete a person record by ID from the in-memory store."
|
||||||
|
)
|
||||||
|
public boolean deletePerson(int id) {
|
||||||
|
boolean removed = personStore.remove(id) != null;
|
||||||
|
log.debug("Delete attempt for ID {}: {}", id, removed ? "Successful" : "Failed (Not Found)");
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Tool(
|
||||||
|
name = "ps_search_by_job_title",
|
||||||
|
description = "Search for persons by job title in the in-memory store."
|
||||||
|
)
|
||||||
|
public List<Person> searchByJobTitle(String jobTitleQuery) {
|
||||||
|
if (jobTitleQuery == null || jobTitleQuery.isBlank()) {
|
||||||
|
log.debug("Search by job title skipped due to blank query.");
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
String lowerCaseQuery = jobTitleQuery.toLowerCase();
|
||||||
|
List<Person> results = personStore.values().stream()
|
||||||
|
.filter(person -> person.jobTitle() != null && person.jobTitle().toLowerCase().contains(lowerCaseQuery))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
log.debug("Search by job title '{}' found {} results.", jobTitleQuery, results.size());
|
||||||
|
return Collections.unmodifiableList(results);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Tool(
|
||||||
|
name = "ps_filter_by_sex",
|
||||||
|
description = "Filters Persons by sex (case-insensitive)."
|
||||||
|
)
|
||||||
|
public List<Person> filterBySex(String sex) {
|
||||||
|
if (sex == null || sex.isBlank()) {
|
||||||
|
log.debug("Filter by sex skipped due to blank filter.");
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
List<Person> results = personStore.values().stream()
|
||||||
|
.filter(person -> person.sex() != null && person.sex().equalsIgnoreCase(sex))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
log.debug("Filter by sex '{}' found {} results.", sex, results.size());
|
||||||
|
return Collections.unmodifiableList(results);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Tool(
|
||||||
|
name = "ps_filter_by_age",
|
||||||
|
description = "Filters Persons by age."
|
||||||
|
)
|
||||||
|
public List<Person> filterByAge(int age) {
|
||||||
|
if (age < 0) {
|
||||||
|
log.debug("Filter by age skipped due to negative age: {}", age);
|
||||||
|
return Collections.emptyList(); // Or throw IllegalArgumentException based on requirements
|
||||||
|
}
|
||||||
|
List<Person> results = personStore.values().stream()
|
||||||
|
.filter(person -> person.age() == age)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
log.debug("Filter by age {} found {} results.", age, results.size());
|
||||||
|
return Collections.unmodifiableList(results);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
/**
|
||||||
|
* 参考 <a href="https://docs.spring.io/spring-ai/reference/api/tools.html#_methods_as_tools">Tool Calling —— Methods as Tools</a>
|
||||||
|
*/
|
||||||
|
package cn.iocoder.yudao.module.ai.tool.method;
|
||||||
@@ -196,6 +196,13 @@ spring:
|
|||||||
model: deepseek-chat
|
model: deepseek-chat
|
||||||
model:
|
model:
|
||||||
rerank: false # 是否开启“通义千问”的 Rerank 模型,填写 dashscope 开启
|
rerank: false # 是否开启“通义千问”的 Rerank 模型,填写 dashscope 开启
|
||||||
|
mcp:
|
||||||
|
server:
|
||||||
|
enabled: true
|
||||||
|
name: yudao-mcp-server
|
||||||
|
version: 1.0.0
|
||||||
|
instructions: 一个 MCP 示例服务
|
||||||
|
sse-endpoint: /sse
|
||||||
|
|
||||||
yudao:
|
yudao:
|
||||||
ai:
|
ai:
|
||||||
|
|||||||
Reference in New Issue
Block a user