├── .circleci └── config.yml ├── .gitignore ├── README.md ├── docker-compose.yml ├── pom.xml ├── renovate.json └── src ├── main ├── java │ └── pl │ │ └── piomin │ │ └── services │ │ ├── SpringAIShowcase.java │ │ ├── controller │ │ ├── ChatShowcaseController.java │ │ ├── ImageController.java │ │ ├── PersonController.java │ │ ├── StockController.java │ │ └── WalletController.java │ │ ├── functions │ │ ├── stock │ │ │ ├── StockRequest.java │ │ │ ├── StockResponse.java │ │ │ ├── StockService.java │ │ │ └── api │ │ │ │ ├── DailyShareQuote.java │ │ │ │ ├── DailyStockData.java │ │ │ │ ├── Stock.java │ │ │ │ ├── StockData.java │ │ │ │ └── StockList.java │ │ └── wallet │ │ │ ├── Share.java │ │ │ ├── WalletRepository.java │ │ │ ├── WalletResponse.java │ │ │ └── WalletService.java │ │ ├── model │ │ ├── Gender.java │ │ ├── ImageDescription.java │ │ ├── Item.java │ │ ├── Person.java │ │ ├── Photo.java │ │ └── Wallet.java │ │ └── tools │ │ ├── StockTools.java │ │ └── WalletTools.java └── resources │ ├── application-azure-ai.properties │ ├── application-deepseek.properties │ ├── application-huggingface.properties │ ├── application-vllm.properties │ ├── application.properties │ ├── images │ ├── animals-2.png │ ├── animals-3.png │ ├── animals-4.png │ ├── animals-5.png │ ├── animals.png │ ├── fruits-2.png │ ├── fruits-3.png │ ├── fruits-4.png │ ├── fruits-5.png │ └── fruits.png │ └── import.sql └── test └── java └── pl └── piomin └── services └── controller ├── ImageControllerTest.java ├── PersonControllerTest.java └── WalletControllerTest.java /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | jobs: 4 | build: 5 | docker: 6 | - image: 'cimg/openjdk:21.0.6' 7 | steps: 8 | - checkout 9 | - run: 10 | name: Analyze on SonarCloud 11 | command: mvn verify sonar:sonar -DskipTests 12 | 13 | executors: 14 | jdk: 15 | docker: 16 | - image: 'cimg/openjdk:21.0.6' 17 | 18 | orbs: 19 | maven: circleci/maven@2.0.0 20 | 21 | workflows: 22 | maven_test: 23 | jobs: 24 | - maven/test: 25 | executor: jdk 26 | context: OpenAI 27 | - build: 28 | context: SonarCloud -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | !.mvn/wrapper/maven-wrapper.jar 3 | !**/src/main/**/target/ 4 | !**/src/test/**/target/ 5 | 6 | ### IntelliJ IDEA ### 7 | .idea/modules.xml 8 | .idea/jarRepositories.xml 9 | .idea/compiler.xml 10 | .idea/libraries/ 11 | *.iws 12 | *.iml 13 | *.ipr 14 | 15 | ### Eclipse ### 16 | .apt_generated 17 | .classpath 18 | .factorypath 19 | .project 20 | .settings 21 | .springBeans 22 | .sts4-cache 23 | 24 | ### NetBeans ### 25 | /nbproject/private/ 26 | /nbbuild/ 27 | /dist/ 28 | /nbdist/ 29 | /.nb-gradle/ 30 | build/ 31 | !**/src/main/**/build/ 32 | !**/src/test/**/build/ 33 | 34 | ### VS Code ### 35 | .vscode/ 36 | 37 | ### Mac OS ### 38 | .DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spring AI Showcase Demo Project [![Twitter](https://img.shields.io/twitter/follow/piotr_minkowski.svg?style=social&logo=twitter&label=Follow%20Me)](https://twitter.com/piotr_minkowski) 2 | 3 | [![CircleCI](https://circleci.com/gh/piomin/spring-ai-showcase.svg?style=svg)](https://circleci.com/gh/piomin/spring-ai-showcase) 4 | 5 | [![SonarCloud](https://sonarcloud.io/images/project_badges/sonarcloud-black.svg)](https://sonarcloud.io/dashboard?id=piomin_spring-ai-showcase) 6 | [![Bugs](https://sonarcloud.io/api/project_badges/measure?project=piomin_spring-ai-showcase&metric=bugs)](https://sonarcloud.io/dashboard?id=piomin_spring-ai-showcase) 7 | [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=piomin_spring-ai-showcase&metric=coverage)](https://sonarcloud.io/dashboard?id=piomin_spring-ai-showcase) 8 | [![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=piomin_spring-ai-showcase&metric=ncloc)](https://sonarcloud.io/dashboard?id=piomin_spring-ai-showcase) 9 | 10 | This project demonstrates the integration of AI capabilities within a Spring Boot application, utilizing the [Spring AI](https://github.com/spring-projects/spring-ai) framework. 11 | 12 | ----- 13 | 14 | ## Table of Contents 15 | 16 | - [Architecture](#architecture) 17 | - [Running the Application](#running-the-application) 18 | - [Articles](#articles) 19 | 20 | ## Architecture 21 | 22 | Currently, there are four `@RestController`s that show Spring AI features: 23 | 24 | `pl.piomin.services.controller.PersonController` - prompt template, chat memory, and structured output based on a simple example that asks AI model to generate some persons 25 | 26 | `pl.piomin.services.controller.WalletController` - function calling that calculates a value of our wallet stored in local database in conjunction with the latest stock prices 27 | 28 | `pl.piomin.services.controller.StockController` - RAG with a Pinecone vector store and OpenAI based on stock prices API 29 | 30 | `pl.piomin.services.controller.ImageController` - image model and multimodality 31 | 32 | The architecture is designed to be modular and scalable, focusing on demonstrating how AI features can be incorporated into Spring-based applications. 33 | 34 | ## Running the Application 35 | 36 | Follow these steps to run the application locally. 37 | ```bash 38 | git clone https://github.com/piomin/spring-ai-showcase.git 39 | cd spring-ai-showcase 40 | ``` 41 | 42 | By default, this sample Spring AI app connects to OpenAI. So, before running the app you must set a token: 43 | ```shell 44 | export OPEN_AI_TOKEN= 45 | mvn spring-boot:run 46 | ``` 47 | 48 | To enable integration with Mistral, we should activate the `mistral-ai` profile: 49 | ```shell 50 | export MISTRAL_AI_TOKEN= 51 | mvn spring-boot:run -Pmistral-ai 52 | ``` 53 | 54 | To enable integration with Ollama, we should activate the `ollama-ai` profile: 55 | ```shell 56 | mvn spring-boot:run -Pollama-ai 57 | ``` 58 | 59 | Before that, we must run the model on Ollama, e.g.: 60 | ```shell 61 | ollama run llava 62 | ``` 63 | 64 | To enable integration with Azure OpenAI, we should activate the `azure-ai` profile and activate the Spring Boot `azure-ai` profile: 65 | ```shell 66 | mvn spring-boot:run -Pazure-ai -Dspring-boot.run.profiles=azure-ai 67 | ``` 68 | 69 | You should also export the Azure OpenAI credentials: 70 | ```shell 71 | export AZURE_OPENAI_API_KEY= 72 | ``` 73 | 74 | For scenarios with a vector store (`StockController`, `ImageController`) you need to export the following ENV: 75 | ```shell 76 | export PINECONE_TOKEN= 77 | ``` 78 | 79 | For scenarios with a stock API (`StockController`, `WalletController`) you need to export the following ENV: 80 | ```shell 81 | export STOCK_API_KEY= 82 | ``` 83 | 84 | More details in the articles. 85 | 86 | # Articles 87 | 1. Getting started with Spring AI **Chat Model** and easily switch between different AI providers including **OpenAI**, **Mistral AI** and **Ollama**. The example is available in the branch [master](https://github.com/piomin/spring-ai-showcase/tree/master). A detailed guide may be found in the following article: [Getting Started with Spring AI and Chat Model](https://piotrminkowski.com/2025/01/28/getting-started-with-spring-ai-and-chat-model) 88 | 2. Getting started with Spring AI **Function Calling** for OpenAI chat models. The example is available in the branch [master](https://github.com/piomin/spring-ai-showcase/tree/master). A detailed guide may be found in the following article: [Getting Started with Spring AI Function Calling](https://piotrminkowski.com/2025/01/30/getting-started-with-spring-ai-function-calling) 89 | 3. Using **RAG** (Retrieval Augmented Generation) and **Vector Store** with Spring AI. The example is available in the branch [master](https://github.com/piomin/spring-ai-showcase/tree/master). A detailed guide may be found in the following article: [Using RAG and Vector Store with Spring AI](https://piotrminkowski.com/2025/02/24/using-rag-and-vector-store-with-spring-ai/) 90 | 4. Using **Multimodality** feature and **Image Model** with Spring AI and OpenAI. The example is available in the branch [master](https://github.com/piomin/spring-ai-showcase/tree/master). A detailed guide may be found in the following article: [Spring AI with Multimodality and Images](https://piotrminkowski.com/2025/03/04/spring-ai-with-multimodality-and-images/) 91 | 5. Running multiple models with **Ollama** and integration through Spring AI. The example is available in the branch [master](https://github.com/piomin/spring-ai-showcase/tree/master). A detailed guide may be found in the following article: [Using Ollama with Spring AI](https://piotrminkowski.com/2025/03/10/using-ollama-with-spring-ai/) 92 | 6. Getting started with Spring AI **Tool Calling** for OpenAI/MistralAI chat models. The example is available in the branch [master](https://github.com/piomin/spring-ai-showcase/tree/master). A detailed guide may be found in the following article: [Tool Calling with Spring AI](https://piotrminkowski.com/2025/03/13/tool-calling-with-spring-ai/) 93 | 7. Integrate Spring AI with **Azure OpenAI** for chat models, image generation, tool calling and RAG. The example is available in the branch [master](https://github.com/piomin/spring-ai-showcase/tree/master). A detailed guide may be found in the following article: [Spring AI with Azure OpenAI](https://piotrminkowski.com/2025/03/25/spring-ai-with-azure-openai/) 94 | 95 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.9' 2 | 3 | networks: 4 | net: 5 | driver: bridge 6 | services: 7 | server: 8 | image: ghcr.io/chroma-core/chroma:latest 9 | environment: 10 | - IS_PERSISTENT=TRUE 11 | volumes: 12 | - chroma-data:/chroma/chroma/ 13 | ports: 14 | - 8000:8000 15 | networks: 16 | - net 17 | 18 | volumes: 19 | chroma-data: 20 | driver: local -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | org.springframework.boot 9 | spring-boot-starter-parent 10 | 3.5.0 11 | 12 | 13 | 14 | pl.piomin.services 15 | spring-ai-showcase 16 | 1.0-SNAPSHOT 17 | 18 | 19 | 21 20 | 1.0.0 21 | piomin_spring-ai-showcase 22 | piomin 23 | https://sonarcloud.io 24 | 25 | 26 | 27 | 28 | org.springframework.boot 29 | spring-boot-starter-web 30 | 31 | 32 | org.springframework.boot 33 | spring-boot-starter-data-jpa 34 | 35 | 36 | com.h2database 37 | h2 38 | runtime 39 | 40 | 41 | org.springframework.ai 42 | spring-ai-starter-vector-store-pinecone 43 | 44 | 45 | org.springframework.ai 46 | spring-ai-advisors-vector-store 47 | 48 | 49 | org.springframework.ai 50 | spring-ai-rag 51 | 52 | 53 | 54 | 55 | 56 | 57 | org.springframework.boot 58 | spring-boot-starter-test 59 | test 60 | 61 | 62 | 63 | 64 | 65 | open-ai 66 | 67 | true 68 | 69 | 70 | 71 | org.springframework.ai 72 | spring-ai-starter-model-openai 73 | 74 | 75 | org.springframework.ai 76 | spring-ai-autoconfigure-model-openai 77 | 78 | 79 | org.springframework.ai 80 | spring-ai-starter-vector-store-pinecone 81 | 82 | 83 | 84 | 85 | mistral-ai 86 | 87 | 88 | org.springframework.ai 89 | spring-ai-starter-model-mistral-ai 90 | 91 | 92 | org.springframework.ai 93 | spring-ai-starter-vector-store-pinecone 94 | 95 | 96 | 97 | 98 | ollama-ai 99 | 100 | 101 | org.springframework.ai 102 | spring-ai-starter-model-ollama 103 | 104 | 105 | org.springframework.ai 106 | spring-ai-starter-vector-store-pinecone 107 | 108 | 109 | 110 | 111 | azure-ai 112 | 113 | 114 | org.springframework.ai 115 | spring-ai-starter-model-azure-openai 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | docker-compose 125 | 126 | 127 | org.springframework.boot 128 | spring-boot-docker-compose 129 | runtime 130 | true 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | org.springframework.ai 140 | spring-ai-bom 141 | ${spring-ai.version} 142 | pom 143 | import 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | org.springframework.boot 152 | spring-boot-maven-plugin 153 | 154 | 155 | 156 | 157 | 158 | 159 | central 160 | Central 161 | https://repo1.maven.org/maven2/ 162 | 163 | 164 | spring-milestones 165 | Spring Milestones 166 | https://repo.spring.io/milestone 167 | 168 | false 169 | 170 | 171 | 172 | 173 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base",":dependencyDashboard" 5 | ], 6 | "packageRules": [ 7 | { 8 | "matchUpdateTypes": ["minor", "patch", "pin", "digest"], 9 | "automerge": true 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /src/main/java/pl/piomin/services/SpringAIShowcase.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services; 2 | 3 | import io.micrometer.observation.ObservationRegistry; 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Description; 8 | import org.springframework.web.client.RestTemplate; 9 | import pl.piomin.services.functions.stock.StockRequest; 10 | import pl.piomin.services.functions.stock.StockResponse; 11 | import pl.piomin.services.functions.stock.StockService; 12 | import pl.piomin.services.functions.wallet.WalletRepository; 13 | import pl.piomin.services.functions.wallet.WalletResponse; 14 | import pl.piomin.services.functions.wallet.WalletService; 15 | import pl.piomin.services.tools.StockTools; 16 | import pl.piomin.services.tools.WalletTools; 17 | 18 | import java.util.function.Function; 19 | import java.util.function.Supplier; 20 | 21 | @SpringBootApplication 22 | public class SpringAIShowcase { 23 | 24 | public static void main(String[] args) { 25 | SpringApplication.run(SpringAIShowcase.class, args); 26 | } 27 | 28 | @Bean 29 | @Description("Number of shares for each company in my portfolio") 30 | public Supplier numberOfShares(WalletRepository walletRepository) { 31 | return new WalletService(walletRepository); 32 | } 33 | 34 | @Bean 35 | @Description("Latest stock prices") 36 | public Function latestStockPrices() { 37 | return new StockService(restTemplate()); 38 | } 39 | 40 | @Bean 41 | public RestTemplate restTemplate() { 42 | return new RestTemplate(); 43 | } 44 | 45 | @Bean 46 | public StockTools stockTools() { 47 | return new StockTools(restTemplate()); 48 | } 49 | 50 | @Bean 51 | public WalletTools walletTools(WalletRepository walletRepository) { 52 | return new WalletTools(walletRepository); 53 | } 54 | 55 | @Bean 56 | public ObservationRegistry observationRegistry() { 57 | return ObservationRegistry.create(); 58 | } 59 | 60 | // @Bean 61 | // @ConditionalOnMissingBean(VectorStore.class) 62 | // VectorStore simpleVectorStore(EmbeddingModel embeddingModel) { 63 | // return SimpleVectorStore.builder(embeddingModel).build(); 64 | // } 65 | 66 | // @Bean 67 | // public BatchingStrategy customTokenCountBatchingStrategy() { 68 | // return new TokenCountBatchingStrategy( 69 | // EncodingType.CL100K_BASE, // Specify the encoding type 70 | // 8000, // Set the maximum input token count 71 | // 0.1,// Set the reserve percentage 72 | // new MediaContentFormatter(), 73 | // MetadataMode.ALL 74 | // ); 75 | // } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /src/main/java/pl/piomin/services/controller/ChatShowcaseController.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.controller; 2 | 3 | import org.springframework.ai.chat.client.ChatClient; 4 | import org.springframework.ai.chat.client.advisor.PromptChatMemoryAdvisor; 5 | import org.springframework.ai.chat.memory.ChatMemory; 6 | import org.springframework.ai.chat.prompt.Prompt; 7 | import org.springframework.ai.chat.prompt.PromptTemplate; 8 | import org.springframework.web.bind.annotation.PathVariable; 9 | import org.springframework.web.bind.annotation.RequestMapping; 10 | import org.springframework.web.bind.annotation.RestController; 11 | 12 | import java.util.Map; 13 | 14 | @RestController 15 | @RequestMapping("/api") 16 | public class ChatShowcaseController { 17 | 18 | private final ChatClient chatClient; 19 | 20 | public ChatShowcaseController(ChatClient.Builder chatClientBuilder, ChatMemory chatMemory) { 21 | this.chatClient = chatClientBuilder 22 | .defaultAdvisors(PromptChatMemoryAdvisor.builder(chatMemory).build()) 23 | .build(); 24 | } 25 | 26 | @RequestMapping("/{entity}") 27 | String dynamicApiAll(@PathVariable String entity) { 28 | PromptTemplate pt = new PromptTemplate(""" 29 | Generate a list of {entity} with random values with basic info about them in separated fields. 30 | The number of fields can't be lower than 4 and higher than 7. Additionally each object in the list should contain an auto-incremented id field. 31 | Do not include any explanations or additional text. 32 | """); 33 | Prompt p = pt.create(Map.of("entity", entity)); 34 | 35 | return this.chatClient.prompt(p) 36 | .call() 37 | .content(); 38 | } 39 | 40 | @RequestMapping("/{entity}/{id}") 41 | String dynamicApiById(@PathVariable String entity, @PathVariable String id) { 42 | PromptTemplate pt = new PromptTemplate(""" 43 | Find and return the object with id {id} in a current list of {entity}. 44 | """); 45 | Prompt p = pt.create(Map.of("entity", entity, "id", id)); 46 | return this.chatClient.prompt(p) 47 | .call() 48 | .content(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/pl/piomin/services/controller/ImageController.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.controller; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import jakarta.transaction.NotSupportedException; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import org.springframework.ai.chat.client.ChatClient; 9 | import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor; 10 | import org.springframework.ai.chat.messages.UserMessage; 11 | import org.springframework.ai.chat.prompt.Prompt; 12 | import org.springframework.ai.document.Document; 13 | import org.springframework.ai.image.ImageModel; 14 | import org.springframework.ai.image.ImageOptionsBuilder; 15 | import org.springframework.ai.image.ImagePrompt; 16 | import org.springframework.ai.image.ImageResponse; 17 | import org.springframework.ai.content.Media; 18 | import org.springframework.ai.vectorstore.SearchRequest; 19 | import org.springframework.ai.vectorstore.VectorStore; 20 | import org.springframework.core.ParameterizedTypeReference; 21 | import org.springframework.core.io.ClassPathResource; 22 | import org.springframework.core.io.UrlResource; 23 | import org.springframework.http.MediaType; 24 | import org.springframework.util.MimeTypeUtils; 25 | import org.springframework.web.bind.annotation.*; 26 | import pl.piomin.services.model.ImageDescription; 27 | import pl.piomin.services.model.Item; 28 | 29 | import java.io.IOException; 30 | import java.util.ArrayList; 31 | import java.util.List; 32 | import java.util.Optional; 33 | import java.util.UUID; 34 | import java.util.stream.Stream; 35 | 36 | @RestController 37 | @RequestMapping("/images") 38 | public class ImageController { 39 | 40 | private final static Logger LOG = LoggerFactory.getLogger(ImageController.class); 41 | private final ObjectMapper mapper = new ObjectMapper(); 42 | 43 | private final ChatClient chatClient; 44 | private ImageModel imageModel; 45 | private final VectorStore store; 46 | private List images; 47 | private List dynamicImages = new ArrayList<>(); 48 | 49 | public ImageController(ChatClient.Builder chatClientBuilder, 50 | Optional imageModel, 51 | VectorStore store) { 52 | this.chatClient = chatClientBuilder 53 | .defaultAdvisors(new SimpleLoggerAdvisor()) 54 | .build(); 55 | imageModel.ifPresent(model -> this.imageModel = model); 56 | this.store = store; 57 | 58 | this.images = List.of( 59 | Media.builder().id("fruits").mimeType(MimeTypeUtils.IMAGE_PNG).data(new ClassPathResource("images/fruits.png")).build(), 60 | Media.builder().id("fruits-2").mimeType(MimeTypeUtils.IMAGE_PNG).data(new ClassPathResource("images/fruits-2.png")).build(), 61 | Media.builder().id("fruits-3").mimeType(MimeTypeUtils.IMAGE_PNG).data(new ClassPathResource("images/fruits-3.png")).build(), 62 | Media.builder().id("fruits-4").mimeType(MimeTypeUtils.IMAGE_PNG).data(new ClassPathResource("images/fruits-4.png")).build(), 63 | Media.builder().id("fruits-5").mimeType(MimeTypeUtils.IMAGE_PNG).data(new ClassPathResource("images/fruits-5.png")).build(), 64 | Media.builder().id("animals").mimeType(MimeTypeUtils.IMAGE_PNG).data(new ClassPathResource("images/animals.png")).build(), 65 | Media.builder().id("animals-2").mimeType(MimeTypeUtils.IMAGE_PNG).data(new ClassPathResource("images/animals-2.png")).build(), 66 | Media.builder().id("animals-3").mimeType(MimeTypeUtils.IMAGE_PNG).data(new ClassPathResource("images/animals-3.png")).build(), 67 | Media.builder().id("animals-4").mimeType(MimeTypeUtils.IMAGE_PNG).data(new ClassPathResource("images/animals-4.png")).build(), 68 | Media.builder().id("animals-5").mimeType(MimeTypeUtils.IMAGE_PNG).data(new ClassPathResource("images/animals-5.png")).build() 69 | ); 70 | } 71 | 72 | @GetMapping(value = "/find/{object}", produces = MediaType.IMAGE_PNG_VALUE) 73 | @ResponseBody byte[] analyze(@PathVariable String object) { 74 | String msg = """ 75 | Which picture contains %s. 76 | Return only a single picture. 77 | Return only the number that indicates its position in the media list. 78 | """.formatted(object); 79 | LOG.info(msg); 80 | 81 | UserMessage um = UserMessage.builder().text(msg).media(images).build(); 82 | 83 | String content = this.chatClient.prompt(new Prompt(um)) 84 | .call() 85 | .content(); 86 | 87 | assert content != null; 88 | return images.get(Integer.parseInt(content)-1).getDataAsByteArray(); 89 | } 90 | 91 | @GetMapping(value = "/generate/{object}", produces = MediaType.IMAGE_PNG_VALUE) 92 | byte[] generate(@PathVariable String object) throws IOException, NotSupportedException { 93 | if (imageModel == null) 94 | throw new NotSupportedException("Image model is not supported"); 95 | ImageResponse ir = imageModel.call(new ImagePrompt("Generate an image with " + object, ImageOptionsBuilder.builder() 96 | .height(1024) 97 | .width(1024) 98 | .N(1) 99 | .responseFormat("url") 100 | .build())); 101 | String url = ir.getResult().getOutput().getUrl(); 102 | UrlResource resource = new UrlResource(url); 103 | LOG.info("Generated URL: {}", url); 104 | dynamicImages.add(Media.builder() 105 | .id(UUID.randomUUID().toString()) 106 | .mimeType(MimeTypeUtils.IMAGE_PNG) 107 | .data(url) 108 | .build()); 109 | return resource.getContentAsByteArray(); 110 | } 111 | 112 | @GetMapping("/describe") 113 | String[] describe() { 114 | UserMessage um = UserMessage.builder().text(""" 115 | Explain what do you see on each image in the input list. 116 | Return data in RFC8259 compliant JSON format. 117 | """).media(List.copyOf(Stream.concat(images.stream(), dynamicImages.stream()).toList())).build(); 118 | 119 | return this.chatClient.prompt(new Prompt(um)) 120 | .call() 121 | .entity(String[].class); 122 | } 123 | 124 | @GetMapping("/describe/{image}") 125 | List describeImage(@PathVariable String image) { 126 | Media media = Media.builder() 127 | .id(image) 128 | .mimeType(MimeTypeUtils.IMAGE_PNG) 129 | .data(new ClassPathResource("images/" + image + ".png")) 130 | .build(); 131 | 132 | UserMessage um = UserMessage.builder().text(""" 133 | List all items you see on the image and define their category. 134 | Return items inside the JSON array in RFC8259 compliant JSON format. 135 | """).media(media).build(); 136 | 137 | return this.chatClient.prompt(new Prompt(um)) 138 | .call() 139 | .entity(new ParameterizedTypeReference<>() {}); 140 | } 141 | 142 | @GetMapping("/load") 143 | void load() throws JsonProcessingException { 144 | String msg = """ 145 | Explain what do you see on the image. 146 | Generate a compact description that explains only what is visible. 147 | """; 148 | for (Media image : images) { 149 | UserMessage um = UserMessage.builder() 150 | .text(msg) 151 | .media(image) 152 | .build(); 153 | String content = this.chatClient.prompt(new Prompt(um)) 154 | .call() 155 | .content(); 156 | 157 | var doc = Document.builder() 158 | .id(image.getId()) 159 | .text(mapper.writeValueAsString(new ImageDescription(image.getId(), content))) 160 | .build(); 161 | store.add(List.of(doc)); 162 | LOG.info("Document added: {}", image.getId()); 163 | } 164 | } 165 | 166 | @GetMapping("/generate-and-match/{object}") 167 | List generateAndMatch(@PathVariable String object) throws IOException { 168 | ImageResponse ir = imageModel.call(new ImagePrompt("Generate an image with " + object, ImageOptionsBuilder.builder() 169 | .height(1024) 170 | .width(1024) 171 | .N(1) 172 | .responseFormat("url") 173 | .build())); 174 | UrlResource url = new UrlResource(ir.getResult().getOutput().getUrl()); 175 | LOG.info("URL: {}", ir.getResult().getOutput().getUrl()); 176 | 177 | String msg = """ 178 | Explain what do you see on the image. 179 | Generate a compact description that explains only what is visible. 180 | """; 181 | 182 | UserMessage um = UserMessage.builder() 183 | .text(msg) 184 | .media(new Media(MimeTypeUtils.IMAGE_PNG, url)) 185 | .build(); 186 | 187 | String content = this.chatClient.prompt(new Prompt(um)) 188 | .call() 189 | .content(); 190 | 191 | SearchRequest searchRequest = SearchRequest.builder() 192 | .query("Find the most similar description to this: " + content) 193 | .topK(2) 194 | .build(); 195 | 196 | return store.similaritySearch(searchRequest); 197 | } 198 | 199 | } 200 | -------------------------------------------------------------------------------- /src/main/java/pl/piomin/services/controller/PersonController.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.controller; 2 | 3 | import org.springframework.ai.chat.client.ChatClient; 4 | import org.springframework.ai.chat.client.advisor.PromptChatMemoryAdvisor; 5 | import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor; 6 | import org.springframework.ai.chat.memory.ChatMemory; 7 | import org.springframework.ai.chat.prompt.Prompt; 8 | import org.springframework.ai.chat.prompt.PromptTemplate; 9 | import org.springframework.core.ParameterizedTypeReference; 10 | import org.springframework.web.bind.annotation.GetMapping; 11 | import org.springframework.web.bind.annotation.PathVariable; 12 | import org.springframework.web.bind.annotation.RequestMapping; 13 | import org.springframework.web.bind.annotation.RestController; 14 | import pl.piomin.services.model.Person; 15 | 16 | import java.util.List; 17 | import java.util.Map; 18 | 19 | @RestController 20 | @RequestMapping("/persons") 21 | public class PersonController { 22 | 23 | private final ChatClient chatClient; 24 | 25 | public PersonController(ChatClient.Builder chatClientBuilder, 26 | ChatMemory chatMemory) { 27 | this.chatClient = chatClientBuilder 28 | .defaultAdvisors( 29 | PromptChatMemoryAdvisor.builder(chatMemory).build(), 30 | new SimpleLoggerAdvisor()) 31 | .build(); 32 | } 33 | 34 | @GetMapping 35 | List findAll() { 36 | PromptTemplate pt = new PromptTemplate(""" 37 | Return a current list of 10 persons if exists or generate a new list with random values. 38 | Each object should contain an auto-incremented id field. 39 | The age value should be a random number between 18 and 99. 40 | Do not include any explanations or additional text. 41 | Return data in RFC8259 compliant JSON format. 42 | """); 43 | 44 | return this.chatClient.prompt(pt.create()) 45 | .call() 46 | .entity(new ParameterizedTypeReference<>() {}); 47 | } 48 | 49 | @GetMapping("/{id}") 50 | Person findById(@PathVariable String id) { 51 | PromptTemplate pt = new PromptTemplate(""" 52 | Find and return the object with id {id} in a current list of persons. 53 | """); 54 | Prompt p = pt.create(Map.of("id", id)); 55 | return this.chatClient.prompt(p) 56 | .call() 57 | .entity(Person.class); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/pl/piomin/services/controller/StockController.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.controller; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.ai.chat.client.ChatClient; 8 | import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor; 9 | import org.springframework.ai.chat.client.advisor.api.Advisor; 10 | import org.springframework.ai.chat.client.advisor.vectorstore.QuestionAnswerAdvisor; 11 | import org.springframework.ai.chat.prompt.Prompt; 12 | import org.springframework.ai.chat.prompt.PromptTemplate; 13 | import org.springframework.ai.document.Document; 14 | import org.springframework.ai.rag.advisor.RetrievalAugmentationAdvisor; 15 | import org.springframework.ai.rag.preretrieval.query.transformation.RewriteQueryTransformer; 16 | import org.springframework.ai.rag.retrieval.search.VectorStoreDocumentRetriever; 17 | import org.springframework.ai.vectorstore.SearchRequest; 18 | import org.springframework.ai.vectorstore.VectorStore; 19 | import org.springframework.beans.factory.annotation.Value; 20 | import org.springframework.web.bind.annotation.GetMapping; 21 | import org.springframework.web.bind.annotation.RequestMapping; 22 | import org.springframework.web.bind.annotation.RestController; 23 | import org.springframework.web.client.RestTemplate; 24 | import pl.piomin.services.functions.stock.api.DailyStockData; 25 | import pl.piomin.services.functions.stock.api.Stock; 26 | import pl.piomin.services.functions.stock.api.StockData; 27 | 28 | import java.util.List; 29 | import java.util.Map; 30 | 31 | @RestController 32 | @RequestMapping("/stocks") 33 | public class StockController { 34 | 35 | private final ObjectMapper mapper = new ObjectMapper(); 36 | private final static Logger LOG = LoggerFactory.getLogger(StockController.class); 37 | private final ChatClient chatClient; 38 | private final RewriteQueryTransformer.Builder rqtBuilder; 39 | private final RestTemplate restTemplate; 40 | private final VectorStore store; 41 | 42 | @Value("${STOCK_API_KEY:none}") 43 | private String apiKey; 44 | 45 | public StockController(ChatClient.Builder chatClientBuilder, 46 | VectorStore store, 47 | RestTemplate restTemplate) { 48 | this.chatClient = chatClientBuilder 49 | .defaultAdvisors(new SimpleLoggerAdvisor()) 50 | .build(); 51 | this.rqtBuilder = RewriteQueryTransformer.builder() 52 | .chatClientBuilder(chatClientBuilder); 53 | this.store = store; 54 | this.restTemplate = restTemplate; 55 | } 56 | 57 | @GetMapping("/load-data") 58 | void load() throws JsonProcessingException { 59 | final List companies = List.of("AAPL", "MSFT", "GOOG", "AMZN", "META", "NVDA"); 60 | for (String company : companies) { 61 | StockData data = restTemplate.getForObject("https://api.twelvedata.com/time_series?symbol={0}&interval=1day&outputsize=10&apikey={1}", 62 | StockData.class, 63 | company, 64 | apiKey); 65 | if (data != null && data.getValues() != null) { 66 | var list = data.getValues().stream().map(DailyStockData::getClose).toList(); 67 | var doc = Document.builder() 68 | .id(company) 69 | .text(mapper.writeValueAsString(new Stock(company, list))) 70 | .build(); 71 | store.add(List.of(doc)); 72 | LOG.info("Document added: {}", company); 73 | } 74 | } 75 | } 76 | 77 | @GetMapping("/docs") 78 | List query() { 79 | SearchRequest searchRequest = SearchRequest.builder() 80 | .query("Find the most growth trends") 81 | .topK(2) 82 | .build(); 83 | List docs = store.similaritySearch(searchRequest); 84 | 85 | return docs; 86 | } 87 | 88 | @RequestMapping("/v1/most-growth-trend") 89 | String getBestTrend() { 90 | PromptTemplate pt = new PromptTemplate(""" 91 | {query}. 92 | Which {target} is the most % growth? 93 | The 0 element in the prices table is the latest price, while the last element is the oldest price. 94 | """); 95 | 96 | Prompt p = pt.create( 97 | Map.of("query", "Find the most growth trends", 98 | "target", "share") 99 | ); 100 | 101 | return this.chatClient.prompt(p) 102 | .advisors(new QuestionAnswerAdvisor(store)) 103 | .call() 104 | .content(); 105 | } 106 | 107 | @RequestMapping("/v1-1/most-growth-trend") 108 | String getBestTrendV11() { 109 | PromptTemplate pt = new PromptTemplate(""" 110 | Which share is the most % growth? 111 | The 0 element in the prices table is the latest price, while the last element is the oldest price. 112 | Return a full name of company instead of a market shortcut. 113 | """); 114 | 115 | SearchRequest searchRequest = SearchRequest.builder() 116 | .query(""" 117 | Find the most growth trends. 118 | The 0 element in the prices table is the latest price, while the last element is the oldest price. 119 | """) 120 | .topK(3) 121 | .similarityThreshold(0.7) 122 | .build(); 123 | 124 | return this.chatClient.prompt(pt.create()) 125 | .advisors(QuestionAnswerAdvisor.builder(store).searchRequest(searchRequest).build()) 126 | .call() 127 | .content(); 128 | } 129 | 130 | @RequestMapping("/v2/most-growth-trend") 131 | String getBestTrendV2() { 132 | PromptTemplate pt = new PromptTemplate(""" 133 | {query}. 134 | Which {target} is the most % growth? 135 | The 0 element in the prices table is the latest price, while the last element is the oldest price. 136 | """); 137 | 138 | Prompt p = pt.create(Map.of("query", "Find the most growth trends", "target", "share")); 139 | 140 | Advisor retrievalAugmentationAdvisor = RetrievalAugmentationAdvisor.builder() 141 | .documentRetriever(VectorStoreDocumentRetriever.builder() 142 | .similarityThreshold(0.7) 143 | .topK(3) 144 | .vectorStore(store) 145 | .build()) 146 | .queryTransformers(rqtBuilder.promptTemplate(pt).build()) 147 | .build(); 148 | 149 | return this.chatClient.prompt(p) 150 | .advisors(retrievalAugmentationAdvisor) 151 | .call() 152 | .content(); 153 | } 154 | 155 | } 156 | -------------------------------------------------------------------------------- /src/main/java/pl/piomin/services/controller/WalletController.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.controller; 2 | 3 | import org.springframework.ai.chat.client.ChatClient; 4 | import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor; 5 | import org.springframework.ai.chat.prompt.PromptTemplate; 6 | //import org.springframework.ai.model.function.FunctionCallingOptions; 7 | import org.springframework.web.bind.annotation.GetMapping; 8 | import org.springframework.web.bind.annotation.PathVariable; 9 | import org.springframework.web.bind.annotation.RequestMapping; 10 | import org.springframework.web.bind.annotation.RestController; 11 | import pl.piomin.services.tools.StockTools; 12 | import pl.piomin.services.tools.WalletTools; 13 | 14 | import java.util.Map; 15 | 16 | @RestController 17 | @RequestMapping("/wallet") 18 | public class WalletController { 19 | 20 | private final ChatClient chatClient; 21 | private final StockTools stockTools; 22 | private final WalletTools walletTools; 23 | 24 | public WalletController(ChatClient.Builder chatClientBuilder, 25 | StockTools stockTools, 26 | WalletTools walletTools) { 27 | this.chatClient = chatClientBuilder 28 | .defaultAdvisors(new SimpleLoggerAdvisor()) 29 | .build(); 30 | this.stockTools = stockTools; 31 | this.walletTools = walletTools; 32 | } 33 | 34 | // @GetMapping 35 | // String calculateWalletValue() { 36 | // PromptTemplate pt = new PromptTemplate(""" 37 | // What’s the current value in dollars of my wallet based on the latest stock daily prices ? 38 | // """); 39 | // 40 | // return this.chatClient.prompt(pt.create( 41 | // FunctionCallingOptions.builder() 42 | // .function("numberOfShares") 43 | // .function("latestStockPrices") 44 | // .build())) 45 | // .call() 46 | // .content(); 47 | // } 48 | 49 | @GetMapping("/with-tools") 50 | String calculateWalletValueWithTools() { 51 | PromptTemplate pt = new PromptTemplate(""" 52 | What’s the current value in dollars of my wallet based on the latest stock daily prices ? 53 | """); 54 | 55 | return this.chatClient.prompt(pt.create()) 56 | .tools(stockTools, walletTools) 57 | .call() 58 | .content(); 59 | } 60 | 61 | @GetMapping("/highest-day/{days}") 62 | String calculateHighestWalletValue(@PathVariable int days) { 63 | PromptTemplate pt = new PromptTemplate(""" 64 | On which day during last {days} days my wallet had the highest value in dollars based on the historical daily stock prices ? 65 | """); 66 | 67 | return this.chatClient.prompt(pt.create(Map.of("days", days))) 68 | .tools(stockTools, walletTools) 69 | .call() 70 | .content(); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/pl/piomin/services/functions/stock/StockRequest.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.functions.stock; 2 | 3 | public record StockRequest(String company) { 4 | } 5 | -------------------------------------------------------------------------------- /src/main/java/pl/piomin/services/functions/stock/StockResponse.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.functions.stock; 2 | 3 | public record StockResponse(Float price) { 4 | } 5 | -------------------------------------------------------------------------------- /src/main/java/pl/piomin/services/functions/stock/StockService.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.functions.stock; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.beans.factory.annotation.Value; 7 | import org.springframework.web.client.RestTemplate; 8 | import pl.piomin.services.functions.stock.api.DailyStockData; 9 | import pl.piomin.services.functions.stock.api.StockData; 10 | 11 | import java.util.function.Function; 12 | 13 | public class StockService implements Function { 14 | 15 | private static final Logger LOG = LoggerFactory.getLogger(StockService.class); 16 | private RestTemplate restTemplate; 17 | 18 | public StockService(RestTemplate restTemplate) { 19 | this.restTemplate = restTemplate; 20 | } 21 | 22 | @Value("${STOCK_API_KEY:none}") 23 | String apiKey; 24 | 25 | @Override 26 | public StockResponse apply(StockRequest stockRequest) { 27 | StockData data = restTemplate.getForObject("https://api.twelvedata.com/time_series?symbol={0}&interval=1min&outputsize=1&apikey={1}", 28 | StockData.class, 29 | stockRequest.company(), 30 | apiKey); 31 | DailyStockData latestData = data.getValues().get(0); 32 | LOG.info("Get stock prices: {} -> {}", stockRequest.company(), latestData.getClose()); 33 | return new StockResponse(Float.parseFloat(latestData.getClose())); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/pl/piomin/services/functions/stock/api/DailyShareQuote.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.functions.stock.api; 2 | 3 | public record DailyShareQuote(String company, float price, String datetime) { 4 | } 5 | -------------------------------------------------------------------------------- /src/main/java/pl/piomin/services/functions/stock/api/DailyStockData.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.functions.stock.api; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | public class DailyStockData { 6 | @JsonProperty("datetime") 7 | private String datetime; 8 | 9 | @JsonProperty("open") 10 | private String open; 11 | 12 | @JsonProperty("high") 13 | private String high; 14 | 15 | @JsonProperty("low") 16 | private String low; 17 | 18 | @JsonProperty("close") 19 | private String close; 20 | 21 | @JsonProperty("volume") 22 | private String volume; 23 | 24 | // Getters and Setters 25 | public String getDatetime() { 26 | return datetime; 27 | } 28 | 29 | public void setDatetime(String datetime) { 30 | this.datetime = datetime; 31 | } 32 | 33 | public String getOpen() { 34 | return open; 35 | } 36 | 37 | public void setOpen(String open) { 38 | this.open = open; 39 | } 40 | 41 | public String getHigh() { 42 | return high; 43 | } 44 | 45 | public void setHigh(String high) { 46 | this.high = high; 47 | } 48 | 49 | public String getLow() { 50 | return low; 51 | } 52 | 53 | public void setLow(String low) { 54 | this.low = low; 55 | } 56 | 57 | public String getClose() { 58 | return close; 59 | } 60 | 61 | public void setClose(String close) { 62 | this.close = close; 63 | } 64 | 65 | public String getVolume() { 66 | return volume; 67 | } 68 | 69 | public void setVolume(String volume) { 70 | this.volume = volume; 71 | } 72 | } -------------------------------------------------------------------------------- /src/main/java/pl/piomin/services/functions/stock/api/Stock.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.functions.stock.api; 2 | 3 | import java.util.List; 4 | 5 | public class Stock { 6 | 7 | private String name; 8 | private String symbol; 9 | private List prices; 10 | 11 | public Stock() { 12 | } 13 | 14 | public Stock(String name, List prices) { 15 | this.prices = prices; 16 | this.name = name; 17 | } 18 | 19 | public String getName() { 20 | return name; 21 | } 22 | 23 | public void setName(String name) { 24 | this.name = name; 25 | } 26 | 27 | public String getSymbol() { 28 | return symbol; 29 | } 30 | 31 | public void setSymbol(String symbol) { 32 | this.symbol = symbol; 33 | } 34 | 35 | public List getPrices() { 36 | return prices; 37 | } 38 | 39 | public void setPrices(List prices) { 40 | this.prices = prices; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/pl/piomin/services/functions/stock/api/StockData.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.functions.stock.api; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | import java.util.List; 6 | 7 | public class StockData { 8 | 9 | private List values; 10 | 11 | public List getValues() { 12 | return values; 13 | } 14 | 15 | public void setValues(List values) { 16 | this.values = values; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/pl/piomin/services/functions/stock/api/StockList.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.functions.stock.api; 2 | 3 | import java.util.List; 4 | 5 | public class StockList { 6 | 7 | private List stocks; 8 | 9 | public List getStocks() { 10 | return stocks; 11 | } 12 | 13 | public void setStocks(List stocks) { 14 | this.stocks = stocks; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/pl/piomin/services/functions/wallet/Share.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.functions.wallet; 2 | 3 | import jakarta.persistence.Entity; 4 | import jakarta.persistence.GeneratedValue; 5 | import jakarta.persistence.GenerationType; 6 | import jakarta.persistence.Id; 7 | 8 | @Entity 9 | public class Share { 10 | 11 | @Id 12 | @GeneratedValue(strategy = GenerationType.IDENTITY) 13 | private Long id; 14 | private String company; 15 | private int quantity; 16 | 17 | public Long getId() { 18 | return id; 19 | } 20 | 21 | public void setId(Long id) { 22 | this.id = id; 23 | } 24 | 25 | public String getCompany() { 26 | return company; 27 | } 28 | 29 | public void setCompany(String company) { 30 | this.company = company; 31 | } 32 | 33 | public int getQuantity() { 34 | return quantity; 35 | } 36 | 37 | public void setQuantity(int quantity) { 38 | this.quantity = quantity; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/pl/piomin/services/functions/wallet/WalletRepository.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.functions.wallet; 2 | 3 | import org.springframework.data.repository.CrudRepository; 4 | 5 | public interface WalletRepository extends CrudRepository { 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/pl/piomin/services/functions/wallet/WalletResponse.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.functions.wallet; 2 | 3 | import java.util.List; 4 | 5 | public record WalletResponse(List shares) { 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/pl/piomin/services/functions/wallet/WalletService.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.functions.wallet; 2 | 3 | import java.util.List; 4 | import java.util.function.Supplier; 5 | 6 | public class WalletService implements Supplier { 7 | 8 | private WalletRepository walletRepository; 9 | 10 | public WalletService(WalletRepository walletRepository) { 11 | this.walletRepository = walletRepository; 12 | } 13 | 14 | @Override 15 | public WalletResponse get() { 16 | return new WalletResponse((List) walletRepository.findAll()); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/pl/piomin/services/model/Gender.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.model; 2 | 3 | public enum Gender { 4 | 5 | MALE, FEMALE; 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/pl/piomin/services/model/ImageDescription.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.model; 2 | 3 | public record ImageDescription(String name, String imageDescription) { 4 | } 5 | -------------------------------------------------------------------------------- /src/main/java/pl/piomin/services/model/Item.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.model; 2 | 3 | public class Item { 4 | 5 | private String category; 6 | private String name; 7 | 8 | public String getCategory() { 9 | return category; 10 | } 11 | 12 | public void setCategory(String category) { 13 | this.category = category; 14 | } 15 | 16 | public String getName() { 17 | return name; 18 | } 19 | 20 | public void setName(String name) { 21 | this.name = name; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/pl/piomin/services/model/Person.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.model; 2 | 3 | public class Person { 4 | 5 | private Integer id; 6 | private String firstName; 7 | private String lastName; 8 | private int age; 9 | private Gender gender; 10 | private String nationality; 11 | 12 | public Integer getId() { 13 | return id; 14 | } 15 | 16 | public void setId(Integer id) { 17 | this.id = id; 18 | } 19 | 20 | public String getFirstName() { 21 | return firstName; 22 | } 23 | 24 | public void setFirstName(String firstName) { 25 | this.firstName = firstName; 26 | } 27 | 28 | public String getLastName() { 29 | return lastName; 30 | } 31 | 32 | public void setLastName(String lastName) { 33 | this.lastName = lastName; 34 | } 35 | 36 | public int getAge() { 37 | return age; 38 | } 39 | 40 | public void setAge(int age) { 41 | this.age = age; 42 | } 43 | 44 | public Gender getGender() { 45 | return gender; 46 | } 47 | 48 | public void setGender(Gender gender) { 49 | this.gender = gender; 50 | } 51 | 52 | public String getNationality() { 53 | return nationality; 54 | } 55 | 56 | public void setNationality(String nationality) { 57 | this.nationality = nationality; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/pl/piomin/services/model/Photo.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.model; 2 | 3 | public class Photo { 4 | 5 | 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/pl/piomin/services/model/Wallet.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.model; 2 | 3 | public class Wallet { 4 | 5 | String totalValueInDollars; 6 | 7 | public String getTotalValueInDollars() { 8 | return totalValueInDollars; 9 | } 10 | 11 | public void setTotalValueInDollars(String totalValueInDollars) { 12 | this.totalValueInDollars = totalValueInDollars; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/pl/piomin/services/tools/StockTools.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.tools; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.ai.tool.annotation.Tool; 6 | import org.springframework.ai.tool.annotation.ToolParam; 7 | import org.springframework.beans.factory.annotation.Value; 8 | import org.springframework.web.client.RestTemplate; 9 | import pl.piomin.services.functions.stock.StockResponse; 10 | import pl.piomin.services.functions.stock.api.DailyStockData; 11 | import pl.piomin.services.functions.stock.api.DailyShareQuote; 12 | import pl.piomin.services.functions.stock.api.StockData; 13 | 14 | import java.util.List; 15 | 16 | public class StockTools { 17 | 18 | private static final Logger LOG = LoggerFactory.getLogger(StockTools.class); 19 | 20 | private RestTemplate restTemplate; 21 | @Value("${STOCK_API_KEY:none}") 22 | String apiKey; 23 | 24 | public StockTools(RestTemplate restTemplate) { 25 | this.restTemplate = restTemplate; 26 | } 27 | 28 | @Tool(description = "Latest stock prices") 29 | public StockResponse getLatestStockPrices(@ToolParam(description = "Name of company") String company) { 30 | LOG.info("Get stock prices for: {}", company); 31 | StockData data = restTemplate.getForObject("https://api.twelvedata.com/time_series?symbol={0}&interval=1min&outputsize=1&apikey={1}", 32 | StockData.class, 33 | company, 34 | apiKey); 35 | DailyStockData latestData = data.getValues().get(0); 36 | LOG.info("Get stock prices ({}) -> {}", company, latestData.getClose()); 37 | return new StockResponse(Float.parseFloat(latestData.getClose())); 38 | } 39 | 40 | @Tool(description = "Historical daily stock prices") 41 | public List getHistoricalStockPrices(@ToolParam(description = "Search period in days") int days, 42 | @ToolParam(description = "Name of company") String company) { 43 | LOG.info("Get historical stock prices: {} for {} days", company, days); 44 | StockData data = restTemplate.getForObject("https://api.twelvedata.com/time_series?symbol={0}&interval=1day&outputsize={1}&apikey={2}", 45 | StockData.class, 46 | company, 47 | days, 48 | apiKey); 49 | return data.getValues().stream() 50 | .map(d -> new DailyShareQuote(company, Float.parseFloat(d.getClose()), d.getDatetime())) 51 | .toList(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/pl/piomin/services/tools/WalletTools.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.tools; 2 | 3 | import org.springframework.ai.tool.annotation.Tool; 4 | import pl.piomin.services.functions.wallet.Share; 5 | import pl.piomin.services.functions.wallet.WalletRepository; 6 | 7 | import java.util.List; 8 | 9 | public class WalletTools { 10 | 11 | private WalletRepository walletRepository; 12 | 13 | public WalletTools(WalletRepository walletRepository) { 14 | this.walletRepository = walletRepository; 15 | } 16 | 17 | @Tool(description = "Number of shares for each company in my wallet") 18 | public List getNumberOfShares() { 19 | return (List) walletRepository.findAll(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/resources/application-azure-ai.properties: -------------------------------------------------------------------------------- 1 | spring.ai.azure.openai.api-key = ${AZURE_OPENAI_API_KEY} 2 | spring.ai.azure.openai.endpoint = https://piomin-azure-openai.openai.azure.com/ 3 | spring.ai.azure.openai.image.options.deployment-name = dall-e-3 4 | spring.ai.vectorstore.cosmosdb.endpoint = https://piomin-ai-cosmos.documents.azure.com:443/ 5 | spring.ai.vectorstore.cosmosdb.key = ${AZURE_VECTORSTORE_API_KEY} 6 | spring.ai.vectorstore.cosmosdb.databaseName = spring-ai 7 | spring.ai.vectorstore.cosmosdb.containerName = spring-ai 8 | spring.ai.vectorstore.cosmosdb.partitionKeyPath = /id -------------------------------------------------------------------------------- /src/main/resources/application-deepseek.properties: -------------------------------------------------------------------------------- 1 | spring.ai.openai.api-key = ${DEEPSEEK_AI_TOKEN} 2 | spring.ai.openai.chat.options.model = deepseek-chat 3 | spring.ai.openai.chat.base-url = https://api.deepseek.com 4 | #spring.ai.openai.embedding.enabled = false -------------------------------------------------------------------------------- /src/main/resources/application-huggingface.properties: -------------------------------------------------------------------------------- 1 | #spring.ai.ollama.init.pull-model-strategy = always 2 | #spring.ai.ollama.init.timeout = 180s 3 | #spring.ai.ollama.init.max-retries = 1 4 | spring.ai.ollama.chat.options.model = hf.co/MaziyarPanahi/firefunction-v2-GGUF -------------------------------------------------------------------------------- /src/main/resources/application-vllm.properties: -------------------------------------------------------------------------------- 1 | 2 | spring.ai.openai.api-key = ${OPENAI_API_KEY:dummy} 3 | spring.ai.openai.chat.base-url = https://llama-32-3b-instruct-ai.apps.piomin.ewyw.p1.openshiftapps.com 4 | spring.ai.openai.chat.options.model = llama-32-3b-instruct 5 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.ai.openai.api-key = ${OPEN_AI_TOKEN} 2 | 3 | spring.ai.mistralai.api-key = ${MISTRAL_AI_TOKEN} 4 | spring.ai.mistralai.chat.options.model = mistral-large-latest 5 | 6 | spring.ai.ollama.chat.options.model = llama3.2 7 | 8 | spring.jpa.database-platform = H2 9 | spring.jpa.generate-ddl = true 10 | spring.jpa.hibernate.ddl-auto = create-drop 11 | 12 | logging.level.org.springframework.ai = DEBUG 13 | 14 | 15 | #spring.ai.openai.embedding.model = text-embedding-ada-002 16 | #spring.ai.openai.embedding.lazy-init = true 17 | 18 | #spring.ai.vectorstore.chroma.collection-name = stock 19 | #spring.ai.vectorstore.chroma.initialize-schema = true 20 | 21 | spring.ai.vectorstore.pinecone.apiKey = ${PINECONE_TOKEN} 22 | spring.ai.vectorstore.pinecone.environment = aped-4627-b74a 23 | spring.ai.vectorstore.pinecone.projectId = fsbak04 24 | spring.ai.vectorstore.pinecone.index-name = spring-ai -------------------------------------------------------------------------------- /src/main/resources/images/animals-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piomin/spring-ai-showcase/a2647bd5e7088fe2c0e78effdcfcdb60c830fe38/src/main/resources/images/animals-2.png -------------------------------------------------------------------------------- /src/main/resources/images/animals-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piomin/spring-ai-showcase/a2647bd5e7088fe2c0e78effdcfcdb60c830fe38/src/main/resources/images/animals-3.png -------------------------------------------------------------------------------- /src/main/resources/images/animals-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piomin/spring-ai-showcase/a2647bd5e7088fe2c0e78effdcfcdb60c830fe38/src/main/resources/images/animals-4.png -------------------------------------------------------------------------------- /src/main/resources/images/animals-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piomin/spring-ai-showcase/a2647bd5e7088fe2c0e78effdcfcdb60c830fe38/src/main/resources/images/animals-5.png -------------------------------------------------------------------------------- /src/main/resources/images/animals.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piomin/spring-ai-showcase/a2647bd5e7088fe2c0e78effdcfcdb60c830fe38/src/main/resources/images/animals.png -------------------------------------------------------------------------------- /src/main/resources/images/fruits-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piomin/spring-ai-showcase/a2647bd5e7088fe2c0e78effdcfcdb60c830fe38/src/main/resources/images/fruits-2.png -------------------------------------------------------------------------------- /src/main/resources/images/fruits-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piomin/spring-ai-showcase/a2647bd5e7088fe2c0e78effdcfcdb60c830fe38/src/main/resources/images/fruits-3.png -------------------------------------------------------------------------------- /src/main/resources/images/fruits-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piomin/spring-ai-showcase/a2647bd5e7088fe2c0e78effdcfcdb60c830fe38/src/main/resources/images/fruits-4.png -------------------------------------------------------------------------------- /src/main/resources/images/fruits-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piomin/spring-ai-showcase/a2647bd5e7088fe2c0e78effdcfcdb60c830fe38/src/main/resources/images/fruits-5.png -------------------------------------------------------------------------------- /src/main/resources/images/fruits.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piomin/spring-ai-showcase/a2647bd5e7088fe2c0e78effdcfcdb60c830fe38/src/main/resources/images/fruits.png -------------------------------------------------------------------------------- /src/main/resources/import.sql: -------------------------------------------------------------------------------- 1 | insert into share(id, company, quantity) values (1, 'AAPL', 100); 2 | insert into share(id, company, quantity) values (2, 'AMZN', 300); 3 | insert into share(id, company, quantity) values (3, 'META', 300); 4 | insert into share(id, company, quantity) values (4, 'MSFT', 400); 5 | insert into share(id, company, quantity) values (5, 'NVDA', 200); -------------------------------------------------------------------------------- /src/test/java/pl/piomin/services/controller/ImageControllerTest.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.controller; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | import org.springframework.boot.test.web.client.TestRestTemplate; 7 | import org.springframework.http.ResponseEntity; 8 | 9 | import static org.assertj.core.api.Assertions.assertThat; 10 | 11 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 12 | class ImageControllerTest { 13 | 14 | @Autowired 15 | private TestRestTemplate restTemplate; 16 | 17 | // @Test 18 | void testDescribe() { 19 | ResponseEntity response = restTemplate.getForEntity("/images/describe", String[].class); 20 | assertThat(response.getStatusCode().is2xxSuccessful()).isTrue(); 21 | assertThat(response.getBody()).isNotNull(); 22 | } 23 | 24 | @Test 25 | void testDescribeImage() { 26 | // Use a known image id or fallback to a sample (e.g., "fruits") 27 | String imageId = "fruits"; 28 | ResponseEntity response = restTemplate.getForEntity("/images/describe/" + imageId, String.class); 29 | assertThat(response.getStatusCode().is2xxSuccessful()).isTrue(); 30 | assertThat(response.getBody()).isNotNull(); 31 | } 32 | 33 | // @Test 34 | void testFindObject() { 35 | String object = "apple"; 36 | ResponseEntity response = restTemplate.getForEntity("/images/find/" + object, byte[].class); 37 | assertThat(response.getStatusCode().is2xxSuccessful()).isTrue(); 38 | assertThat(response.getBody()).isNotNull(); 39 | } 40 | 41 | // @Test 42 | // void testGenerateImage() { 43 | // String object = "banana"; 44 | // ResponseEntity response = restTemplate.getForEntity("/images/generate/" + object, byte[].class); 45 | // assertThat(response.getStatusCode().is2xxSuccessful()).isTrue(); 46 | // assertThat(response.getBody()).isNotNull(); 47 | // } 48 | 49 | // @Test 50 | // void testGenerateAndMatch() { 51 | // String object = "orange"; 52 | // ResponseEntity response = restTemplate.getForEntity("/images/generate-and-match/" + object, String.class); 53 | // assertThat(response.getStatusCode().is2xxSuccessful()).isTrue(); 54 | // assertThat(response.getBody()).isNotNull(); 55 | // } 56 | } 57 | -------------------------------------------------------------------------------- /src/test/java/pl/piomin/services/controller/PersonControllerTest.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.controller; 2 | 3 | import org.junit.jupiter.api.MethodOrderer; 4 | import org.junit.jupiter.api.Order; 5 | import org.junit.jupiter.api.Test; 6 | import org.junit.jupiter.api.TestMethodOrder; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.boot.test.context.SpringBootTest; 9 | import org.springframework.boot.test.web.client.TestRestTemplate; 10 | import org.springframework.http.ResponseEntity; 11 | import pl.piomin.services.model.Person; 12 | 13 | import java.util.Objects; 14 | 15 | import static org.assertj.core.api.Assertions.assertThat; 16 | 17 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 18 | @TestMethodOrder(MethodOrderer.OrderAnnotation.class) 19 | class PersonControllerTest { 20 | 21 | @Autowired 22 | private TestRestTemplate restTemplate; 23 | 24 | @Test 25 | @Order(1) 26 | void testFindAllPersons() { 27 | ResponseEntity response = restTemplate.getForEntity("/persons", Person[].class); 28 | assertThat(response.getStatusCode().is2xxSuccessful()).isTrue(); 29 | assertThat(Objects.requireNonNull(response.getBody()).length).isEqualTo(10); 30 | } 31 | 32 | @Test 33 | @Order(2) 34 | void testFindPersonById() { 35 | int id = 4; 36 | ResponseEntity byIdResponse = restTemplate.getForEntity("/persons/" + id, Person.class); 37 | assertThat(byIdResponse.getStatusCode().is2xxSuccessful()).isTrue(); 38 | assertThat(byIdResponse.getBody()).isNotNull(); 39 | assertThat(byIdResponse.getBody().getId()).isEqualTo(id); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/test/java/pl/piomin/services/controller/WalletControllerTest.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.controller; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | import org.springframework.boot.test.web.client.TestRestTemplate; 7 | import org.springframework.http.ResponseEntity; 8 | 9 | import static org.assertj.core.api.Assertions.assertThat; 10 | 11 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 12 | class WalletControllerTest { 13 | 14 | @Autowired 15 | private TestRestTemplate restTemplate; 16 | 17 | @Test 18 | void testWalletValueWithTools() { 19 | ResponseEntity response = restTemplate.getForEntity("/wallet/with-tools", String.class); 20 | assertThat(response.getStatusCode().is2xxSuccessful()).isTrue(); 21 | assertThat(response.getBody()).isNotNull(); 22 | } 23 | 24 | // @Test 25 | // void testHighestWalletValue() { 26 | // int days = 5; 27 | // ResponseEntity response = restTemplate.getForEntity("/wallet/highest-day/" + days, String.class); 28 | // assertThat(response.getStatusCode().is2xxSuccessful()).isTrue(); 29 | // assertThat(response.getBody()).isNotNull(); 30 | // } 31 | } 32 | --------------------------------------------------------------------------------