Commit 5b31f27bd5958aa1a4e10907c0f69a4e7893a0e2
1 parent
4afa67f3
feat:【ai 大模型】增加 MCP Server Boot Starter 示例
Showing
14 changed files
with
514 additions
and
5 deletions
yudao-module-ai/pom.xml
| ... | ... | @@ -187,6 +187,12 @@ |
| 187 | 187 | </exclusions> |
| 188 | 188 | </dependency> |
| 189 | 189 | |
| 190 | + <dependency> | |
| 191 | + <groupId>org.springframework.ai</groupId> | |
| 192 | + <artifactId>spring-ai-starter-mcp-server-webmvc</artifactId> | |
| 193 | + <version>${spring-ai.version}</version> | |
| 194 | + </dependency> | |
| 195 | + | |
| 190 | 196 | <!-- TinyFlow:AI 工作流 --> |
| 191 | 197 | <dependency> |
| 192 | 198 | <groupId>dev.tinyflow</groupId> | ... | ... |
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiToolDO.java
| 1 | 1 | package cn.iocoder.yudao.module.ai.dal.dataobject.model; |
| 2 | 2 | |
| 3 | 3 | import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; |
| 4 | -import cn.iocoder.yudao.module.ai.service.model.tool.DirectoryListToolFunction; | |
| 5 | -import cn.iocoder.yudao.module.ai.service.model.tool.WeatherQueryToolFunction; | |
| 4 | +import cn.iocoder.yudao.module.ai.tool.function.DirectoryListToolFunction; | |
| 5 | +import cn.iocoder.yudao.module.ai.tool.function.WeatherQueryToolFunction; | |
| 6 | 6 | import com.baomidou.mybatisplus.annotation.KeySequence; |
| 7 | 7 | import com.baomidou.mybatisplus.annotation.TableId; |
| 8 | 8 | import com.baomidou.mybatisplus.annotation.TableName; | ... | ... |
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/AiAutoConfiguration.java
| ... | ... | @@ -15,6 +15,7 @@ import cn.iocoder.yudao.module.ai.framework.ai.core.model.suno.api.SunoApi; |
| 15 | 15 | import cn.iocoder.yudao.module.ai.framework.ai.core.model.xinghuo.XingHuoChatModel; |
| 16 | 16 | import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchClient; |
| 17 | 17 | import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.bocha.AiBoChaWebSearchClient; |
| 18 | +import cn.iocoder.yudao.module.ai.tool.method.PersonService; | |
| 18 | 19 | import lombok.extern.slf4j.Slf4j; |
| 19 | 20 | import org.springframework.ai.deepseek.DeepSeekChatModel; |
| 20 | 21 | import org.springframework.ai.deepseek.DeepSeekChatOptions; |
| ... | ... | @@ -25,8 +26,10 @@ import org.springframework.ai.model.tool.ToolCallingManager; |
| 25 | 26 | import org.springframework.ai.openai.OpenAiChatModel; |
| 26 | 27 | import org.springframework.ai.openai.OpenAiChatOptions; |
| 27 | 28 | import org.springframework.ai.openai.api.OpenAiApi; |
| 29 | +import org.springframework.ai.support.ToolCallbacks; | |
| 28 | 30 | import org.springframework.ai.tokenizer.JTokkitTokenCountEstimator; |
| 29 | 31 | import org.springframework.ai.tokenizer.TokenCountEstimator; |
| 32 | +import org.springframework.ai.tool.ToolCallback; | |
| 30 | 33 | import org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusServiceClientProperties; |
| 31 | 34 | import org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusVectorStoreProperties; |
| 32 | 35 | import org.springframework.ai.vectorstore.qdrant.autoconfigure.QdrantVectorStoreProperties; |
| ... | ... | @@ -36,6 +39,8 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties |
| 36 | 39 | import org.springframework.context.annotation.Bean; |
| 37 | 40 | import org.springframework.context.annotation.Configuration; |
| 38 | 41 | |
| 42 | +import java.util.List; | |
| 43 | + | |
| 39 | 44 | /** |
| 40 | 45 | * 芋道 AI 自动配置 |
| 41 | 46 | * |
| ... | ... | @@ -271,4 +276,14 @@ public class AiAutoConfiguration { |
| 271 | 276 | return new AiBoChaWebSearchClient(yudaoAiProperties.getWebSearch().getApiKey()); |
| 272 | 277 | } |
| 273 | 278 | |
| 279 | + // ========== MCP 相关 ========== | |
| 280 | + | |
| 281 | + /** | |
| 282 | + * 参考自 <a href="https://docs.spring.io/spring-ai/reference/api/mcp/mcp-client-boot-starter-docs.html">MCP Server Boot Starter</> | |
| 283 | + */ | |
| 284 | + @Bean | |
| 285 | + public List<ToolCallback> toolCallbacks(PersonService personService) { | |
| 286 | + return List.of(ToolCallbacks.from(personService)); | |
| 287 | + } | |
| 288 | + | |
| 274 | 289 | } |
| 275 | 290 | \ No newline at end of file | ... | ... |
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/security/config/SecurityConfiguration.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.ai.framework.security.config; | |
| 2 | + | |
| 3 | +import cn.iocoder.yudao.framework.security.config.AuthorizeRequestsCustomizer; | |
| 4 | +import jakarta.annotation.Resource; | |
| 5 | +import org.springframework.ai.mcp.server.autoconfigure.McpServerProperties; | |
| 6 | +import org.springframework.context.annotation.Bean; | |
| 7 | +import org.springframework.context.annotation.Configuration; | |
| 8 | +import org.springframework.security.config.annotation.web.builders.HttpSecurity; | |
| 9 | +import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer; | |
| 10 | + | |
| 11 | +/** | |
| 12 | + * AI 模块的 Security 配置 | |
| 13 | + */ | |
| 14 | +@Configuration(proxyBeanMethods = false, value = "aiSecurityConfiguration") | |
| 15 | +public class SecurityConfiguration { | |
| 16 | + | |
| 17 | + @Resource | |
| 18 | + private McpServerProperties serverProperties; | |
| 19 | + | |
| 20 | + @Bean("aiAuthorizeRequestsCustomizer") | |
| 21 | + public AuthorizeRequestsCustomizer authorizeRequestsCustomizer() { | |
| 22 | + return new AuthorizeRequestsCustomizer() { | |
| 23 | + | |
| 24 | + @Override | |
| 25 | + public void customize(AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry registry) { | |
| 26 | + // MCP Server | |
| 27 | + registry.requestMatchers(serverProperties.getSseEndpoint()).permitAll(); | |
| 28 | + registry.requestMatchers(serverProperties.getSseMessageEndpoint()).permitAll(); | |
| 29 | + } | |
| 30 | + | |
| 31 | + }; | |
| 32 | + } | |
| 33 | + | |
| 34 | +} | ... | ... |
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/security/core/package-info.java
0 → 100644
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/tool/DirectoryListToolFunction.java renamed to yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/function/DirectoryListToolFunction.java
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/tool/UserProfileQueryToolFunction.java renamed to yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/function/UserProfileQueryToolFunction.java
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/tool/WeatherQueryToolFunction.java renamed to yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/function/WeatherQueryToolFunction.java
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/function/package-info.java
0 → 100644
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/method/Person.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.ai.tool.method; | |
| 2 | + | |
| 3 | +/** | |
| 4 | + * 来自 Spring AI 官方文档 | |
| 5 | + * | |
| 6 | + * Represents a person with basic information. | |
| 7 | + * This is an immutable record. | |
| 8 | + */ | |
| 9 | +public record Person( | |
| 10 | + int id, | |
| 11 | + String firstName, | |
| 12 | + String lastName, | |
| 13 | + String email, | |
| 14 | + String sex, | |
| 15 | + String ipAddress, | |
| 16 | + String jobTitle, | |
| 17 | + int age | |
| 18 | +) { | |
| 19 | +} | |
| 0 | 20 | \ No newline at end of file | ... | ... |
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/method/PersonService.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.ai.tool.method; | |
| 2 | + | |
| 3 | +import java.util.List; | |
| 4 | +import java.util.Optional; | |
| 5 | + | |
| 6 | +/** | |
| 7 | + * 来自 Spring AI 官方文档 | |
| 8 | + * | |
| 9 | + * Service interface for managing Person data. | |
| 10 | + * Defines the contract for CRUD operations and search/filter functionalities. | |
| 11 | + */ | |
| 12 | +public interface PersonService { | |
| 13 | + | |
| 14 | + /** | |
| 15 | + * Creates a new Person record. | |
| 16 | + * Assigns a unique ID to the person and stores it. | |
| 17 | + * | |
| 18 | + * @param personData The data for the new person (ID field is ignored). Must not be null. | |
| 19 | + * @return The created Person record, including the generated ID. | |
| 20 | + */ | |
| 21 | + Person createPerson(Person personData); | |
| 22 | + | |
| 23 | + /** | |
| 24 | + * Retrieves a Person by their unique ID. | |
| 25 | + * | |
| 26 | + * @param id The ID of the person to retrieve. | |
| 27 | + * @return An Optional containing the found Person, or an empty Optional if not found. | |
| 28 | + */ | |
| 29 | + Optional<Person> getPersonById(int id); | |
| 30 | + | |
| 31 | + /** | |
| 32 | + * Retrieves all Person records currently stored. | |
| 33 | + * | |
| 34 | + * @return An unmodifiable List containing all Persons. Returns an empty list if none exist. | |
| 35 | + */ | |
| 36 | + List<Person> getAllPersons(); | |
| 37 | + | |
| 38 | + /** | |
| 39 | + * Updates an existing Person record identified by ID. | |
| 40 | + * Replaces the existing data with the provided data, keeping the original ID. | |
| 41 | + * | |
| 42 | + * @param id The ID of the person to update. | |
| 43 | + * @param updatedPersonData The new data for the person (ID field is ignored). Must not be null. | |
| 44 | + * @return true if the person was found and updated, false otherwise. | |
| 45 | + */ | |
| 46 | + boolean updatePerson(int id, Person updatedPersonData); | |
| 47 | + | |
| 48 | + /** | |
| 49 | + * Deletes a Person record identified by ID. | |
| 50 | + * | |
| 51 | + * @param id The ID of the person to delete. | |
| 52 | + * @return true if the person was found and deleted, false otherwise. | |
| 53 | + */ | |
| 54 | + boolean deletePerson(int id); | |
| 55 | + | |
| 56 | + /** | |
| 57 | + * Searches for Persons whose job title contains the given query string (case-insensitive). | |
| 58 | + * | |
| 59 | + * @param jobTitleQuery The string to search for within job titles. Can be null or blank. | |
| 60 | + * @return An unmodifiable List of matching Persons. Returns an empty list if no matches or query is invalid. | |
| 61 | + */ | |
| 62 | + List<Person> searchByJobTitle(String jobTitleQuery); | |
| 63 | + | |
| 64 | + /** | |
| 65 | + * Filters Persons by their exact sex (case-insensitive). | |
| 66 | + * | |
| 67 | + * @param sex The sex to filter by (e.g., "Male", "Female"). Can be null or blank. | |
| 68 | + * @return An unmodifiable List of matching Persons. Returns an empty list if no matches or filter is invalid. | |
| 69 | + */ | |
| 70 | + List<Person> filterBySex(String sex); | |
| 71 | + | |
| 72 | + /** | |
| 73 | + * Filters Persons by their exact age. | |
| 74 | + * | |
| 75 | + * @param age The age to filter by. | |
| 76 | + * @return An unmodifiable List of matching Persons. Returns an empty list if no matches. | |
| 77 | + */ | |
| 78 | + List<Person> filterByAge(int age); | |
| 79 | + | |
| 80 | +} | |
| 0 | 81 | \ No newline at end of file | ... | ... |
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/method/PersonServiceImpl.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.ai.tool.method; | |
| 2 | + | |
| 3 | +import jakarta.annotation.PostConstruct; | |
| 4 | +import lombok.extern.slf4j.Slf4j; | |
| 5 | +import org.springframework.ai.tool.annotation.Tool; | |
| 6 | +import org.springframework.stereotype.Service; | |
| 7 | + | |
| 8 | +import java.util.Collections; | |
| 9 | +import java.util.List; | |
| 10 | +import java.util.Map; | |
| 11 | +import java.util.Optional; | |
| 12 | +import java.util.concurrent.ConcurrentHashMap; | |
| 13 | +import java.util.concurrent.atomic.AtomicInteger; | |
| 14 | +import java.util.stream.Collectors; | |
| 15 | +import java.util.stream.Stream; | |
| 16 | + | |
| 17 | +/** | |
| 18 | + * 来自 Spring AI 官方文档 | |
| 19 | + * | |
| 20 | + * Implementation of the PersonService interface using an in-memory data store. | |
| 21 | + * Manages a collection of Person objects loaded from embedded CSV data. | |
| 22 | + * This class is thread-safe due to the use of ConcurrentHashMap and AtomicInteger. | |
| 23 | + */ | |
| 24 | +@Service | |
| 25 | +@Slf4j | |
| 26 | +public class PersonServiceImpl implements PersonService { | |
| 27 | + | |
| 28 | + private final Map<Integer, Person> personStore = new ConcurrentHashMap<>(); | |
| 29 | + | |
| 30 | + private AtomicInteger idGenerator; | |
| 31 | + | |
| 32 | + /** | |
| 33 | + * Embedded CSV data for initial population | |
| 34 | + */ | |
| 35 | + private static final String CSV_DATA = """ | |
| 36 | + Id,FirstName,LastName,Email,Sex,IpAddress,JobTitle,Age | |
| 37 | + 1,Fons,Tollfree,ftollfree0@senate.gov,Male,55.1 Tollfree Lane,Research Associate,31 | |
| 38 | + 2,Emlynne,Tabourier,etabourier1@networksolutions.com,Female,18 Tabourier Way,Associate Professor,38 | |
| 39 | + 3,Shae,Johncey,sjohncey2@yellowpages.com,Male,1 Johncey Circle,Structural Analysis Engineer,30 | |
| 40 | + 4,Sebastien,Bradly,sbradly3@mapquest.com,Male,2 Bradly Hill,Chief Executive Officer,40 | |
| 41 | + 5,Harriott,Kitteringham,hkitteringham4@typepad.com,Female,3 Kitteringham Drive,VP Sales,47 | |
| 42 | + 6,Anallise,Parradine,aparradine5@miibeian.gov.cn,Female,4 Parradine Street,Analog Circuit Design manager,44 | |
| 43 | + 7,Gorden,Kirkbright,gkirkbright6@reuters.com,Male,5 Kirkbright Plaza,Senior Editor,40 | |
| 44 | + 8,Veradis,Ledwitch,vledwitch7@google.com.au,Female,6 Ledwitch Avenue,Computer Systems Analyst IV,44 | |
| 45 | + 9,Agnesse,Penhalurick,apenhalurick8@google.it,Female,7 Penhalurick Terrace,Automation Specialist IV,41 | |
| 46 | + 10,Bibby,Hutable,bhutable9@craigslist.org,Female,8 Hutable Place,Account Representative I,43 | |
| 47 | + 11,Karoly,Lightoller,klightollera@rakuten.co.jp,Female,9 Lightoller Parkway,Senior Developer,46 | |
| 48 | + 12,Cristine,Durrad,cdurradb@aol.com,Female,10 Durrad Center,Senior Developer,48 | |
| 49 | + 13,Aggy,Napier,anapierc@hostgator.com,Female,11 Napier Court,VP Product Management,44 | |
| 50 | + 14,Prisca,Caddens,pcaddensd@vinaora.com,Female,12 Caddens Alley,Business Systems Development Analyst,41 | |
| 51 | + 15,Khalil,McKernan,kmckernane@google.fr,Male,13 McKernan Pass,Engineer IV,44 | |
| 52 | + 16,Lorry,MacTrusty,lmactrustyf@eventbrite.com,Male,14 MacTrusty Junction,Design Engineer,42 | |
| 53 | + 17,Casandra,Worsell,cworsellg@goo.gl,Female,15 Worsell Point,Systems Administrator IV,45 | |
| 54 | + 18,Ulrikaumeko,Haveline,uhavelineh@usgs.gov,Female,16 Haveline Trail,Financial Advisor,42 | |
| 55 | + 19,Shurlocke,Albany,salbanyi@artisteer.com,Male,17 Albany Plaza,Software Test Engineer III,46 | |
| 56 | + 20,Myrilla,Brimilcombe,mbrimilcombej@accuweather.com,Female,18 Brimilcombe Road,Programmer Analyst I,48 | |
| 57 | + 21,Carlina,Scimonelli,cscimonellik@va.gov,Female,19 Scimonelli Pass,Help Desk Technician,45 | |
| 58 | + 22,Tina,Goullee,tgoulleel@miibeian.gov.cn,Female,20 Goullee Crossing,Accountant IV,43 | |
| 59 | + 23,Adriaens,Storek,astorekm@devhub.com,Female,21 Storek Avenue,Recruiting Manager,40 | |
| 60 | + 24,Tedra,Giraudot,tgiraudotn@wiley.com,Female,22 Giraudot Terrace,Speech Pathologist,47 | |
| 61 | + 25,Josiah,Soares,jsoareso@google.nl,Male,23 Soares Street,Tax Accountant,45 | |
| 62 | + 26,Kayle,Gaukrodge,kgaukrodgep@wikispaces.com,Female,24 Gaukrodge Parkway,Accountant II,43 | |
| 63 | + 27,Ardys,Chuter,achuterq@ustream.tv,Female,25 Chuter Drive,Engineer IV,41 | |
| 64 | + 28,Francyne,Baudinet,fbaudinetr@newyorker.com,Female,26 Baudinet Center,VP Accounting,48 | |
| 65 | + 29,Gerick,Bullan,gbullans@seesaa.net,Male,27 Bullan Way,Senior Financial Analyst,43 | |
| 66 | + 30,Northrup,Grivori,ngrivorit@unc.edu,Male,28 Grivori Plaza,Systems Administrator I,45 | |
| 67 | + 31,Town,Duguid,tduguidu@squarespace.com,Male,29 Duguid Pass,Safety Technician IV,46 | |
| 68 | + 32,Pierette,Kopisch,pkopischv@google.com.br,Female,30 Kopisch Lane,Director of Sales,41 | |
| 69 | + 33,Jacquenetta,Le Prevost,jleprevostw@netlog.com,Female,31 Le Prevost Trail,Senior Developer,47 | |
| 70 | + 34,Garvy,Rusted,grustedx@aboutads.info,Male,32 Rusted Junction,Senior Developer,42 | |
| 71 | + 35,Clarice,Aysh,cayshy@merriam-webster.com,Female,33 Aysh Avenue,VP Quality Control,40 | |
| 72 | + 36,Tracie,Fedorski,tfedorskiz@bloglines.com,Male,34 Fedorski Terrace,Design Engineer,44 | |
| 73 | + 37,Noelyn,Matushenko,nmatushenko10@globo.com,Female,35 Matushenko Place,VP Sales,48 | |
| 74 | + 38,Rudiger,Klaesson,rklaesson11@usnews.com,Male,36 Klaesson Road,Database Administrator IV,43 | |
| 75 | + 39,Mirella,Syddie,msyddie12@geocities.jp,Female,37 Syddie Circle,Geological Engineer,46 | |
| 76 | + 40,Donalt,O'Lunny,dolunny13@elpais.com,Male,38 O'Lunny Center,Analog Circuit Design manager,41 | |
| 77 | + 41,Guntar,Deniskevich,gdeniskevich14@google.com.hk,Male,39 Deniskevich Way,Structural Engineer,47 | |
| 78 | + 42,Hort,Shufflebotham,hshufflebotham15@about.me,Male,40 Shufflebotham Court,Structural Analysis Engineer,45 | |
| 79 | + 43,Dominique,Thickett,dthickett16@slashdot.org,Male,41 Thickett Crossing,Safety Technician I,42 | |
| 80 | + 44,Zebulen,Piscopello,zpiscopello17@umich.edu,Male,42 Piscopello Parkway,Web Developer II,40 | |
| 81 | + 45,Mellicent,Mac Giany,mmacgiany18@state.tx.us,Female,43 Mac Giany Pass,Assistant Manager,44 | |
| 82 | + 46,Merle,Bounds,mbounds19@amazon.co.jp,Female,44 Bounds Alley,Systems Administrator III,41 | |
| 83 | + 47,Madelle,Farbrace,mfarbrace1a@xinhuanet.com,Female,45 Farbrace Terrace,Quality Engineer,48 | |
| 84 | + 48,Galvin,O'Sheeryne,gosheeryne1b@addtoany.com,Male,46 O'Sheeryne Way,Environmental Specialist,43 | |
| 85 | + 49,Guillemette,Bootherstone,gbootherstone1c@nationalgeographic.com,Female,47 Bootherstone Plaza,Professor,46 | |
| 86 | + 50,Letti,Aylmore,laylmore1d@vinaora.com,Female,48 Aylmore Circle,Automation Specialist I,40 | |
| 87 | + 51,Nonie,Rivalland,nrivalland1e@weather.com,Female,49 Rivalland Avenue,Software Test Engineer IV,45 | |
| 88 | + 52,Jacquelynn,Halfacre,jhalfacre1f@surveymonkey.com,Female,50 Halfacre Pass,Geologist II,42 | |
| 89 | + 53,Anderea,MacKibbon,amackibbon1g@weibo.com,Female,51 MacKibbon Parkway,Automation Specialist II,47 | |
| 90 | + 54,Wash,Klimko,wklimko1h@slashdot.org,Male,52 Klimko Alley,Database Administrator I,40 | |
| 91 | + 55,Flori,Kynett,fkynett1i@auda.org.au,Female,53 Kynett Trail,Quality Control Specialist,46 | |
| 92 | + 56,Libbey,Penswick,lpenswick1j@google.co.uk,Female,54 Penswick Point,VP Accounting,43 | |
| 93 | + 57,Silvanus,Skellorne,sskellorne1k@booking.com,Male,55 Skellorne Drive,Account Executive,48 | |
| 94 | + 58,Carmine,Mateos,cmateos1l@plala.or.jp,Male,56 Mateos Terrace,Systems Administrator I,41 | |
| 95 | + 59,Sheffie,Blazewicz,sblazewicz1m@google.com.au,Male,57 Blazewicz Center,VP Sales,44 | |
| 96 | + 60,Leanor,Worsnop,lworsnop1n@uol.com.br,Female,58 Worsnop Plaza,Systems Administrator III,45 | |
| 97 | + 61,Caspar,Pamment,cpamment1o@google.co.jp,Male,59 Pamment Court,Senior Financial Analyst,42 | |
| 98 | + 62,Justinian,Pentycost,jpentycost1p@sciencedaily.com,Male,60 Pentycost Way,Senior Quality Engineer,47 | |
| 99 | + 63,Gerianne,Jarnell,gjarnell1q@bing.com,Female,61 Jarnell Avenue,Help Desk Operator,40 | |
| 100 | + 64,Boycie,Zanetto,bzanetto1r@about.com,Male,62 Zanetto Place,Quality Engineer,46 | |
| 101 | + 65,Camilla,Mac Giany,cmacgiany1s@state.gov,Female,63 Mac Giany Parkway,Senior Cost Accountant,43 | |
| 102 | + 66,Hadlee,Piscopiello,hpiscopiello1t@artisteer.com,Male,64 Piscopiello Street,Account Representative III,48 | |
| 103 | + 67,Bobbie,Penvarden,bpenvarden1u@google.cn,Male,65 Penvarden Lane,Help Desk Operator,41 | |
| 104 | + 68,Ali,Gowlett,agowlett1v@parallels.com,Male,66 Gowlett Pass,VP Marketing,44 | |
| 105 | + 69,Olivette,Acome,oacome1w@qq.com,Female,67 Acome Hill,VP Product Management,45 | |
| 106 | + 70,Jehanna,Brotherheed,jbrotherheed1x@google.nl,Female,68 Brotherheed Junction,Database Administrator III,42 | |
| 107 | + 71,Morgan,Berthomieu,mberthomieu1y@artisteer.com,Male,69 Berthomieu Alley,Systems Administrator II,47 | |
| 108 | + 72,Linzy,Shilladay,lshilladay1z@icq.com,Female,70 Shilladay Trail,Research Assistant IV,40 | |
| 109 | + 73,Faydra,Brimner,fbrimner20@mozilla.org,Female,71 Brimner Road,Senior Editor,46 | |
| 110 | + 74,Gwenore,Oxlee,goxlee21@devhub.com,Female,72 Oxlee Terrace,Systems Administrator II,43 | |
| 111 | + 75,Evangelin,Beinke,ebeinke22@mozilla.com,Female,73 Beinke Circle,Accountant I,48 | |
| 112 | + 76,Missy,Cockling,mcockling23@si.edu,Female,74 Cockling Way,Software Engineer I,41 | |
| 113 | + 77,Suzanne,Klimschak,sklimschak24@etsy.com,Female,75 Klimschak Plaza,Tax Accountant,44 | |
| 114 | + 78,Candide,Goricke,cgoricke25@weebly.com,Female,76 Goricke Pass,Sales Associate,45 | |
| 115 | + 79,Gerome,Pinsent,gpinsent26@google.com.au,Male,77 Pinsent Junction,Software Consultant,42 | |
| 116 | + 80,Lezley,Mac Giany,lmacgiany27@scribd.com,Male,78 Mac Giany Alley,Operator,47 | |
| 117 | + 81,Tobiah,Durn,tdurn28@state.tx.us,Male,79 Durn Court,VP Sales,40 | |
| 118 | + 82,Sherlocke,Cockshoot,scockshoot29@yelp.com,Male,80 Cockshoot Street,Senior Financial Analyst,46 | |
| 119 | + 83,Myrle,Speenden,mspeenden2a@utexas.edu,Female,81 Speenden Center,Senior Developer,43 | |
| 120 | + 84,Isidore,Gorries,igorries2b@flavors.me,Male,82 Gorries Parkway,Sales Representative,48 | |
| 121 | + 85,Isac,Kitchingman,ikitchingman2c@businessinsider.com,Male,83 Kitchingman Drive,VP Accounting,41 | |
| 122 | + 86,Benedetta,Purrier,bpurrier2d@admin.ch,Female,84 Purrier Trail,VP Accounting,44 | |
| 123 | + 87,Tera,Fitchell,tfitchell2e@fotki.com,Female,85 Fitchell Place,Software Engineer IV,45 | |
| 124 | + 88,Abbe,Pamment,apamment2f@about.com,Male,86 Pamment Avenue,VP Sales,42 | |
| 125 | + 89,Jandy,Gommowe,jgommowe2g@angelfire.com,Female,87 Gommowe Road,Financial Analyst,47 | |
| 126 | + 90,Karena,Fussey,kfussey2h@google.com.au,Female,88 Fussey Point,Assistant Professor,40 | |
| 127 | + 91,Gaspar,Pammenter,gpammenter2i@google.com.br,Male,89 Pammenter Hill,Help Desk Operator,46 | |
| 128 | + 92,Stanwood,Mac Giany,smacgiany2j@prlog.org,Male,90 Mac Giany Terrace,Research Associate,43 | |
| 129 | + 93,Byrom,Beedell,bbeedell2k@google.co.jp,Male,91 Beedell Way,VP Sales,48 | |
| 130 | + 94,Annabella,Rowbottom,arowbottom2l@google.com.au,Female,92 Rowbottom Plaza,Help Desk Operator,41 | |
| 131 | + 95,Rodolphe,Debell,rdebell2m@imageshack.us,Male,93 Debell Pass,Design Engineer,44 | |
| 132 | + 96,Tyne,Gommey,tgommey2n@joomla.org,Female,94 Gommey Junction,VP Marketing,45 | |
| 133 | + 97,Christoper,Pincked,cpincked2o@icq.com,Male,95 Pincked Alley,Human Resources Manager,42 | |
| 134 | + 98,Kore,Le Prevost,kleprevost2p@tripadvisor.com,Female,96 Le Prevost Street,VP Quality Control,47 | |
| 135 | + 99,Ceciley,Petrolli,cpetrolli2q@oaic.gov.au,Female,97 Petrolli Court,Senior Developer,40 | |
| 136 | + 100,Elspeth,Mac Giany,emacgiany2r@icio.us,Female,98 Mac Giany Parkway,Internal Auditor,46 | |
| 137 | + """; | |
| 138 | + | |
| 139 | + /** | |
| 140 | + * Initializes the service after dependency injection by loading data from the CSV string. | |
| 141 | + * Uses @PostConstruct to ensure this runs after the bean is created. | |
| 142 | + */ | |
| 143 | + @PostConstruct | |
| 144 | + private void initializeData() { | |
| 145 | + log.info("Initializing PersonService data store..."); | |
| 146 | + int maxId = loadDataFromCsv(); | |
| 147 | + idGenerator = new AtomicInteger(maxId); | |
| 148 | + log.info("PersonService initialized with {} records. Next ID: {}", personStore.size(), idGenerator.get() + 1); | |
| 149 | + } | |
| 150 | + | |
| 151 | + /** | |
| 152 | + * Parses the embedded CSV data and populates the in-memory store. | |
| 153 | + * Calculates the maximum ID found in the data to initialize the ID generator. | |
| 154 | + * | |
| 155 | + * @return The maximum ID found in the loaded CSV data. | |
| 156 | + */ | |
| 157 | + private int loadDataFromCsv() { | |
| 158 | + final AtomicInteger currentMaxId = new AtomicInteger(0); | |
| 159 | + // Clear existing data before loading (important for tests or re-initialization scenarios) | |
| 160 | + personStore.clear(); | |
| 161 | + try (Stream<String> lines = CSV_DATA.lines().skip(1)) { // Skip header row | |
| 162 | + lines.forEach(line -> { | |
| 163 | + try { | |
| 164 | + // Split carefully, handling potential commas within quoted fields if necessary (simple split here) | |
| 165 | + String[] fields = line.split(",", 8); // Limit split to handle potential commas in job title | |
| 166 | + if (fields.length == 8) { | |
| 167 | + int id = Integer.parseInt(fields[0].trim()); | |
| 168 | + String firstName = fields[1].trim(); | |
| 169 | + String lastName = fields[2].trim(); | |
| 170 | + String email = fields[3].trim(); | |
| 171 | + String sex = fields[4].trim(); | |
| 172 | + String ipAddress = fields[5].trim(); | |
| 173 | + String jobTitle = fields[6].trim(); | |
| 174 | + int age = Integer.parseInt(fields[7].trim()); | |
| 175 | + | |
| 176 | + Person person = new Person(id, firstName, lastName, email, sex, ipAddress, jobTitle, age); | |
| 177 | + personStore.put(id, person); | |
| 178 | + currentMaxId.updateAndGet(max -> Math.max(max, id)); | |
| 179 | + } else { | |
| 180 | + log.warn("Skipping malformed CSV line (expected 8 fields, found {}): {}", fields.length, line); | |
| 181 | + } | |
| 182 | + } catch (NumberFormatException e) { | |
| 183 | + log.warn("Skipping line due to parsing error (ID or Age): {} - Error: {}", line, e.getMessage()); | |
| 184 | + } catch (Exception e) { | |
| 185 | + log.error("Skipping line due to unexpected error: {} - Error: {}", line, e.getMessage(), e); | |
| 186 | + } | |
| 187 | + }); | |
| 188 | + } catch (Exception e) { | |
| 189 | + log.error("Fatal error reading embedded CSV data: {}", e.getMessage(), e); | |
| 190 | + // In a real application, might throw a specific initialization exception | |
| 191 | + } | |
| 192 | + return currentMaxId.get(); | |
| 193 | + } | |
| 194 | + | |
| 195 | + @Override | |
| 196 | + @Tool( | |
| 197 | + name = "ps_create_person", | |
| 198 | + description = "Create a new person record in the in-memory store." | |
| 199 | + ) | |
| 200 | + public Person createPerson(Person personData) { | |
| 201 | + if (personData == null) { | |
| 202 | + throw new IllegalArgumentException("Person data cannot be null"); | |
| 203 | + } | |
| 204 | + int newId = idGenerator.incrementAndGet(); | |
| 205 | + // Create a new Person record using data from the input, but with the generated ID | |
| 206 | + Person newPerson = new Person( | |
| 207 | + newId, | |
| 208 | + personData.firstName(), | |
| 209 | + personData.lastName(), | |
| 210 | + personData.email(), | |
| 211 | + personData.sex(), | |
| 212 | + personData.ipAddress(), | |
| 213 | + personData.jobTitle(), | |
| 214 | + personData.age() | |
| 215 | + ); | |
| 216 | + personStore.put(newId, newPerson); | |
| 217 | + log.debug("Created person: {}", newPerson); | |
| 218 | + return newPerson; | |
| 219 | + } | |
| 220 | + | |
| 221 | + @Override | |
| 222 | + @Tool( | |
| 223 | + name = "ps_get_person_by_id", | |
| 224 | + description = "Retrieve a person record by ID from the in-memory store." | |
| 225 | + ) | |
| 226 | + public Optional<Person> getPersonById(int id) { | |
| 227 | + Person person = personStore.get(id); | |
| 228 | + log.debug("Retrieved person by ID {}: {}", id, person); | |
| 229 | + return Optional.ofNullable(person); | |
| 230 | + } | |
| 231 | + | |
| 232 | + @Override | |
| 233 | + @Tool( | |
| 234 | + name = "ps_get_all_persons", | |
| 235 | + description = "Retrieve all person records from the in-memory store." | |
| 236 | + ) | |
| 237 | + public List<Person> getAllPersons() { | |
| 238 | + // Return an unmodifiable view of the values | |
| 239 | + List<Person> allPersons = personStore.values().stream().toList(); | |
| 240 | + log.debug("Retrieved all persons (count: {})", allPersons.size()); | |
| 241 | + return allPersons; | |
| 242 | + } | |
| 243 | + | |
| 244 | + @Override | |
| 245 | + @Tool( | |
| 246 | + name = "ps_update_person", | |
| 247 | + description = "Update an existing person record by ID in the in-memory store." | |
| 248 | + ) | |
| 249 | + public boolean updatePerson(int id, Person updatedPersonData) { | |
| 250 | + if (updatedPersonData == null) { | |
| 251 | + throw new IllegalArgumentException("Updated person data cannot be null"); | |
| 252 | + } | |
| 253 | + // Use computeIfPresent for atomic update if the key exists | |
| 254 | + Person result = personStore.computeIfPresent(id, (key, existingPerson) -> | |
| 255 | + // Create a new Person record with the original ID but updated data | |
| 256 | + new Person( | |
| 257 | + id, // Keep original ID | |
| 258 | + updatedPersonData.firstName(), | |
| 259 | + updatedPersonData.lastName(), | |
| 260 | + updatedPersonData.email(), | |
| 261 | + updatedPersonData.sex(), | |
| 262 | + updatedPersonData.ipAddress(), | |
| 263 | + updatedPersonData.jobTitle(), | |
| 264 | + updatedPersonData.age() | |
| 265 | + ) | |
| 266 | + ); | |
| 267 | + boolean updated = result != null; | |
| 268 | + log.debug("Update attempt for ID {}: {}", id, updated ? "Successful" : "Failed (Not Found)"); | |
| 269 | + if(updated) log.trace("Updated person data for ID {}: {}", id, result); | |
| 270 | + return updated; | |
| 271 | + } | |
| 272 | + | |
| 273 | + @Override | |
| 274 | + @Tool( | |
| 275 | + name = "ps_delete_person", | |
| 276 | + description = "Delete a person record by ID from the in-memory store." | |
| 277 | + ) | |
| 278 | + public boolean deletePerson(int id) { | |
| 279 | + boolean removed = personStore.remove(id) != null; | |
| 280 | + log.debug("Delete attempt for ID {}: {}", id, removed ? "Successful" : "Failed (Not Found)"); | |
| 281 | + return removed; | |
| 282 | + } | |
| 283 | + | |
| 284 | + @Override | |
| 285 | + @Tool( | |
| 286 | + name = "ps_search_by_job_title", | |
| 287 | + description = "Search for persons by job title in the in-memory store." | |
| 288 | + ) | |
| 289 | + public List<Person> searchByJobTitle(String jobTitleQuery) { | |
| 290 | + if (jobTitleQuery == null || jobTitleQuery.isBlank()) { | |
| 291 | + log.debug("Search by job title skipped due to blank query."); | |
| 292 | + return Collections.emptyList(); | |
| 293 | + } | |
| 294 | + String lowerCaseQuery = jobTitleQuery.toLowerCase(); | |
| 295 | + List<Person> results = personStore.values().stream() | |
| 296 | + .filter(person -> person.jobTitle() != null && person.jobTitle().toLowerCase().contains(lowerCaseQuery)) | |
| 297 | + .collect(Collectors.toList()); | |
| 298 | + log.debug("Search by job title '{}' found {} results.", jobTitleQuery, results.size()); | |
| 299 | + return Collections.unmodifiableList(results); | |
| 300 | + } | |
| 301 | + | |
| 302 | + @Override | |
| 303 | + @Tool( | |
| 304 | + name = "ps_filter_by_sex", | |
| 305 | + description = "Filters Persons by sex (case-insensitive)." | |
| 306 | + ) | |
| 307 | + public List<Person> filterBySex(String sex) { | |
| 308 | + if (sex == null || sex.isBlank()) { | |
| 309 | + log.debug("Filter by sex skipped due to blank filter."); | |
| 310 | + return Collections.emptyList(); | |
| 311 | + } | |
| 312 | + List<Person> results = personStore.values().stream() | |
| 313 | + .filter(person -> person.sex() != null && person.sex().equalsIgnoreCase(sex)) | |
| 314 | + .collect(Collectors.toList()); | |
| 315 | + log.debug("Filter by sex '{}' found {} results.", sex, results.size()); | |
| 316 | + return Collections.unmodifiableList(results); | |
| 317 | + } | |
| 318 | + | |
| 319 | + @Override | |
| 320 | + @Tool( | |
| 321 | + name = "ps_filter_by_age", | |
| 322 | + description = "Filters Persons by age." | |
| 323 | + ) | |
| 324 | + public List<Person> filterByAge(int age) { | |
| 325 | + if (age < 0) { | |
| 326 | + log.debug("Filter by age skipped due to negative age: {}", age); | |
| 327 | + return Collections.emptyList(); // Or throw IllegalArgumentException based on requirements | |
| 328 | + } | |
| 329 | + List<Person> results = personStore.values().stream() | |
| 330 | + .filter(person -> person.age() == age) | |
| 331 | + .collect(Collectors.toList()); | |
| 332 | + log.debug("Filter by age {} found {} results.", age, results.size()); | |
| 333 | + return Collections.unmodifiableList(results); | |
| 334 | + } | |
| 335 | + | |
| 336 | +} | |
| 0 | 337 | \ No newline at end of file | ... | ... |
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/method/package-info.java
0 → 100644
yudao-server/src/main/resources/application.yaml
| ... | ... | @@ -196,6 +196,13 @@ spring: |
| 196 | 196 | model: deepseek-chat |
| 197 | 197 | model: |
| 198 | 198 | rerank: false # 是否开启“通义千问”的 Rerank 模型,填写 dashscope 开启 |
| 199 | + mcp: | |
| 200 | + server: | |
| 201 | + enabled: true | |
| 202 | + name: yudao-mcp-server | |
| 203 | + version: 1.0.0 | |
| 204 | + instructions: 一个 MCP 示例服务 | |
| 205 | + sse-endpoint: /sse | |
| 199 | 206 | |
| 200 | 207 | yudao: |
| 201 | 208 | ai: | ... | ... |