From 5b31f27bd5958aa1a4e10907c0f69a4e7893a0e2 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Tue, 26 Aug 2025 22:37:17 +0800 Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9A=E3=80=90ai=20=E5=A4=A7=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=E3=80=91=E5=A2=9E=E5=8A=A0=20MCP=20Server=20Boot=20St?= =?UTF-8?q?arter=20=E7=A4=BA=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yudao-module-ai/pom.xml | 6 + .../ai/dal/dataobject/model/AiToolDO.java | 4 +- .../ai/config/AiAutoConfiguration.java | 15 + .../config/SecurityConfiguration.java | 34 ++ .../framework/security/core/package-info.java | 4 + .../function}/DirectoryListToolFunction.java | 2 +- .../UserProfileQueryToolFunction.java | 2 +- .../function}/WeatherQueryToolFunction.java | 2 +- .../module/ai/tool/function/package-info.java | 4 + .../yudao/module/ai/tool/method/Person.java | 19 + .../module/ai/tool/method/PersonService.java | 80 +++++ .../ai/tool/method/PersonServiceImpl.java | 336 ++++++++++++++++++ .../module/ai/tool/method/package-info.java | 4 + .../src/main/resources/application.yaml | 7 + 14 files changed, 514 insertions(+), 5 deletions(-) create mode 100644 yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/security/config/SecurityConfiguration.java create mode 100644 yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/security/core/package-info.java rename yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/{service/model/tool => tool/function}/DirectoryListToolFunction.java (98%) rename yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/{service/model/tool => tool/function}/UserProfileQueryToolFunction.java (97%) rename yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/{service/model/tool => tool/function}/WeatherQueryToolFunction.java (98%) create mode 100644 yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/function/package-info.java create mode 100644 yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/method/Person.java create mode 100644 yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/method/PersonService.java create mode 100644 yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/method/PersonServiceImpl.java create mode 100644 yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/method/package-info.java diff --git a/yudao-module-ai/pom.xml b/yudao-module-ai/pom.xml index b81be3aa42..04df6fac0d 100644 --- a/yudao-module-ai/pom.xml +++ b/yudao-module-ai/pom.xml @@ -187,6 +187,12 @@ + + org.springframework.ai + spring-ai-starter-mcp-server-webmvc + ${spring-ai.version} + + dev.tinyflow diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiToolDO.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiToolDO.java index 7773e978cc..71322132f3 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiToolDO.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiToolDO.java @@ -1,8 +1,8 @@ package cn.iocoder.yudao.module.ai.dal.dataobject.model; 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.service.model.tool.WeatherQueryToolFunction; +import cn.iocoder.yudao.module.ai.tool.function.DirectoryListToolFunction; +import cn.iocoder.yudao.module.ai.tool.function.WeatherQueryToolFunction; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/AiAutoConfiguration.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/AiAutoConfiguration.java index 6fd62bcabd..26fbe0ad41 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/AiAutoConfiguration.java +++ b/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; 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.bocha.AiBoChaWebSearchClient; +import cn.iocoder.yudao.module.ai.tool.method.PersonService; import lombok.extern.slf4j.Slf4j; import org.springframework.ai.deepseek.DeepSeekChatModel; 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.OpenAiChatOptions; import org.springframework.ai.openai.api.OpenAiApi; +import org.springframework.ai.support.ToolCallbacks; import org.springframework.ai.tokenizer.JTokkitTokenCountEstimator; 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.MilvusVectorStoreProperties; 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.Configuration; +import java.util.List; + /** * 芋道 AI 自动配置 * @@ -271,4 +276,14 @@ public class AiAutoConfiguration { return new AiBoChaWebSearchClient(yudaoAiProperties.getWebSearch().getApiKey()); } + // ========== MCP 相关 ========== + + /** + * 参考自 MCP Server Boot Starter + */ + @Bean + public List toolCallbacks(PersonService personService) { + return List.of(ToolCallbacks.from(personService)); + } + } \ No newline at end of file diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/security/config/SecurityConfiguration.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/security/config/SecurityConfiguration.java new file mode 100644 index 0000000000..bd13a2c146 --- /dev/null +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/security/config/SecurityConfiguration.java @@ -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.AuthorizationManagerRequestMatcherRegistry registry) { + // MCP Server + registry.requestMatchers(serverProperties.getSseEndpoint()).permitAll(); + registry.requestMatchers(serverProperties.getSseMessageEndpoint()).permitAll(); + } + + }; + } + +} diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/security/core/package-info.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/security/core/package-info.java new file mode 100644 index 0000000000..87969449d8 --- /dev/null +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/security/core/package-info.java @@ -0,0 +1,4 @@ +/** + * 占位 + */ +package cn.iocoder.yudao.module.ai.framework.security.core; diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/tool/DirectoryListToolFunction.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/function/DirectoryListToolFunction.java similarity index 98% rename from yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/tool/DirectoryListToolFunction.java rename to yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/function/DirectoryListToolFunction.java index 787b2e7728..8e75d5d9e0 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/tool/DirectoryListToolFunction.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/function/DirectoryListToolFunction.java @@ -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.io.FileUtil; diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/tool/UserProfileQueryToolFunction.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/function/UserProfileQueryToolFunction.java similarity index 97% rename from yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/tool/UserProfileQueryToolFunction.java rename to yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/function/UserProfileQueryToolFunction.java index 5656d39292..a4e00d644f 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/tool/UserProfileQueryToolFunction.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/function/UserProfileQueryToolFunction.java @@ -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.framework.common.util.object.BeanUtils; diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/tool/WeatherQueryToolFunction.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/function/WeatherQueryToolFunction.java similarity index 98% rename from yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/tool/WeatherQueryToolFunction.java rename to yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/function/WeatherQueryToolFunction.java index 99262fafad..689ea00460 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/tool/WeatherQueryToolFunction.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/function/WeatherQueryToolFunction.java @@ -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.util.RandomUtil; diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/function/package-info.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/function/package-info.java new file mode 100644 index 0000000000..0b59656352 --- /dev/null +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/function/package-info.java @@ -0,0 +1,4 @@ +/** + * 参考 Tool Calling —— Methods as Tools + */ +package cn.iocoder.yudao.module.ai.tool.function; \ No newline at end of file diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/method/Person.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/method/Person.java new file mode 100644 index 0000000000..66bab5a7fc --- /dev/null +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/method/Person.java @@ -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 +) { +} \ No newline at end of file diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/method/PersonService.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/method/PersonService.java new file mode 100644 index 0000000000..52c8954945 --- /dev/null +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/method/PersonService.java @@ -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 getPersonById(int id); + + /** + * Retrieves all Person records currently stored. + * + * @return An unmodifiable List containing all Persons. Returns an empty list if none exist. + */ + List 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 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 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 filterByAge(int age); + +} \ No newline at end of file diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/method/PersonServiceImpl.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/method/PersonServiceImpl.java new file mode 100644 index 0000000000..3b8c31b420 --- /dev/null +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/method/PersonServiceImpl.java @@ -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 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 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 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 getAllPersons() { + // Return an unmodifiable view of the values + List 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 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 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 filterBySex(String sex) { + if (sex == null || sex.isBlank()) { + log.debug("Filter by sex skipped due to blank filter."); + return Collections.emptyList(); + } + List 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 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 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); + } + +} \ No newline at end of file diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/method/package-info.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/method/package-info.java new file mode 100644 index 0000000000..44b53e1974 --- /dev/null +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/method/package-info.java @@ -0,0 +1,4 @@ +/** + * 参考 Tool Calling —— Methods as Tools + */ +package cn.iocoder.yudao.module.ai.tool.method; \ No newline at end of file diff --git a/yudao-server/src/main/resources/application.yaml b/yudao-server/src/main/resources/application.yaml index 1468dc2019..7a04290cad 100644 --- a/yudao-server/src/main/resources/application.yaml +++ b/yudao-server/src/main/resources/application.yaml @@ -196,6 +196,13 @@ spring: model: deepseek-chat model: rerank: false # 是否开启“通义千问”的 Rerank 模型,填写 dashscope 开启 + mcp: + server: + enabled: true + name: yudao-mcp-server + version: 1.0.0 + instructions: 一个 MCP 示例服务 + sse-endpoint: /sse yudao: ai: