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,6 +187,12 @@ | ||
| 187 | </exclusions> | 187 | </exclusions> |
| 188 | </dependency> | 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 | <!-- TinyFlow:AI 工作流 --> | 196 | <!-- TinyFlow:AI 工作流 --> |
| 191 | <dependency> | 197 | <dependency> |
| 192 | <groupId>dev.tinyflow</groupId> | 198 | <groupId>dev.tinyflow</groupId> |
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiToolDO.java
| 1 | package cn.iocoder.yudao.module.ai.dal.dataobject.model; | 1 | package cn.iocoder.yudao.module.ai.dal.dataobject.model; |
| 2 | 2 | ||
| 3 | import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; | 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 | import com.baomidou.mybatisplus.annotation.KeySequence; | 6 | import com.baomidou.mybatisplus.annotation.KeySequence; |
| 7 | import com.baomidou.mybatisplus.annotation.TableId; | 7 | import com.baomidou.mybatisplus.annotation.TableId; |
| 8 | import com.baomidou.mybatisplus.annotation.TableName; | 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,6 +15,7 @@ import cn.iocoder.yudao.module.ai.framework.ai.core.model.suno.api.SunoApi; | ||
| 15 | import cn.iocoder.yudao.module.ai.framework.ai.core.model.xinghuo.XingHuoChatModel; | 15 | import cn.iocoder.yudao.module.ai.framework.ai.core.model.xinghuo.XingHuoChatModel; |
| 16 | import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchClient; | 16 | import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchClient; |
| 17 | import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.bocha.AiBoChaWebSearchClient; | 17 | import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.bocha.AiBoChaWebSearchClient; |
| 18 | +import cn.iocoder.yudao.module.ai.tool.method.PersonService; | ||
| 18 | import lombok.extern.slf4j.Slf4j; | 19 | import lombok.extern.slf4j.Slf4j; |
| 19 | import org.springframework.ai.deepseek.DeepSeekChatModel; | 20 | import org.springframework.ai.deepseek.DeepSeekChatModel; |
| 20 | import org.springframework.ai.deepseek.DeepSeekChatOptions; | 21 | import org.springframework.ai.deepseek.DeepSeekChatOptions; |
| @@ -25,8 +26,10 @@ import org.springframework.ai.model.tool.ToolCallingManager; | @@ -25,8 +26,10 @@ import org.springframework.ai.model.tool.ToolCallingManager; | ||
| 25 | import org.springframework.ai.openai.OpenAiChatModel; | 26 | import org.springframework.ai.openai.OpenAiChatModel; |
| 26 | import org.springframework.ai.openai.OpenAiChatOptions; | 27 | import org.springframework.ai.openai.OpenAiChatOptions; |
| 27 | import org.springframework.ai.openai.api.OpenAiApi; | 28 | import org.springframework.ai.openai.api.OpenAiApi; |
| 29 | +import org.springframework.ai.support.ToolCallbacks; | ||
| 28 | import org.springframework.ai.tokenizer.JTokkitTokenCountEstimator; | 30 | import org.springframework.ai.tokenizer.JTokkitTokenCountEstimator; |
| 29 | import org.springframework.ai.tokenizer.TokenCountEstimator; | 31 | import org.springframework.ai.tokenizer.TokenCountEstimator; |
| 32 | +import org.springframework.ai.tool.ToolCallback; | ||
| 30 | import org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusServiceClientProperties; | 33 | import org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusServiceClientProperties; |
| 31 | import org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusVectorStoreProperties; | 34 | import org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusVectorStoreProperties; |
| 32 | import org.springframework.ai.vectorstore.qdrant.autoconfigure.QdrantVectorStoreProperties; | 35 | import org.springframework.ai.vectorstore.qdrant.autoconfigure.QdrantVectorStoreProperties; |
| @@ -36,6 +39,8 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties | @@ -36,6 +39,8 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties | ||
| 36 | import org.springframework.context.annotation.Bean; | 39 | import org.springframework.context.annotation.Bean; |
| 37 | import org.springframework.context.annotation.Configuration; | 40 | import org.springframework.context.annotation.Configuration; |
| 38 | 41 | ||
| 42 | +import java.util.List; | ||
| 43 | + | ||
| 39 | /** | 44 | /** |
| 40 | * 芋道 AI 自动配置 | 45 | * 芋道 AI 自动配置 |
| 41 | * | 46 | * |
| @@ -271,4 +276,14 @@ public class AiAutoConfiguration { | @@ -271,4 +276,14 @@ public class AiAutoConfiguration { | ||
| 271 | return new AiBoChaWebSearchClient(yudaoAiProperties.getWebSearch().getApiKey()); | 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 | \ No newline at end of file | 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
| 1 | -package cn.iocoder.yudao.module.ai.service.model.tool; | 1 | +package cn.iocoder.yudao.module.ai.tool.function; |
| 2 | 2 | ||
| 3 | import cn.iocoder.yudao.module.ai.util.AiUtils; | 3 | import cn.iocoder.yudao.module.ai.util.AiUtils; |
| 4 | import cn.iocoder.yudao.framework.common.util.object.BeanUtils; | 4 | import cn.iocoder.yudao.framework.common.util.object.BeanUtils; |
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 | \ No newline at end of file | 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 | \ No newline at end of file | 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 | \ No newline at end of file | 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,6 +196,13 @@ spring: | ||
| 196 | model: deepseek-chat | 196 | model: deepseek-chat |
| 197 | model: | 197 | model: |
| 198 | rerank: false # 是否开启“通义千问”的 Rerank 模型,填写 dashscope 开启 | 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 | yudao: | 207 | yudao: |
| 201 | ai: | 208 | ai: |