1 line
570 KiB
JSON
1 line
570 KiB
JSON
{"repo": "chriszachariadis/langchain4j", "files": {"langchain4j/src/test/java/dev/langchain4j/service/StreamingAiServicesIT.java": {"fcde6ad6baf89c5dea8e76999999aef66e26f037": {"t": 1785745675.0533144, "n": 30990, "h": "ba366ff52f847171", "c": "package dev.langchain4j.service;\n\nimport dev.langchain4j.agent.tool.P;\nimport dev.langchain4j.agent.tool.Tool;\nimport dev.langchain4j.agent.tool.ToolExecutionRequest;\nimport dev.langchain4j.data.message.AiMessage;\nimport dev.langchain4j.data.message.ChatMessage;\nimport dev.langchain4j.data.message.ToolExecutionResultMessage;\nimport dev.langchain4j.data.message.UserMessage;\nimport dev.langchain4j.memory.ChatMemory;\nimport dev.langchain4j.memory.chat.MessageWindowChatMemory;\nimport dev.langchain4j.model.chat.StreamingChatModel;\nimport dev.langchain4j.model.chat.request.ChatRequestParameters;\nimport dev.langchain4j.model.chat.response.ChatResponse;\nimport dev.langchain4j.model.chat.response.PartialThinking;\nimport dev.langchain4j.model.chat.response.PartialThinkingContext;\nimport dev.langchain4j.model.openai.OpenAiStreamingChatModel;\nimport dev.langchain4j.model.output.TokenUsage;\nimport dev.langchain4j.rag.AugmentationResult;\nimport dev.langchain4j.rag.RetrievalAugmentor;\nimport dev.langchain4j.rag.content.Content;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.MethodSource;\nimport org.mockito.InOrder;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Queue;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.ConcurrentLinkedQueue;\nimport java.util.concurrent.TimeoutException;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.stream.Stream;\n\nimport static dev.langchain4j.model.openai.OpenAiChatModelName.GPT_4_O_MINI;\nimport static dev.langchain4j.model.output.FinishReason.STOP;\nimport static java.util.concurrent.TimeUnit.SECONDS;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.Mockito.any;\nimport static org.mockito.Mockito.atLeastOnce;\nimport static org.mockito.Mockito.inOrder;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.spy;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.verifyNoMoreInteractions;\nimport static org.mockito.Mockito.when;\n\n@EnabledIfEnvironmentVariable(named = \"OPENAI_API_KEY\", matches = \".+\")\nclass StreamingAiServicesIT {\n\n static Stream<StreamingChatModel> models() {\n return Stream.of(\n OpenAiStreamingChatModel.builder()\n .baseUrl(System.getenv(\"OPENAI_BASE_URL\"))\n .apiKey(System.getenv(\"OPENAI_API_KEY\"))\n .organizationId(System.getenv(\"OPENAI_ORGANIZATION_ID\"))\n .modelName(GPT_4_O_MINI)\n .logRequests(true)\n .logResponses(true)\n .build()\n // TODO add more models\n );\n }\n\n interface Assistant {\n\n TokenStream chat(String userMessage);\n }\n\n @ParameterizedTest\n @MethodSource(\"models\")\n void should_stream_answer(StreamingChatModel model) throws Exception {\n\n Assistant assistant = AiServices.create(Assistant.class, model);\n\n StringBuilder answerBuilder = new StringBuilder();\n Queue<ChatResponse> intermediateResponses = new ConcurrentLinkedQueue<>();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<ChatResponse> futureResponse = new CompletableFuture<>();\n\n assistant.chat(\"What is the capital of Germany?\")\n .onPartialResponse(answerBuilder::append)\n .onIntermediateResponse(intermediateResponses::add)\n .onCompleteResponse(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n ChatResponse response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"Berlin\");\n assertThat(response.aiMessage().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isPositive();\n assertThat(tokenUsage.outputTokenCount()).isPositive();\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n assertThat(intermediateResponses).isEmpty();\n }\n\n @ParameterizedTest\n @MethodSource(\"models\")\n void should_callback_with_content(StreamingChatModel model) throws Exception {\n\n List<Content> contents = new ArrayList<>();\n contents.add(Content.from(\"This is additional content\"));\n\n RetrievalAugmentor retrievalAugmentor = mock(RetrievalAugmentor.class);\n ChatMessage message = UserMessage.from(\"What is the capital of Germany?\");\n\n AugmentationResult result = AugmentationResult.builder()\n .chatMessage(message)\n .contents(contents)\n .build();\n when(retrievalAugmentor.augment(any())).thenReturn(result);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatModel(model)\n .retrievalAugmentor(retrievalAugmentor)\n .build();\n\n CompletableFuture<List<Content>> futureContent = new CompletableFuture<>();\n\n assistant.chat(\"What is the capital of Germany?\")\n .onPartialResponse(ignored -> {})\n .onRetrieved(futureContent::complete)\n .ignoreErrors()\n .start();\n\n List<Content> returnedContents = futureContent.get(30, SECONDS);\n\n assertThat(returnedContents)\n .hasSize(1)\n .anySatisfy(c -> assertThat(c.textSegment().text()).isEqualTo(\"This is additional content\"));\n }\n\n @ParameterizedTest\n @MethodSource(\"models\")\n void should_stream_answers_with_memory(StreamingChatModel model) throws Exception {\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatModel(model)\n .chatMemory(chatMemory)\n .build();\n\n\n String firstUserMessage = \"Hi, my name is Klaus\";\n CompletableFuture<ChatResponse> firstResponseFuture = new CompletableFuture<>();\n\n assistant.chat(firstUserMessage)\n .onPartialResponse(System.out::println)\n .onCompleteResponse(firstResponseFuture::complete)\n .onError(firstResponseFuture::completeExceptionally)\n .start();\n\n ChatResponse firstResponse = firstResponseFuture.get(30, SECONDS);\n assertThat(firstResponse.aiMessage().text()).contains(\"Klaus\");\n\n\n String secondUserMessage = \"What is my name?\";\n CompletableFuture<ChatResponse> secondResponseFuture = new CompletableFuture<>();\n\n assistant.chat(secondUserMessage)\n .onPartialResponse(System.out::println)\n .onCompleteResponse(secondResponseFuture::complete)\n .onError(secondResponseFuture::completeExceptionally)\n .start();\n\n ChatResponse secondResponse = secondResponseFuture.get(30, SECONDS);\n assertThat(secondResponse.aiMessage().text()).contains(\"Klaus\");\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(4);\n\n assertThat(messages.get(0)).isInstanceOf(UserMessage.class);\n assertThat(((UserMessage) messages.get(0)).singleText()).isEqualTo(firstUserMessage);\n\n assertThat(messages.get(1)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(1)).isEqualTo(firstResponse.aiMessage());\n\n assertThat(messages.get(2)).isInstanceOf(UserMessage.class);\n assertThat(((UserMessage) messages.get(2)).singleText()).isEqualTo(secondUserMessage);\n\n assertThat(messages.get(3)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(3)).isEqualTo(secondResponse.aiMessage());\n }\n\n static class Calculator {\n\n @Tool(\"calculates the square root of the provided number\")\n double squareRoot(@P(\"number to operate on\") double number) {\n return Math.sqrt(number);\n }\n }\n\n @ParameterizedTest\n @MethodSource(\"models\")\n void should_execute_a_tool_then_stream_answer(StreamingChatModel model) throws Exception {\n\n Calculator calculator = spy(new Calculator());\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatModel(model)\n .chatMemory(chatMemory)\n .tools(calculator)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n Queue<ChatResponse> intermediateResponses = new ConcurrentLinkedQueue<>();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<ChatResponse> futureResponse = new CompletableFuture<>();\n\n String userMessage = \"What is the square root of 485906798473894056 in scientific notation?\";\n\n assistant.chat(userMessage)\n .onPartialResponse(answerBuilder::append)\n .onIntermediateResponse(intermediateResponses::add)\n .onCompleteResponse(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n ChatResponse response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"6.97\");\n assertThat(response.aiMessage().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isPositive();\n assertThat(tokenUsage.outputTokenCount()).isPositive();\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n\n verify(calculator).squareRoot(485906798473894056.0);\n verifyNoMoreInteractions(calculator);\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(4);\n\n assertThat(messages.get(0)).isInstanceOf(UserMessage.class);\n assertThat(((UserMessage) messages.get(0)).singleText()).isEqualTo(userMessage);\n\n AiMessage aiMessage = (AiMessage) messages.get(1);\n assertThat(aiMessage.text()).isNull();\n assertThat(aiMessage.toolExecutionRequests()).hasSize(1);\n ToolExecutionRequest toolExecutionRequest = aiMessage.toolExecutionRequests().get(0);\n assertThat(toolExecutionRequest.id()).isNotBlank();\n\n assertThat(toolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 485906798473894056}\");\n\n ToolExecutionResultMessage toolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(2);\n assertThat(toolExecutionResultMessage.id()).isEqualTo(toolExecutionRequest.id());\n assertThat(toolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionResultMessage.text()).isEqualTo(\"6.97070153193991E8\");\n\n assertThat(messages.get(3)).isInstanceOf(AiMessage.class);\n assertThat(((AiMessage) messages.get(3)).text()).contains(\"6.97\");\n\n assertThat(intermediateResponses).hasSize(1);\n assertThat(intermediateResponses.poll().aiMessage()).isEqualTo(aiMessage);\n }\n\n @Test\n void should_execute_multiple_tools_sequentially_then_answer() throws Exception {\n\n // TODO test more models\n StreamingChatModel streamingChatModel = OpenAiStreamingChatModel.builder()\n .baseUrl(System.getenv(\"OPENAI_BASE_URL\"))\n .apiKey(System.getenv(\"OPENAI_API_KEY\"))\n .organizationId(System.getenv(\"OPENAI_ORGANIZATION_ID\"))\n .modelName(GPT_4_O_MINI)\n .parallelToolCalls(false) // to force the model to call tools sequentially\n .temperature(0.0)\n .logRequests(true)\n .logResponses(true)\n .build();\n\n Calculator calculator = spy(new Calculator());\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatModel(streamingChatModel)\n .chatMemory(chatMemory)\n .tools(calculator)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n Queue<ChatResponse> intermediateResponses = new ConcurrentLinkedQueue<>();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<ChatResponse> futureResponse = new CompletableFuture<>();\n\n String userMessage = \"What is the square root of 485906798473894056 and 97866249624785 in scientific notation?\";\n\n assistant.chat(userMessage)\n .onPartialResponse(answerBuilder::append)\n .onIntermediateResponse(intermediateResponses::add)\n .onCompleteResponse(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n ChatResponse response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"6.97\", \"9.89\");\n assertThat(response.aiMessage().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isPositive(); // TODO\n assertThat(tokenUsage.outputTokenCount()).isPositive();\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n\n verify(calculator).squareRoot(485906798473894056.0);\n verify(calculator).squareRoot(97866249624785.0);\n verifyNoMoreInteractions(calculator);\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(6);\n\n assertThat(messages.get(0)).isInstanceOf(dev.langchain4j.data.message.UserMessage.class);\n assertThat(((UserMessage) messages.get(0)).singleText()).isEqualTo(userMessage);\n\n AiMessage aiMessage = (AiMessage) messages.get(1);\n assertThat(aiMessage.text()).isNull();\n assertThat(aiMessage.toolExecutionRequests()).hasSize(1);\n ToolExecutionRequest toolExecutionRequest = aiMessage.toolExecutionRequests().get(0);\n assertThat(toolExecutionRequest.id()).isNotBlank();\n assertThat(toolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 485906798473894056}\");\n\n ToolExecutionResultMessage toolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(2);\n assertThat(toolExecutionResultMessage.id()).isEqualTo(toolExecutionRequest.id());\n assertThat(toolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionResultMessage.text()).isEqualTo(\"6.97070153193991E8\");\n\n AiMessage secondAiMessage = (AiMessage) messages.get(3);\n assertThat(secondAiMessage.text()).isNull();\n assertThat(secondAiMessage.toolExecutionRequests()).hasSize(1);\n ToolExecutionRequest secondToolExecutionRequest = secondAiMessage.toolExecutionRequests().get(0);\n assertThat(secondToolExecutionRequest.id()).isNotBlank();\n assertThat(secondToolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 97866249624785}\");\n\n ToolExecutionResultMessage secondToolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(4);\n assertThat(secondToolExecutionResultMessage.id()).isEqualTo(secondToolExecutionRequest.id());\n assertThat(secondToolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionResultMessage.text()).isEqualTo(\"9892737.215997653\");\n\n assertThat(messages.get(5)).isInstanceOf(AiMessage.class);\n assertThat(((AiMessage) messages.get(5)).text()).contains(\"6.97\", \"9.89\");\n\n assertThat(intermediateResponses).hasSize(2);\n assertThat(intermediateResponses.poll().aiMessage()).isEqualTo(aiMessage);\n assertThat(intermediateResponses.poll().aiMessage()).isEqualTo(secondAiMessage);\n }\n\n @Test\n void should_execute_multiple_tools_in_parallel_then_answer() throws Exception {\n\n Calculator calculator = spy(new Calculator());\n\n // TODO test more models\n StreamingChatModel streamingChatModel = OpenAiStreamingChatModel.builder()\n .baseUrl(System.getenv(\"OPENAI_BASE_URL\"))\n .apiKey(System.getenv(\"OPENAI_API_KEY\"))\n .organizationId(System.getenv(\"OPENAI_ORGANIZATION_ID\"))\n .modelName(GPT_4_O_MINI)\n .temperature(0.0)\n .logRequests(true)\n .logResponses(true)\n .build();\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatModel(streamingChatModel)\n .chatMemory(chatMemory)\n .tools(calculator)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<ChatResponse> futureResponse = new CompletableFuture<>();\n\n String userMessage = \"What is the square root of 485906798473894056 and 97866249624785 in scientific notation?\";\n\n assistant.chat(userMessage)\n .onPartialResponse(answerBuilder::append)\n .onCompleteResponse(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n ChatResponse response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"6.97\", \"9.89\");\n assertThat(response.aiMessage().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isPositive(); // TODO\n assertThat(tokenUsage.outputTokenCount()).isPositive();\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n\n verify(calculator).squareRoot(485906798473894056.0);\n verify(calculator).squareRoot(97866249624785.0);\n verifyNoMoreInteractions(calculator);\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(5);\n\n assertThat(messages.get(0)).isInstanceOf(dev.langchain4j.data.message.UserMessage.class);\n assertThat(((UserMessage) messages.get(0)).singleText()).isEqualTo(userMessage);\n\n AiMessage aiMessage = (AiMessage) messages.get(1);\n assertThat(aiMessage.text()).isNull();\n assertThat(aiMessage.toolExecutionRequests()).hasSize(2);\n\n ToolExecutionRequest firstToolExecutionRequest = aiMessage.toolExecutionRequests().get(0);\n assertThat(firstToolExecutionRequest.id()).isNotBlank();\n assertThat(firstToolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(firstToolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 485906798473894056}\");\n\n ToolExecutionRequest secondToolExecutionRequest = aiMessage.toolExecutionRequests().get(1);\n assertThat(secondToolExecutionRequest.id()).isNotBlank();\n assertThat(secondToolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 97866249624785}\");\n\n ToolExecutionResultMessage firstToolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(2);\n assertThat(firstToolExecutionResultMessage.id()).isEqualTo(firstToolExecutionRequest.id());\n assertThat(firstToolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(firstToolExecutionResultMessage.text()).isEqualTo(\"6.97070153193991E8\");\n\n ToolExecutionResultMessage secondToolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(3);\n assertThat(secondToolExecutionResultMessage.id()).isEqualTo(secondToolExecutionRequest.id());\n assertThat(secondToolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionResultMessage.text()).isEqualTo(\"9892737.215997653\");\n\n assertThat(messages.get(4)).isInstanceOf(AiMessage.class);\n assertThat(((AiMessage) messages.get(4)).text()).contains(\"6.97\", \"9.89\");\n }\n\n @Test\n @EnabledIfEnvironmentVariable(named = \"DEEPSEEK_API_KEY\", matches = \".+\")\n void should_stream_thinking() throws Exception {\n\n // given\n boolean returnThinking = true;\n\n StreamingChatModel streamingChatModel = OpenAiStreamingChatModel.builder()\n .baseUrl(\"https://api.deepseek.com/v1\")\n .apiKey(System.getenv(\"DEEPSEEK_API_KEY\"))\n .modelName(\"deepseek-reasoner\")\n .returnThinking(returnThinking)\n .logRequests(true)\n .logResponses(true)\n .build();\n\n Assistant assistant = AiServices.create(Assistant.class, streamingChatModel);\n\n interface TokenStreamHandler {\n void onPartialThinking(PartialThinking partialThinking);\n void onPartialResponse(String partialResponse);\n void onError(Throwable error);\n void onCompleteResponse(ChatResponse completeResponse);\n }\n\n TokenStreamHandler tokenStreamHandler = mock(TokenStreamHandler.class);\n CompletableFuture<ChatResponse> futureResponse = new CompletableFuture<>();\n\n // when\n assistant.chat(\"What is the capital of Germany?\")\n .onPartialThinking(partialThinking -> tokenStreamHandler.onPartialThinking(partialThinking))\n .onPartialResponse(partialResponse -> tokenStreamHandler.onPartialResponse(partialResponse))\n .onError(error -> {\n tokenStreamHandler.onError(error);\n futureResponse.completeExceptionally(error);\n })\n .onCompleteResponse(completeResponse -> {\n tokenStreamHandler.onCompleteResponse(completeResponse);\n futureResponse.complete(completeResponse);\n })\n .start();\n\n // then\n ChatResponse chatResponse = futureResponse.get(60, SECONDS);\n\n assertThat(chatResponse.aiMessage().text()).contains(\"Berlin\");\n\n InOrder inOrder = inOrder(tokenStreamHandler);\n inOrder.verify(tokenStreamHandler, atLeastOnce()).onPartialThinking(any());\n inOrder.verify(tokenStreamHandler, atLeastOnce()).onPartialResponse(any());\n inOrder.verify(tokenStreamHandler).onCompleteResponse(any());\n inOrder.verifyNoMoreInteractions();\n verifyNoMoreInteractions(tokenStreamHandler);\n }\n\n @ParameterizedTest\n @MethodSource(\"models\")\n void should_cancel_streaming_from_onPartialResponse_callback(StreamingChatModel model) throws Exception {\n\n int partialResponsesBeforeCancellation = 5;\n AtomicInteger partialResponsesCounter = new AtomicInteger();\n\n StringBuilder partialResponsesBuilder = new StringBuilder();\n CompletableFuture<ChatResponse> futureResponse = new CompletableFuture<>();\n\n Assistant assistant = AiServices.create(Assistant.class, model);\n assistant.chat(\"What is the capital of Germany?\")\n .onPartialResponseWithContext((partialResponse, context) -> {\n partialResponsesBuilder.append(partialResponse);\n if (partialResponsesCounter.incrementAndGet() >= partialResponsesBeforeCancellation) {\n context.streamingHandle().cancel();\n }\n })\n .onCompleteResponse(response -> {\n futureResponse.complete(response);\n })\n .onError(futureResponse::completeExceptionally)\n .start();\n\n ChatResponse response = null;\n try {\n response = futureResponse.get(5, SECONDS);\n } catch (TimeoutException ignored) {\n }\n\n assertThat(response).isNull();\n assertThat(partialResponsesBuilder.toString()).isNotEmpty();\n assertThat(partialResponsesCounter).hasValue(partialResponsesBeforeCancellation);\n }\n\n @Test\n @EnabledIfEnvironmentVariable(named = \"DEEPSEEK_API_KEY\", matches = \".+\")\n void should_cancel_streaming_from_onPartialThinking_callback() throws Exception {\n\n // given\n int partialThoughtsBeforeCancellation = 5;\n AtomicInteger partialThoughtsCounter = new AtomicInteger();\n\n StreamingChatModel streamingChatModel = OpenAiStreamingChatModel.builder()\n .baseUrl(\"https://api.deepseek.com/v1\")\n .apiKey(System.getenv(\"DEEPSEEK_API_KEY\"))\n .modelName(\"deepseek-reasoner\")\n .returnThinking(true)\n .logRequests(true)\n .logResponses(true)\n .build();\n\n Assistant assistant = AiServices.create(Assistant.class, streamingChatModel);\n\n interface TokenStreamHandler {\n void onPartialThinking(PartialThinking partialThinking, PartialThinkingContext context);\n\n void onPartialResponse(String partialResponse);\n\n void onError(Throwable error);\n\n void onCompleteResponse(ChatResponse completeResponse);\n }\n\n TokenStreamHandler tokenStreamHandler = mock(TokenStreamHandler.class);\n CompletableFuture<ChatResponse> futureResponse = new CompletableFuture<>();\n\n // when\n assistant.chat(\"What is the capital of Germany?\")\n .onPartialThinkingWithContext((partialThinking, context) -> {\n tokenStreamHandler.onPartialThinking(partialThinking, context);\n if (partialThoughtsCounter.incrementAndGet() >= partialThoughtsBeforeCancellation) {\n context.streamingHandle().cancel();\n }\n })\n .onPartialResponse(partialResponse -> tokenStreamHandler.onPartialResponse(partialResponse))\n .onError(error -> {\n tokenStreamHandler.onError(error);\n futureResponse.completeExceptionally(error);\n })\n .onCompleteResponse(completeResponse -> {\n tokenStreamHandler.onCompleteResponse(completeResponse);\n futureResponse.complete(completeResponse);\n })\n .start();\n\n // then\n ChatResponse chatResponse = null;\n try {\n chatResponse = futureResponse.get(5, SECONDS);\n } catch (TimeoutException ignored) {\n\n }\n\n assertThat(chatResponse).isNull();\n\n InOrder inOrder = inOrder(tokenStreamHandler);\n inOrder.verify(tokenStreamHandler, times(partialThoughtsBeforeCancellation)).onPartialThinking(any(), any());\n inOrder.verifyNoMoreInteractions();\n verifyNoMoreInteractions(tokenStreamHandler);\n }\n\n interface AssistantWithChatRequestParams {\n\n TokenStream chat(@dev.langchain4j.service.UserMessage String userMessage, ChatRequestParameters params);\n }\n\n @ParameterizedTest\n @MethodSource(\"models\")\n void should_use_custom_chat_request_parameters_with_stream_answer(StreamingChatModel model) throws Exception {\n\n ChatRequestParameters customParams = ChatRequestParameters.builder()\n .temperature(0.01)\n .stopSequences(\"DONE\")\n .build();\n\n AssistantWithChatRequestParams assistant = AiServices.builder(AssistantWithChatRequestParams.class)\n .streamingChatModel(model)\n .chatRequestTransformer(chatRequest -> {\n assertThat(chatRequest.parameters().temperature()).isEqualTo(0.01);\n assertThat(chatRequest.parameters().stopSequences()).containsExactly(\"DONE\");\n return chatRequest;\n })\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n Queue<ChatResponse> intermediateResponses = new ConcurrentLinkedQueue<>();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<ChatResponse> futureResponse = new CompletableFuture<>();\n\n assistant.chat(\"What is the capital of Germany?\", customParams)\n .onPartialResponse(answerBuilder::append)\n .onIntermediateResponse(intermediateResponses::add)\n .onCompleteResponse(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n ChatResponse response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"Berlin\");\n assertThat(response.aiMessage().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isPositive();\n assertThat(tokenUsage.outputTokenCount()).isPositive();\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n assertThat(intermediateResponses).isEmpty();\n }\n}\n"}, "918b19894a926ffcf461f792d370ba73845546b9": {"t": 1785755651.7363975, "n": 30990, "h": "ba366ff52f847171", "c": "package dev.langchain4j.service;\n\nimport dev.langchain4j.agent.tool.P;\nimport dev.langchain4j.agent.tool.Tool;\nimport dev.langchain4j.agent.tool.ToolExecutionRequest;\nimport dev.langchain4j.data.message.AiMessage;\nimport dev.langchain4j.data.message.ChatMessage;\nimport dev.langchain4j.data.message.ToolExecutionResultMessage;\nimport dev.langchain4j.data.message.UserMessage;\nimport dev.langchain4j.memory.ChatMemory;\nimport dev.langchain4j.memory.chat.MessageWindowChatMemory;\nimport dev.langchain4j.model.chat.StreamingChatModel;\nimport dev.langchain4j.model.chat.request.ChatRequestParameters;\nimport dev.langchain4j.model.chat.response.ChatResponse;\nimport dev.langchain4j.model.chat.response.PartialThinking;\nimport dev.langchain4j.model.chat.response.PartialThinkingContext;\nimport dev.langchain4j.model.openai.OpenAiStreamingChatModel;\nimport dev.langchain4j.model.output.TokenUsage;\nimport dev.langchain4j.rag.AugmentationResult;\nimport dev.langchain4j.rag.RetrievalAugmentor;\nimport dev.langchain4j.rag.content.Content;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.MethodSource;\nimport org.mockito.InOrder;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Queue;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.ConcurrentLinkedQueue;\nimport java.util.concurrent.TimeoutException;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.stream.Stream;\n\nimport static dev.langchain4j.model.openai.OpenAiChatModelName.GPT_4_O_MINI;\nimport static dev.langchain4j.model.output.FinishReason.STOP;\nimport static java.util.concurrent.TimeUnit.SECONDS;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.Mockito.any;\nimport static org.mockito.Mockito.atLeastOnce;\nimport static org.mockito.Mockito.inOrder;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.spy;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.verifyNoMoreInteractions;\nimport static org.mockito.Mockito.when;\n\n@EnabledIfEnvironmentVariable(named = \"OPENAI_API_KEY\", matches = \".+\")\nclass StreamingAiServicesIT {\n\n static Stream<StreamingChatModel> models() {\n return Stream.of(\n OpenAiStreamingChatModel.builder()\n .baseUrl(System.getenv(\"OPENAI_BASE_URL\"))\n .apiKey(System.getenv(\"OPENAI_API_KEY\"))\n .organizationId(System.getenv(\"OPENAI_ORGANIZATION_ID\"))\n .modelName(GPT_4_O_MINI)\n .logRequests(true)\n .logResponses(true)\n .build()\n // TODO add more models\n );\n }\n\n interface Assistant {\n\n TokenStream chat(String userMessage);\n }\n\n @ParameterizedTest\n @MethodSource(\"models\")\n void should_stream_answer(StreamingChatModel model) throws Exception {\n\n Assistant assistant = AiServices.create(Assistant.class, model);\n\n StringBuilder answerBuilder = new StringBuilder();\n Queue<ChatResponse> intermediateResponses = new ConcurrentLinkedQueue<>();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<ChatResponse> futureResponse = new CompletableFuture<>();\n\n assistant.chat(\"What is the capital of Germany?\")\n .onPartialResponse(answerBuilder::append)\n .onIntermediateResponse(intermediateResponses::add)\n .onCompleteResponse(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n ChatResponse response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"Berlin\");\n assertThat(response.aiMessage().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isPositive();\n assertThat(tokenUsage.outputTokenCount()).isPositive();\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n assertThat(intermediateResponses).isEmpty();\n }\n\n @ParameterizedTest\n @MethodSource(\"models\")\n void should_callback_with_content(StreamingChatModel model) throws Exception {\n\n List<Content> contents = new ArrayList<>();\n contents.add(Content.from(\"This is additional content\"));\n\n RetrievalAugmentor retrievalAugmentor = mock(RetrievalAugmentor.class);\n ChatMessage message = UserMessage.from(\"What is the capital of Germany?\");\n\n AugmentationResult result = AugmentationResult.builder()\n .chatMessage(message)\n .contents(contents)\n .build();\n when(retrievalAugmentor.augment(any())).thenReturn(result);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatModel(model)\n .retrievalAugmentor(retrievalAugmentor)\n .build();\n\n CompletableFuture<List<Content>> futureContent = new CompletableFuture<>();\n\n assistant.chat(\"What is the capital of Germany?\")\n .onPartialResponse(ignored -> {})\n .onRetrieved(futureContent::complete)\n .ignoreErrors()\n .start();\n\n List<Content> returnedContents = futureContent.get(30, SECONDS);\n\n assertThat(returnedContents)\n .hasSize(1)\n .anySatisfy(c -> assertThat(c.textSegment().text()).isEqualTo(\"This is additional content\"));\n }\n\n @ParameterizedTest\n @MethodSource(\"models\")\n void should_stream_answers_with_memory(StreamingChatModel model) throws Exception {\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatModel(model)\n .chatMemory(chatMemory)\n .build();\n\n\n String firstUserMessage = \"Hi, my name is Klaus\";\n CompletableFuture<ChatResponse> firstResponseFuture = new CompletableFuture<>();\n\n assistant.chat(firstUserMessage)\n .onPartialResponse(System.out::println)\n .onCompleteResponse(firstResponseFuture::complete)\n .onError(firstResponseFuture::completeExceptionally)\n .start();\n\n ChatResponse firstResponse = firstResponseFuture.get(30, SECONDS);\n assertThat(firstResponse.aiMessage().text()).contains(\"Klaus\");\n\n\n String secondUserMessage = \"What is my name?\";\n CompletableFuture<ChatResponse> secondResponseFuture = new CompletableFuture<>();\n\n assistant.chat(secondUserMessage)\n .onPartialResponse(System.out::println)\n .onCompleteResponse(secondResponseFuture::complete)\n .onError(secondResponseFuture::completeExceptionally)\n .start();\n\n ChatResponse secondResponse = secondResponseFuture.get(30, SECONDS);\n assertThat(secondResponse.aiMessage().text()).contains(\"Klaus\");\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(4);\n\n assertThat(messages.get(0)).isInstanceOf(UserMessage.class);\n assertThat(((UserMessage) messages.get(0)).singleText()).isEqualTo(firstUserMessage);\n\n assertThat(messages.get(1)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(1)).isEqualTo(firstResponse.aiMessage());\n\n assertThat(messages.get(2)).isInstanceOf(UserMessage.class);\n assertThat(((UserMessage) messages.get(2)).singleText()).isEqualTo(secondUserMessage);\n\n assertThat(messages.get(3)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(3)).isEqualTo(secondResponse.aiMessage());\n }\n\n static class Calculator {\n\n @Tool(\"calculates the square root of the provided number\")\n double squareRoot(@P(\"number to operate on\") double number) {\n return Math.sqrt(number);\n }\n }\n\n @ParameterizedTest\n @MethodSource(\"models\")\n void should_execute_a_tool_then_stream_answer(StreamingChatModel model) throws Exception {\n\n Calculator calculator = spy(new Calculator());\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatModel(model)\n .chatMemory(chatMemory)\n .tools(calculator)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n Queue<ChatResponse> intermediateResponses = new ConcurrentLinkedQueue<>();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<ChatResponse> futureResponse = new CompletableFuture<>();\n\n String userMessage = \"What is the square root of 485906798473894056 in scientific notation?\";\n\n assistant.chat(userMessage)\n .onPartialResponse(answerBuilder::append)\n .onIntermediateResponse(intermediateResponses::add)\n .onCompleteResponse(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n ChatResponse response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"6.97\");\n assertThat(response.aiMessage().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isPositive();\n assertThat(tokenUsage.outputTokenCount()).isPositive();\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n\n verify(calculator).squareRoot(485906798473894056.0);\n verifyNoMoreInteractions(calculator);\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(4);\n\n assertThat(messages.get(0)).isInstanceOf(UserMessage.class);\n assertThat(((UserMessage) messages.get(0)).singleText()).isEqualTo(userMessage);\n\n AiMessage aiMessage = (AiMessage) messages.get(1);\n assertThat(aiMessage.text()).isNull();\n assertThat(aiMessage.toolExecutionRequests()).hasSize(1);\n ToolExecutionRequest toolExecutionRequest = aiMessage.toolExecutionRequests().get(0);\n assertThat(toolExecutionRequest.id()).isNotBlank();\n\n assertThat(toolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 485906798473894056}\");\n\n ToolExecutionResultMessage toolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(2);\n assertThat(toolExecutionResultMessage.id()).isEqualTo(toolExecutionRequest.id());\n assertThat(toolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionResultMessage.text()).isEqualTo(\"6.97070153193991E8\");\n\n assertThat(messages.get(3)).isInstanceOf(AiMessage.class);\n assertThat(((AiMessage) messages.get(3)).text()).contains(\"6.97\");\n\n assertThat(intermediateResponses).hasSize(1);\n assertThat(intermediateResponses.poll().aiMessage()).isEqualTo(aiMessage);\n }\n\n @Test\n void should_execute_multiple_tools_sequentially_then_answer() throws Exception {\n\n // TODO test more models\n StreamingChatModel streamingChatModel = OpenAiStreamingChatModel.builder()\n .baseUrl(System.getenv(\"OPENAI_BASE_URL\"))\n .apiKey(System.getenv(\"OPENAI_API_KEY\"))\n .organizationId(System.getenv(\"OPENAI_ORGANIZATION_ID\"))\n .modelName(GPT_4_O_MINI)\n .parallelToolCalls(false) // to force the model to call tools sequentially\n .temperature(0.0)\n .logRequests(true)\n .logResponses(true)\n .build();\n\n Calculator calculator = spy(new Calculator());\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatModel(streamingChatModel)\n .chatMemory(chatMemory)\n .tools(calculator)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n Queue<ChatResponse> intermediateResponses = new ConcurrentLinkedQueue<>();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<ChatResponse> futureResponse = new CompletableFuture<>();\n\n String userMessage = \"What is the square root of 485906798473894056 and 97866249624785 in scientific notation?\";\n\n assistant.chat(userMessage)\n .onPartialResponse(answerBuilder::append)\n .onIntermediateResponse(intermediateResponses::add)\n .onCompleteResponse(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n ChatResponse response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"6.97\", \"9.89\");\n assertThat(response.aiMessage().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isPositive(); // TODO\n assertThat(tokenUsage.outputTokenCount()).isPositive();\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n\n verify(calculator).squareRoot(485906798473894056.0);\n verify(calculator).squareRoot(97866249624785.0);\n verifyNoMoreInteractions(calculator);\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(6);\n\n assertThat(messages.get(0)).isInstanceOf(dev.langchain4j.data.message.UserMessage.class);\n assertThat(((UserMessage) messages.get(0)).singleText()).isEqualTo(userMessage);\n\n AiMessage aiMessage = (AiMessage) messages.get(1);\n assertThat(aiMessage.text()).isNull();\n assertThat(aiMessage.toolExecutionRequests()).hasSize(1);\n ToolExecutionRequest toolExecutionRequest = aiMessage.toolExecutionRequests().get(0);\n assertThat(toolExecutionRequest.id()).isNotBlank();\n assertThat(toolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 485906798473894056}\");\n\n ToolExecutionResultMessage toolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(2);\n assertThat(toolExecutionResultMessage.id()).isEqualTo(toolExecutionRequest.id());\n assertThat(toolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionResultMessage.text()).isEqualTo(\"6.97070153193991E8\");\n\n AiMessage secondAiMessage = (AiMessage) messages.get(3);\n assertThat(secondAiMessage.text()).isNull();\n assertThat(secondAiMessage.toolExecutionRequests()).hasSize(1);\n ToolExecutionRequest secondToolExecutionRequest = secondAiMessage.toolExecutionRequests().get(0);\n assertThat(secondToolExecutionRequest.id()).isNotBlank();\n assertThat(secondToolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 97866249624785}\");\n\n ToolExecutionResultMessage secondToolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(4);\n assertThat(secondToolExecutionResultMessage.id()).isEqualTo(secondToolExecutionRequest.id());\n assertThat(secondToolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionResultMessage.text()).isEqualTo(\"9892737.215997653\");\n\n assertThat(messages.get(5)).isInstanceOf(AiMessage.class);\n assertThat(((AiMessage) messages.get(5)).text()).contains(\"6.97\", \"9.89\");\n\n assertThat(intermediateResponses).hasSize(2);\n assertThat(intermediateResponses.poll().aiMessage()).isEqualTo(aiMessage);\n assertThat(intermediateResponses.poll().aiMessage()).isEqualTo(secondAiMessage);\n }\n\n @Test\n void should_execute_multiple_tools_in_parallel_then_answer() throws Exception {\n\n Calculator calculator = spy(new Calculator());\n\n // TODO test more models\n StreamingChatModel streamingChatModel = OpenAiStreamingChatModel.builder()\n .baseUrl(System.getenv(\"OPENAI_BASE_URL\"))\n .apiKey(System.getenv(\"OPENAI_API_KEY\"))\n .organizationId(System.getenv(\"OPENAI_ORGANIZATION_ID\"))\n .modelName(GPT_4_O_MINI)\n .temperature(0.0)\n .logRequests(true)\n .logResponses(true)\n .build();\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatModel(streamingChatModel)\n .chatMemory(chatMemory)\n .tools(calculator)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<ChatResponse> futureResponse = new CompletableFuture<>();\n\n String userMessage = \"What is the square root of 485906798473894056 and 97866249624785 in scientific notation?\";\n\n assistant.chat(userMessage)\n .onPartialResponse(answerBuilder::append)\n .onCompleteResponse(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n ChatResponse response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"6.97\", \"9.89\");\n assertThat(response.aiMessage().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isPositive(); // TODO\n assertThat(tokenUsage.outputTokenCount()).isPositive();\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n\n verify(calculator).squareRoot(485906798473894056.0);\n verify(calculator).squareRoot(97866249624785.0);\n verifyNoMoreInteractions(calculator);\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(5);\n\n assertThat(messages.get(0)).isInstanceOf(dev.langchain4j.data.message.UserMessage.class);\n assertThat(((UserMessage) messages.get(0)).singleText()).isEqualTo(userMessage);\n\n AiMessage aiMessage = (AiMessage) messages.get(1);\n assertThat(aiMessage.text()).isNull();\n assertThat(aiMessage.toolExecutionRequests()).hasSize(2);\n\n ToolExecutionRequest firstToolExecutionRequest = aiMessage.toolExecutionRequests().get(0);\n assertThat(firstToolExecutionRequest.id()).isNotBlank();\n assertThat(firstToolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(firstToolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 485906798473894056}\");\n\n ToolExecutionRequest secondToolExecutionRequest = aiMessage.toolExecutionRequests().get(1);\n assertThat(secondToolExecutionRequest.id()).isNotBlank();\n assertThat(secondToolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 97866249624785}\");\n\n ToolExecutionResultMessage firstToolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(2);\n assertThat(firstToolExecutionResultMessage.id()).isEqualTo(firstToolExecutionRequest.id());\n assertThat(firstToolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(firstToolExecutionResultMessage.text()).isEqualTo(\"6.97070153193991E8\");\n\n ToolExecutionResultMessage secondToolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(3);\n assertThat(secondToolExecutionResultMessage.id()).isEqualTo(secondToolExecutionRequest.id());\n assertThat(secondToolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionResultMessage.text()).isEqualTo(\"9892737.215997653\");\n\n assertThat(messages.get(4)).isInstanceOf(AiMessage.class);\n assertThat(((AiMessage) messages.get(4)).text()).contains(\"6.97\", \"9.89\");\n }\n\n @Test\n @EnabledIfEnvironmentVariable(named = \"DEEPSEEK_API_KEY\", matches = \".+\")\n void should_stream_thinking() throws Exception {\n\n // given\n boolean returnThinking = true;\n\n StreamingChatModel streamingChatModel = OpenAiStreamingChatModel.builder()\n .baseUrl(\"https://api.deepseek.com/v1\")\n .apiKey(System.getenv(\"DEEPSEEK_API_KEY\"))\n .modelName(\"deepseek-reasoner\")\n .returnThinking(returnThinking)\n .logRequests(true)\n .logResponses(true)\n .build();\n\n Assistant assistant = AiServices.create(Assistant.class, streamingChatModel);\n\n interface TokenStreamHandler {\n void onPartialThinking(PartialThinking partialThinking);\n void onPartialResponse(String partialResponse);\n void onError(Throwable error);\n void onCompleteResponse(ChatResponse completeResponse);\n }\n\n TokenStreamHandler tokenStreamHandler = mock(TokenStreamHandler.class);\n CompletableFuture<ChatResponse> futureResponse = new CompletableFuture<>();\n\n // when\n assistant.chat(\"What is the capital of Germany?\")\n .onPartialThinking(partialThinking -> tokenStreamHandler.onPartialThinking(partialThinking))\n .onPartialResponse(partialResponse -> tokenStreamHandler.onPartialResponse(partialResponse))\n .onError(error -> {\n tokenStreamHandler.onError(error);\n futureResponse.completeExceptionally(error);\n })\n .onCompleteResponse(completeResponse -> {\n tokenStreamHandler.onCompleteResponse(completeResponse);\n futureResponse.complete(completeResponse);\n })\n .start();\n\n // then\n ChatResponse chatResponse = futureResponse.get(60, SECONDS);\n\n assertThat(chatResponse.aiMessage().text()).contains(\"Berlin\");\n\n InOrder inOrder = inOrder(tokenStreamHandler);\n inOrder.verify(tokenStreamHandler, atLeastOnce()).onPartialThinking(any());\n inOrder.verify(tokenStreamHandler, atLeastOnce()).onPartialResponse(any());\n inOrder.verify(tokenStreamHandler).onCompleteResponse(any());\n inOrder.verifyNoMoreInteractions();\n verifyNoMoreInteractions(tokenStreamHandler);\n }\n\n @ParameterizedTest\n @MethodSource(\"models\")\n void should_cancel_streaming_from_onPartialResponse_callback(StreamingChatModel model) throws Exception {\n\n int partialResponsesBeforeCancellation = 5;\n AtomicInteger partialResponsesCounter = new AtomicInteger();\n\n StringBuilder partialResponsesBuilder = new StringBuilder();\n CompletableFuture<ChatResponse> futureResponse = new CompletableFuture<>();\n\n Assistant assistant = AiServices.create(Assistant.class, model);\n assistant.chat(\"What is the capital of Germany?\")\n .onPartialResponseWithContext((partialResponse, context) -> {\n partialResponsesBuilder.append(partialResponse);\n if (partialResponsesCounter.incrementAndGet() >= partialResponsesBeforeCancellation) {\n context.streamingHandle().cancel();\n }\n })\n .onCompleteResponse(response -> {\n futureResponse.complete(response);\n })\n .onError(futureResponse::completeExceptionally)\n .start();\n\n ChatResponse response = null;\n try {\n response = futureResponse.get(5, SECONDS);\n } catch (TimeoutException ignored) {\n }\n\n assertThat(response).isNull();\n assertThat(partialResponsesBuilder.toString()).isNotEmpty();\n assertThat(partialResponsesCounter).hasValue(partialResponsesBeforeCancellation);\n }\n\n @Test\n @EnabledIfEnvironmentVariable(named = \"DEEPSEEK_API_KEY\", matches = \".+\")\n void should_cancel_streaming_from_onPartialThinking_callback() throws Exception {\n\n // given\n int partialThoughtsBeforeCancellation = 5;\n AtomicInteger partialThoughtsCounter = new AtomicInteger();\n\n StreamingChatModel streamingChatModel = OpenAiStreamingChatModel.builder()\n .baseUrl(\"https://api.deepseek.com/v1\")\n .apiKey(System.getenv(\"DEEPSEEK_API_KEY\"))\n .modelName(\"deepseek-reasoner\")\n .returnThinking(true)\n .logRequests(true)\n .logResponses(true)\n .build();\n\n Assistant assistant = AiServices.create(Assistant.class, streamingChatModel);\n\n interface TokenStreamHandler {\n void onPartialThinking(PartialThinking partialThinking, PartialThinkingContext context);\n\n void onPartialResponse(String partialResponse);\n\n void onError(Throwable error);\n\n void onCompleteResponse(ChatResponse completeResponse);\n }\n\n TokenStreamHandler tokenStreamHandler = mock(TokenStreamHandler.class);\n CompletableFuture<ChatResponse> futureResponse = new CompletableFuture<>();\n\n // when\n assistant.chat(\"What is the capital of Germany?\")\n .onPartialThinkingWithContext((partialThinking, context) -> {\n tokenStreamHandler.onPartialThinking(partialThinking, context);\n if (partialThoughtsCounter.incrementAndGet() >= partialThoughtsBeforeCancellation) {\n context.streamingHandle().cancel();\n }\n })\n .onPartialResponse(partialResponse -> tokenStreamHandler.onPartialResponse(partialResponse))\n .onError(error -> {\n tokenStreamHandler.onError(error);\n futureResponse.completeExceptionally(error);\n })\n .onCompleteResponse(completeResponse -> {\n tokenStreamHandler.onCompleteResponse(completeResponse);\n futureResponse.complete(completeResponse);\n })\n .start();\n\n // then\n ChatResponse chatResponse = null;\n try {\n chatResponse = futureResponse.get(5, SECONDS);\n } catch (TimeoutException ignored) {\n\n }\n\n assertThat(chatResponse).isNull();\n\n InOrder inOrder = inOrder(tokenStreamHandler);\n inOrder.verify(tokenStreamHandler, times(partialThoughtsBeforeCancellation)).onPartialThinking(any(), any());\n inOrder.verifyNoMoreInteractions();\n verifyNoMoreInteractions(tokenStreamHandler);\n }\n\n interface AssistantWithChatRequestParams {\n\n TokenStream chat(@dev.langchain4j.service.UserMessage String userMessage, ChatRequestParameters params);\n }\n\n @ParameterizedTest\n @MethodSource(\"models\")\n void should_use_custom_chat_request_parameters_with_stream_answer(StreamingChatModel model) throws Exception {\n\n ChatRequestParameters customParams = ChatRequestParameters.builder()\n .temperature(0.01)\n .stopSequences(\"DONE\")\n .build();\n\n AssistantWithChatRequestParams assistant = AiServices.builder(AssistantWithChatRequestParams.class)\n .streamingChatModel(model)\n .chatRequestTransformer(chatRequest -> {\n assertThat(chatRequest.parameters().temperature()).isEqualTo(0.01);\n assertThat(chatRequest.parameters().stopSequences()).containsExactly(\"DONE\");\n return chatRequest;\n })\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n Queue<ChatResponse> intermediateResponses = new ConcurrentLinkedQueue<>();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<ChatResponse> futureResponse = new CompletableFuture<>();\n\n assistant.chat(\"What is the capital of Germany?\", customParams)\n .onPartialResponse(answerBuilder::append)\n .onIntermediateResponse(intermediateResponses::add)\n .onCompleteResponse(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n ChatResponse response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"Berlin\");\n assertThat(response.aiMessage().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isPositive();\n assertThat(tokenUsage.outputTokenCount()).isPositive();\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n assertThat(intermediateResponses).isEmpty();\n }\n}\n"}, "ecc754ba1fce9231c88085a0e75b6f8a49142d4b": {"t": 1785755653.1912427, "n": 28469, "h": "f4e75cd8bae7a08b", "c": "package dev.langchain4j.service;\n\nimport dev.langchain4j.agent.tool.P;\nimport dev.langchain4j.agent.tool.Tool;\nimport dev.langchain4j.agent.tool.ToolExecutionRequest;\nimport dev.langchain4j.data.message.AiMessage;\nimport dev.langchain4j.data.message.ChatMessage;\nimport dev.langchain4j.data.message.ToolExecutionResultMessage;\nimport dev.langchain4j.data.message.UserMessage;\nimport dev.langchain4j.memory.ChatMemory;\nimport dev.langchain4j.memory.chat.MessageWindowChatMemory;\nimport dev.langchain4j.model.chat.StreamingChatModel;\nimport dev.langchain4j.model.chat.response.ChatResponse;\nimport dev.langchain4j.model.chat.response.PartialThinking;\nimport dev.langchain4j.model.chat.response.PartialThinkingContext;\nimport dev.langchain4j.model.openai.OpenAiStreamingChatModel;\nimport dev.langchain4j.model.output.TokenUsage;\nimport dev.langchain4j.rag.AugmentationResult;\nimport dev.langchain4j.rag.RetrievalAugmentor;\nimport dev.langchain4j.rag.content.Content;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.MethodSource;\nimport org.mockito.InOrder;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Queue;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.ConcurrentLinkedQueue;\nimport java.util.concurrent.TimeoutException;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.stream.Stream;\n\nimport static dev.langchain4j.model.openai.OpenAiChatModelName.GPT_4_O_MINI;\nimport static dev.langchain4j.model.output.FinishReason.STOP;\nimport static java.util.concurrent.TimeUnit.SECONDS;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.Mockito.any;\nimport static org.mockito.Mockito.atLeastOnce;\nimport static org.mockito.Mockito.inOrder;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.spy;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.verifyNoMoreInteractions;\nimport static org.mockito.Mockito.when;\n\n@EnabledIfEnvironmentVariable(named = \"OPENAI_API_KEY\", matches = \".+\")\nclass StreamingAiServicesIT {\n\n static Stream<StreamingChatModel> models() {\n return Stream.of(\n OpenAiStreamingChatModel.builder()\n .baseUrl(System.getenv(\"OPENAI_BASE_URL\"))\n .apiKey(System.getenv(\"OPENAI_API_KEY\"))\n .organizationId(System.getenv(\"OPENAI_ORGANIZATION_ID\"))\n .modelName(GPT_4_O_MINI)\n .logRequests(true)\n .logResponses(true)\n .build()\n // TODO add more models\n );\n }\n\n interface Assistant {\n\n TokenStream chat(String userMessage);\n }\n\n @ParameterizedTest\n @MethodSource(\"models\")\n void should_stream_answer(StreamingChatModel model) throws Exception {\n\n Assistant assistant = AiServices.create(Assistant.class, model);\n\n StringBuilder answerBuilder = new StringBuilder();\n Queue<ChatResponse> intermediateResponses = new ConcurrentLinkedQueue<>();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<ChatResponse> futureResponse = new CompletableFuture<>();\n\n assistant.chat(\"What is the capital of Germany?\")\n .onPartialResponse(answerBuilder::append)\n .onIntermediateResponse(intermediateResponses::add)\n .onCompleteResponse(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n ChatResponse response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"Berlin\");\n assertThat(response.aiMessage().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isPositive();\n assertThat(tokenUsage.outputTokenCount()).isPositive();\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n assertThat(intermediateResponses).isEmpty();\n }\n\n @ParameterizedTest\n @MethodSource(\"models\")\n void should_callback_with_content(StreamingChatModel model) throws Exception {\n\n List<Content> contents = new ArrayList<>();\n contents.add(Content.from(\"This is additional content\"));\n\n RetrievalAugmentor retrievalAugmentor = mock(RetrievalAugmentor.class);\n ChatMessage message = UserMessage.from(\"What is the capital of Germany?\");\n\n AugmentationResult result = AugmentationResult.builder()\n .chatMessage(message)\n .contents(contents)\n .build();\n when(retrievalAugmentor.augment(any())).thenReturn(result);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatModel(model)\n .retrievalAugmentor(retrievalAugmentor)\n .build();\n\n CompletableFuture<List<Content>> futureContent = new CompletableFuture<>();\n\n assistant.chat(\"What is the capital of Germany?\")\n .onPartialResponse(ignored -> {})\n .onRetrieved(futureContent::complete)\n .ignoreErrors()\n .start();\n\n List<Content> returnedContents = futureContent.get(30, SECONDS);\n\n assertThat(returnedContents)\n .hasSize(1)\n .anySatisfy(c -> assertThat(c.textSegment().text()).isEqualTo(\"This is additional content\"));\n }\n\n @ParameterizedTest\n @MethodSource(\"models\")\n void should_stream_answers_with_memory(StreamingChatModel model) throws Exception {\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatModel(model)\n .chatMemory(chatMemory)\n .build();\n\n\n String firstUserMessage = \"Hi, my name is Klaus\";\n CompletableFuture<ChatResponse> firstResponseFuture = new CompletableFuture<>();\n\n assistant.chat(firstUserMessage)\n .onPartialResponse(System.out::println)\n .onCompleteResponse(firstResponseFuture::complete)\n .onError(firstResponseFuture::completeExceptionally)\n .start();\n\n ChatResponse firstResponse = firstResponseFuture.get(30, SECONDS);\n assertThat(firstResponse.aiMessage().text()).contains(\"Klaus\");\n\n\n String secondUserMessage = \"What is my name?\";\n CompletableFuture<ChatResponse> secondResponseFuture = new CompletableFuture<>();\n\n assistant.chat(secondUserMessage)\n .onPartialResponse(System.out::println)\n .onCompleteResponse(secondResponseFuture::complete)\n .onError(secondResponseFuture::completeExceptionally)\n .start();\n\n ChatResponse secondResponse = secondResponseFuture.get(30, SECONDS);\n assertThat(secondResponse.aiMessage().text()).contains(\"Klaus\");\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(4);\n\n assertThat(messages.get(0)).isInstanceOf(UserMessage.class);\n assertThat(((UserMessage) messages.get(0)).singleText()).isEqualTo(firstUserMessage);\n\n assertThat(messages.get(1)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(1)).isEqualTo(firstResponse.aiMessage());\n\n assertThat(messages.get(2)).isInstanceOf(UserMessage.class);\n assertThat(((UserMessage) messages.get(2)).singleText()).isEqualTo(secondUserMessage);\n\n assertThat(messages.get(3)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(3)).isEqualTo(secondResponse.aiMessage());\n }\n\n static class Calculator {\n\n @Tool(\"calculates the square root of the provided number\")\n double squareRoot(@P(\"number to operate on\") double number) {\n return Math.sqrt(number);\n }\n }\n\n @ParameterizedTest\n @MethodSource(\"models\")\n void should_execute_a_tool_then_stream_answer(StreamingChatModel model) throws Exception {\n\n Calculator calculator = spy(new Calculator());\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatModel(model)\n .chatMemory(chatMemory)\n .tools(calculator)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n Queue<ChatResponse> intermediateResponses = new ConcurrentLinkedQueue<>();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<ChatResponse> futureResponse = new CompletableFuture<>();\n\n String userMessage = \"What is the square root of 485906798473894056 in scientific notation?\";\n\n assistant.chat(userMessage)\n .onPartialResponse(answerBuilder::append)\n .onIntermediateResponse(intermediateResponses::add)\n .onCompleteResponse(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n ChatResponse response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"6.97\");\n assertThat(response.aiMessage().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isPositive();\n assertThat(tokenUsage.outputTokenCount()).isPositive();\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n\n verify(calculator).squareRoot(485906798473894056.0);\n verifyNoMoreInteractions(calculator);\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(4);\n\n assertThat(messages.get(0)).isInstanceOf(UserMessage.class);\n assertThat(((UserMessage) messages.get(0)).singleText()).isEqualTo(userMessage);\n\n AiMessage aiMessage = (AiMessage) messages.get(1);\n assertThat(aiMessage.text()).isNull();\n assertThat(aiMessage.toolExecutionRequests()).hasSize(1);\n ToolExecutionRequest toolExecutionRequest = aiMessage.toolExecutionRequests().get(0);\n assertThat(toolExecutionRequest.id()).isNotBlank();\n\n assertThat(toolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 485906798473894056}\");\n\n ToolExecutionResultMessage toolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(2);\n assertThat(toolExecutionResultMessage.id()).isEqualTo(toolExecutionRequest.id());\n assertThat(toolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionResultMessage.text()).isEqualTo(\"6.97070153193991E8\");\n\n assertThat(messages.get(3)).isInstanceOf(AiMessage.class);\n assertThat(((AiMessage) messages.get(3)).text()).contains(\"6.97\");\n\n assertThat(intermediateResponses).hasSize(1);\n assertThat(intermediateResponses.poll().aiMessage()).isEqualTo(aiMessage);\n }\n\n @Test\n void should_execute_multiple_tools_sequentially_then_answer() throws Exception {\n\n // TODO test more models\n StreamingChatModel streamingChatModel = OpenAiStreamingChatModel.builder()\n .baseUrl(System.getenv(\"OPENAI_BASE_URL\"))\n .apiKey(System.getenv(\"OPENAI_API_KEY\"))\n .organizationId(System.getenv(\"OPENAI_ORGANIZATION_ID\"))\n .modelName(GPT_4_O_MINI)\n .parallelToolCalls(false) // to force the model to call tools sequentially\n .temperature(0.0)\n .logRequests(true)\n .logResponses(true)\n .build();\n\n Calculator calculator = spy(new Calculator());\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatModel(streamingChatModel)\n .chatMemory(chatMemory)\n .tools(calculator)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n Queue<ChatResponse> intermediateResponses = new ConcurrentLinkedQueue<>();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<ChatResponse> futureResponse = new CompletableFuture<>();\n\n String userMessage = \"What is the square root of 485906798473894056 and 97866249624785 in scientific notation?\";\n\n assistant.chat(userMessage)\n .onPartialResponse(answerBuilder::append)\n .onIntermediateResponse(intermediateResponses::add)\n .onCompleteResponse(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n ChatResponse response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"6.97\", \"9.89\");\n assertThat(response.aiMessage().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isPositive(); // TODO\n assertThat(tokenUsage.outputTokenCount()).isPositive();\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n\n verify(calculator).squareRoot(485906798473894056.0);\n verify(calculator).squareRoot(97866249624785.0);\n verifyNoMoreInteractions(calculator);\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(6);\n\n assertThat(messages.get(0)).isInstanceOf(dev.langchain4j.data.message.UserMessage.class);\n assertThat(((UserMessage) messages.get(0)).singleText()).isEqualTo(userMessage);\n\n AiMessage aiMessage = (AiMessage) messages.get(1);\n assertThat(aiMessage.text()).isNull();\n assertThat(aiMessage.toolExecutionRequests()).hasSize(1);\n ToolExecutionRequest toolExecutionRequest = aiMessage.toolExecutionRequests().get(0);\n assertThat(toolExecutionRequest.id()).isNotBlank();\n assertThat(toolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 485906798473894056}\");\n\n ToolExecutionResultMessage toolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(2);\n assertThat(toolExecutionResultMessage.id()).isEqualTo(toolExecutionRequest.id());\n assertThat(toolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionResultMessage.text()).isEqualTo(\"6.97070153193991E8\");\n\n AiMessage secondAiMessage = (AiMessage) messages.get(3);\n assertThat(secondAiMessage.text()).isNull();\n assertThat(secondAiMessage.toolExecutionRequests()).hasSize(1);\n ToolExecutionRequest secondToolExecutionRequest = secondAiMessage.toolExecutionRequests().get(0);\n assertThat(secondToolExecutionRequest.id()).isNotBlank();\n assertThat(secondToolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 97866249624785}\");\n\n ToolExecutionResultMessage secondToolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(4);\n assertThat(secondToolExecutionResultMessage.id()).isEqualTo(secondToolExecutionRequest.id());\n assertThat(secondToolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionResultMessage.text()).isEqualTo(\"9892737.215997653\");\n\n assertThat(messages.get(5)).isInstanceOf(AiMessage.class);\n assertThat(((AiMessage) messages.get(5)).text()).contains(\"6.97\", \"9.89\");\n\n assertThat(intermediateResponses).hasSize(2);\n assertThat(intermediateResponses.poll().aiMessage()).isEqualTo(aiMessage);\n assertThat(intermediateResponses.poll().aiMessage()).isEqualTo(secondAiMessage);\n }\n\n @Test\n void should_execute_multiple_tools_in_parallel_then_answer() throws Exception {\n\n Calculator calculator = spy(new Calculator());\n\n // TODO test more models\n StreamingChatModel streamingChatModel = OpenAiStreamingChatModel.builder()\n .baseUrl(System.getenv(\"OPENAI_BASE_URL\"))\n .apiKey(System.getenv(\"OPENAI_API_KEY\"))\n .organizationId(System.getenv(\"OPENAI_ORGANIZATION_ID\"))\n .modelName(GPT_4_O_MINI)\n .temperature(0.0)\n .logRequests(true)\n .logResponses(true)\n .build();\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatModel(streamingChatModel)\n .chatMemory(chatMemory)\n .tools(calculator)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<ChatResponse> futureResponse = new CompletableFuture<>();\n\n String userMessage = \"What is the square root of 485906798473894056 and 97866249624785 in scientific notation?\";\n\n assistant.chat(userMessage)\n .onPartialResponse(answerBuilder::append)\n .onCompleteResponse(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n ChatResponse response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"6.97\", \"9.89\");\n assertThat(response.aiMessage().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isPositive(); // TODO\n assertThat(tokenUsage.outputTokenCount()).isPositive();\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n\n verify(calculator).squareRoot(485906798473894056.0);\n verify(calculator).squareRoot(97866249624785.0);\n verifyNoMoreInteractions(calculator);\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(5);\n\n assertThat(messages.get(0)).isInstanceOf(dev.langchain4j.data.message.UserMessage.class);\n assertThat(((UserMessage) messages.get(0)).singleText()).isEqualTo(userMessage);\n\n AiMessage aiMessage = (AiMessage) messages.get(1);\n assertThat(aiMessage.text()).isNull();\n assertThat(aiMessage.toolExecutionRequests()).hasSize(2);\n\n ToolExecutionRequest firstToolExecutionRequest = aiMessage.toolExecutionRequests().get(0);\n assertThat(firstToolExecutionRequest.id()).isNotBlank();\n assertThat(firstToolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(firstToolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 485906798473894056}\");\n\n ToolExecutionRequest secondToolExecutionRequest = aiMessage.toolExecutionRequests().get(1);\n assertThat(secondToolExecutionRequest.id()).isNotBlank();\n assertThat(secondToolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 97866249624785}\");\n\n ToolExecutionResultMessage firstToolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(2);\n assertThat(firstToolExecutionResultMessage.id()).isEqualTo(firstToolExecutionRequest.id());\n assertThat(firstToolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(firstToolExecutionResultMessage.text()).isEqualTo(\"6.97070153193991E8\");\n\n ToolExecutionResultMessage secondToolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(3);\n assertThat(secondToolExecutionResultMessage.id()).isEqualTo(secondToolExecutionRequest.id());\n assertThat(secondToolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionResultMessage.text()).isEqualTo(\"9892737.215997653\");\n\n assertThat(messages.get(4)).isInstanceOf(AiMessage.class);\n assertThat(((AiMessage) messages.get(4)).text()).contains(\"6.97\", \"9.89\");\n }\n\n @Test\n @EnabledIfEnvironmentVariable(named = \"DEEPSEEK_API_KEY\", matches = \".+\")\n void should_stream_thinking() throws Exception {\n\n // given\n boolean returnThinking = true;\n\n StreamingChatModel streamingChatModel = OpenAiStreamingChatModel.builder()\n .baseUrl(\"https://api.deepseek.com/v1\")\n .apiKey(System.getenv(\"DEEPSEEK_API_KEY\"))\n .modelName(\"deepseek-reasoner\")\n .returnThinking(returnThinking)\n .logRequests(true)\n .logResponses(true)\n .build();\n\n Assistant assistant = AiServices.create(Assistant.class, streamingChatModel);\n\n interface TokenStreamHandler {\n void onPartialThinking(PartialThinking partialThinking);\n void onPartialResponse(String partialResponse);\n void onError(Throwable error);\n void onCompleteResponse(ChatResponse completeResponse);\n }\n\n TokenStreamHandler tokenStreamHandler = mock(TokenStreamHandler.class);\n CompletableFuture<ChatResponse> futureResponse = new CompletableFuture<>();\n\n // when\n assistant.chat(\"What is the capital of Germany?\")\n .onPartialThinking(partialThinking -> tokenStreamHandler.onPartialThinking(partialThinking))\n .onPartialResponse(partialResponse -> tokenStreamHandler.onPartialResponse(partialResponse))\n .onError(error -> {\n tokenStreamHandler.onError(error);\n futureResponse.completeExceptionally(error);\n })\n .onCompleteResponse(completeResponse -> {\n tokenStreamHandler.onCompleteResponse(completeResponse);\n futureResponse.complete(completeResponse);\n })\n .start();\n\n // then\n ChatResponse chatResponse = futureResponse.get(60, SECONDS);\n\n assertThat(chatResponse.aiMessage().text()).contains(\"Berlin\");\n\n InOrder inOrder = inOrder(tokenStreamHandler);\n inOrder.verify(tokenStreamHandler, atLeastOnce()).onPartialThinking(any());\n inOrder.verify(tokenStreamHandler, atLeastOnce()).onPartialResponse(any());\n inOrder.verify(tokenStreamHandler).onCompleteResponse(any());\n inOrder.verifyNoMoreInteractions();\n verifyNoMoreInteractions(tokenStreamHandler);\n }\n\n @ParameterizedTest\n @MethodSource(\"models\")\n void should_cancel_streaming_from_onPartialResponse_callback(StreamingChatModel model) throws Exception {\n\n int partialResponsesBeforeCancellation = 5;\n AtomicInteger partialResponsesCounter = new AtomicInteger();\n\n StringBuilder partialResponsesBuilder = new StringBuilder();\n CompletableFuture<ChatResponse> futureResponse = new CompletableFuture<>();\n\n Assistant assistant = AiServices.create(Assistant.class, model);\n assistant.chat(\"What is the capital of Germany?\")\n .onPartialResponseWithContext((partialResponse, context) -> {\n partialResponsesBuilder.append(partialResponse);\n if (partialResponsesCounter.incrementAndGet() >= partialResponsesBeforeCancellation) {\n context.streamingHandle().cancel();\n }\n })\n .onCompleteResponse(response -> {\n futureResponse.complete(response);\n })\n .onError(futureResponse::completeExceptionally)\n .start();\n\n ChatResponse response = null;\n try {\n response = futureResponse.get(5, SECONDS);\n } catch (TimeoutException ignored) {\n }\n\n assertThat(response).isNull();\n assertThat(partialResponsesBuilder.toString()).isNotEmpty();\n assertThat(partialResponsesCounter).hasValue(partialResponsesBeforeCancellation);\n }\n\n @Test\n @EnabledIfEnvironmentVariable(named = \"DEEPSEEK_API_KEY\", matches = \".+\")\n void should_cancel_streaming_from_onPartialThinking_callback() throws Exception {\n\n // given\n int partialThoughtsBeforeCancellation = 5;\n AtomicInteger partialThoughtsCounter = new AtomicInteger();\n\n StreamingChatModel streamingChatModel = OpenAiStreamingChatModel.builder()\n .baseUrl(\"https://api.deepseek.com/v1\")\n .apiKey(System.getenv(\"DEEPSEEK_API_KEY\"))\n .modelName(\"deepseek-reasoner\")\n .returnThinking(true)\n .logRequests(true)\n .logResponses(true)\n .build();\n\n Assistant assistant = AiServices.create(Assistant.class, streamingChatModel);\n\n interface TokenStreamHandler {\n void onPartialThinking(PartialThinking partialThinking, PartialThinkingContext context);\n\n void onPartialResponse(String partialResponse);\n\n void onError(Throwable error);\n\n void onCompleteResponse(ChatResponse completeResponse);\n }\n\n TokenStreamHandler tokenStreamHandler = mock(TokenStreamHandler.class);\n CompletableFuture<ChatResponse> futureResponse = new CompletableFuture<>();\n\n // when\n assistant.chat(\"What is the capital of Germany?\")\n .onPartialThinkingWithContext((partialThinking, context) -> {\n tokenStreamHandler.onPartialThinking(partialThinking, context);\n if (partialThoughtsCounter.incrementAndGet() >= partialThoughtsBeforeCancellation) {\n context.streamingHandle().cancel();\n }\n })\n .onPartialResponse(partialResponse -> tokenStreamHandler.onPartialResponse(partialResponse))\n .onError(error -> {\n tokenStreamHandler.onError(error);\n futureResponse.completeExceptionally(error);\n })\n .onCompleteResponse(completeResponse -> {\n tokenStreamHandler.onCompleteResponse(completeResponse);\n futureResponse.complete(completeResponse);\n })\n .start();\n\n // then\n ChatResponse chatResponse = null;\n try {\n chatResponse = futureResponse.get(5, SECONDS);\n } catch (TimeoutException ignored) {\n\n }\n\n assertThat(chatResponse).isNull();\n\n InOrder inOrder = inOrder(tokenStreamHandler);\n inOrder.verify(tokenStreamHandler, times(partialThoughtsBeforeCancellation)).onPartialThinking(any(), any());\n inOrder.verifyNoMoreInteractions();\n verifyNoMoreInteractions(tokenStreamHandler);\n }\n}\n"}, "5d03cac134b9500e896abd7fa7e8bb994467f4bd": {"t": 1785755655.0438972, "n": 24007, "h": "6729e77f6706c4b6", "c": "package dev.langchain4j.service;\n\nimport dev.langchain4j.agent.tool.P;\nimport dev.langchain4j.agent.tool.Tool;\nimport dev.langchain4j.agent.tool.ToolExecutionRequest;\nimport dev.langchain4j.data.message.AiMessage;\nimport dev.langchain4j.data.message.ChatMessage;\nimport dev.langchain4j.data.message.ToolExecutionResultMessage;\nimport dev.langchain4j.data.message.UserMessage;\nimport dev.langchain4j.memory.ChatMemory;\nimport dev.langchain4j.memory.chat.MessageWindowChatMemory;\nimport dev.langchain4j.model.chat.StreamingChatModel;\nimport dev.langchain4j.model.chat.response.ChatResponse;\nimport dev.langchain4j.model.chat.response.PartialThinking;\nimport dev.langchain4j.model.openai.OpenAiStreamingChatModel;\nimport dev.langchain4j.model.output.TokenUsage;\nimport dev.langchain4j.rag.AugmentationResult;\nimport dev.langchain4j.rag.RetrievalAugmentor;\nimport dev.langchain4j.rag.content.Content;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.MethodSource;\nimport org.mockito.InOrder;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Queue;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.ConcurrentLinkedQueue;\nimport java.util.stream.Stream;\n\nimport static dev.langchain4j.model.openai.OpenAiChatModelName.GPT_4_O_MINI;\nimport static dev.langchain4j.model.output.FinishReason.STOP;\nimport static java.util.concurrent.TimeUnit.SECONDS;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.Mockito.any;\nimport static org.mockito.Mockito.atLeastOnce;\nimport static org.mockito.Mockito.inOrder;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.spy;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.verifyNoMoreInteractions;\nimport static org.mockito.Mockito.when;\n\n@EnabledIfEnvironmentVariable(named = \"OPENAI_API_KEY\", matches = \".+\")\nclass StreamingAiServicesIT {\n\n static Stream<StreamingChatModel> models() {\n return Stream.of(\n OpenAiStreamingChatModel.builder()\n .baseUrl(System.getenv(\"OPENAI_BASE_URL\"))\n .apiKey(System.getenv(\"OPENAI_API_KEY\"))\n .organizationId(System.getenv(\"OPENAI_ORGANIZATION_ID\"))\n .modelName(GPT_4_O_MINI)\n .logRequests(true)\n .logResponses(true)\n .build()\n // TODO add more models\n );\n }\n\n interface Assistant {\n\n TokenStream chat(String userMessage);\n }\n\n @ParameterizedTest\n @MethodSource(\"models\")\n void should_stream_answer(StreamingChatModel model) throws Exception {\n\n Assistant assistant = AiServices.create(Assistant.class, model);\n\n StringBuilder answerBuilder = new StringBuilder();\n Queue<ChatResponse> intermediateResponses = new ConcurrentLinkedQueue<>();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<ChatResponse> futureResponse = new CompletableFuture<>();\n\n assistant.chat(\"What is the capital of Germany?\")\n .onPartialResponse(answerBuilder::append)\n .onIntermediateResponse(intermediateResponses::add)\n .onCompleteResponse(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n ChatResponse response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"Berlin\");\n assertThat(response.aiMessage().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isPositive();\n assertThat(tokenUsage.outputTokenCount()).isPositive();\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n assertThat(intermediateResponses).isEmpty();\n }\n\n @ParameterizedTest\n @MethodSource(\"models\")\n void should_callback_with_content(StreamingChatModel model) throws Exception {\n\n List<Content> contents = new ArrayList<>();\n contents.add(Content.from(\"This is additional content\"));\n\n RetrievalAugmentor retrievalAugmentor = mock(RetrievalAugmentor.class);\n ChatMessage message = UserMessage.from(\"What is the capital of Germany?\");\n\n AugmentationResult result = AugmentationResult.builder()\n .chatMessage(message)\n .contents(contents)\n .build();\n when(retrievalAugmentor.augment(any())).thenReturn(result);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatModel(model)\n .retrievalAugmentor(retrievalAugmentor)\n .build();\n\n CompletableFuture<List<Content>> futureContent = new CompletableFuture<>();\n\n assistant.chat(\"What is the capital of Germany?\")\n .onPartialResponse(ignored -> {})\n .onRetrieved(futureContent::complete)\n .ignoreErrors()\n .start();\n\n List<Content> returnedContents = futureContent.get(30, SECONDS);\n\n assertThat(returnedContents)\n .hasSize(1)\n .anySatisfy(c -> assertThat(c.textSegment().text()).isEqualTo(\"This is additional content\"));\n }\n\n @ParameterizedTest\n @MethodSource(\"models\")\n void should_stream_answers_with_memory(StreamingChatModel model) throws Exception {\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatModel(model)\n .chatMemory(chatMemory)\n .build();\n\n\n String firstUserMessage = \"Hi, my name is Klaus\";\n CompletableFuture<ChatResponse> firstResponseFuture = new CompletableFuture<>();\n\n assistant.chat(firstUserMessage)\n .onPartialResponse(System.out::println)\n .onCompleteResponse(firstResponseFuture::complete)\n .onError(firstResponseFuture::completeExceptionally)\n .start();\n\n ChatResponse firstResponse = firstResponseFuture.get(30, SECONDS);\n assertThat(firstResponse.aiMessage().text()).contains(\"Klaus\");\n\n\n String secondUserMessage = \"What is my name?\";\n CompletableFuture<ChatResponse> secondResponseFuture = new CompletableFuture<>();\n\n assistant.chat(secondUserMessage)\n .onPartialResponse(System.out::println)\n .onCompleteResponse(secondResponseFuture::complete)\n .onError(secondResponseFuture::completeExceptionally)\n .start();\n\n ChatResponse secondResponse = secondResponseFuture.get(30, SECONDS);\n assertThat(secondResponse.aiMessage().text()).contains(\"Klaus\");\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(4);\n\n assertThat(messages.get(0)).isInstanceOf(UserMessage.class);\n assertThat(((UserMessage) messages.get(0)).singleText()).isEqualTo(firstUserMessage);\n\n assertThat(messages.get(1)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(1)).isEqualTo(firstResponse.aiMessage());\n\n assertThat(messages.get(2)).isInstanceOf(UserMessage.class);\n assertThat(((UserMessage) messages.get(2)).singleText()).isEqualTo(secondUserMessage);\n\n assertThat(messages.get(3)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(3)).isEqualTo(secondResponse.aiMessage());\n }\n\n static class Calculator {\n\n @Tool(\"calculates the square root of the provided number\")\n double squareRoot(@P(\"number to operate on\") double number) {\n return Math.sqrt(number);\n }\n }\n\n @ParameterizedTest\n @MethodSource(\"models\")\n void should_execute_a_tool_then_stream_answer(StreamingChatModel model) throws Exception {\n\n Calculator calculator = spy(new Calculator());\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatModel(model)\n .chatMemory(chatMemory)\n .tools(calculator)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n Queue<ChatResponse> intermediateResponses = new ConcurrentLinkedQueue<>();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<ChatResponse> futureResponse = new CompletableFuture<>();\n\n String userMessage = \"What is the square root of 485906798473894056 in scientific notation?\";\n\n assistant.chat(userMessage)\n .onPartialResponse(answerBuilder::append)\n .onIntermediateResponse(intermediateResponses::add)\n .onCompleteResponse(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n ChatResponse response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"6.97\");\n assertThat(response.aiMessage().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isPositive();\n assertThat(tokenUsage.outputTokenCount()).isPositive();\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n\n verify(calculator).squareRoot(485906798473894056.0);\n verifyNoMoreInteractions(calculator);\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(4);\n\n assertThat(messages.get(0)).isInstanceOf(UserMessage.class);\n assertThat(((UserMessage) messages.get(0)).singleText()).isEqualTo(userMessage);\n\n AiMessage aiMessage = (AiMessage) messages.get(1);\n assertThat(aiMessage.text()).isNull();\n assertThat(aiMessage.toolExecutionRequests()).hasSize(1);\n ToolExecutionRequest toolExecutionRequest = aiMessage.toolExecutionRequests().get(0);\n assertThat(toolExecutionRequest.id()).isNotBlank();\n\n assertThat(toolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 485906798473894056}\");\n\n ToolExecutionResultMessage toolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(2);\n assertThat(toolExecutionResultMessage.id()).isEqualTo(toolExecutionRequest.id());\n assertThat(toolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionResultMessage.text()).isEqualTo(\"6.97070153193991E8\");\n\n assertThat(messages.get(3)).isInstanceOf(AiMessage.class);\n assertThat(((AiMessage) messages.get(3)).text()).contains(\"6.97\");\n\n assertThat(intermediateResponses).hasSize(1);\n assertThat(intermediateResponses.poll().aiMessage()).isEqualTo(aiMessage);\n }\n\n @Test\n void should_execute_multiple_tools_sequentially_then_answer() throws Exception {\n\n // TODO test more models\n StreamingChatModel streamingChatModel = OpenAiStreamingChatModel.builder()\n .baseUrl(System.getenv(\"OPENAI_BASE_URL\"))\n .apiKey(System.getenv(\"OPENAI_API_KEY\"))\n .organizationId(System.getenv(\"OPENAI_ORGANIZATION_ID\"))\n .modelName(GPT_4_O_MINI)\n .parallelToolCalls(false) // to force the model to call tools sequentially\n .temperature(0.0)\n .logRequests(true)\n .logResponses(true)\n .build();\n\n Calculator calculator = spy(new Calculator());\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatModel(streamingChatModel)\n .chatMemory(chatMemory)\n .tools(calculator)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n Queue<ChatResponse> intermediateResponses = new ConcurrentLinkedQueue<>();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<ChatResponse> futureResponse = new CompletableFuture<>();\n\n String userMessage = \"What is the square root of 485906798473894056 and 97866249624785 in scientific notation?\";\n\n assistant.chat(userMessage)\n .onPartialResponse(answerBuilder::append)\n .onIntermediateResponse(intermediateResponses::add)\n .onCompleteResponse(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n ChatResponse response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"6.97\", \"9.89\");\n assertThat(response.aiMessage().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isPositive(); // TODO\n assertThat(tokenUsage.outputTokenCount()).isPositive();\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n\n verify(calculator).squareRoot(485906798473894056.0);\n verify(calculator).squareRoot(97866249624785.0);\n verifyNoMoreInteractions(calculator);\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(6);\n\n assertThat(messages.get(0)).isInstanceOf(dev.langchain4j.data.message.UserMessage.class);\n assertThat(((UserMessage) messages.get(0)).singleText()).isEqualTo(userMessage);\n\n AiMessage aiMessage = (AiMessage) messages.get(1);\n assertThat(aiMessage.text()).isNull();\n assertThat(aiMessage.toolExecutionRequests()).hasSize(1);\n ToolExecutionRequest toolExecutionRequest = aiMessage.toolExecutionRequests().get(0);\n assertThat(toolExecutionRequest.id()).isNotBlank();\n assertThat(toolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 485906798473894056}\");\n\n ToolExecutionResultMessage toolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(2);\n assertThat(toolExecutionResultMessage.id()).isEqualTo(toolExecutionRequest.id());\n assertThat(toolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionResultMessage.text()).isEqualTo(\"6.97070153193991E8\");\n\n AiMessage secondAiMessage = (AiMessage) messages.get(3);\n assertThat(secondAiMessage.text()).isNull();\n assertThat(secondAiMessage.toolExecutionRequests()).hasSize(1);\n ToolExecutionRequest secondToolExecutionRequest = secondAiMessage.toolExecutionRequests().get(0);\n assertThat(secondToolExecutionRequest.id()).isNotBlank();\n assertThat(secondToolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 97866249624785}\");\n\n ToolExecutionResultMessage secondToolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(4);\n assertThat(secondToolExecutionResultMessage.id()).isEqualTo(secondToolExecutionRequest.id());\n assertThat(secondToolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionResultMessage.text()).isEqualTo(\"9892737.215997653\");\n\n assertThat(messages.get(5)).isInstanceOf(AiMessage.class);\n assertThat(((AiMessage) messages.get(5)).text()).contains(\"6.97\", \"9.89\");\n\n assertThat(intermediateResponses).hasSize(2);\n assertThat(intermediateResponses.poll().aiMessage()).isEqualTo(aiMessage);\n assertThat(intermediateResponses.poll().aiMessage()).isEqualTo(secondAiMessage);\n }\n\n @Test\n void should_execute_multiple_tools_in_parallel_then_answer() throws Exception {\n\n Calculator calculator = spy(new Calculator());\n\n // TODO test more models\n StreamingChatModel streamingChatModel = OpenAiStreamingChatModel.builder()\n .baseUrl(System.getenv(\"OPENAI_BASE_URL\"))\n .apiKey(System.getenv(\"OPENAI_API_KEY\"))\n .organizationId(System.getenv(\"OPENAI_ORGANIZATION_ID\"))\n .modelName(GPT_4_O_MINI)\n .temperature(0.0)\n .logRequests(true)\n .logResponses(true)\n .build();\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatModel(streamingChatModel)\n .chatMemory(chatMemory)\n .tools(calculator)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<ChatResponse> futureResponse = new CompletableFuture<>();\n\n String userMessage = \"What is the square root of 485906798473894056 and 97866249624785 in scientific notation?\";\n\n assistant.chat(userMessage)\n .onPartialResponse(answerBuilder::append)\n .onCompleteResponse(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n ChatResponse response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"6.97\", \"9.89\");\n assertThat(response.aiMessage().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isPositive(); // TODO\n assertThat(tokenUsage.outputTokenCount()).isPositive();\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n\n verify(calculator).squareRoot(485906798473894056.0);\n verify(calculator).squareRoot(97866249624785.0);\n verifyNoMoreInteractions(calculator);\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(5);\n\n assertThat(messages.get(0)).isInstanceOf(dev.langchain4j.data.message.UserMessage.class);\n assertThat(((UserMessage) messages.get(0)).singleText()).isEqualTo(userMessage);\n\n AiMessage aiMessage = (AiMessage) messages.get(1);\n assertThat(aiMessage.text()).isNull();\n assertThat(aiMessage.toolExecutionRequests()).hasSize(2);\n\n ToolExecutionRequest firstToolExecutionRequest = aiMessage.toolExecutionRequests().get(0);\n assertThat(firstToolExecutionRequest.id()).isNotBlank();\n assertThat(firstToolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(firstToolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 485906798473894056}\");\n\n ToolExecutionRequest secondToolExecutionRequest = aiMessage.toolExecutionRequests().get(1);\n assertThat(secondToolExecutionRequest.id()).isNotBlank();\n assertThat(secondToolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 97866249624785}\");\n\n ToolExecutionResultMessage firstToolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(2);\n assertThat(firstToolExecutionResultMessage.id()).isEqualTo(firstToolExecutionRequest.id());\n assertThat(firstToolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(firstToolExecutionResultMessage.text()).isEqualTo(\"6.97070153193991E8\");\n\n ToolExecutionResultMessage secondToolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(3);\n assertThat(secondToolExecutionResultMessage.id()).isEqualTo(secondToolExecutionRequest.id());\n assertThat(secondToolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionResultMessage.text()).isEqualTo(\"9892737.215997653\");\n\n assertThat(messages.get(4)).isInstanceOf(AiMessage.class);\n assertThat(((AiMessage) messages.get(4)).text()).contains(\"6.97\", \"9.89\");\n }\n\n @Test\n @EnabledIfEnvironmentVariable(named = \"DEEPSEEK_API_KEY\", matches = \".+\")\n void should_stream_thinking() throws Exception {\n\n // given\n boolean returnThinking = true;\n\n StreamingChatModel streamingChatModel = OpenAiStreamingChatModel.builder()\n .baseUrl(\"https://api.deepseek.com/v1\")\n .apiKey(System.getenv(\"DEEPSEEK_API_KEY\"))\n .modelName(\"deepseek-reasoner\")\n .returnThinking(returnThinking)\n .logRequests(true)\n .logResponses(true)\n .build();\n\n Assistant assistant = AiServices.create(Assistant.class, streamingChatModel);\n\n interface TokenStreamHandler {\n void onPartialThinking(PartialThinking partialThinking);\n void onPartialResponse(String partialResponse);\n void onError(Throwable error);\n void onCompleteResponse(ChatResponse completeResponse);\n }\n\n TokenStreamHandler tokenStreamHandler = mock(TokenStreamHandler.class);\n CompletableFuture<ChatResponse> futureResponse = new CompletableFuture<>();\n\n // when\n assistant.chat(\"What is the capital of Germany?\")\n .onPartialThinking(partialThinking -> tokenStreamHandler.onPartialThinking(partialThinking))\n .onPartialResponse(partialResponse -> tokenStreamHandler.onPartialResponse(partialResponse))\n .onError(error -> {\n tokenStreamHandler.onError(error);\n futureResponse.completeExceptionally(error);\n })\n .onCompleteResponse(completeResponse -> {\n tokenStreamHandler.onCompleteResponse(completeResponse);\n futureResponse.complete(completeResponse);\n })\n .start();\n\n // then\n ChatResponse chatResponse = futureResponse.get(60, SECONDS);\n\n assertThat(chatResponse.aiMessage().text()).contains(\"Berlin\");\n\n InOrder inOrder = inOrder(tokenStreamHandler);\n inOrder.verify(tokenStreamHandler, atLeastOnce()).onPartialThinking(any());\n inOrder.verify(tokenStreamHandler, atLeastOnce()).onPartialResponse(any());\n inOrder.verify(tokenStreamHandler).onCompleteResponse(any());\n inOrder.verifyNoMoreInteractions();\n verifyNoMoreInteractions(tokenStreamHandler);\n }\n}\n"}, "03930ba8500f888083f59a9f4165468cf84e0ec1": {"t": 1785755656.6311593, "n": 23060, "h": "ff7af7d0092961f8", "c": "package dev.langchain4j.service;\n\nimport dev.langchain4j.agent.tool.P;\nimport dev.langchain4j.agent.tool.Tool;\nimport dev.langchain4j.agent.tool.ToolExecutionRequest;\nimport dev.langchain4j.data.message.AiMessage;\nimport dev.langchain4j.data.message.ChatMessage;\nimport dev.langchain4j.data.message.ToolExecutionResultMessage;\nimport dev.langchain4j.data.message.UserMessage;\nimport dev.langchain4j.memory.ChatMemory;\nimport dev.langchain4j.memory.chat.MessageWindowChatMemory;\nimport dev.langchain4j.model.chat.StreamingChatModel;\nimport dev.langchain4j.model.chat.response.ChatResponse;\nimport dev.langchain4j.model.chat.response.PartialThinking;\nimport dev.langchain4j.model.openai.OpenAiStreamingChatModel;\nimport dev.langchain4j.model.output.TokenUsage;\nimport dev.langchain4j.rag.AugmentationResult;\nimport dev.langchain4j.rag.RetrievalAugmentor;\nimport dev.langchain4j.rag.content.Content;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.MethodSource;\nimport org.mockito.InOrder;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.stream.Stream;\n\nimport static dev.langchain4j.model.openai.OpenAiChatModelName.GPT_4_O_MINI;\nimport static dev.langchain4j.model.output.FinishReason.STOP;\nimport static java.util.concurrent.TimeUnit.SECONDS;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.Mockito.any;\nimport static org.mockito.Mockito.atLeastOnce;\nimport static org.mockito.Mockito.inOrder;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.spy;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.verifyNoMoreInteractions;\nimport static org.mockito.Mockito.when;\n\n@EnabledIfEnvironmentVariable(named = \"OPENAI_API_KEY\", matches = \".+\")\nclass StreamingAiServicesIT {\n\n static Stream<StreamingChatModel> models() {\n return Stream.of(\n OpenAiStreamingChatModel.builder()\n .baseUrl(System.getenv(\"OPENAI_BASE_URL\"))\n .apiKey(System.getenv(\"OPENAI_API_KEY\"))\n .organizationId(System.getenv(\"OPENAI_ORGANIZATION_ID\"))\n .modelName(GPT_4_O_MINI)\n .logRequests(true)\n .logResponses(true)\n .build()\n // TODO add more models\n );\n }\n\n interface Assistant {\n\n TokenStream chat(String userMessage);\n }\n\n @ParameterizedTest\n @MethodSource(\"models\")\n void should_stream_answer(StreamingChatModel model) throws Exception {\n\n Assistant assistant = AiServices.create(Assistant.class, model);\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<ChatResponse> futureResponse = new CompletableFuture<>();\n\n assistant.chat(\"What is the capital of Germany?\")\n .onPartialResponse(answerBuilder::append)\n .onCompleteResponse(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n ChatResponse response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"Berlin\");\n assertThat(response.aiMessage().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isPositive();\n assertThat(tokenUsage.outputTokenCount()).isPositive();\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n }\n\n @ParameterizedTest\n @MethodSource(\"models\")\n void should_callback_with_content(StreamingChatModel model) throws Exception {\n\n List<Content> contents = new ArrayList<>();\n contents.add(Content.from(\"This is additional content\"));\n\n RetrievalAugmentor retrievalAugmentor = mock(RetrievalAugmentor.class);\n ChatMessage message = UserMessage.from(\"What is the capital of Germany?\");\n\n AugmentationResult result = AugmentationResult.builder()\n .chatMessage(message)\n .contents(contents)\n .build();\n when(retrievalAugmentor.augment(any())).thenReturn(result);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatModel(model)\n .retrievalAugmentor(retrievalAugmentor)\n .build();\n\n CompletableFuture<List<Content>> futureContent = new CompletableFuture<>();\n\n assistant.chat(\"What is the capital of Germany?\")\n .onPartialResponse(ignored -> {})\n .onRetrieved(futureContent::complete)\n .ignoreErrors()\n .start();\n\n List<Content> returnedContents = futureContent.get(30, SECONDS);\n\n assertThat(returnedContents)\n .hasSize(1)\n .anySatisfy(c -> assertThat(c.textSegment().text()).isEqualTo(\"This is additional content\"));\n }\n\n @ParameterizedTest\n @MethodSource(\"models\")\n void should_stream_answers_with_memory(StreamingChatModel model) throws Exception {\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatModel(model)\n .chatMemory(chatMemory)\n .build();\n\n\n String firstUserMessage = \"Hi, my name is Klaus\";\n CompletableFuture<ChatResponse> firstResponseFuture = new CompletableFuture<>();\n\n assistant.chat(firstUserMessage)\n .onPartialResponse(System.out::println)\n .onCompleteResponse(firstResponseFuture::complete)\n .onError(firstResponseFuture::completeExceptionally)\n .start();\n\n ChatResponse firstResponse = firstResponseFuture.get(30, SECONDS);\n assertThat(firstResponse.aiMessage().text()).contains(\"Klaus\");\n\n\n String secondUserMessage = \"What is my name?\";\n CompletableFuture<ChatResponse> secondResponseFuture = new CompletableFuture<>();\n\n assistant.chat(secondUserMessage)\n .onPartialResponse(System.out::println)\n .onCompleteResponse(secondResponseFuture::complete)\n .onError(secondResponseFuture::completeExceptionally)\n .start();\n\n ChatResponse secondResponse = secondResponseFuture.get(30, SECONDS);\n assertThat(secondResponse.aiMessage().text()).contains(\"Klaus\");\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(4);\n\n assertThat(messages.get(0)).isInstanceOf(UserMessage.class);\n assertThat(((UserMessage) messages.get(0)).singleText()).isEqualTo(firstUserMessage);\n\n assertThat(messages.get(1)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(1)).isEqualTo(firstResponse.aiMessage());\n\n assertThat(messages.get(2)).isInstanceOf(UserMessage.class);\n assertThat(((UserMessage) messages.get(2)).singleText()).isEqualTo(secondUserMessage);\n\n assertThat(messages.get(3)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(3)).isEqualTo(secondResponse.aiMessage());\n }\n\n static class Calculator {\n\n @Tool(\"calculates the square root of the provided number\")\n double squareRoot(@P(\"number to operate on\") double number) {\n return Math.sqrt(number);\n }\n }\n\n @ParameterizedTest\n @MethodSource(\"models\")\n void should_execute_a_tool_then_stream_answer(StreamingChatModel model) throws Exception {\n\n Calculator calculator = spy(new Calculator());\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatModel(model)\n .chatMemory(chatMemory)\n .tools(calculator)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<ChatResponse> futureResponse = new CompletableFuture<>();\n\n String userMessage = \"What is the square root of 485906798473894056 in scientific notation?\";\n\n assistant.chat(userMessage)\n .onPartialResponse(answerBuilder::append)\n .onCompleteResponse(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n ChatResponse response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"6.97\");\n assertThat(response.aiMessage().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isPositive();\n assertThat(tokenUsage.outputTokenCount()).isPositive();\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n\n verify(calculator).squareRoot(485906798473894056.0);\n verifyNoMoreInteractions(calculator);\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(4);\n\n assertThat(messages.get(0)).isInstanceOf(UserMessage.class);\n assertThat(((UserMessage) messages.get(0)).singleText()).isEqualTo(userMessage);\n\n AiMessage aiMessage = (AiMessage) messages.get(1);\n assertThat(aiMessage.text()).isNull();\n assertThat(aiMessage.toolExecutionRequests()).hasSize(1);\n ToolExecutionRequest toolExecutionRequest = aiMessage.toolExecutionRequests().get(0);\n assertThat(toolExecutionRequest.id()).isNotBlank();\n\n assertThat(toolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 485906798473894056}\");\n\n ToolExecutionResultMessage toolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(2);\n assertThat(toolExecutionResultMessage.id()).isEqualTo(toolExecutionRequest.id());\n assertThat(toolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionResultMessage.text()).isEqualTo(\"6.97070153193991E8\");\n\n assertThat(messages.get(3)).isInstanceOf(AiMessage.class);\n assertThat(((AiMessage) messages.get(3)).text()).contains(\"6.97\");\n }\n\n @Test\n void should_execute_multiple_tools_sequentially_then_answer() throws Exception {\n\n // TODO test more models\n StreamingChatModel streamingChatModel = OpenAiStreamingChatModel.builder()\n .baseUrl(System.getenv(\"OPENAI_BASE_URL\"))\n .apiKey(System.getenv(\"OPENAI_API_KEY\"))\n .organizationId(System.getenv(\"OPENAI_ORGANIZATION_ID\"))\n .modelName(GPT_4_O_MINI)\n .parallelToolCalls(false) // to force the model to call tools sequentially\n .temperature(0.0)\n .logRequests(true)\n .logResponses(true)\n .build();\n\n Calculator calculator = spy(new Calculator());\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatModel(streamingChatModel)\n .chatMemory(chatMemory)\n .tools(calculator)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<ChatResponse> futureResponse = new CompletableFuture<>();\n\n String userMessage = \"What is the square root of 485906798473894056 and 97866249624785 in scientific notation?\";\n\n assistant.chat(userMessage)\n .onPartialResponse(answerBuilder::append)\n .onCompleteResponse(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n ChatResponse response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"6.97\", \"9.89\");\n assertThat(response.aiMessage().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isPositive(); // TODO\n assertThat(tokenUsage.outputTokenCount()).isPositive();\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n\n verify(calculator).squareRoot(485906798473894056.0);\n verify(calculator).squareRoot(97866249624785.0);\n verifyNoMoreInteractions(calculator);\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(6);\n\n assertThat(messages.get(0)).isInstanceOf(dev.langchain4j.data.message.UserMessage.class);\n assertThat(((UserMessage) messages.get(0)).singleText()).isEqualTo(userMessage);\n\n AiMessage aiMessage = (AiMessage) messages.get(1);\n assertThat(aiMessage.text()).isNull();\n assertThat(aiMessage.toolExecutionRequests()).hasSize(1);\n ToolExecutionRequest toolExecutionRequest = aiMessage.toolExecutionRequests().get(0);\n assertThat(toolExecutionRequest.id()).isNotBlank();\n assertThat(toolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 485906798473894056}\");\n\n ToolExecutionResultMessage toolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(2);\n assertThat(toolExecutionResultMessage.id()).isEqualTo(toolExecutionRequest.id());\n assertThat(toolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionResultMessage.text()).isEqualTo(\"6.97070153193991E8\");\n\n AiMessage secondAiMessage = (AiMessage) messages.get(3);\n assertThat(secondAiMessage.text()).isNull();\n assertThat(secondAiMessage.toolExecutionRequests()).hasSize(1);\n ToolExecutionRequest secondToolExecutionRequest = secondAiMessage.toolExecutionRequests().get(0);\n assertThat(secondToolExecutionRequest.id()).isNotBlank();\n assertThat(secondToolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 97866249624785}\");\n\n ToolExecutionResultMessage secondToolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(4);\n assertThat(secondToolExecutionResultMessage.id()).isEqualTo(secondToolExecutionRequest.id());\n assertThat(secondToolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionResultMessage.text()).isEqualTo(\"9892737.215997653\");\n\n assertThat(messages.get(5)).isInstanceOf(AiMessage.class);\n assertThat(((AiMessage) messages.get(5)).text()).contains(\"6.97\", \"9.89\");\n }\n\n @Test\n void should_execute_multiple_tools_in_parallel_then_answer() throws Exception {\n\n Calculator calculator = spy(new Calculator());\n\n // TODO test more models\n StreamingChatModel streamingChatModel = OpenAiStreamingChatModel.builder()\n .baseUrl(System.getenv(\"OPENAI_BASE_URL\"))\n .apiKey(System.getenv(\"OPENAI_API_KEY\"))\n .organizationId(System.getenv(\"OPENAI_ORGANIZATION_ID\"))\n .modelName(GPT_4_O_MINI)\n .temperature(0.0)\n .logRequests(true)\n .logResponses(true)\n .build();\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatModel(streamingChatModel)\n .chatMemory(chatMemory)\n .tools(calculator)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<ChatResponse> futureResponse = new CompletableFuture<>();\n\n String userMessage = \"What is the square root of 485906798473894056 and 97866249624785 in scientific notation?\";\n\n assistant.chat(userMessage)\n .onPartialResponse(answerBuilder::append)\n .onCompleteResponse(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n ChatResponse response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"6.97\", \"9.89\");\n assertThat(response.aiMessage().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isPositive(); // TODO\n assertThat(tokenUsage.outputTokenCount()).isPositive();\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n\n verify(calculator).squareRoot(485906798473894056.0);\n verify(calculator).squareRoot(97866249624785.0);\n verifyNoMoreInteractions(calculator);\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(5);\n\n assertThat(messages.get(0)).isInstanceOf(dev.langchain4j.data.message.UserMessage.class);\n assertThat(((UserMessage) messages.get(0)).singleText()).isEqualTo(userMessage);\n\n AiMessage aiMessage = (AiMessage) messages.get(1);\n assertThat(aiMessage.text()).isNull();\n assertThat(aiMessage.toolExecutionRequests()).hasSize(2);\n\n ToolExecutionRequest firstToolExecutionRequest = aiMessage.toolExecutionRequests().get(0);\n assertThat(firstToolExecutionRequest.id()).isNotBlank();\n assertThat(firstToolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(firstToolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 485906798473894056}\");\n\n ToolExecutionRequest secondToolExecutionRequest = aiMessage.toolExecutionRequests().get(1);\n assertThat(secondToolExecutionRequest.id()).isNotBlank();\n assertThat(secondToolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 97866249624785}\");\n\n ToolExecutionResultMessage firstToolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(2);\n assertThat(firstToolExecutionResultMessage.id()).isEqualTo(firstToolExecutionRequest.id());\n assertThat(firstToolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(firstToolExecutionResultMessage.text()).isEqualTo(\"6.97070153193991E8\");\n\n ToolExecutionResultMessage secondToolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(3);\n assertThat(secondToolExecutionResultMessage.id()).isEqualTo(secondToolExecutionRequest.id());\n assertThat(secondToolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionResultMessage.text()).isEqualTo(\"9892737.215997653\");\n\n assertThat(messages.get(4)).isInstanceOf(AiMessage.class);\n assertThat(((AiMessage) messages.get(4)).text()).contains(\"6.97\", \"9.89\");\n }\n\n @Test\n @EnabledIfEnvironmentVariable(named = \"DEEPSEEK_API_KEY\", matches = \".+\")\n void should_stream_thinking() throws Exception {\n\n // given\n boolean returnThinking = true;\n\n StreamingChatModel streamingChatModel = OpenAiStreamingChatModel.builder()\n .baseUrl(\"https://api.deepseek.com/v1\")\n .apiKey(System.getenv(\"DEEPSEEK_API_KEY\"))\n .modelName(\"deepseek-reasoner\")\n .returnThinking(returnThinking)\n .logRequests(true)\n .logResponses(true)\n .build();\n\n Assistant assistant = AiServices.create(Assistant.class, streamingChatModel);\n\n interface TokenStreamHandler {\n void onPartialThinking(PartialThinking partialThinking);\n void onPartialResponse(String partialResponse);\n void onError(Throwable error);\n void onCompleteResponse(ChatResponse completeResponse);\n }\n\n TokenStreamHandler tokenStreamHandler = mock(TokenStreamHandler.class);\n CompletableFuture<ChatResponse> futureResponse = new CompletableFuture<>();\n\n // when\n assistant.chat(\"What is the capital of Germany?\")\n .onPartialThinking(partialThinking -> tokenStreamHandler.onPartialThinking(partialThinking))\n .onPartialResponse(partialResponse -> tokenStreamHandler.onPartialResponse(partialResponse))\n .onError(error -> {\n tokenStreamHandler.onError(error);\n futureResponse.completeExceptionally(error);\n })\n .onCompleteResponse(completeResponse -> {\n tokenStreamHandler.onCompleteResponse(completeResponse);\n futureResponse.complete(completeResponse);\n })\n .start();\n\n // then\n ChatResponse chatResponse = futureResponse.get(60, SECONDS);\n\n assertThat(chatResponse.aiMessage().text()).contains(\"Berlin\");\n\n InOrder inOrder = inOrder(tokenStreamHandler);\n inOrder.verify(tokenStreamHandler, atLeastOnce()).onPartialThinking(any());\n inOrder.verify(tokenStreamHandler, atLeastOnce()).onPartialResponse(any());\n inOrder.verify(tokenStreamHandler).onCompleteResponse(any());\n inOrder.verifyNoMoreInteractions();\n verifyNoMoreInteractions(tokenStreamHandler);\n }\n}\n"}, "13ad410fe887ca5f95c16923c6a370176b1865be": {"t": 1785755658.436305, "n": 20481, "h": "4f33ed11c311d417", "c": "package dev.langchain4j.service;\n\nimport dev.langchain4j.agent.tool.P;\nimport dev.langchain4j.agent.tool.Tool;\nimport dev.langchain4j.agent.tool.ToolExecutionRequest;\nimport dev.langchain4j.data.message.AiMessage;\nimport dev.langchain4j.data.message.ChatMessage;\nimport dev.langchain4j.data.message.ToolExecutionResultMessage;\nimport dev.langchain4j.data.message.UserMessage;\nimport dev.langchain4j.memory.ChatMemory;\nimport dev.langchain4j.memory.chat.MessageWindowChatMemory;\nimport dev.langchain4j.model.chat.StreamingChatModel;\nimport dev.langchain4j.model.chat.response.ChatResponse;\nimport dev.langchain4j.model.openai.OpenAiStreamingChatModel;\nimport dev.langchain4j.model.output.TokenUsage;\nimport dev.langchain4j.rag.AugmentationResult;\nimport dev.langchain4j.rag.RetrievalAugmentor;\nimport dev.langchain4j.rag.content.Content;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.MethodSource;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.stream.Stream;\n\nimport static dev.langchain4j.model.openai.OpenAiChatModelName.GPT_4_O_MINI;\nimport static dev.langchain4j.model.output.FinishReason.STOP;\nimport static java.util.concurrent.TimeUnit.SECONDS;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.Mockito.any;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.spy;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.verifyNoMoreInteractions;\nimport static org.mockito.Mockito.when;\n\n@EnabledIfEnvironmentVariable(named = \"OPENAI_API_KEY\", matches = \".+\")\nclass StreamingAiServicesIT {\n\n static Stream<StreamingChatModel> models() {\n return Stream.of(\n OpenAiStreamingChatModel.builder()\n .baseUrl(System.getenv(\"OPENAI_BASE_URL\"))\n .apiKey(System.getenv(\"OPENAI_API_KEY\"))\n .organizationId(System.getenv(\"OPENAI_ORGANIZATION_ID\"))\n .modelName(GPT_4_O_MINI)\n .logRequests(true)\n .logResponses(true)\n .build()\n // TODO add more models\n );\n }\n\n interface Assistant {\n\n TokenStream chat(String userMessage);\n }\n\n @ParameterizedTest\n @MethodSource(\"models\")\n void should_stream_answer(StreamingChatModel model) throws Exception {\n\n Assistant assistant = AiServices.create(Assistant.class, model);\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<ChatResponse> futureResponse = new CompletableFuture<>();\n\n assistant.chat(\"What is the capital of Germany?\")\n .onPartialResponse(answerBuilder::append)\n .onCompleteResponse(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n ChatResponse response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"Berlin\");\n assertThat(response.aiMessage().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isPositive();\n assertThat(tokenUsage.outputTokenCount()).isPositive();\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n }\n\n @ParameterizedTest\n @MethodSource(\"models\")\n void should_callback_with_content(StreamingChatModel model) throws Exception {\n\n List<Content> contents = new ArrayList<>();\n contents.add(Content.from(\"This is additional content\"));\n\n RetrievalAugmentor retrievalAugmentor = mock(RetrievalAugmentor.class);\n ChatMessage message = UserMessage.from(\"What is the capital of Germany?\");\n\n AugmentationResult result = AugmentationResult.builder()\n .chatMessage(message)\n .contents(contents)\n .build();\n when(retrievalAugmentor.augment(any())).thenReturn(result);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatModel(model)\n .retrievalAugmentor(retrievalAugmentor)\n .build();\n\n CompletableFuture<List<Content>> futureContent = new CompletableFuture<>();\n\n assistant.chat(\"What is the capital of Germany?\")\n .onPartialResponse(ignored -> {})\n .onRetrieved(futureContent::complete)\n .ignoreErrors()\n .start();\n\n List<Content> returnedContents = futureContent.get(30, SECONDS);\n\n assertThat(returnedContents)\n .hasSize(1)\n .anySatisfy(c -> assertThat(c.textSegment().text()).isEqualTo(\"This is additional content\"));\n }\n\n @ParameterizedTest\n @MethodSource(\"models\")\n void should_stream_answers_with_memory(StreamingChatModel model) throws Exception {\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatModel(model)\n .chatMemory(chatMemory)\n .build();\n\n\n String firstUserMessage = \"Hi, my name is Klaus\";\n CompletableFuture<ChatResponse> firstResponseFuture = new CompletableFuture<>();\n\n assistant.chat(firstUserMessage)\n .onPartialResponse(System.out::println)\n .onCompleteResponse(firstResponseFuture::complete)\n .onError(firstResponseFuture::completeExceptionally)\n .start();\n\n ChatResponse firstResponse = firstResponseFuture.get(30, SECONDS);\n assertThat(firstResponse.aiMessage().text()).contains(\"Klaus\");\n\n\n String secondUserMessage = \"What is my name?\";\n CompletableFuture<ChatResponse> secondResponseFuture = new CompletableFuture<>();\n\n assistant.chat(secondUserMessage)\n .onPartialResponse(System.out::println)\n .onCompleteResponse(secondResponseFuture::complete)\n .onError(secondResponseFuture::completeExceptionally)\n .start();\n\n ChatResponse secondResponse = secondResponseFuture.get(30, SECONDS);\n assertThat(secondResponse.aiMessage().text()).contains(\"Klaus\");\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(4);\n\n assertThat(messages.get(0)).isInstanceOf(UserMessage.class);\n assertThat(((UserMessage) messages.get(0)).singleText()).isEqualTo(firstUserMessage);\n\n assertThat(messages.get(1)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(1)).isEqualTo(firstResponse.aiMessage());\n\n assertThat(messages.get(2)).isInstanceOf(UserMessage.class);\n assertThat(((UserMessage) messages.get(2)).singleText()).isEqualTo(secondUserMessage);\n\n assertThat(messages.get(3)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(3)).isEqualTo(secondResponse.aiMessage());\n }\n\n static class Calculator {\n\n @Tool(\"calculates the square root of the provided number\")\n double squareRoot(@P(\"number to operate on\") double number) {\n return Math.sqrt(number);\n }\n }\n\n @ParameterizedTest\n @MethodSource(\"models\")\n void should_execute_a_tool_then_stream_answer(StreamingChatModel model) throws Exception {\n\n Calculator calculator = spy(new Calculator());\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatModel(model)\n .chatMemory(chatMemory)\n .tools(calculator)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<ChatResponse> futureResponse = new CompletableFuture<>();\n\n String userMessage = \"What is the square root of 485906798473894056 in scientific notation?\";\n\n assistant.chat(userMessage)\n .onPartialResponse(answerBuilder::append)\n .onCompleteResponse(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n ChatResponse response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"6.97\");\n assertThat(response.aiMessage().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isPositive();\n assertThat(tokenUsage.outputTokenCount()).isPositive();\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n\n verify(calculator).squareRoot(485906798473894056.0);\n verifyNoMoreInteractions(calculator);\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(4);\n\n assertThat(messages.get(0)).isInstanceOf(UserMessage.class);\n assertThat(((UserMessage) messages.get(0)).singleText()).isEqualTo(userMessage);\n\n AiMessage aiMessage = (AiMessage) messages.get(1);\n assertThat(aiMessage.text()).isNull();\n assertThat(aiMessage.toolExecutionRequests()).hasSize(1);\n ToolExecutionRequest toolExecutionRequest = aiMessage.toolExecutionRequests().get(0);\n assertThat(toolExecutionRequest.id()).isNotBlank();\n\n assertThat(toolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 485906798473894056}\");\n\n ToolExecutionResultMessage toolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(2);\n assertThat(toolExecutionResultMessage.id()).isEqualTo(toolExecutionRequest.id());\n assertThat(toolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionResultMessage.text()).isEqualTo(\"6.97070153193991E8\");\n\n assertThat(messages.get(3)).isInstanceOf(AiMessage.class);\n assertThat(((AiMessage) messages.get(3)).text()).contains(\"6.97\");\n }\n\n @Test\n void should_execute_multiple_tools_sequentially_then_answer() throws Exception {\n\n // TODO test more models\n StreamingChatModel streamingChatModel = OpenAiStreamingChatModel.builder()\n .baseUrl(System.getenv(\"OPENAI_BASE_URL\"))\n .apiKey(System.getenv(\"OPENAI_API_KEY\"))\n .organizationId(System.getenv(\"OPENAI_ORGANIZATION_ID\"))\n .modelName(GPT_4_O_MINI)\n .parallelToolCalls(false) // to force the model to call tools sequentially\n .temperature(0.0)\n .logRequests(true)\n .logResponses(true)\n .build();\n\n Calculator calculator = spy(new Calculator());\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatModel(streamingChatModel)\n .chatMemory(chatMemory)\n .tools(calculator)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<ChatResponse> futureResponse = new CompletableFuture<>();\n\n String userMessage = \"What is the square root of 485906798473894056 and 97866249624785 in scientific notation?\";\n\n assistant.chat(userMessage)\n .onPartialResponse(answerBuilder::append)\n .onCompleteResponse(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n ChatResponse response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"6.97\", \"9.89\");\n assertThat(response.aiMessage().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isPositive(); // TODO\n assertThat(tokenUsage.outputTokenCount()).isPositive();\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n\n verify(calculator).squareRoot(485906798473894056.0);\n verify(calculator).squareRoot(97866249624785.0);\n verifyNoMoreInteractions(calculator);\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(6);\n\n assertThat(messages.get(0)).isInstanceOf(dev.langchain4j.data.message.UserMessage.class);\n assertThat(((UserMessage) messages.get(0)).singleText()).isEqualTo(userMessage);\n\n AiMessage aiMessage = (AiMessage) messages.get(1);\n assertThat(aiMessage.text()).isNull();\n assertThat(aiMessage.toolExecutionRequests()).hasSize(1);\n ToolExecutionRequest toolExecutionRequest = aiMessage.toolExecutionRequests().get(0);\n assertThat(toolExecutionRequest.id()).isNotBlank();\n assertThat(toolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 485906798473894056}\");\n\n ToolExecutionResultMessage toolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(2);\n assertThat(toolExecutionResultMessage.id()).isEqualTo(toolExecutionRequest.id());\n assertThat(toolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionResultMessage.text()).isEqualTo(\"6.97070153193991E8\");\n\n AiMessage secondAiMessage = (AiMessage) messages.get(3);\n assertThat(secondAiMessage.text()).isNull();\n assertThat(secondAiMessage.toolExecutionRequests()).hasSize(1);\n ToolExecutionRequest secondToolExecutionRequest = secondAiMessage.toolExecutionRequests().get(0);\n assertThat(secondToolExecutionRequest.id()).isNotBlank();\n assertThat(secondToolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 97866249624785}\");\n\n ToolExecutionResultMessage secondToolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(4);\n assertThat(secondToolExecutionResultMessage.id()).isEqualTo(secondToolExecutionRequest.id());\n assertThat(secondToolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionResultMessage.text()).isEqualTo(\"9892737.215997653\");\n\n assertThat(messages.get(5)).isInstanceOf(AiMessage.class);\n assertThat(((AiMessage) messages.get(5)).text()).contains(\"6.97\", \"9.89\");\n }\n\n @Test\n void should_execute_multiple_tools_in_parallel_then_answer() throws Exception {\n\n Calculator calculator = spy(new Calculator());\n\n // TODO test more models\n StreamingChatModel streamingChatModel = OpenAiStreamingChatModel.builder()\n .baseUrl(System.getenv(\"OPENAI_BASE_URL\"))\n .apiKey(System.getenv(\"OPENAI_API_KEY\"))\n .organizationId(System.getenv(\"OPENAI_ORGANIZATION_ID\"))\n .modelName(GPT_4_O_MINI)\n .temperature(0.0)\n .logRequests(true)\n .logResponses(true)\n .build();\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatModel(streamingChatModel)\n .chatMemory(chatMemory)\n .tools(calculator)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<ChatResponse> futureResponse = new CompletableFuture<>();\n\n String userMessage = \"What is the square root of 485906798473894056 and 97866249624785 in scientific notation?\";\n\n assistant.chat(userMessage)\n .onPartialResponse(answerBuilder::append)\n .onCompleteResponse(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n ChatResponse response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"6.97\", \"9.89\");\n assertThat(response.aiMessage().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isPositive(); // TODO\n assertThat(tokenUsage.outputTokenCount()).isPositive();\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n\n verify(calculator).squareRoot(485906798473894056.0);\n verify(calculator).squareRoot(97866249624785.0);\n verifyNoMoreInteractions(calculator);\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(5);\n\n assertThat(messages.get(0)).isInstanceOf(dev.langchain4j.data.message.UserMessage.class);\n assertThat(((UserMessage) messages.get(0)).singleText()).isEqualTo(userMessage);\n\n AiMessage aiMessage = (AiMessage) messages.get(1);\n assertThat(aiMessage.text()).isNull();\n assertThat(aiMessage.toolExecutionRequests()).hasSize(2);\n\n ToolExecutionRequest firstToolExecutionRequest = aiMessage.toolExecutionRequests().get(0);\n assertThat(firstToolExecutionRequest.id()).isNotBlank();\n assertThat(firstToolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(firstToolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 485906798473894056}\");\n\n ToolExecutionRequest secondToolExecutionRequest = aiMessage.toolExecutionRequests().get(1);\n assertThat(secondToolExecutionRequest.id()).isNotBlank();\n assertThat(secondToolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 97866249624785}\");\n\n ToolExecutionResultMessage firstToolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(2);\n assertThat(firstToolExecutionResultMessage.id()).isEqualTo(firstToolExecutionRequest.id());\n assertThat(firstToolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(firstToolExecutionResultMessage.text()).isEqualTo(\"6.97070153193991E8\");\n\n ToolExecutionResultMessage secondToolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(3);\n assertThat(secondToolExecutionResultMessage.id()).isEqualTo(secondToolExecutionRequest.id());\n assertThat(secondToolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionResultMessage.text()).isEqualTo(\"9892737.215997653\");\n\n assertThat(messages.get(4)).isInstanceOf(AiMessage.class);\n assertThat(((AiMessage) messages.get(4)).text()).contains(\"6.97\", \"9.89\");\n }\n}\n"}, "d86c4b999d54ee3565aecc71234a8c433cc78be6": {"t": 1785755659.810657, "n": 20585, "h": "2cb927242d07ce8e", "c": "package dev.langchain4j.service;\n\nimport dev.langchain4j.agent.tool.P;\nimport dev.langchain4j.agent.tool.Tool;\nimport dev.langchain4j.agent.tool.ToolExecutionRequest;\nimport dev.langchain4j.data.message.AiMessage;\nimport dev.langchain4j.data.message.ChatMessage;\nimport dev.langchain4j.data.message.ToolExecutionResultMessage;\nimport dev.langchain4j.data.message.UserMessage;\nimport dev.langchain4j.memory.ChatMemory;\nimport dev.langchain4j.memory.chat.MessageWindowChatMemory;\nimport dev.langchain4j.model.chat.StreamingChatLanguageModel;\nimport dev.langchain4j.model.chat.response.ChatResponse;\nimport dev.langchain4j.model.openai.OpenAiStreamingChatModel;\nimport dev.langchain4j.model.output.TokenUsage;\nimport dev.langchain4j.rag.AugmentationResult;\nimport dev.langchain4j.rag.RetrievalAugmentor;\nimport dev.langchain4j.rag.content.Content;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.MethodSource;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.stream.Stream;\n\nimport static dev.langchain4j.model.openai.OpenAiChatModelName.GPT_4_O_MINI;\nimport static dev.langchain4j.model.output.FinishReason.STOP;\nimport static java.util.concurrent.TimeUnit.SECONDS;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.Mockito.any;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.spy;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.verifyNoMoreInteractions;\nimport static org.mockito.Mockito.when;\n\n@EnabledIfEnvironmentVariable(named = \"OPENAI_API_KEY\", matches = \".+\")\nclass StreamingAiServicesIT {\n\n static Stream<StreamingChatLanguageModel> models() {\n return Stream.of(\n OpenAiStreamingChatModel.builder()\n .baseUrl(System.getenv(\"OPENAI_BASE_URL\"))\n .apiKey(System.getenv(\"OPENAI_API_KEY\"))\n .organizationId(System.getenv(\"OPENAI_ORGANIZATION_ID\"))\n .modelName(GPT_4_O_MINI)\n .logRequests(true)\n .logResponses(true)\n .build()\n // TODO add more models\n );\n }\n\n interface Assistant {\n\n TokenStream chat(String userMessage);\n }\n\n @ParameterizedTest\n @MethodSource(\"models\")\n void should_stream_answer(StreamingChatLanguageModel model) throws Exception {\n\n Assistant assistant = AiServices.create(Assistant.class, model);\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<ChatResponse> futureResponse = new CompletableFuture<>();\n\n assistant.chat(\"What is the capital of Germany?\")\n .onPartialResponse(answerBuilder::append)\n .onCompleteResponse(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n ChatResponse response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"Berlin\");\n assertThat(response.aiMessage().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isPositive();\n assertThat(tokenUsage.outputTokenCount()).isPositive();\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n }\n\n @ParameterizedTest\n @MethodSource(\"models\")\n void should_callback_with_content(StreamingChatLanguageModel model) throws Exception {\n\n List<Content> contents = new ArrayList<>();\n contents.add(Content.from(\"This is additional content\"));\n\n RetrievalAugmentor retrievalAugmentor = mock(RetrievalAugmentor.class);\n ChatMessage message = UserMessage.from(\"What is the capital of Germany?\");\n\n AugmentationResult result = AugmentationResult.builder()\n .chatMessage(message)\n .contents(contents)\n .build();\n when(retrievalAugmentor.augment(any())).thenReturn(result);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(model)\n .retrievalAugmentor(retrievalAugmentor)\n .build();\n\n CompletableFuture<List<Content>> futureContent = new CompletableFuture<>();\n\n assistant.chat(\"What is the capital of Germany?\")\n .onPartialResponse(ignored -> {})\n .onRetrieved(futureContent::complete)\n .ignoreErrors()\n .start();\n\n List<Content> returnedContents = futureContent.get(30, SECONDS);\n\n assertThat(returnedContents)\n .hasSize(1)\n .anySatisfy(c -> assertThat(c.textSegment().text()).isEqualTo(\"This is additional content\"));\n }\n\n @ParameterizedTest\n @MethodSource(\"models\")\n void should_stream_answers_with_memory(StreamingChatLanguageModel model) throws Exception {\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(model)\n .chatMemory(chatMemory)\n .build();\n\n\n String firstUserMessage = \"Hi, my name is Klaus\";\n CompletableFuture<ChatResponse> firstResponseFuture = new CompletableFuture<>();\n\n assistant.chat(firstUserMessage)\n .onPartialResponse(System.out::println)\n .onCompleteResponse(firstResponseFuture::complete)\n .onError(firstResponseFuture::completeExceptionally)\n .start();\n\n ChatResponse firstResponse = firstResponseFuture.get(30, SECONDS);\n assertThat(firstResponse.aiMessage().text()).contains(\"Klaus\");\n\n\n String secondUserMessage = \"What is my name?\";\n CompletableFuture<ChatResponse> secondResponseFuture = new CompletableFuture<>();\n\n assistant.chat(secondUserMessage)\n .onPartialResponse(System.out::println)\n .onCompleteResponse(secondResponseFuture::complete)\n .onError(secondResponseFuture::completeExceptionally)\n .start();\n\n ChatResponse secondResponse = secondResponseFuture.get(30, SECONDS);\n assertThat(secondResponse.aiMessage().text()).contains(\"Klaus\");\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(4);\n\n assertThat(messages.get(0)).isInstanceOf(UserMessage.class);\n assertThat(((UserMessage) messages.get(0)).singleText()).isEqualTo(firstUserMessage);\n\n assertThat(messages.get(1)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(1)).isEqualTo(firstResponse.aiMessage());\n\n assertThat(messages.get(2)).isInstanceOf(UserMessage.class);\n assertThat(((UserMessage) messages.get(2)).singleText()).isEqualTo(secondUserMessage);\n\n assertThat(messages.get(3)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(3)).isEqualTo(secondResponse.aiMessage());\n }\n\n static class Calculator {\n\n @Tool(\"calculates the square root of the provided number\")\n double squareRoot(@P(\"number to operate on\") double number) {\n return Math.sqrt(number);\n }\n }\n\n @ParameterizedTest\n @MethodSource(\"models\")\n void should_execute_a_tool_then_stream_answer(StreamingChatLanguageModel model) throws Exception {\n\n Calculator calculator = spy(new Calculator());\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(model)\n .chatMemory(chatMemory)\n .tools(calculator)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<ChatResponse> futureResponse = new CompletableFuture<>();\n\n String userMessage = \"What is the square root of 485906798473894056 in scientific notation?\";\n\n assistant.chat(userMessage)\n .onPartialResponse(answerBuilder::append)\n .onCompleteResponse(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n ChatResponse response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"6.97\");\n assertThat(response.aiMessage().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isPositive();\n assertThat(tokenUsage.outputTokenCount()).isPositive();\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n\n verify(calculator).squareRoot(485906798473894056.0);\n verifyNoMoreInteractions(calculator);\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(4);\n\n assertThat(messages.get(0)).isInstanceOf(UserMessage.class);\n assertThat(((UserMessage) messages.get(0)).singleText()).isEqualTo(userMessage);\n\n AiMessage aiMessage = (AiMessage) messages.get(1);\n assertThat(aiMessage.text()).isNull();\n assertThat(aiMessage.toolExecutionRequests()).hasSize(1);\n ToolExecutionRequest toolExecutionRequest = aiMessage.toolExecutionRequests().get(0);\n assertThat(toolExecutionRequest.id()).isNotBlank();\n\n assertThat(toolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 485906798473894056}\");\n\n ToolExecutionResultMessage toolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(2);\n assertThat(toolExecutionResultMessage.id()).isEqualTo(toolExecutionRequest.id());\n assertThat(toolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionResultMessage.text()).isEqualTo(\"6.97070153193991E8\");\n\n assertThat(messages.get(3)).isInstanceOf(AiMessage.class);\n assertThat(((AiMessage) messages.get(3)).text()).contains(\"6.97\");\n }\n\n @Test\n void should_execute_multiple_tools_sequentially_then_answer() throws Exception {\n\n // TODO test more models\n StreamingChatLanguageModel streamingChatModel = OpenAiStreamingChatModel.builder()\n .baseUrl(System.getenv(\"OPENAI_BASE_URL\"))\n .apiKey(System.getenv(\"OPENAI_API_KEY\"))\n .organizationId(System.getenv(\"OPENAI_ORGANIZATION_ID\"))\n .modelName(GPT_4_O_MINI)\n .parallelToolCalls(false) // to force the model to call tools sequentially\n .temperature(0.0)\n .logRequests(true)\n .logResponses(true)\n .build();\n\n Calculator calculator = spy(new Calculator());\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(streamingChatModel)\n .chatMemory(chatMemory)\n .tools(calculator)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<ChatResponse> futureResponse = new CompletableFuture<>();\n\n String userMessage = \"What is the square root of 485906798473894056 and 97866249624785 in scientific notation?\";\n\n assistant.chat(userMessage)\n .onPartialResponse(answerBuilder::append)\n .onCompleteResponse(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n ChatResponse response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"6.97\", \"9.89\");\n assertThat(response.aiMessage().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isPositive(); // TODO\n assertThat(tokenUsage.outputTokenCount()).isPositive();\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n\n verify(calculator).squareRoot(485906798473894056.0);\n verify(calculator).squareRoot(97866249624785.0);\n verifyNoMoreInteractions(calculator);\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(6);\n\n assertThat(messages.get(0)).isInstanceOf(dev.langchain4j.data.message.UserMessage.class);\n assertThat(((UserMessage) messages.get(0)).singleText()).isEqualTo(userMessage);\n\n AiMessage aiMessage = (AiMessage) messages.get(1);\n assertThat(aiMessage.text()).isNull();\n assertThat(aiMessage.toolExecutionRequests()).hasSize(1);\n ToolExecutionRequest toolExecutionRequest = aiMessage.toolExecutionRequests().get(0);\n assertThat(toolExecutionRequest.id()).isNotBlank();\n assertThat(toolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 485906798473894056}\");\n\n ToolExecutionResultMessage toolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(2);\n assertThat(toolExecutionResultMessage.id()).isEqualTo(toolExecutionRequest.id());\n assertThat(toolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionResultMessage.text()).isEqualTo(\"6.97070153193991E8\");\n\n AiMessage secondAiMessage = (AiMessage) messages.get(3);\n assertThat(secondAiMessage.text()).isNull();\n assertThat(secondAiMessage.toolExecutionRequests()).hasSize(1);\n ToolExecutionRequest secondToolExecutionRequest = secondAiMessage.toolExecutionRequests().get(0);\n assertThat(secondToolExecutionRequest.id()).isNotBlank();\n assertThat(secondToolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 97866249624785}\");\n\n ToolExecutionResultMessage secondToolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(4);\n assertThat(secondToolExecutionResultMessage.id()).isEqualTo(secondToolExecutionRequest.id());\n assertThat(secondToolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionResultMessage.text()).isEqualTo(\"9892737.215997653\");\n\n assertThat(messages.get(5)).isInstanceOf(AiMessage.class);\n assertThat(((AiMessage) messages.get(5)).text()).contains(\"6.97\", \"9.89\");\n }\n\n @Test\n void should_execute_multiple_tools_in_parallel_then_answer() throws Exception {\n\n Calculator calculator = spy(new Calculator());\n\n // TODO test more models\n StreamingChatLanguageModel streamingChatModel = OpenAiStreamingChatModel.builder()\n .baseUrl(System.getenv(\"OPENAI_BASE_URL\"))\n .apiKey(System.getenv(\"OPENAI_API_KEY\"))\n .organizationId(System.getenv(\"OPENAI_ORGANIZATION_ID\"))\n .modelName(GPT_4_O_MINI)\n .temperature(0.0)\n .logRequests(true)\n .logResponses(true)\n .build();\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(streamingChatModel)\n .chatMemory(chatMemory)\n .tools(calculator)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<ChatResponse> futureResponse = new CompletableFuture<>();\n\n String userMessage = \"What is the square root of 485906798473894056 and 97866249624785 in scientific notation?\";\n\n assistant.chat(userMessage)\n .onPartialResponse(answerBuilder::append)\n .onCompleteResponse(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n ChatResponse response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"6.97\", \"9.89\");\n assertThat(response.aiMessage().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isPositive(); // TODO\n assertThat(tokenUsage.outputTokenCount()).isPositive();\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n\n verify(calculator).squareRoot(485906798473894056.0);\n verify(calculator).squareRoot(97866249624785.0);\n verifyNoMoreInteractions(calculator);\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(5);\n\n assertThat(messages.get(0)).isInstanceOf(dev.langchain4j.data.message.UserMessage.class);\n assertThat(((UserMessage) messages.get(0)).singleText()).isEqualTo(userMessage);\n\n AiMessage aiMessage = (AiMessage) messages.get(1);\n assertThat(aiMessage.text()).isNull();\n assertThat(aiMessage.toolExecutionRequests()).hasSize(2);\n\n ToolExecutionRequest firstToolExecutionRequest = aiMessage.toolExecutionRequests().get(0);\n assertThat(firstToolExecutionRequest.id()).isNotBlank();\n assertThat(firstToolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(firstToolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 485906798473894056}\");\n\n ToolExecutionRequest secondToolExecutionRequest = aiMessage.toolExecutionRequests().get(1);\n assertThat(secondToolExecutionRequest.id()).isNotBlank();\n assertThat(secondToolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 97866249624785}\");\n\n ToolExecutionResultMessage firstToolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(2);\n assertThat(firstToolExecutionResultMessage.id()).isEqualTo(firstToolExecutionRequest.id());\n assertThat(firstToolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(firstToolExecutionResultMessage.text()).isEqualTo(\"6.97070153193991E8\");\n\n ToolExecutionResultMessage secondToolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(3);\n assertThat(secondToolExecutionResultMessage.id()).isEqualTo(secondToolExecutionRequest.id());\n assertThat(secondToolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionResultMessage.text()).isEqualTo(\"9892737.215997653\");\n\n assertThat(messages.get(4)).isInstanceOf(AiMessage.class);\n assertThat(((AiMessage) messages.get(4)).text()).contains(\"6.97\", \"9.89\");\n }\n}\n"}, "b3dec0a455da08589df882e23b18eb58cbbf83da": {"t": 1785755661.286726, "n": 20433, "h": "84e41da88f92844c", "c": "package dev.langchain4j.service;\n\nimport dev.langchain4j.agent.tool.P;\nimport dev.langchain4j.agent.tool.Tool;\nimport dev.langchain4j.agent.tool.ToolExecutionRequest;\nimport dev.langchain4j.data.message.AiMessage;\nimport dev.langchain4j.data.message.ChatMessage;\nimport dev.langchain4j.data.message.ToolExecutionResultMessage;\nimport dev.langchain4j.data.message.UserMessage;\nimport dev.langchain4j.memory.ChatMemory;\nimport dev.langchain4j.memory.chat.MessageWindowChatMemory;\nimport dev.langchain4j.model.chat.StreamingChatLanguageModel;\nimport dev.langchain4j.model.chat.response.ChatResponse;\nimport dev.langchain4j.model.openai.OpenAiStreamingChatModel;\nimport dev.langchain4j.model.output.TokenUsage;\nimport dev.langchain4j.rag.AugmentationResult;\nimport dev.langchain4j.rag.RetrievalAugmentor;\nimport dev.langchain4j.rag.content.Content;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.MethodSource;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.stream.Stream;\n\nimport static dev.langchain4j.model.openai.OpenAiChatModelName.GPT_4_O_MINI;\nimport static dev.langchain4j.model.output.FinishReason.STOP;\nimport static java.util.concurrent.TimeUnit.SECONDS;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.Mockito.any;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.spy;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.verifyNoMoreInteractions;\nimport static org.mockito.Mockito.when;\n\n@EnabledIfEnvironmentVariable(named = \"OPENAI_API_KEY\", matches = \".+\")\nclass StreamingAiServicesIT {\n\n static Stream<StreamingChatLanguageModel> models() {\n return Stream.of(\n OpenAiStreamingChatModel.builder()\n .baseUrl(System.getenv(\"OPENAI_BASE_URL\"))\n .apiKey(System.getenv(\"OPENAI_API_KEY\"))\n .organizationId(System.getenv(\"OPENAI_ORGANIZATION_ID\"))\n .modelName(GPT_4_O_MINI)\n .logRequests(true)\n .logResponses(true)\n .build()\n // TODO add more models\n );\n }\n\n interface Assistant {\n\n TokenStream chat(String userMessage);\n }\n\n @ParameterizedTest\n @MethodSource(\"models\")\n void should_stream_answer(StreamingChatLanguageModel model) throws Exception {\n\n Assistant assistant = AiServices.create(Assistant.class, model);\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<ChatResponse> futureResponse = new CompletableFuture<>();\n\n assistant.chat(\"What is the capital of Germany?\")\n .onPartialResponse(answerBuilder::append)\n .onCompleteResponse(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n ChatResponse response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"Berlin\");\n assertThat(response.aiMessage().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isPositive();\n assertThat(tokenUsage.outputTokenCount()).isPositive();\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n }\n\n @ParameterizedTest\n @MethodSource(\"models\")\n void should_callback_with_content(StreamingChatLanguageModel model) throws Exception {\n\n List<Content> contents = new ArrayList<>();\n contents.add(Content.from(\"This is additional content\"));\n\n RetrievalAugmentor retrievalAugmentor = mock(RetrievalAugmentor.class);\n ChatMessage message = UserMessage.from(\"What is the capital of Germany?\");\n\n AugmentationResult result = AugmentationResult.builder()\n .chatMessage(message)\n .contents(contents)\n .build();\n when(retrievalAugmentor.augment(any())).thenReturn(result);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(model)\n .retrievalAugmentor(retrievalAugmentor)\n .build();\n\n CompletableFuture<List<Content>> futureContent = new CompletableFuture<>();\n\n assistant.chat(\"What is the capital of Germany?\")\n .onPartialResponse(ignored -> {})\n .onRetrieved(futureContent::complete)\n .ignoreErrors()\n .start();\n\n List<Content> returnedContents = futureContent.get(30, SECONDS);\n\n assertThat(returnedContents)\n .hasSize(1)\n .anySatisfy(c -> assertThat(c.textSegment().text()).isEqualTo(\"This is additional content\"));\n }\n\n @ParameterizedTest\n @MethodSource(\"models\")\n void should_stream_answers_with_memory(StreamingChatLanguageModel model) throws Exception {\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(model)\n .chatMemory(chatMemory)\n .build();\n\n\n String firstUserMessage = \"Hi, my name is Klaus\";\n CompletableFuture<ChatResponse> firstResponseFuture = new CompletableFuture<>();\n\n assistant.chat(firstUserMessage)\n .onPartialResponse(System.out::println)\n .onCompleteResponse(firstResponseFuture::complete)\n .onError(firstResponseFuture::completeExceptionally)\n .start();\n\n ChatResponse firstResponse = firstResponseFuture.get(30, SECONDS);\n assertThat(firstResponse.aiMessage().text()).contains(\"Klaus\");\n\n\n String secondUserMessage = \"What is my name?\";\n CompletableFuture<ChatResponse> secondResponseFuture = new CompletableFuture<>();\n\n assistant.chat(secondUserMessage)\n .onPartialResponse(System.out::println)\n .onCompleteResponse(secondResponseFuture::complete)\n .onError(secondResponseFuture::completeExceptionally)\n .start();\n\n ChatResponse secondResponse = secondResponseFuture.get(30, SECONDS);\n assertThat(secondResponse.aiMessage().text()).contains(\"Klaus\");\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(4);\n\n assertThat(messages.get(0)).isInstanceOf(UserMessage.class);\n assertThat(messages.get(0).text()).isEqualTo(firstUserMessage);\n\n assertThat(messages.get(1)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(1)).isEqualTo(firstResponse.aiMessage());\n\n assertThat(messages.get(2)).isInstanceOf(UserMessage.class);\n assertThat(messages.get(2).text()).isEqualTo(secondUserMessage);\n\n assertThat(messages.get(3)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(3)).isEqualTo(secondResponse.aiMessage());\n }\n\n static class Calculator {\n\n @Tool(\"calculates the square root of the provided number\")\n double squareRoot(@P(\"number to operate on\") double number) {\n return Math.sqrt(number);\n }\n }\n\n @ParameterizedTest\n @MethodSource(\"models\")\n void should_execute_a_tool_then_stream_answer(StreamingChatLanguageModel model) throws Exception {\n\n Calculator calculator = spy(new Calculator());\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(model)\n .chatMemory(chatMemory)\n .tools(calculator)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<ChatResponse> futureResponse = new CompletableFuture<>();\n\n String userMessage = \"What is the square root of 485906798473894056 in scientific notation?\";\n\n assistant.chat(userMessage)\n .onPartialResponse(answerBuilder::append)\n .onCompleteResponse(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n ChatResponse response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"6.97\");\n assertThat(response.aiMessage().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isPositive();\n assertThat(tokenUsage.outputTokenCount()).isPositive();\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n\n verify(calculator).squareRoot(485906798473894056.0);\n verifyNoMoreInteractions(calculator);\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(4);\n\n assertThat(messages.get(0)).isInstanceOf(UserMessage.class);\n assertThat(messages.get(0).text()).isEqualTo(userMessage);\n\n AiMessage aiMessage = (AiMessage) messages.get(1);\n assertThat(aiMessage.text()).isNull();\n assertThat(aiMessage.toolExecutionRequests()).hasSize(1);\n ToolExecutionRequest toolExecutionRequest = aiMessage.toolExecutionRequests().get(0);\n assertThat(toolExecutionRequest.id()).isNotBlank();\n\n assertThat(toolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 485906798473894056}\");\n\n ToolExecutionResultMessage toolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(2);\n assertThat(toolExecutionResultMessage.id()).isEqualTo(toolExecutionRequest.id());\n assertThat(toolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionResultMessage.text()).isEqualTo(\"6.97070153193991E8\");\n\n assertThat(messages.get(3)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(3).text()).contains(\"6.97\");\n }\n\n @Test\n void should_execute_multiple_tools_sequentially_then_answer() throws Exception {\n\n // TODO test more models\n StreamingChatLanguageModel streamingChatModel = OpenAiStreamingChatModel.builder()\n .baseUrl(System.getenv(\"OPENAI_BASE_URL\"))\n .apiKey(System.getenv(\"OPENAI_API_KEY\"))\n .organizationId(System.getenv(\"OPENAI_ORGANIZATION_ID\"))\n .modelName(GPT_4_O_MINI)\n .parallelToolCalls(false) // to force the model to call tools sequentially\n .temperature(0.0)\n .logRequests(true)\n .logResponses(true)\n .build();\n\n Calculator calculator = spy(new Calculator());\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(streamingChatModel)\n .chatMemory(chatMemory)\n .tools(calculator)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<ChatResponse> futureResponse = new CompletableFuture<>();\n\n String userMessage = \"What is the square root of 485906798473894056 and 97866249624785 in scientific notation?\";\n\n assistant.chat(userMessage)\n .onPartialResponse(answerBuilder::append)\n .onCompleteResponse(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n ChatResponse response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"6.97\", \"9.89\");\n assertThat(response.aiMessage().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isPositive(); // TODO\n assertThat(tokenUsage.outputTokenCount()).isPositive();\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n\n verify(calculator).squareRoot(485906798473894056.0);\n verify(calculator).squareRoot(97866249624785.0);\n verifyNoMoreInteractions(calculator);\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(6);\n\n assertThat(messages.get(0)).isInstanceOf(dev.langchain4j.data.message.UserMessage.class);\n assertThat(messages.get(0).text()).isEqualTo(userMessage);\n\n AiMessage aiMessage = (AiMessage) messages.get(1);\n assertThat(aiMessage.text()).isNull();\n assertThat(aiMessage.toolExecutionRequests()).hasSize(1);\n ToolExecutionRequest toolExecutionRequest = aiMessage.toolExecutionRequests().get(0);\n assertThat(toolExecutionRequest.id()).isNotBlank();\n assertThat(toolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 485906798473894056}\");\n\n ToolExecutionResultMessage toolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(2);\n assertThat(toolExecutionResultMessage.id()).isEqualTo(toolExecutionRequest.id());\n assertThat(toolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionResultMessage.text()).isEqualTo(\"6.97070153193991E8\");\n\n AiMessage secondAiMessage = (AiMessage) messages.get(3);\n assertThat(secondAiMessage.text()).isNull();\n assertThat(secondAiMessage.toolExecutionRequests()).hasSize(1);\n ToolExecutionRequest secondToolExecutionRequest = secondAiMessage.toolExecutionRequests().get(0);\n assertThat(secondToolExecutionRequest.id()).isNotBlank();\n assertThat(secondToolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 97866249624785}\");\n\n ToolExecutionResultMessage secondToolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(4);\n assertThat(secondToolExecutionResultMessage.id()).isEqualTo(secondToolExecutionRequest.id());\n assertThat(secondToolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionResultMessage.text()).isEqualTo(\"9892737.215997653\");\n\n assertThat(messages.get(5)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(5).text()).contains(\"6.97\", \"9.89\");\n }\n\n @Test\n void should_execute_multiple_tools_in_parallel_then_answer() throws Exception {\n\n Calculator calculator = spy(new Calculator());\n\n // TODO test more models\n StreamingChatLanguageModel streamingChatModel = OpenAiStreamingChatModel.builder()\n .baseUrl(System.getenv(\"OPENAI_BASE_URL\"))\n .apiKey(System.getenv(\"OPENAI_API_KEY\"))\n .organizationId(System.getenv(\"OPENAI_ORGANIZATION_ID\"))\n .modelName(GPT_4_O_MINI)\n .temperature(0.0)\n .logRequests(true)\n .logResponses(true)\n .build();\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(streamingChatModel)\n .chatMemory(chatMemory)\n .tools(calculator)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<ChatResponse> futureResponse = new CompletableFuture<>();\n\n String userMessage = \"What is the square root of 485906798473894056 and 97866249624785 in scientific notation?\";\n\n assistant.chat(userMessage)\n .onPartialResponse(answerBuilder::append)\n .onCompleteResponse(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n ChatResponse response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"6.97\", \"9.89\");\n assertThat(response.aiMessage().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isPositive(); // TODO\n assertThat(tokenUsage.outputTokenCount()).isPositive();\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n\n verify(calculator).squareRoot(485906798473894056.0);\n verify(calculator).squareRoot(97866249624785.0);\n verifyNoMoreInteractions(calculator);\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(5);\n\n assertThat(messages.get(0)).isInstanceOf(dev.langchain4j.data.message.UserMessage.class);\n assertThat(messages.get(0).text()).isEqualTo(userMessage);\n\n AiMessage aiMessage = (AiMessage) messages.get(1);\n assertThat(aiMessage.text()).isNull();\n assertThat(aiMessage.toolExecutionRequests()).hasSize(2);\n\n ToolExecutionRequest firstToolExecutionRequest = aiMessage.toolExecutionRequests().get(0);\n assertThat(firstToolExecutionRequest.id()).isNotBlank();\n assertThat(firstToolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(firstToolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 485906798473894056}\");\n\n ToolExecutionRequest secondToolExecutionRequest = aiMessage.toolExecutionRequests().get(1);\n assertThat(secondToolExecutionRequest.id()).isNotBlank();\n assertThat(secondToolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 97866249624785}\");\n\n ToolExecutionResultMessage firstToolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(2);\n assertThat(firstToolExecutionResultMessage.id()).isEqualTo(firstToolExecutionRequest.id());\n assertThat(firstToolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(firstToolExecutionResultMessage.text()).isEqualTo(\"6.97070153193991E8\");\n\n ToolExecutionResultMessage secondToolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(3);\n assertThat(secondToolExecutionResultMessage.id()).isEqualTo(secondToolExecutionRequest.id());\n assertThat(secondToolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionResultMessage.text()).isEqualTo(\"9892737.215997653\");\n\n assertThat(messages.get(4)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(4).text()).contains(\"6.97\", \"9.89\");\n }\n}\n"}, "0a75a33c2ef1e822acd88cc7110e7e7b79b89b12": {"t": 1785755662.7932274, "n": 20416, "h": "0a2770def8232a42", "c": "package dev.langchain4j.service;\n\nimport dev.langchain4j.agent.tool.P;\nimport dev.langchain4j.agent.tool.Tool;\nimport dev.langchain4j.agent.tool.ToolExecutionRequest;\nimport dev.langchain4j.data.message.AiMessage;\nimport dev.langchain4j.data.message.ChatMessage;\nimport dev.langchain4j.data.message.ToolExecutionResultMessage;\nimport dev.langchain4j.data.message.UserMessage;\nimport dev.langchain4j.memory.ChatMemory;\nimport dev.langchain4j.memory.chat.MessageWindowChatMemory;\nimport dev.langchain4j.model.chat.StreamingChatLanguageModel;\nimport dev.langchain4j.model.openai.OpenAiStreamingChatModel;\nimport dev.langchain4j.model.output.Response;\nimport dev.langchain4j.model.output.TokenUsage;\nimport dev.langchain4j.rag.AugmentationResult;\nimport dev.langchain4j.rag.RetrievalAugmentor;\nimport dev.langchain4j.rag.content.Content;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.MethodSource;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.stream.Stream;\n\nimport static dev.langchain4j.model.openai.OpenAiChatModelName.GPT_4_O_MINI;\nimport static dev.langchain4j.model.output.FinishReason.STOP;\nimport static java.util.concurrent.TimeUnit.SECONDS;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.Mockito.any;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.spy;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.verifyNoMoreInteractions;\nimport static org.mockito.Mockito.when;\n\n@EnabledIfEnvironmentVariable(named = \"OPENAI_API_KEY\", matches = \".+\")\nclass StreamingAiServicesIT {\n\n static Stream<StreamingChatLanguageModel> models() {\n return Stream.of(\n OpenAiStreamingChatModel.builder()\n .baseUrl(System.getenv(\"OPENAI_BASE_URL\"))\n .apiKey(System.getenv(\"OPENAI_API_KEY\"))\n .organizationId(System.getenv(\"OPENAI_ORGANIZATION_ID\"))\n .modelName(GPT_4_O_MINI)\n .logRequests(true)\n .logResponses(true)\n .build()\n // TODO add more models\n );\n }\n\n interface Assistant {\n\n TokenStream chat(String userMessage);\n }\n\n @ParameterizedTest\n @MethodSource(\"models\")\n void should_stream_answer(StreamingChatLanguageModel model) throws Exception {\n\n Assistant assistant = AiServices.create(Assistant.class, model);\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<Response<AiMessage>> futureResponse = new CompletableFuture<>();\n\n assistant.chat(\"What is the capital of Germany?\")\n .onNext(answerBuilder::append)\n .onComplete(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n Response<AiMessage> response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"Berlin\");\n assertThat(response.content().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isPositive();\n assertThat(tokenUsage.outputTokenCount()).isPositive();\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n }\n\n @ParameterizedTest\n @MethodSource(\"models\")\n void should_callback_with_content(StreamingChatLanguageModel model) throws Exception {\n\n List<Content> contents = new ArrayList<>();\n contents.add(Content.from(\"This is additional content\"));\n\n RetrievalAugmentor retrievalAugmentor = mock(RetrievalAugmentor.class);\n ChatMessage message = UserMessage.from(\"What is the capital of Germany?\");\n\n AugmentationResult result = AugmentationResult.builder()\n .chatMessage(message)\n .contents(contents)\n .build();\n when(retrievalAugmentor.augment(any())).thenReturn(result);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(model)\n .retrievalAugmentor(retrievalAugmentor)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<List<Content>> futureContent = new CompletableFuture<>();\n\n assistant.chat(\"What is the capital of Germany?\")\n .onNext(answerBuilder::append)\n .onRetrieved(futureContent::complete)\n .ignoreErrors()\n .start();\n\n List<Content> returnedContents = futureContent.get(30, SECONDS);\n\n assertThat(returnedContents)\n .hasSize(1)\n .anySatisfy(c -> assertThat(c.textSegment().text()).isEqualTo(\"This is additional content\"));\n }\n\n @ParameterizedTest\n @MethodSource(\"models\")\n void should_stream_answers_with_memory(StreamingChatLanguageModel model) throws Exception {\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(model)\n .chatMemory(chatMemory)\n .build();\n\n\n String firstUserMessage = \"Hi, my name is Klaus\";\n CompletableFuture<Response<AiMessage>> firstResultFuture = new CompletableFuture<>();\n\n assistant.chat(firstUserMessage)\n .onNext(System.out::println)\n .onComplete(firstResultFuture::complete)\n .onError(firstResultFuture::completeExceptionally)\n .start();\n\n Response<AiMessage> firstResponse = firstResultFuture.get(30, SECONDS);\n assertThat(firstResponse.content().text()).contains(\"Klaus\");\n\n\n String secondUserMessage = \"What is my name?\";\n CompletableFuture<Response<AiMessage>> secondResultFuture = new CompletableFuture<>();\n\n assistant.chat(secondUserMessage)\n .onNext(System.out::println)\n .onComplete(secondResultFuture::complete)\n .onError(secondResultFuture::completeExceptionally)\n .start();\n\n Response<AiMessage> secondResponse = secondResultFuture.get(30, SECONDS);\n assertThat(secondResponse.content().text()).contains(\"Klaus\");\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(4);\n\n assertThat(messages.get(0)).isInstanceOf(UserMessage.class);\n assertThat(messages.get(0).text()).isEqualTo(firstUserMessage);\n\n assertThat(messages.get(1)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(1)).isEqualTo(firstResponse.content());\n\n assertThat(messages.get(2)).isInstanceOf(UserMessage.class);\n assertThat(messages.get(2).text()).isEqualTo(secondUserMessage);\n\n assertThat(messages.get(3)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(3)).isEqualTo(secondResponse.content());\n }\n\n static class Calculator {\n\n @Tool(\"calculates the square root of the provided number\")\n double squareRoot(@P(\"number to operate on\") double number) {\n return Math.sqrt(number);\n }\n }\n\n @ParameterizedTest\n @MethodSource(\"models\")\n void should_execute_a_tool_then_stream_answer(StreamingChatLanguageModel model) throws Exception {\n\n Calculator calculator = spy(new Calculator());\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(model)\n .chatMemory(chatMemory)\n .tools(calculator)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<Response<AiMessage>> futureResponse = new CompletableFuture<>();\n\n String userMessage = \"What is the square root of 485906798473894056 in scientific notation?\";\n\n assistant.chat(userMessage)\n .onNext(answerBuilder::append)\n .onComplete(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n Response<AiMessage> response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"6.97\");\n assertThat(response.content().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isPositive();\n assertThat(tokenUsage.outputTokenCount()).isPositive();\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n\n verify(calculator).squareRoot(485906798473894056.0);\n verifyNoMoreInteractions(calculator);\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(4);\n\n assertThat(messages.get(0)).isInstanceOf(UserMessage.class);\n assertThat(messages.get(0).text()).isEqualTo(userMessage);\n\n AiMessage aiMessage = (AiMessage) messages.get(1);\n assertThat(aiMessage.text()).isNull();\n assertThat(aiMessage.toolExecutionRequests()).hasSize(1);\n ToolExecutionRequest toolExecutionRequest = aiMessage.toolExecutionRequests().get(0);\n assertThat(toolExecutionRequest.id()).isNotBlank();\n\n assertThat(toolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 485906798473894056}\");\n\n ToolExecutionResultMessage toolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(2);\n assertThat(toolExecutionResultMessage.id()).isEqualTo(toolExecutionRequest.id());\n assertThat(toolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionResultMessage.text()).isEqualTo(\"6.97070153193991E8\");\n\n assertThat(messages.get(3)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(3).text()).contains(\"6.97\");\n }\n\n @Test\n void should_execute_multiple_tools_sequentially_then_answer() throws Exception {\n\n // TODO test more models\n StreamingChatLanguageModel streamingChatModel = OpenAiStreamingChatModel.builder()\n .baseUrl(System.getenv(\"OPENAI_BASE_URL\"))\n .apiKey(System.getenv(\"OPENAI_API_KEY\"))\n .organizationId(System.getenv(\"OPENAI_ORGANIZATION_ID\"))\n .modelName(GPT_4_O_MINI)\n .parallelToolCalls(false) // to force the model to call tools sequentially\n .temperature(0.0)\n .logRequests(true)\n .logResponses(true)\n .build();\n\n Calculator calculator = spy(new Calculator());\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(streamingChatModel)\n .chatMemory(chatMemory)\n .tools(calculator)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<Response<AiMessage>> futureResponse = new CompletableFuture<>();\n\n String userMessage = \"What is the square root of 485906798473894056 and 97866249624785 in scientific notation?\";\n\n assistant.chat(userMessage)\n .onNext(answerBuilder::append)\n .onComplete(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n Response<AiMessage> response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"6.97\", \"9.89\");\n assertThat(response.content().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isPositive(); // TODO\n assertThat(tokenUsage.outputTokenCount()).isPositive();\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n\n verify(calculator).squareRoot(485906798473894056.0);\n verify(calculator).squareRoot(97866249624785.0);\n verifyNoMoreInteractions(calculator);\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(6);\n\n assertThat(messages.get(0)).isInstanceOf(dev.langchain4j.data.message.UserMessage.class);\n assertThat(messages.get(0).text()).isEqualTo(userMessage);\n\n AiMessage aiMessage = (AiMessage) messages.get(1);\n assertThat(aiMessage.text()).isNull();\n assertThat(aiMessage.toolExecutionRequests()).hasSize(1);\n ToolExecutionRequest toolExecutionRequest = aiMessage.toolExecutionRequests().get(0);\n assertThat(toolExecutionRequest.id()).isNotBlank();\n assertThat(toolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 485906798473894056}\");\n\n ToolExecutionResultMessage toolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(2);\n assertThat(toolExecutionResultMessage.id()).isEqualTo(toolExecutionRequest.id());\n assertThat(toolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionResultMessage.text()).isEqualTo(\"6.97070153193991E8\");\n\n AiMessage secondAiMessage = (AiMessage) messages.get(3);\n assertThat(secondAiMessage.text()).isNull();\n assertThat(secondAiMessage.toolExecutionRequests()).hasSize(1);\n ToolExecutionRequest secondToolExecutionRequest = secondAiMessage.toolExecutionRequests().get(0);\n assertThat(secondToolExecutionRequest.id()).isNotBlank();\n assertThat(secondToolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 97866249624785}\");\n\n ToolExecutionResultMessage secondToolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(4);\n assertThat(secondToolExecutionResultMessage.id()).isEqualTo(secondToolExecutionRequest.id());\n assertThat(secondToolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionResultMessage.text()).isEqualTo(\"9892737.215997653\");\n\n assertThat(messages.get(5)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(5).text()).contains(\"6.97\", \"9.89\");\n }\n\n @Test\n void should_execute_multiple_tools_in_parallel_then_answer() throws Exception {\n\n Calculator calculator = spy(new Calculator());\n\n // TODO test more models\n StreamingChatLanguageModel streamingChatModel = OpenAiStreamingChatModel.builder()\n .baseUrl(System.getenv(\"OPENAI_BASE_URL\"))\n .apiKey(System.getenv(\"OPENAI_API_KEY\"))\n .organizationId(System.getenv(\"OPENAI_ORGANIZATION_ID\"))\n .modelName(GPT_4_O_MINI)\n .temperature(0.0)\n .logRequests(true)\n .logResponses(true)\n .build();\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(streamingChatModel)\n .chatMemory(chatMemory)\n .tools(calculator)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<Response<AiMessage>> futureResponse = new CompletableFuture<>();\n\n String userMessage = \"What is the square root of 485906798473894056 and 97866249624785 in scientific notation?\";\n\n assistant.chat(userMessage)\n .onNext(answerBuilder::append)\n .onComplete(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n Response<AiMessage> response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"6.97\", \"9.89\");\n assertThat(response.content().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isPositive(); // TODO\n assertThat(tokenUsage.outputTokenCount()).isPositive();\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n\n verify(calculator).squareRoot(485906798473894056.0);\n verify(calculator).squareRoot(97866249624785.0);\n verifyNoMoreInteractions(calculator);\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(5);\n\n assertThat(messages.get(0)).isInstanceOf(dev.langchain4j.data.message.UserMessage.class);\n assertThat(messages.get(0).text()).isEqualTo(userMessage);\n\n AiMessage aiMessage = (AiMessage) messages.get(1);\n assertThat(aiMessage.text()).isNull();\n assertThat(aiMessage.toolExecutionRequests()).hasSize(2);\n\n ToolExecutionRequest firstToolExecutionRequest = aiMessage.toolExecutionRequests().get(0);\n assertThat(firstToolExecutionRequest.id()).isNotBlank();\n assertThat(firstToolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(firstToolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 485906798473894056}\");\n\n ToolExecutionRequest secondToolExecutionRequest = aiMessage.toolExecutionRequests().get(1);\n assertThat(secondToolExecutionRequest.id()).isNotBlank();\n assertThat(secondToolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 97866249624785}\");\n\n ToolExecutionResultMessage firstToolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(2);\n assertThat(firstToolExecutionResultMessage.id()).isEqualTo(firstToolExecutionRequest.id());\n assertThat(firstToolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(firstToolExecutionResultMessage.text()).isEqualTo(\"6.97070153193991E8\");\n\n ToolExecutionResultMessage secondToolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(3);\n assertThat(secondToolExecutionResultMessage.id()).isEqualTo(secondToolExecutionRequest.id());\n assertThat(secondToolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionResultMessage.text()).isEqualTo(\"9892737.215997653\");\n\n assertThat(messages.get(4)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(4).text()).contains(\"6.97\", \"9.89\");\n }\n}\n"}, "79f2a31d46de4a18a86ffc9ff44849b544e07464": {"t": 1785755664.2040942, "n": 20326, "h": "25572f6db4d303b7", "c": "package dev.langchain4j.service;\n\nimport dev.langchain4j.agent.tool.P;\nimport dev.langchain4j.agent.tool.Tool;\nimport dev.langchain4j.agent.tool.ToolExecutionRequest;\nimport dev.langchain4j.data.message.AiMessage;\nimport dev.langchain4j.data.message.ChatMessage;\nimport dev.langchain4j.data.message.ToolExecutionResultMessage;\nimport dev.langchain4j.data.message.UserMessage;\nimport dev.langchain4j.memory.ChatMemory;\nimport dev.langchain4j.memory.chat.MessageWindowChatMemory;\nimport dev.langchain4j.model.chat.StreamingChatLanguageModel;\nimport dev.langchain4j.model.openai.OpenAiStreamingChatModel;\nimport dev.langchain4j.model.output.Response;\nimport dev.langchain4j.model.output.TokenUsage;\nimport dev.langchain4j.rag.AugmentationResult;\nimport dev.langchain4j.rag.RetrievalAugmentor;\nimport dev.langchain4j.rag.content.Content;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.MethodSource;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.stream.Stream;\n\nimport static dev.langchain4j.model.openai.OpenAiChatModelName.GPT_4_O_MINI;\nimport static dev.langchain4j.model.output.FinishReason.STOP;\nimport static java.util.concurrent.TimeUnit.SECONDS;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.Mockito.any;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.spy;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.verifyNoMoreInteractions;\nimport static org.mockito.Mockito.when;\n\n@EnabledIfEnvironmentVariable(named = \"OPENAI_API_KEY\", matches = \".+\")\nclass StreamingAiServicesIT {\n\n static Stream<StreamingChatLanguageModel> models() {\n return Stream.of(\n OpenAiStreamingChatModel.builder()\n .baseUrl(System.getenv(\"OPENAI_BASE_URL\"))\n .apiKey(System.getenv(\"OPENAI_API_KEY\"))\n .organizationId(System.getenv(\"OPENAI_ORGANIZATION_ID\"))\n .logRequests(true)\n .logResponses(true)\n .build()\n // TODO add more models\n );\n }\n\n interface Assistant {\n\n TokenStream chat(String userMessage);\n }\n\n @ParameterizedTest\n @MethodSource(\"models\")\n void should_stream_answer(StreamingChatLanguageModel model) throws Exception {\n\n Assistant assistant = AiServices.create(Assistant.class, model);\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<Response<AiMessage>> futureResponse = new CompletableFuture<>();\n\n assistant.chat(\"What is the capital of Germany?\")\n .onNext(answerBuilder::append)\n .onComplete(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n Response<AiMessage> response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"Berlin\");\n assertThat(response.content().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isPositive();\n assertThat(tokenUsage.outputTokenCount()).isPositive();\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n }\n\n @ParameterizedTest\n @MethodSource(\"models\")\n void should_callback_with_content(StreamingChatLanguageModel model) throws Exception {\n\n List<Content> contents = new ArrayList<>();\n contents.add(Content.from(\"This is additional content\"));\n\n RetrievalAugmentor retrievalAugmentor = mock(RetrievalAugmentor.class);\n ChatMessage message = UserMessage.from(\"What is the capital of Germany?\");\n\n AugmentationResult result = AugmentationResult.builder()\n .chatMessage(message)\n .contents(contents)\n .build();\n when(retrievalAugmentor.augment(any())).thenReturn(result);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(model)\n .retrievalAugmentor(retrievalAugmentor)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<List<Content>> futureContent = new CompletableFuture<>();\n\n assistant.chat(\"What is the capital of Germany?\")\n .onNext(answerBuilder::append)\n .onRetrieved(futureContent::complete)\n .ignoreErrors()\n .start();\n\n List<Content> returnedContents = futureContent.get(30, SECONDS);\n\n assertThat(returnedContents)\n .hasSize(1)\n .anySatisfy(c -> assertThat(c.textSegment().text()).isEqualTo(\"This is additional content\"));\n }\n\n @ParameterizedTest\n @MethodSource(\"models\")\n void should_stream_answers_with_memory(StreamingChatLanguageModel model) throws Exception {\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(model)\n .chatMemory(chatMemory)\n .build();\n\n\n String firstUserMessage = \"Hi, my name is Klaus\";\n CompletableFuture<Response<AiMessage>> firstResultFuture = new CompletableFuture<>();\n\n assistant.chat(firstUserMessage)\n .onNext(System.out::println)\n .onComplete(firstResultFuture::complete)\n .onError(firstResultFuture::completeExceptionally)\n .start();\n\n Response<AiMessage> firstResponse = firstResultFuture.get(30, SECONDS);\n assertThat(firstResponse.content().text()).contains(\"Klaus\");\n\n\n String secondUserMessage = \"What is my name?\";\n CompletableFuture<Response<AiMessage>> secondResultFuture = new CompletableFuture<>();\n\n assistant.chat(secondUserMessage)\n .onNext(System.out::println)\n .onComplete(secondResultFuture::complete)\n .onError(secondResultFuture::completeExceptionally)\n .start();\n\n Response<AiMessage> secondResponse = secondResultFuture.get(30, SECONDS);\n assertThat(secondResponse.content().text()).contains(\"Klaus\");\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(4);\n\n assertThat(messages.get(0)).isInstanceOf(UserMessage.class);\n assertThat(messages.get(0).text()).isEqualTo(firstUserMessage);\n\n assertThat(messages.get(1)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(1)).isEqualTo(firstResponse.content());\n\n assertThat(messages.get(2)).isInstanceOf(UserMessage.class);\n assertThat(messages.get(2).text()).isEqualTo(secondUserMessage);\n\n assertThat(messages.get(3)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(3)).isEqualTo(secondResponse.content());\n }\n\n static class Calculator {\n\n @Tool(\"calculates the square root of the provided number\")\n double squareRoot(@P(\"number to operate on\") double number) {\n return Math.sqrt(number);\n }\n }\n\n @ParameterizedTest\n @MethodSource(\"models\")\n void should_execute_a_tool_then_stream_answer(StreamingChatLanguageModel model) throws Exception {\n\n Calculator calculator = spy(new Calculator());\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(model)\n .chatMemory(chatMemory)\n .tools(calculator)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<Response<AiMessage>> futureResponse = new CompletableFuture<>();\n\n String userMessage = \"What is the square root of 485906798473894056 in scientific notation?\";\n\n assistant.chat(userMessage)\n .onNext(answerBuilder::append)\n .onComplete(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n Response<AiMessage> response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"6.97\");\n assertThat(response.content().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isPositive();\n assertThat(tokenUsage.outputTokenCount()).isPositive();\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n\n verify(calculator).squareRoot(485906798473894056.0);\n verifyNoMoreInteractions(calculator);\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(4);\n\n assertThat(messages.get(0)).isInstanceOf(UserMessage.class);\n assertThat(messages.get(0).text()).isEqualTo(userMessage);\n\n AiMessage aiMessage = (AiMessage) messages.get(1);\n assertThat(aiMessage.text()).isNull();\n assertThat(aiMessage.toolExecutionRequests()).hasSize(1);\n ToolExecutionRequest toolExecutionRequest = aiMessage.toolExecutionRequests().get(0);\n assertThat(toolExecutionRequest.id()).isNotBlank();\n\n assertThat(toolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 485906798473894056}\");\n\n ToolExecutionResultMessage toolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(2);\n assertThat(toolExecutionResultMessage.id()).isEqualTo(toolExecutionRequest.id());\n assertThat(toolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionResultMessage.text()).isEqualTo(\"6.97070153193991E8\");\n\n assertThat(messages.get(3)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(3).text()).contains(\"6.97\");\n }\n\n @Test\n void should_execute_multiple_tools_sequentially_then_answer() throws Exception {\n\n // TODO test more models\n StreamingChatLanguageModel streamingChatModel = OpenAiStreamingChatModel.builder()\n .baseUrl(System.getenv(\"OPENAI_BASE_URL\"))\n .apiKey(System.getenv(\"OPENAI_API_KEY\"))\n .organizationId(System.getenv(\"OPENAI_ORGANIZATION_ID\"))\n .modelName(GPT_4_O_MINI)\n .parallelToolCalls(false) // to force the model to call tools sequentially\n .temperature(0.0)\n .logRequests(true)\n .logResponses(true)\n .build();\n\n Calculator calculator = spy(new Calculator());\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(streamingChatModel)\n .chatMemory(chatMemory)\n .tools(calculator)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<Response<AiMessage>> futureResponse = new CompletableFuture<>();\n\n String userMessage = \"What is the square root of 485906798473894056 and 97866249624785 in scientific notation?\";\n\n assistant.chat(userMessage)\n .onNext(answerBuilder::append)\n .onComplete(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n Response<AiMessage> response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"6.97\", \"9.89\");\n assertThat(response.content().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isPositive(); // TODO\n assertThat(tokenUsage.outputTokenCount()).isPositive();\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n\n verify(calculator).squareRoot(485906798473894056.0);\n verify(calculator).squareRoot(97866249624785.0);\n verifyNoMoreInteractions(calculator);\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(6);\n\n assertThat(messages.get(0)).isInstanceOf(dev.langchain4j.data.message.UserMessage.class);\n assertThat(messages.get(0).text()).isEqualTo(userMessage);\n\n AiMessage aiMessage = (AiMessage) messages.get(1);\n assertThat(aiMessage.text()).isNull();\n assertThat(aiMessage.toolExecutionRequests()).hasSize(1);\n ToolExecutionRequest toolExecutionRequest = aiMessage.toolExecutionRequests().get(0);\n assertThat(toolExecutionRequest.id()).isNotBlank();\n assertThat(toolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 485906798473894056}\");\n\n ToolExecutionResultMessage toolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(2);\n assertThat(toolExecutionResultMessage.id()).isEqualTo(toolExecutionRequest.id());\n assertThat(toolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionResultMessage.text()).isEqualTo(\"6.97070153193991E8\");\n\n AiMessage secondAiMessage = (AiMessage) messages.get(3);\n assertThat(secondAiMessage.text()).isNull();\n assertThat(secondAiMessage.toolExecutionRequests()).hasSize(1);\n ToolExecutionRequest secondToolExecutionRequest = secondAiMessage.toolExecutionRequests().get(0);\n assertThat(secondToolExecutionRequest.id()).isNotBlank();\n assertThat(secondToolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 97866249624785}\");\n\n ToolExecutionResultMessage secondToolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(4);\n assertThat(secondToolExecutionResultMessage.id()).isEqualTo(secondToolExecutionRequest.id());\n assertThat(secondToolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionResultMessage.text()).isEqualTo(\"9892737.215997653\");\n\n assertThat(messages.get(5)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(5).text()).contains(\"6.97\", \"9.89\");\n }\n\n @Test\n void should_execute_multiple_tools_in_parallel_then_answer() throws Exception {\n\n Calculator calculator = spy(new Calculator());\n\n // TODO test more models\n StreamingChatLanguageModel streamingChatModel = OpenAiStreamingChatModel.builder()\n .baseUrl(System.getenv(\"OPENAI_BASE_URL\"))\n .apiKey(System.getenv(\"OPENAI_API_KEY\"))\n .organizationId(System.getenv(\"OPENAI_ORGANIZATION_ID\"))\n .temperature(0.0)\n .logRequests(true)\n .logResponses(true)\n .build();\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(streamingChatModel)\n .chatMemory(chatMemory)\n .tools(calculator)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<Response<AiMessage>> futureResponse = new CompletableFuture<>();\n\n String userMessage = \"What is the square root of 485906798473894056 and 97866249624785 in scientific notation?\";\n\n assistant.chat(userMessage)\n .onNext(answerBuilder::append)\n .onComplete(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n Response<AiMessage> response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"6.97\", \"9.89\");\n assertThat(response.content().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isPositive(); // TODO\n assertThat(tokenUsage.outputTokenCount()).isPositive();\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n\n verify(calculator).squareRoot(485906798473894056.0);\n verify(calculator).squareRoot(97866249624785.0);\n verifyNoMoreInteractions(calculator);\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(5);\n\n assertThat(messages.get(0)).isInstanceOf(dev.langchain4j.data.message.UserMessage.class);\n assertThat(messages.get(0).text()).isEqualTo(userMessage);\n\n AiMessage aiMessage = (AiMessage) messages.get(1);\n assertThat(aiMessage.text()).isNull();\n assertThat(aiMessage.toolExecutionRequests()).hasSize(2);\n\n ToolExecutionRequest firstToolExecutionRequest = aiMessage.toolExecutionRequests().get(0);\n assertThat(firstToolExecutionRequest.id()).isNotBlank();\n assertThat(firstToolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(firstToolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 485906798473894056}\");\n\n ToolExecutionRequest secondToolExecutionRequest = aiMessage.toolExecutionRequests().get(1);\n assertThat(secondToolExecutionRequest.id()).isNotBlank();\n assertThat(secondToolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 97866249624785}\");\n\n ToolExecutionResultMessage firstToolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(2);\n assertThat(firstToolExecutionResultMessage.id()).isEqualTo(firstToolExecutionRequest.id());\n assertThat(firstToolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(firstToolExecutionResultMessage.text()).isEqualTo(\"6.97070153193991E8\");\n\n ToolExecutionResultMessage secondToolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(3);\n assertThat(secondToolExecutionResultMessage.id()).isEqualTo(secondToolExecutionRequest.id());\n assertThat(secondToolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionResultMessage.text()).isEqualTo(\"9892737.215997653\");\n\n assertThat(messages.get(4)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(4).text()).contains(\"6.97\", \"9.89\");\n }\n}\n"}, "28c2c11a9bf0b114d89dbccc8891860902a11b61": {"t": 1785755666.0996122, "n": 20358, "h": "93ca0d5070affd77", "c": "package dev.langchain4j.service;\n\nimport dev.langchain4j.agent.tool.P;\nimport dev.langchain4j.agent.tool.Tool;\nimport dev.langchain4j.agent.tool.ToolExecutionRequest;\nimport dev.langchain4j.data.message.AiMessage;\nimport dev.langchain4j.data.message.ChatMessage;\nimport dev.langchain4j.data.message.ToolExecutionResultMessage;\nimport dev.langchain4j.data.message.UserMessage;\nimport dev.langchain4j.memory.ChatMemory;\nimport dev.langchain4j.memory.chat.MessageWindowChatMemory;\nimport dev.langchain4j.model.chat.StreamingChatLanguageModel;\nimport dev.langchain4j.model.openai.OpenAiStreamingChatModel;\nimport dev.langchain4j.model.output.Response;\nimport dev.langchain4j.model.output.TokenUsage;\nimport dev.langchain4j.rag.AugmentationResult;\nimport dev.langchain4j.rag.RetrievalAugmentor;\nimport dev.langchain4j.rag.content.Content;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.MethodSource;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.stream.Stream;\n\nimport static dev.langchain4j.model.openai.OpenAiChatModelName.GPT_4_O_MINI;\nimport static dev.langchain4j.model.output.FinishReason.STOP;\nimport static java.util.concurrent.TimeUnit.SECONDS;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.Mockito.any;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.spy;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.verifyNoMoreInteractions;\nimport static org.mockito.Mockito.when;\n\n@EnabledIfEnvironmentVariable(named = \"OPENAI_API_KEY\", matches = \".+\")\nclass StreamingAiServicesIT {\n\n static Stream<StreamingChatLanguageModel> models() {\n return Stream.of(\n OpenAiStreamingChatModel.builder()\n .baseUrl(System.getenv(\"OPENAI_BASE_URL\"))\n .apiKey(System.getenv(\"OPENAI_API_KEY\"))\n .organizationId(System.getenv(\"OPENAI_ORGANIZATION_ID\"))\n .logRequests(true)\n .logResponses(true)\n .build()\n // TODO add more models\n );\n }\n\n interface Assistant {\n\n TokenStream chat(String userMessage);\n }\n\n @ParameterizedTest\n @MethodSource(\"models\")\n void should_stream_answer(StreamingChatLanguageModel model) throws Exception {\n\n Assistant assistant = AiServices.create(Assistant.class, model);\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<Response<AiMessage>> futureResponse = new CompletableFuture<>();\n\n assistant.chat(\"What is the capital of Germany?\")\n .onNext(answerBuilder::append)\n .onComplete(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n Response<AiMessage> response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"Berlin\");\n assertThat(response.content().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isGreaterThan(0);\n assertThat(tokenUsage.outputTokenCount()).isGreaterThan(0);\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n }\n\n @ParameterizedTest\n @MethodSource(\"models\")\n void should_callback_with_content(StreamingChatLanguageModel model) throws Exception {\n\n List<Content> contents = new ArrayList<>();\n contents.add(Content.from(\"This is additional content\"));\n\n RetrievalAugmentor retrievalAugmentor = mock(RetrievalAugmentor.class);\n ChatMessage message = UserMessage.from(\"What is the capital of Germany?\");\n\n AugmentationResult result = AugmentationResult.builder()\n .chatMessage(message)\n .contents(contents)\n .build();\n when(retrievalAugmentor.augment(any())).thenReturn(result);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(model)\n .retrievalAugmentor(retrievalAugmentor)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<List<Content>> futureContent = new CompletableFuture<>();\n\n assistant.chat(\"What is the capital of Germany?\")\n .onNext(answerBuilder::append)\n .onRetrieved(futureContent::complete)\n .ignoreErrors()\n .start();\n\n List<Content> returnedContents = futureContent.get(30, SECONDS);\n\n assertThat(returnedContents)\n .hasSize(1)\n .anySatisfy(c -> assertThat(c.textSegment().text()).isEqualTo(\"This is additional content\"));\n }\n\n @ParameterizedTest\n @MethodSource(\"models\")\n void should_stream_answers_with_memory(StreamingChatLanguageModel model) throws Exception {\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(model)\n .chatMemory(chatMemory)\n .build();\n\n\n String firstUserMessage = \"Hi, my name is Klaus\";\n CompletableFuture<Response<AiMessage>> firstResultFuture = new CompletableFuture<>();\n\n assistant.chat(firstUserMessage)\n .onNext(System.out::println)\n .onComplete(firstResultFuture::complete)\n .onError(firstResultFuture::completeExceptionally)\n .start();\n\n Response<AiMessage> firstResponse = firstResultFuture.get(30, SECONDS);\n assertThat(firstResponse.content().text()).contains(\"Klaus\");\n\n\n String secondUserMessage = \"What is my name?\";\n CompletableFuture<Response<AiMessage>> secondResultFuture = new CompletableFuture<>();\n\n assistant.chat(secondUserMessage)\n .onNext(System.out::println)\n .onComplete(secondResultFuture::complete)\n .onError(secondResultFuture::completeExceptionally)\n .start();\n\n Response<AiMessage> secondResponse = secondResultFuture.get(30, SECONDS);\n assertThat(secondResponse.content().text()).contains(\"Klaus\");\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(4);\n\n assertThat(messages.get(0)).isInstanceOf(UserMessage.class);\n assertThat(messages.get(0).text()).isEqualTo(firstUserMessage);\n\n assertThat(messages.get(1)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(1)).isEqualTo(firstResponse.content());\n\n assertThat(messages.get(2)).isInstanceOf(UserMessage.class);\n assertThat(messages.get(2).text()).isEqualTo(secondUserMessage);\n\n assertThat(messages.get(3)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(3)).isEqualTo(secondResponse.content());\n }\n\n static class Calculator {\n\n @Tool(\"calculates the square root of the provided number\")\n double squareRoot(@P(\"number to operate on\") double number) {\n return Math.sqrt(number);\n }\n }\n\n @ParameterizedTest\n @MethodSource(\"models\")\n void should_execute_a_tool_then_stream_answer(StreamingChatLanguageModel model) throws Exception {\n\n Calculator calculator = spy(new Calculator());\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(model)\n .chatMemory(chatMemory)\n .tools(calculator)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<Response<AiMessage>> futureResponse = new CompletableFuture<>();\n\n String userMessage = \"What is the square root of 485906798473894056 in scientific notation?\";\n\n assistant.chat(userMessage)\n .onNext(answerBuilder::append)\n .onComplete(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n Response<AiMessage> response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"6.97\");\n assertThat(response.content().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isGreaterThan(0);\n assertThat(tokenUsage.outputTokenCount()).isGreaterThan(0);\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n\n verify(calculator).squareRoot(485906798473894056.0);\n verifyNoMoreInteractions(calculator);\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(4);\n\n assertThat(messages.get(0)).isInstanceOf(UserMessage.class);\n assertThat(messages.get(0).text()).isEqualTo(userMessage);\n\n AiMessage aiMessage = (AiMessage) messages.get(1);\n assertThat(aiMessage.text()).isNull();\n assertThat(aiMessage.toolExecutionRequests()).hasSize(1);\n ToolExecutionRequest toolExecutionRequest = aiMessage.toolExecutionRequests().get(0);\n assertThat(toolExecutionRequest.id()).isNotBlank();\n\n assertThat(toolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 485906798473894056}\");\n\n ToolExecutionResultMessage toolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(2);\n assertThat(toolExecutionResultMessage.id()).isEqualTo(toolExecutionRequest.id());\n assertThat(toolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionResultMessage.text()).isEqualTo(\"6.97070153193991E8\");\n\n assertThat(messages.get(3)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(3).text()).contains(\"6.97\");\n }\n\n @Test\n void should_execute_multiple_tools_sequentially_then_answer() throws Exception {\n\n // TODO test more models\n StreamingChatLanguageModel streamingChatModel = OpenAiStreamingChatModel.builder()\n .baseUrl(System.getenv(\"OPENAI_BASE_URL\"))\n .apiKey(System.getenv(\"OPENAI_API_KEY\"))\n .organizationId(System.getenv(\"OPENAI_ORGANIZATION_ID\"))\n .modelName(GPT_4_O_MINI)\n .parallelToolCalls(false) // to force the model to call tools sequentially\n .temperature(0.0)\n .logRequests(true)\n .logResponses(true)\n .build();\n\n Calculator calculator = spy(new Calculator());\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(streamingChatModel)\n .chatMemory(chatMemory)\n .tools(calculator)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<Response<AiMessage>> futureResponse = new CompletableFuture<>();\n\n String userMessage = \"What is the square root of 485906798473894056 and 97866249624785 in scientific notation?\";\n\n assistant.chat(userMessage)\n .onNext(answerBuilder::append)\n .onComplete(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n Response<AiMessage> response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"6.97\", \"9.89\");\n assertThat(response.content().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isGreaterThan(0); // TODO\n assertThat(tokenUsage.outputTokenCount()).isGreaterThan(0);\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n\n verify(calculator).squareRoot(485906798473894056.0);\n verify(calculator).squareRoot(97866249624785.0);\n verifyNoMoreInteractions(calculator);\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(6);\n\n assertThat(messages.get(0)).isInstanceOf(dev.langchain4j.data.message.UserMessage.class);\n assertThat(messages.get(0).text()).isEqualTo(userMessage);\n\n AiMessage aiMessage = (AiMessage) messages.get(1);\n assertThat(aiMessage.text()).isNull();\n assertThat(aiMessage.toolExecutionRequests()).hasSize(1);\n ToolExecutionRequest toolExecutionRequest = aiMessage.toolExecutionRequests().get(0);\n assertThat(toolExecutionRequest.id()).isNotBlank();\n assertThat(toolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 485906798473894056}\");\n\n ToolExecutionResultMessage toolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(2);\n assertThat(toolExecutionResultMessage.id()).isEqualTo(toolExecutionRequest.id());\n assertThat(toolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionResultMessage.text()).isEqualTo(\"6.97070153193991E8\");\n\n AiMessage secondAiMessage = (AiMessage) messages.get(3);\n assertThat(secondAiMessage.text()).isNull();\n assertThat(secondAiMessage.toolExecutionRequests()).hasSize(1);\n ToolExecutionRequest secondToolExecutionRequest = secondAiMessage.toolExecutionRequests().get(0);\n assertThat(secondToolExecutionRequest.id()).isNotBlank();\n assertThat(secondToolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 97866249624785}\");\n\n ToolExecutionResultMessage secondToolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(4);\n assertThat(secondToolExecutionResultMessage.id()).isEqualTo(secondToolExecutionRequest.id());\n assertThat(secondToolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionResultMessage.text()).isEqualTo(\"9892737.215997653\");\n\n assertThat(messages.get(5)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(5).text()).contains(\"6.97\", \"9.89\");\n }\n\n @Test\n void should_execute_multiple_tools_in_parallel_then_answer() throws Exception {\n\n Calculator calculator = spy(new Calculator());\n\n // TODO test more models\n StreamingChatLanguageModel streamingChatModel = OpenAiStreamingChatModel.builder()\n .baseUrl(System.getenv(\"OPENAI_BASE_URL\"))\n .apiKey(System.getenv(\"OPENAI_API_KEY\"))\n .organizationId(System.getenv(\"OPENAI_ORGANIZATION_ID\"))\n .temperature(0.0)\n .logRequests(true)\n .logResponses(true)\n .build();\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(streamingChatModel)\n .chatMemory(chatMemory)\n .tools(calculator)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<Response<AiMessage>> futureResponse = new CompletableFuture<>();\n\n String userMessage = \"What is the square root of 485906798473894056 and 97866249624785 in scientific notation?\";\n\n assistant.chat(userMessage)\n .onNext(answerBuilder::append)\n .onComplete(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n Response<AiMessage> response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"6.97\", \"9.89\");\n assertThat(response.content().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isGreaterThan(0); // TODO\n assertThat(tokenUsage.outputTokenCount()).isGreaterThan(0);\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n\n verify(calculator).squareRoot(485906798473894056.0);\n verify(calculator).squareRoot(97866249624785.0);\n verifyNoMoreInteractions(calculator);\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(5);\n\n assertThat(messages.get(0)).isInstanceOf(dev.langchain4j.data.message.UserMessage.class);\n assertThat(messages.get(0).text()).isEqualTo(userMessage);\n\n AiMessage aiMessage = (AiMessage) messages.get(1);\n assertThat(aiMessage.text()).isNull();\n assertThat(aiMessage.toolExecutionRequests()).hasSize(2);\n\n ToolExecutionRequest firstToolExecutionRequest = aiMessage.toolExecutionRequests().get(0);\n assertThat(firstToolExecutionRequest.id()).isNotBlank();\n assertThat(firstToolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(firstToolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 485906798473894056}\");\n\n ToolExecutionRequest secondToolExecutionRequest = aiMessage.toolExecutionRequests().get(1);\n assertThat(secondToolExecutionRequest.id()).isNotBlank();\n assertThat(secondToolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 97866249624785}\");\n\n ToolExecutionResultMessage firstToolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(2);\n assertThat(firstToolExecutionResultMessage.id()).isEqualTo(firstToolExecutionRequest.id());\n assertThat(firstToolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(firstToolExecutionResultMessage.text()).isEqualTo(\"6.97070153193991E8\");\n\n ToolExecutionResultMessage secondToolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(3);\n assertThat(secondToolExecutionResultMessage.id()).isEqualTo(secondToolExecutionRequest.id());\n assertThat(secondToolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionResultMessage.text()).isEqualTo(\"9892737.215997653\");\n\n assertThat(messages.get(4)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(4).text()).contains(\"6.97\", \"9.89\");\n }\n}\n"}, "c6f42779167f09a2431b478f082723f34728fde6": {"t": 1785755667.5221996, "n": 20224, "h": "b09d18e90886be6e", "c": "package dev.langchain4j.service;\n\nimport dev.langchain4j.agent.tool.P;\nimport dev.langchain4j.agent.tool.Tool;\nimport dev.langchain4j.agent.tool.ToolExecutionRequest;\nimport dev.langchain4j.data.message.AiMessage;\nimport dev.langchain4j.data.message.ChatMessage;\nimport dev.langchain4j.data.message.ToolExecutionResultMessage;\nimport dev.langchain4j.data.message.UserMessage;\nimport dev.langchain4j.memory.ChatMemory;\nimport dev.langchain4j.memory.chat.MessageWindowChatMemory;\nimport dev.langchain4j.model.chat.StreamingChatLanguageModel;\nimport dev.langchain4j.model.openai.OpenAiStreamingChatModel;\nimport dev.langchain4j.model.output.Response;\nimport dev.langchain4j.model.output.TokenUsage;\nimport dev.langchain4j.rag.AugmentationResult;\nimport dev.langchain4j.rag.RetrievalAugmentor;\nimport dev.langchain4j.rag.content.Content;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.MethodSource;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.stream.Stream;\n\nimport static dev.langchain4j.model.openai.OpenAiChatModelName.GPT_4_O_MINI;\nimport static dev.langchain4j.model.output.FinishReason.STOP;\nimport static java.util.concurrent.TimeUnit.SECONDS;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.Mockito.any;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.spy;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.verifyNoMoreInteractions;\nimport static org.mockito.Mockito.when;\n\npublic class StreamingAiServicesIT {\n\n static Stream<StreamingChatLanguageModel> models() {\n return Stream.of(\n OpenAiStreamingChatModel.builder()\n .baseUrl(System.getenv(\"OPENAI_BASE_URL\"))\n .apiKey(System.getenv(\"OPENAI_API_KEY\"))\n .organizationId(System.getenv(\"OPENAI_ORGANIZATION_ID\"))\n .logRequests(true)\n .logResponses(true)\n .build()\n // TODO add more models\n );\n }\n\n interface Assistant {\n\n TokenStream chat(String userMessage);\n }\n\n @ParameterizedTest\n @MethodSource(\"models\")\n void should_stream_answer(StreamingChatLanguageModel model) throws Exception {\n\n Assistant assistant = AiServices.create(Assistant.class, model);\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<Response<AiMessage>> futureResponse = new CompletableFuture<>();\n\n assistant.chat(\"What is the capital of Germany?\")\n .onNext(answerBuilder::append)\n .onComplete(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n Response<AiMessage> response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"Berlin\");\n assertThat(response.content().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isGreaterThan(0);\n assertThat(tokenUsage.outputTokenCount()).isGreaterThan(0);\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n }\n\n @ParameterizedTest\n @MethodSource(\"models\")\n void should_callback_with_content(StreamingChatLanguageModel model) throws Exception {\n\n List<Content> contents = new ArrayList<>();\n contents.add(Content.from(\"This is additional content\"));\n\n RetrievalAugmentor retrievalAugmentor = mock(RetrievalAugmentor.class);\n ChatMessage message = UserMessage.from(\"What is the capital of Germany?\");\n\n AugmentationResult result = AugmentationResult.builder()\n .chatMessage(message)\n .contents(contents)\n .build();\n when(retrievalAugmentor.augment(any())).thenReturn(result);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(model)\n .retrievalAugmentor(retrievalAugmentor)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<List<Content>> futureContent = new CompletableFuture<>();\n\n assistant.chat(\"What is the capital of Germany?\")\n .onNext(answerBuilder::append)\n .onRetrieved(futureContent::complete)\n .ignoreErrors()\n .start();\n\n List<Content> returnedContents = futureContent.get(30, SECONDS);\n\n assertThat(returnedContents)\n .hasSize(1)\n .anySatisfy(c -> assertThat(c.textSegment().text()).isEqualTo(\"This is additional content\"));\n }\n\n @ParameterizedTest\n @MethodSource(\"models\")\n void should_stream_answers_with_memory(StreamingChatLanguageModel model) throws Exception {\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(model)\n .chatMemory(chatMemory)\n .build();\n\n\n String firstUserMessage = \"Hi, my name is Klaus\";\n CompletableFuture<Response<AiMessage>> firstResultFuture = new CompletableFuture<>();\n\n assistant.chat(firstUserMessage)\n .onNext(System.out::println)\n .onComplete(firstResultFuture::complete)\n .onError(firstResultFuture::completeExceptionally)\n .start();\n\n Response<AiMessage> firstResponse = firstResultFuture.get(30, SECONDS);\n assertThat(firstResponse.content().text()).contains(\"Klaus\");\n\n\n String secondUserMessage = \"What is my name?\";\n CompletableFuture<Response<AiMessage>> secondResultFuture = new CompletableFuture<>();\n\n assistant.chat(secondUserMessage)\n .onNext(System.out::println)\n .onComplete(secondResultFuture::complete)\n .onError(secondResultFuture::completeExceptionally)\n .start();\n\n Response<AiMessage> secondResponse = secondResultFuture.get(30, SECONDS);\n assertThat(secondResponse.content().text()).contains(\"Klaus\");\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(4);\n\n assertThat(messages.get(0)).isInstanceOf(UserMessage.class);\n assertThat(messages.get(0).text()).isEqualTo(firstUserMessage);\n\n assertThat(messages.get(1)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(1)).isEqualTo(firstResponse.content());\n\n assertThat(messages.get(2)).isInstanceOf(UserMessage.class);\n assertThat(messages.get(2).text()).isEqualTo(secondUserMessage);\n\n assertThat(messages.get(3)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(3)).isEqualTo(secondResponse.content());\n }\n\n static class Calculator {\n\n @Tool(\"calculates the square root of the provided number\")\n double squareRoot(@P(\"number to operate on\") double number) {\n return Math.sqrt(number);\n }\n }\n\n @ParameterizedTest\n @MethodSource(\"models\")\n void should_execute_a_tool_then_stream_answer(StreamingChatLanguageModel model) throws Exception {\n\n Calculator calculator = spy(new Calculator());\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(model)\n .chatMemory(chatMemory)\n .tools(calculator)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<Response<AiMessage>> futureResponse = new CompletableFuture<>();\n\n String userMessage = \"What is the square root of 485906798473894056 in scientific notation?\";\n\n assistant.chat(userMessage)\n .onNext(answerBuilder::append)\n .onComplete(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n Response<AiMessage> response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"6.97\");\n assertThat(response.content().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isGreaterThan(0);\n assertThat(tokenUsage.outputTokenCount()).isGreaterThan(0);\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n\n verify(calculator).squareRoot(485906798473894056.0);\n verifyNoMoreInteractions(calculator);\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(4);\n\n assertThat(messages.get(0)).isInstanceOf(UserMessage.class);\n assertThat(messages.get(0).text()).isEqualTo(userMessage);\n\n AiMessage aiMessage = (AiMessage) messages.get(1);\n assertThat(aiMessage.text()).isNull();\n assertThat(aiMessage.toolExecutionRequests()).hasSize(1);\n ToolExecutionRequest toolExecutionRequest = aiMessage.toolExecutionRequests().get(0);\n assertThat(toolExecutionRequest.id()).isNotBlank();\n\n assertThat(toolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 485906798473894056}\");\n\n ToolExecutionResultMessage toolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(2);\n assertThat(toolExecutionResultMessage.id()).isEqualTo(toolExecutionRequest.id());\n assertThat(toolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionResultMessage.text()).isEqualTo(\"6.97070153193991E8\");\n\n assertThat(messages.get(3)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(3).text()).contains(\"6.97\");\n }\n\n @Test\n void should_execute_multiple_tools_sequentially_then_answer() throws Exception {\n\n // TODO test more models\n StreamingChatLanguageModel streamingChatModel = OpenAiStreamingChatModel.builder()\n .baseUrl(System.getenv(\"OPENAI_BASE_URL\"))\n .apiKey(System.getenv(\"OPENAI_API_KEY\"))\n .organizationId(System.getenv(\"OPENAI_ORGANIZATION_ID\"))\n .modelName(GPT_4_O_MINI)\n .parallelToolCalls(false) // to force the model to call tools sequentially\n .temperature(0.0)\n .logRequests(true)\n .logResponses(true)\n .build();\n\n Calculator calculator = spy(new Calculator());\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(streamingChatModel)\n .chatMemory(chatMemory)\n .tools(calculator)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<Response<AiMessage>> futureResponse = new CompletableFuture<>();\n\n String userMessage = \"What is the square root of 485906798473894056 and 97866249624785 in scientific notation?\";\n\n assistant.chat(userMessage)\n .onNext(answerBuilder::append)\n .onComplete(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n Response<AiMessage> response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"6.97\", \"9.89\");\n assertThat(response.content().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isGreaterThan(0); // TODO\n assertThat(tokenUsage.outputTokenCount()).isGreaterThan(0);\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n\n verify(calculator).squareRoot(485906798473894056.0);\n verify(calculator).squareRoot(97866249624785.0);\n verifyNoMoreInteractions(calculator);\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(6);\n\n assertThat(messages.get(0)).isInstanceOf(dev.langchain4j.data.message.UserMessage.class);\n assertThat(messages.get(0).text()).isEqualTo(userMessage);\n\n AiMessage aiMessage = (AiMessage) messages.get(1);\n assertThat(aiMessage.text()).isNull();\n assertThat(aiMessage.toolExecutionRequests()).hasSize(1);\n ToolExecutionRequest toolExecutionRequest = aiMessage.toolExecutionRequests().get(0);\n assertThat(toolExecutionRequest.id()).isNotBlank();\n assertThat(toolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 485906798473894056}\");\n\n ToolExecutionResultMessage toolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(2);\n assertThat(toolExecutionResultMessage.id()).isEqualTo(toolExecutionRequest.id());\n assertThat(toolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionResultMessage.text()).isEqualTo(\"6.97070153193991E8\");\n\n AiMessage secondAiMessage = (AiMessage) messages.get(3);\n assertThat(secondAiMessage.text()).isNull();\n assertThat(secondAiMessage.toolExecutionRequests()).hasSize(1);\n ToolExecutionRequest secondToolExecutionRequest = secondAiMessage.toolExecutionRequests().get(0);\n assertThat(secondToolExecutionRequest.id()).isNotBlank();\n assertThat(secondToolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 97866249624785}\");\n\n ToolExecutionResultMessage secondToolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(4);\n assertThat(secondToolExecutionResultMessage.id()).isEqualTo(secondToolExecutionRequest.id());\n assertThat(secondToolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionResultMessage.text()).isEqualTo(\"9892737.215997653\");\n\n assertThat(messages.get(5)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(5).text()).contains(\"6.97\", \"9.89\");\n }\n\n @Test\n void should_execute_multiple_tools_in_parallel_then_answer() throws Exception {\n\n Calculator calculator = spy(new Calculator());\n\n // TODO test more models\n StreamingChatLanguageModel streamingChatModel = OpenAiStreamingChatModel.builder()\n .baseUrl(System.getenv(\"OPENAI_BASE_URL\"))\n .apiKey(System.getenv(\"OPENAI_API_KEY\"))\n .organizationId(System.getenv(\"OPENAI_ORGANIZATION_ID\"))\n .temperature(0.0)\n .logRequests(true)\n .logResponses(true)\n .build();\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(streamingChatModel)\n .chatMemory(chatMemory)\n .tools(calculator)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<Response<AiMessage>> futureResponse = new CompletableFuture<>();\n\n String userMessage = \"What is the square root of 485906798473894056 and 97866249624785 in scientific notation?\";\n\n assistant.chat(userMessage)\n .onNext(answerBuilder::append)\n .onComplete(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n Response<AiMessage> response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"6.97\", \"9.89\");\n assertThat(response.content().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isGreaterThan(0); // TODO\n assertThat(tokenUsage.outputTokenCount()).isGreaterThan(0);\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n\n verify(calculator).squareRoot(485906798473894056.0);\n verify(calculator).squareRoot(97866249624785.0);\n verifyNoMoreInteractions(calculator);\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(5);\n\n assertThat(messages.get(0)).isInstanceOf(dev.langchain4j.data.message.UserMessage.class);\n assertThat(messages.get(0).text()).isEqualTo(userMessage);\n\n AiMessage aiMessage = (AiMessage) messages.get(1);\n assertThat(aiMessage.text()).isNull();\n assertThat(aiMessage.toolExecutionRequests()).hasSize(2);\n\n ToolExecutionRequest firstToolExecutionRequest = aiMessage.toolExecutionRequests().get(0);\n assertThat(firstToolExecutionRequest.id()).isNotBlank();\n assertThat(firstToolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(firstToolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 485906798473894056}\");\n\n ToolExecutionRequest secondToolExecutionRequest = aiMessage.toolExecutionRequests().get(1);\n assertThat(secondToolExecutionRequest.id()).isNotBlank();\n assertThat(secondToolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 97866249624785}\");\n\n ToolExecutionResultMessage firstToolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(2);\n assertThat(firstToolExecutionResultMessage.id()).isEqualTo(firstToolExecutionRequest.id());\n assertThat(firstToolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(firstToolExecutionResultMessage.text()).isEqualTo(\"6.97070153193991E8\");\n\n ToolExecutionResultMessage secondToolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(3);\n assertThat(secondToolExecutionResultMessage.id()).isEqualTo(secondToolExecutionRequest.id());\n assertThat(secondToolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionResultMessage.text()).isEqualTo(\"9892737.215997653\");\n\n assertThat(messages.get(4)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(4).text()).contains(\"6.97\", \"9.89\");\n }\n}\n"}, "3d1c7b8b15896ef7c3d66b0f030031ab09dccfa2": {"t": 1785755668.927226, "n": 21107, "h": "d23c403158850cb7", "c": "package dev.langchain4j.service;\n\nimport dev.langchain4j.agent.tool.P;\nimport dev.langchain4j.agent.tool.Tool;\nimport dev.langchain4j.agent.tool.ToolExecutionRequest;\nimport dev.langchain4j.data.message.AiMessage;\nimport dev.langchain4j.data.message.ChatMessage;\nimport dev.langchain4j.data.message.ToolExecutionResultMessage;\nimport dev.langchain4j.data.message.UserMessage;\nimport dev.langchain4j.memory.ChatMemory;\nimport dev.langchain4j.memory.chat.MessageWindowChatMemory;\nimport dev.langchain4j.model.azure.AzureOpenAiStreamingChatModel;\nimport dev.langchain4j.model.chat.StreamingChatLanguageModel;\nimport dev.langchain4j.model.mistralai.MistralAiStreamingChatModel;\nimport dev.langchain4j.model.openai.OpenAiStreamingChatModel;\nimport dev.langchain4j.model.output.Response;\nimport dev.langchain4j.model.output.TokenUsage;\nimport dev.langchain4j.rag.AugmentationResult;\nimport dev.langchain4j.rag.RetrievalAugmentor;\nimport dev.langchain4j.rag.content.Content;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.MethodSource;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.stream.Stream;\n\nimport static dev.langchain4j.model.mistralai.MistralAiChatModelName.MISTRAL_LARGE_LATEST;\nimport static dev.langchain4j.model.openai.OpenAiChatModelName.GPT_4_O_MINI;\nimport static dev.langchain4j.model.output.FinishReason.STOP;\nimport static java.util.concurrent.TimeUnit.SECONDS;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.Mockito.*;\n\npublic class StreamingAiServicesIT {\n\n static Stream<StreamingChatLanguageModel> models() {\n return Stream.of(\n OpenAiStreamingChatModel.builder()\n .baseUrl(System.getenv(\"OPENAI_BASE_URL\"))\n .apiKey(System.getenv(\"OPENAI_API_KEY\"))\n .organizationId(System.getenv(\"OPENAI_ORGANIZATION_ID\"))\n .logRequests(true)\n .logResponses(true)\n .build(),\n AzureOpenAiStreamingChatModel.builder()\n .endpoint(System.getenv(\"AZURE_OPENAI_ENDPOINT\"))\n .apiKey(System.getenv(\"AZURE_OPENAI_KEY\"))\n .deploymentName(\"gpt-4o\")\n .logRequestsAndResponses(true)\n .build(),\n MistralAiStreamingChatModel.builder()\n .apiKey(System.getenv(\"MISTRAL_AI_API_KEY\"))\n .modelName(MISTRAL_LARGE_LATEST)\n .logRequests(true)\n .logResponses(true)\n .build()\n // TODO add more models\n );\n }\n\n interface Assistant {\n\n TokenStream chat(String userMessage);\n }\n\n @ParameterizedTest\n @MethodSource(\"models\")\n void should_stream_answer(StreamingChatLanguageModel model) throws Exception {\n\n Assistant assistant = AiServices.create(Assistant.class, model);\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<Response<AiMessage>> futureResponse = new CompletableFuture<>();\n\n assistant.chat(\"What is the capital of Germany?\")\n .onNext(answerBuilder::append)\n .onComplete(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n Response<AiMessage> response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"Berlin\");\n assertThat(response.content().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n if (!(model instanceof AzureOpenAiStreamingChatModel)) { // TODO\n assertThat(tokenUsage.inputTokenCount()).isGreaterThan(0);\n assertThat(tokenUsage.outputTokenCount()).isGreaterThan(0);\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n }\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n }\n\n @ParameterizedTest\n @MethodSource(\"models\")\n void should_callback_with_content(StreamingChatLanguageModel model) throws Exception {\n\n List<Content> contents = new ArrayList<>();\n contents.add(Content.from(\"This is additional content\"));\n\n RetrievalAugmentor retrievalAugmentor = mock(RetrievalAugmentor.class);\n ChatMessage message = UserMessage.from(\"What is the capital of Germany?\");\n\n AugmentationResult result = AugmentationResult.builder()\n .chatMessage(message)\n .contents(contents)\n .build();\n when(retrievalAugmentor.augment(any())).thenReturn(result);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(model)\n .retrievalAugmentor(retrievalAugmentor)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<List<Content>> futureContent = new CompletableFuture<>();\n\n assistant.chat(\"What is the capital of Germany?\")\n .onNext(answerBuilder::append)\n .onRetrieved(futureContent::complete)\n .ignoreErrors()\n .start();\n\n List<Content> returnedContents = futureContent.get(30, SECONDS);\n\n assertThat(returnedContents)\n .hasSize(1)\n .anySatisfy(c -> assertThat(c.textSegment().text()).isEqualTo(\"This is additional content\"));\n }\n\n @ParameterizedTest\n @MethodSource(\"models\")\n void should_stream_answers_with_memory(StreamingChatLanguageModel model) throws Exception {\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(model)\n .chatMemory(chatMemory)\n .build();\n\n\n String firstUserMessage = \"Hi, my name is Klaus\";\n CompletableFuture<Response<AiMessage>> firstResultFuture = new CompletableFuture<>();\n\n assistant.chat(firstUserMessage)\n .onNext(System.out::println)\n .onComplete(firstResultFuture::complete)\n .onError(firstResultFuture::completeExceptionally)\n .start();\n\n Response<AiMessage> firstResponse = firstResultFuture.get(30, SECONDS);\n assertThat(firstResponse.content().text()).contains(\"Klaus\");\n\n\n String secondUserMessage = \"What is my name?\";\n CompletableFuture<Response<AiMessage>> secondResultFuture = new CompletableFuture<>();\n\n assistant.chat(secondUserMessage)\n .onNext(System.out::println)\n .onComplete(secondResultFuture::complete)\n .onError(secondResultFuture::completeExceptionally)\n .start();\n\n Response<AiMessage> secondResponse = secondResultFuture.get(30, SECONDS);\n assertThat(secondResponse.content().text()).contains(\"Klaus\");\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(4);\n\n assertThat(messages.get(0)).isInstanceOf(UserMessage.class);\n assertThat(messages.get(0).text()).isEqualTo(firstUserMessage);\n\n assertThat(messages.get(1)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(1)).isEqualTo(firstResponse.content());\n\n assertThat(messages.get(2)).isInstanceOf(UserMessage.class);\n assertThat(messages.get(2).text()).isEqualTo(secondUserMessage);\n\n assertThat(messages.get(3)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(3)).isEqualTo(secondResponse.content());\n }\n\n static class Calculator {\n\n @Tool(\"calculates the square root of the provided number\")\n double squareRoot(@P(\"number to operate on\") double number) {\n return Math.sqrt(number);\n }\n }\n\n @ParameterizedTest\n @MethodSource(\"models\")\n void should_execute_a_tool_then_stream_answer(StreamingChatLanguageModel model) throws Exception {\n\n Calculator calculator = spy(new Calculator());\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(model)\n .chatMemory(chatMemory)\n .tools(calculator)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<Response<AiMessage>> futureResponse = new CompletableFuture<>();\n\n String userMessage = \"What is the square root of 485906798473894056 in scientific notation?\";\n\n assistant.chat(userMessage)\n .onNext(answerBuilder::append)\n .onComplete(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n Response<AiMessage> response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"6.97\");\n assertThat(response.content().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n if (!(model instanceof AzureOpenAiStreamingChatModel)) { // TODO\n assertThat(tokenUsage.inputTokenCount()).isGreaterThan(0);\n assertThat(tokenUsage.outputTokenCount()).isGreaterThan(0);\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n }\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n\n verify(calculator).squareRoot(485906798473894056.0);\n verifyNoMoreInteractions(calculator);\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(4);\n\n assertThat(messages.get(0)).isInstanceOf(UserMessage.class);\n assertThat(messages.get(0).text()).isEqualTo(userMessage);\n\n AiMessage aiMessage = (AiMessage) messages.get(1);\n assertThat(aiMessage.text()).isNull();\n assertThat(aiMessage.toolExecutionRequests()).hasSize(1);\n ToolExecutionRequest toolExecutionRequest = aiMessage.toolExecutionRequests().get(0);\n if (!(model instanceof AzureOpenAiStreamingChatModel)) { // TODO\n assertThat(toolExecutionRequest.id()).isNotBlank();\n }\n\n assertThat(toolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 485906798473894056}\");\n\n ToolExecutionResultMessage toolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(2);\n assertThat(toolExecutionResultMessage.id()).isEqualTo(toolExecutionRequest.id());\n assertThat(toolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionResultMessage.text()).isEqualTo(\"6.97070153193991E8\");\n\n assertThat(messages.get(3)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(3).text()).contains(\"6.97\");\n }\n\n @Test\n void should_execute_multiple_tools_sequentially_then_answer() throws Exception {\n\n // TODO test more models\n StreamingChatLanguageModel streamingChatModel = OpenAiStreamingChatModel.builder()\n .baseUrl(System.getenv(\"OPENAI_BASE_URL\"))\n .apiKey(System.getenv(\"OPENAI_API_KEY\"))\n .organizationId(System.getenv(\"OPENAI_ORGANIZATION_ID\"))\n .modelName(GPT_4_O_MINI)\n .parallelToolCalls(false) // called sequentially\n .temperature(0.0)\n .logRequests(true)\n .logResponses(true)\n .build();\n\n Calculator calculator = spy(new Calculator());\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(streamingChatModel)\n .chatMemory(chatMemory)\n .tools(calculator)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<Response<AiMessage>> futureResponse = new CompletableFuture<>();\n\n String userMessage = \"What is the square root of 485906798473894056 and 97866249624785 in scientific notation?\";\n\n assistant.chat(userMessage)\n .onNext(answerBuilder::append)\n .onComplete(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n Response<AiMessage> response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"6.97\", \"9.89\");\n assertThat(response.content().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isGreaterThan(0); // TODO\n assertThat(tokenUsage.outputTokenCount()).isGreaterThan(0);\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n\n verify(calculator).squareRoot(485906798473894056.0);\n verify(calculator).squareRoot(97866249624785.0);\n verifyNoMoreInteractions(calculator);\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(6);\n\n assertThat(messages.get(0)).isInstanceOf(dev.langchain4j.data.message.UserMessage.class);\n assertThat(messages.get(0).text()).isEqualTo(userMessage);\n\n AiMessage aiMessage = (AiMessage) messages.get(1);\n assertThat(aiMessage.text()).isNull();\n assertThat(aiMessage.toolExecutionRequests()).hasSize(1);\n ToolExecutionRequest toolExecutionRequest = aiMessage.toolExecutionRequests().get(0);\n assertThat(toolExecutionRequest.id()).isNotBlank();\n assertThat(toolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 485906798473894056}\");\n\n ToolExecutionResultMessage toolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(2);\n assertThat(toolExecutionResultMessage.id()).isEqualTo(toolExecutionRequest.id());\n assertThat(toolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionResultMessage.text()).isEqualTo(\"6.97070153193991E8\");\n\n AiMessage secondAiMessage = (AiMessage) messages.get(3);\n assertThat(secondAiMessage.text()).isNull();\n assertThat(secondAiMessage.toolExecutionRequests()).hasSize(1);\n ToolExecutionRequest secondToolExecutionRequest = secondAiMessage.toolExecutionRequests().get(0);\n assertThat(secondToolExecutionRequest.id()).isNotBlank();\n assertThat(secondToolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 97866249624785}\");\n\n ToolExecutionResultMessage secondToolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(4);\n assertThat(secondToolExecutionResultMessage.id()).isEqualTo(secondToolExecutionRequest.id());\n assertThat(secondToolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionResultMessage.text()).isEqualTo(\"9892737.215997653\");\n\n assertThat(messages.get(5)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(5).text()).contains(\"6.97\", \"9.89\");\n }\n\n @Test\n void should_execute_multiple_tools_in_parallel_then_answer() throws Exception {\n\n Calculator calculator = spy(new Calculator());\n\n // TODO test more models\n StreamingChatLanguageModel streamingChatModel = OpenAiStreamingChatModel.builder()\n .baseUrl(System.getenv(\"OPENAI_BASE_URL\"))\n .apiKey(System.getenv(\"OPENAI_API_KEY\"))\n .organizationId(System.getenv(\"OPENAI_ORGANIZATION_ID\"))\n .temperature(0.0)\n .logRequests(true)\n .logResponses(true)\n .build();\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(streamingChatModel)\n .chatMemory(chatMemory)\n .tools(calculator)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<Response<AiMessage>> futureResponse = new CompletableFuture<>();\n\n String userMessage = \"What is the square root of 485906798473894056 and 97866249624785 in scientific notation?\";\n\n assistant.chat(userMessage)\n .onNext(answerBuilder::append)\n .onComplete(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n Response<AiMessage> response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"6.97\", \"9.89\");\n assertThat(response.content().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isGreaterThan(0); // TODO\n assertThat(tokenUsage.outputTokenCount()).isGreaterThan(0);\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n\n verify(calculator).squareRoot(485906798473894056.0);\n verify(calculator).squareRoot(97866249624785.0);\n verifyNoMoreInteractions(calculator);\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(5);\n\n assertThat(messages.get(0)).isInstanceOf(dev.langchain4j.data.message.UserMessage.class);\n assertThat(messages.get(0).text()).isEqualTo(userMessage);\n\n AiMessage aiMessage = (AiMessage) messages.get(1);\n assertThat(aiMessage.text()).isNull();\n assertThat(aiMessage.toolExecutionRequests()).hasSize(2);\n\n ToolExecutionRequest firstToolExecutionRequest = aiMessage.toolExecutionRequests().get(0);\n assertThat(firstToolExecutionRequest.id()).isNotBlank();\n assertThat(firstToolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(firstToolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 485906798473894056}\");\n\n ToolExecutionRequest secondToolExecutionRequest = aiMessage.toolExecutionRequests().get(1);\n assertThat(secondToolExecutionRequest.id()).isNotBlank();\n assertThat(secondToolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 97866249624785}\");\n\n ToolExecutionResultMessage firstToolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(2);\n assertThat(firstToolExecutionResultMessage.id()).isEqualTo(firstToolExecutionRequest.id());\n assertThat(firstToolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(firstToolExecutionResultMessage.text()).isEqualTo(\"6.97070153193991E8\");\n\n ToolExecutionResultMessage secondToolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(3);\n assertThat(secondToolExecutionResultMessage.id()).isEqualTo(secondToolExecutionRequest.id());\n assertThat(secondToolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionResultMessage.text()).isEqualTo(\"9892737.215997653\");\n\n assertThat(messages.get(4)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(4).text()).contains(\"6.97\", \"9.89\");\n }\n}\n"}, "42e8c29873b131baf108bd05882c8635638f211b": {"t": 1785755670.55684, "n": 19479, "h": "47217962d9c07f13", "c": "package dev.langchain4j.service;\n\nimport dev.langchain4j.agent.tool.P;\nimport dev.langchain4j.agent.tool.Tool;\nimport dev.langchain4j.agent.tool.ToolExecutionRequest;\nimport dev.langchain4j.data.message.AiMessage;\nimport dev.langchain4j.data.message.ChatMessage;\nimport dev.langchain4j.data.message.ToolExecutionResultMessage;\nimport dev.langchain4j.data.message.UserMessage;\nimport dev.langchain4j.memory.ChatMemory;\nimport dev.langchain4j.memory.chat.MessageWindowChatMemory;\nimport dev.langchain4j.model.azure.AzureOpenAiStreamingChatModel;\nimport dev.langchain4j.model.chat.StreamingChatLanguageModel;\nimport dev.langchain4j.model.mistralai.MistralAiStreamingChatModel;\nimport dev.langchain4j.model.openai.OpenAiStreamingChatModel;\nimport dev.langchain4j.model.output.Response;\nimport dev.langchain4j.model.output.TokenUsage;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.MethodSource;\n\nimport java.util.List;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.stream.Stream;\n\nimport static dev.langchain4j.model.mistralai.MistralAiChatModelName.MISTRAL_LARGE_LATEST;\nimport static dev.langchain4j.model.openai.OpenAiChatModelName.GPT_4_O_MINI;\nimport static dev.langchain4j.model.output.FinishReason.STOP;\nimport static java.util.concurrent.TimeUnit.SECONDS;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.Mockito.*;\n\npublic class StreamingAiServicesIT {\n\n static Stream<StreamingChatLanguageModel> models() {\n return Stream.of(\n OpenAiStreamingChatModel.builder()\n .baseUrl(System.getenv(\"OPENAI_BASE_URL\"))\n .apiKey(System.getenv(\"OPENAI_API_KEY\"))\n .organizationId(System.getenv(\"OPENAI_ORGANIZATION_ID\"))\n .logRequests(true)\n .logResponses(true)\n .build(),\n AzureOpenAiStreamingChatModel.builder()\n .endpoint(System.getenv(\"AZURE_OPENAI_ENDPOINT\"))\n .apiKey(System.getenv(\"AZURE_OPENAI_KEY\"))\n .deploymentName(\"gpt-4o\")\n .logRequestsAndResponses(true)\n .build(),\n MistralAiStreamingChatModel.builder()\n .apiKey(System.getenv(\"MISTRAL_AI_API_KEY\"))\n .modelName(MISTRAL_LARGE_LATEST)\n .logRequests(true)\n .logResponses(true)\n .build()\n // TODO add more models\n );\n }\n\n interface Assistant {\n\n TokenStream chat(String userMessage);\n }\n\n @ParameterizedTest\n @MethodSource(\"models\")\n void should_stream_answer(StreamingChatLanguageModel model) throws Exception {\n\n Assistant assistant = AiServices.create(Assistant.class, model);\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<Response<AiMessage>> futureResponse = new CompletableFuture<>();\n\n assistant.chat(\"What is the capital of Germany?\")\n .onNext(answerBuilder::append)\n .onComplete(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n Response<AiMessage> response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"Berlin\");\n assertThat(response.content().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n if (!(model instanceof AzureOpenAiStreamingChatModel)) { // TODO\n assertThat(tokenUsage.inputTokenCount()).isGreaterThan(0);\n assertThat(tokenUsage.outputTokenCount()).isGreaterThan(0);\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n }\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n }\n\n @ParameterizedTest\n @MethodSource(\"models\")\n void should_stream_answers_with_memory(StreamingChatLanguageModel model) throws Exception {\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(model)\n .chatMemory(chatMemory)\n .build();\n\n\n String firstUserMessage = \"Hi, my name is Klaus\";\n CompletableFuture<Response<AiMessage>> firstResultFuture = new CompletableFuture<>();\n\n assistant.chat(firstUserMessage)\n .onNext(System.out::println)\n .onComplete(firstResultFuture::complete)\n .onError(firstResultFuture::completeExceptionally)\n .start();\n\n Response<AiMessage> firstResponse = firstResultFuture.get(30, SECONDS);\n assertThat(firstResponse.content().text()).contains(\"Klaus\");\n\n\n String secondUserMessage = \"What is my name?\";\n CompletableFuture<Response<AiMessage>> secondResultFuture = new CompletableFuture<>();\n\n assistant.chat(secondUserMessage)\n .onNext(System.out::println)\n .onComplete(secondResultFuture::complete)\n .onError(secondResultFuture::completeExceptionally)\n .start();\n\n Response<AiMessage> secondResponse = secondResultFuture.get(30, SECONDS);\n assertThat(secondResponse.content().text()).contains(\"Klaus\");\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(4);\n\n assertThat(messages.get(0)).isInstanceOf(UserMessage.class);\n assertThat(messages.get(0).text()).isEqualTo(firstUserMessage);\n\n assertThat(messages.get(1)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(1)).isEqualTo(firstResponse.content());\n\n assertThat(messages.get(2)).isInstanceOf(UserMessage.class);\n assertThat(messages.get(2).text()).isEqualTo(secondUserMessage);\n\n assertThat(messages.get(3)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(3)).isEqualTo(secondResponse.content());\n }\n\n static class Calculator {\n\n @Tool(\"calculates the square root of the provided number\")\n double squareRoot(@P(\"number to operate on\") double number) {\n return Math.sqrt(number);\n }\n }\n\n @ParameterizedTest\n @MethodSource(\"models\")\n void should_execute_a_tool_then_stream_answer(StreamingChatLanguageModel model) throws Exception {\n\n Calculator calculator = spy(new Calculator());\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(model)\n .chatMemory(chatMemory)\n .tools(calculator)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<Response<AiMessage>> futureResponse = new CompletableFuture<>();\n\n String userMessage = \"What is the square root of 485906798473894056 in scientific notation?\";\n\n assistant.chat(userMessage)\n .onNext(answerBuilder::append)\n .onComplete(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n Response<AiMessage> response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"6.97\");\n assertThat(response.content().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n if (!(model instanceof AzureOpenAiStreamingChatModel)) { // TODO\n assertThat(tokenUsage.inputTokenCount()).isGreaterThan(0);\n assertThat(tokenUsage.outputTokenCount()).isGreaterThan(0);\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n }\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n\n verify(calculator).squareRoot(485906798473894056.0);\n verifyNoMoreInteractions(calculator);\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(4);\n\n assertThat(messages.get(0)).isInstanceOf(UserMessage.class);\n assertThat(messages.get(0).text()).isEqualTo(userMessage);\n\n AiMessage aiMessage = (AiMessage) messages.get(1);\n assertThat(aiMessage.text()).isNull();\n assertThat(aiMessage.toolExecutionRequests()).hasSize(1);\n ToolExecutionRequest toolExecutionRequest = aiMessage.toolExecutionRequests().get(0);\n if (!(model instanceof AzureOpenAiStreamingChatModel)) { // TODO\n assertThat(toolExecutionRequest.id()).isNotBlank();\n }\n\n assertThat(toolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 485906798473894056}\");\n\n ToolExecutionResultMessage toolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(2);\n assertThat(toolExecutionResultMessage.id()).isEqualTo(toolExecutionRequest.id());\n assertThat(toolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionResultMessage.text()).isEqualTo(\"6.97070153193991E8\");\n\n assertThat(messages.get(3)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(3).text()).contains(\"6.97\");\n }\n\n @Test\n void should_execute_multiple_tools_sequentially_then_answer() throws Exception {\n\n // TODO test more models\n StreamingChatLanguageModel streamingChatModel = OpenAiStreamingChatModel.builder()\n .baseUrl(System.getenv(\"OPENAI_BASE_URL\"))\n .apiKey(System.getenv(\"OPENAI_API_KEY\"))\n .organizationId(System.getenv(\"OPENAI_ORGANIZATION_ID\"))\n .modelName(GPT_4_O_MINI)\n .parallelToolCalls(false) // called sequentially\n .temperature(0.0)\n .logRequests(true)\n .logResponses(true)\n .build();\n\n Calculator calculator = spy(new Calculator());\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(streamingChatModel)\n .chatMemory(chatMemory)\n .tools(calculator)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<Response<AiMessage>> futureResponse = new CompletableFuture<>();\n\n String userMessage = \"What is the square root of 485906798473894056 and 97866249624785 in scientific notation?\";\n\n assistant.chat(userMessage)\n .onNext(answerBuilder::append)\n .onComplete(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n Response<AiMessage> response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"6.97\", \"9.89\");\n assertThat(response.content().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isGreaterThan(0); // TODO\n assertThat(tokenUsage.outputTokenCount()).isGreaterThan(0);\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n\n verify(calculator).squareRoot(485906798473894056.0);\n verify(calculator).squareRoot(97866249624785.0);\n verifyNoMoreInteractions(calculator);\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(6);\n\n assertThat(messages.get(0)).isInstanceOf(dev.langchain4j.data.message.UserMessage.class);\n assertThat(messages.get(0).text()).isEqualTo(userMessage);\n\n AiMessage aiMessage = (AiMessage) messages.get(1);\n assertThat(aiMessage.text()).isNull();\n assertThat(aiMessage.toolExecutionRequests()).hasSize(1);\n ToolExecutionRequest toolExecutionRequest = aiMessage.toolExecutionRequests().get(0);\n assertThat(toolExecutionRequest.id()).isNotBlank();\n assertThat(toolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 485906798473894056}\");\n\n ToolExecutionResultMessage toolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(2);\n assertThat(toolExecutionResultMessage.id()).isEqualTo(toolExecutionRequest.id());\n assertThat(toolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionResultMessage.text()).isEqualTo(\"6.97070153193991E8\");\n\n AiMessage secondAiMessage = (AiMessage) messages.get(3);\n assertThat(secondAiMessage.text()).isNull();\n assertThat(secondAiMessage.toolExecutionRequests()).hasSize(1);\n ToolExecutionRequest secondToolExecutionRequest = secondAiMessage.toolExecutionRequests().get(0);\n assertThat(secondToolExecutionRequest.id()).isNotBlank();\n assertThat(secondToolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 97866249624785}\");\n\n ToolExecutionResultMessage secondToolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(4);\n assertThat(secondToolExecutionResultMessage.id()).isEqualTo(secondToolExecutionRequest.id());\n assertThat(secondToolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionResultMessage.text()).isEqualTo(\"9892737.215997653\");\n\n assertThat(messages.get(5)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(5).text()).contains(\"6.97\", \"9.89\");\n }\n\n @Test\n void should_execute_multiple_tools_in_parallel_then_answer() throws Exception {\n\n Calculator calculator = spy(new Calculator());\n\n // TODO test more models\n StreamingChatLanguageModel streamingChatModel = OpenAiStreamingChatModel.builder()\n .baseUrl(System.getenv(\"OPENAI_BASE_URL\"))\n .apiKey(System.getenv(\"OPENAI_API_KEY\"))\n .organizationId(System.getenv(\"OPENAI_ORGANIZATION_ID\"))\n .temperature(0.0)\n .logRequests(true)\n .logResponses(true)\n .build();\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(streamingChatModel)\n .chatMemory(chatMemory)\n .tools(calculator)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<Response<AiMessage>> futureResponse = new CompletableFuture<>();\n\n String userMessage = \"What is the square root of 485906798473894056 and 97866249624785 in scientific notation?\";\n\n assistant.chat(userMessage)\n .onNext(answerBuilder::append)\n .onComplete(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n Response<AiMessage> response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"6.97\", \"9.89\");\n assertThat(response.content().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isGreaterThan(0); // TODO\n assertThat(tokenUsage.outputTokenCount()).isGreaterThan(0);\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n\n verify(calculator).squareRoot(485906798473894056.0);\n verify(calculator).squareRoot(97866249624785.0);\n verifyNoMoreInteractions(calculator);\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(5);\n\n assertThat(messages.get(0)).isInstanceOf(dev.langchain4j.data.message.UserMessage.class);\n assertThat(messages.get(0).text()).isEqualTo(userMessage);\n\n AiMessage aiMessage = (AiMessage) messages.get(1);\n assertThat(aiMessage.text()).isNull();\n assertThat(aiMessage.toolExecutionRequests()).hasSize(2);\n\n ToolExecutionRequest firstToolExecutionRequest = aiMessage.toolExecutionRequests().get(0);\n assertThat(firstToolExecutionRequest.id()).isNotBlank();\n assertThat(firstToolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(firstToolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 485906798473894056}\");\n\n ToolExecutionRequest secondToolExecutionRequest = aiMessage.toolExecutionRequests().get(1);\n assertThat(secondToolExecutionRequest.id()).isNotBlank();\n assertThat(secondToolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 97866249624785}\");\n\n ToolExecutionResultMessage firstToolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(2);\n assertThat(firstToolExecutionResultMessage.id()).isEqualTo(firstToolExecutionRequest.id());\n assertThat(firstToolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(firstToolExecutionResultMessage.text()).isEqualTo(\"6.97070153193991E8\");\n\n ToolExecutionResultMessage secondToolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(3);\n assertThat(secondToolExecutionResultMessage.id()).isEqualTo(secondToolExecutionRequest.id());\n assertThat(secondToolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionResultMessage.text()).isEqualTo(\"9892737.215997653\");\n\n assertThat(messages.get(4)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(4).text()).contains(\"6.97\", \"9.89\");\n }\n}\n"}, "4da3fcba6f755920431b6317730537b07ef98e8b": {"t": 1785755673.434051, "n": 19472, "h": "610d0e7af7a2548e", "c": "package dev.langchain4j.service;\n\nimport dev.langchain4j.agent.tool.P;\nimport dev.langchain4j.agent.tool.Tool;\nimport dev.langchain4j.agent.tool.ToolExecutionRequest;\nimport dev.langchain4j.data.message.AiMessage;\nimport dev.langchain4j.data.message.ChatMessage;\nimport dev.langchain4j.data.message.ToolExecutionResultMessage;\nimport dev.langchain4j.data.message.UserMessage;\nimport dev.langchain4j.memory.ChatMemory;\nimport dev.langchain4j.memory.chat.MessageWindowChatMemory;\nimport dev.langchain4j.model.azure.AzureOpenAiStreamingChatModel;\nimport dev.langchain4j.model.chat.StreamingChatLanguageModel;\nimport dev.langchain4j.model.mistralai.MistralAiStreamingChatModel;\nimport dev.langchain4j.model.openai.OpenAiStreamingChatModel;\nimport dev.langchain4j.model.output.Response;\nimport dev.langchain4j.model.output.TokenUsage;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.MethodSource;\n\nimport java.util.List;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.stream.Stream;\n\nimport static dev.langchain4j.model.mistralai.MistralAiChatModelName.MISTRAL_LARGE_LATEST;\nimport static dev.langchain4j.model.openai.OpenAiChatModelName.GPT_3_5_TURBO_0613;\nimport static dev.langchain4j.model.output.FinishReason.STOP;\nimport static java.util.concurrent.TimeUnit.SECONDS;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.Mockito.*;\n\npublic class StreamingAiServicesIT {\n\n static Stream<StreamingChatLanguageModel> models() {\n return Stream.of(\n OpenAiStreamingChatModel.builder()\n .baseUrl(System.getenv(\"OPENAI_BASE_URL\"))\n .apiKey(System.getenv(\"OPENAI_API_KEY\"))\n .organizationId(System.getenv(\"OPENAI_ORGANIZATION_ID\"))\n .logRequests(true)\n .logResponses(true)\n .build(),\n AzureOpenAiStreamingChatModel.builder()\n .endpoint(System.getenv(\"AZURE_OPENAI_ENDPOINT\"))\n .apiKey(System.getenv(\"AZURE_OPENAI_KEY\"))\n .deploymentName(\"gpt-4o\")\n .logRequestsAndResponses(true)\n .build(),\n MistralAiStreamingChatModel.builder()\n .apiKey(System.getenv(\"MISTRAL_AI_API_KEY\"))\n .modelName(MISTRAL_LARGE_LATEST)\n .logRequests(true)\n .logResponses(true)\n .build()\n // TODO add more models\n );\n }\n\n interface Assistant {\n\n TokenStream chat(String userMessage);\n }\n\n @ParameterizedTest\n @MethodSource(\"models\")\n void should_stream_answer(StreamingChatLanguageModel model) throws Exception {\n\n Assistant assistant = AiServices.create(Assistant.class, model);\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<Response<AiMessage>> futureResponse = new CompletableFuture<>();\n\n assistant.chat(\"What is the capital of Germany?\")\n .onNext(answerBuilder::append)\n .onComplete(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n Response<AiMessage> response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"Berlin\");\n assertThat(response.content().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n if (!(model instanceof AzureOpenAiStreamingChatModel)) { // TODO\n assertThat(tokenUsage.inputTokenCount()).isGreaterThan(0);\n assertThat(tokenUsage.outputTokenCount()).isGreaterThan(0);\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n }\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n }\n\n @ParameterizedTest\n @MethodSource(\"models\")\n void should_stream_answers_with_memory(StreamingChatLanguageModel model) throws Exception {\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(model)\n .chatMemory(chatMemory)\n .build();\n\n\n String firstUserMessage = \"Hi, my name is Klaus\";\n CompletableFuture<Response<AiMessage>> firstResultFuture = new CompletableFuture<>();\n\n assistant.chat(firstUserMessage)\n .onNext(System.out::println)\n .onComplete(firstResultFuture::complete)\n .onError(firstResultFuture::completeExceptionally)\n .start();\n\n Response<AiMessage> firstResponse = firstResultFuture.get(30, SECONDS);\n assertThat(firstResponse.content().text()).contains(\"Klaus\");\n\n\n String secondUserMessage = \"What is my name?\";\n CompletableFuture<Response<AiMessage>> secondResultFuture = new CompletableFuture<>();\n\n assistant.chat(secondUserMessage)\n .onNext(System.out::println)\n .onComplete(secondResultFuture::complete)\n .onError(secondResultFuture::completeExceptionally)\n .start();\n\n Response<AiMessage> secondResponse = secondResultFuture.get(30, SECONDS);\n assertThat(secondResponse.content().text()).contains(\"Klaus\");\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(4);\n\n assertThat(messages.get(0)).isInstanceOf(UserMessage.class);\n assertThat(messages.get(0).text()).isEqualTo(firstUserMessage);\n\n assertThat(messages.get(1)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(1)).isEqualTo(firstResponse.content());\n\n assertThat(messages.get(2)).isInstanceOf(UserMessage.class);\n assertThat(messages.get(2).text()).isEqualTo(secondUserMessage);\n\n assertThat(messages.get(3)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(3)).isEqualTo(secondResponse.content());\n }\n\n static class Calculator {\n\n @Tool(\"calculates the square root of the provided number\")\n double squareRoot(@P(\"number to operate on\") double number) {\n return Math.sqrt(number);\n }\n }\n\n @ParameterizedTest\n @MethodSource(\"models\")\n void should_execute_a_tool_then_stream_answer(StreamingChatLanguageModel model) throws Exception {\n\n Calculator calculator = spy(new Calculator());\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(model)\n .chatMemory(chatMemory)\n .tools(calculator)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<Response<AiMessage>> futureResponse = new CompletableFuture<>();\n\n String userMessage = \"What is the square root of 485906798473894056 in scientific notation?\";\n\n assistant.chat(userMessage)\n .onNext(answerBuilder::append)\n .onComplete(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n Response<AiMessage> response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"6.97\");\n assertThat(response.content().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n if (!(model instanceof AzureOpenAiStreamingChatModel)) { // TODO\n assertThat(tokenUsage.inputTokenCount()).isGreaterThan(0);\n assertThat(tokenUsage.outputTokenCount()).isGreaterThan(0);\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n }\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n\n verify(calculator).squareRoot(485906798473894056.0);\n verifyNoMoreInteractions(calculator);\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(4);\n\n assertThat(messages.get(0)).isInstanceOf(UserMessage.class);\n assertThat(messages.get(0).text()).isEqualTo(userMessage);\n\n AiMessage aiMessage = (AiMessage) messages.get(1);\n assertThat(aiMessage.text()).isNull();\n assertThat(aiMessage.toolExecutionRequests()).hasSize(1);\n ToolExecutionRequest toolExecutionRequest = aiMessage.toolExecutionRequests().get(0);\n if (!(model instanceof AzureOpenAiStreamingChatModel)) { // TODO\n assertThat(toolExecutionRequest.id()).isNotBlank();\n }\n\n assertThat(toolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 485906798473894056}\");\n\n ToolExecutionResultMessage toolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(2);\n assertThat(toolExecutionResultMessage.id()).isEqualTo(toolExecutionRequest.id());\n assertThat(toolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionResultMessage.text()).isEqualTo(\"6.97070153193991E8\");\n\n assertThat(messages.get(3)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(3).text()).contains(\"6.97\");\n }\n\n @Test\n void should_execute_multiple_tools_sequentially_then_answer() throws Exception {\n\n // TODO test more models\n StreamingChatLanguageModel streamingChatModel = OpenAiStreamingChatModel.builder()\n .baseUrl(System.getenv(\"OPENAI_BASE_URL\"))\n .apiKey(System.getenv(\"OPENAI_API_KEY\"))\n .organizationId(System.getenv(\"OPENAI_ORGANIZATION_ID\"))\n .modelName(GPT_3_5_TURBO_0613) // this model can only call tools sequentially\n .temperature(0.0)\n .logRequests(true)\n .logResponses(true)\n .build();\n\n Calculator calculator = spy(new Calculator());\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(streamingChatModel)\n .chatMemory(chatMemory)\n .tools(calculator)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<Response<AiMessage>> futureResponse = new CompletableFuture<>();\n\n String userMessage = \"What is the square root of 485906798473894056 and 97866249624785 in scientific notation?\";\n\n assistant.chat(userMessage)\n .onNext(answerBuilder::append)\n .onComplete(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n Response<AiMessage> response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"6.97\", \"9.89\");\n assertThat(response.content().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isGreaterThan(0); // TODO\n assertThat(tokenUsage.outputTokenCount()).isGreaterThan(0);\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n\n verify(calculator).squareRoot(485906798473894056.0);\n verify(calculator).squareRoot(97866249624785.0);\n verifyNoMoreInteractions(calculator);\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(6);\n\n assertThat(messages.get(0)).isInstanceOf(dev.langchain4j.data.message.UserMessage.class);\n assertThat(messages.get(0).text()).isEqualTo(userMessage);\n\n AiMessage aiMessage = (AiMessage) messages.get(1);\n assertThat(aiMessage.text()).isNull();\n assertThat(aiMessage.toolExecutionRequests()).hasSize(1);\n ToolExecutionRequest toolExecutionRequest = aiMessage.toolExecutionRequests().get(0);\n assertThat(toolExecutionRequest.id()).isNotBlank();\n assertThat(toolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 485906798473894056}\");\n\n ToolExecutionResultMessage toolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(2);\n assertThat(toolExecutionResultMessage.id()).isEqualTo(toolExecutionRequest.id());\n assertThat(toolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionResultMessage.text()).isEqualTo(\"6.97070153193991E8\");\n\n AiMessage secondAiMessage = (AiMessage) messages.get(3);\n assertThat(secondAiMessage.text()).isNull();\n assertThat(secondAiMessage.toolExecutionRequests()).hasSize(1);\n ToolExecutionRequest secondToolExecutionRequest = secondAiMessage.toolExecutionRequests().get(0);\n assertThat(secondToolExecutionRequest.id()).isNotBlank();\n assertThat(secondToolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 97866249624785}\");\n\n ToolExecutionResultMessage secondToolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(4);\n assertThat(secondToolExecutionResultMessage.id()).isEqualTo(secondToolExecutionRequest.id());\n assertThat(secondToolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionResultMessage.text()).isEqualTo(\"9892737.215997653\");\n\n assertThat(messages.get(5)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(5).text()).contains(\"6.97\", \"9.89\");\n }\n\n @Test\n void should_execute_multiple_tools_in_parallel_then_answer() throws Exception {\n\n Calculator calculator = spy(new Calculator());\n\n // TODO test more models\n StreamingChatLanguageModel streamingChatModel = OpenAiStreamingChatModel.builder()\n .baseUrl(System.getenv(\"OPENAI_BASE_URL\"))\n .apiKey(System.getenv(\"OPENAI_API_KEY\"))\n .organizationId(System.getenv(\"OPENAI_ORGANIZATION_ID\"))\n .temperature(0.0)\n .logRequests(true)\n .logResponses(true)\n .build();\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(streamingChatModel)\n .chatMemory(chatMemory)\n .tools(calculator)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<Response<AiMessage>> futureResponse = new CompletableFuture<>();\n\n String userMessage = \"What is the square root of 485906798473894056 and 97866249624785 in scientific notation?\";\n\n assistant.chat(userMessage)\n .onNext(answerBuilder::append)\n .onComplete(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n Response<AiMessage> response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"6.97\", \"9.89\");\n assertThat(response.content().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isGreaterThan(0); // TODO\n assertThat(tokenUsage.outputTokenCount()).isGreaterThan(0);\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n\n verify(calculator).squareRoot(485906798473894056.0);\n verify(calculator).squareRoot(97866249624785.0);\n verifyNoMoreInteractions(calculator);\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(5);\n\n assertThat(messages.get(0)).isInstanceOf(dev.langchain4j.data.message.UserMessage.class);\n assertThat(messages.get(0).text()).isEqualTo(userMessage);\n\n AiMessage aiMessage = (AiMessage) messages.get(1);\n assertThat(aiMessage.text()).isNull();\n assertThat(aiMessage.toolExecutionRequests()).hasSize(2);\n\n ToolExecutionRequest firstToolExecutionRequest = aiMessage.toolExecutionRequests().get(0);\n assertThat(firstToolExecutionRequest.id()).isNotBlank();\n assertThat(firstToolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(firstToolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 485906798473894056}\");\n\n ToolExecutionRequest secondToolExecutionRequest = aiMessage.toolExecutionRequests().get(1);\n assertThat(secondToolExecutionRequest.id()).isNotBlank();\n assertThat(secondToolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 97866249624785}\");\n\n ToolExecutionResultMessage firstToolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(2);\n assertThat(firstToolExecutionResultMessage.id()).isEqualTo(firstToolExecutionRequest.id());\n assertThat(firstToolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(firstToolExecutionResultMessage.text()).isEqualTo(\"6.97070153193991E8\");\n\n ToolExecutionResultMessage secondToolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(3);\n assertThat(secondToolExecutionResultMessage.id()).isEqualTo(secondToolExecutionRequest.id());\n assertThat(secondToolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionResultMessage.text()).isEqualTo(\"9892737.215997653\");\n\n assertThat(messages.get(4)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(4).text()).contains(\"6.97\", \"9.89\");\n }\n}\n"}, "d34e1df9caa7747083eb927cb92283d2cc37c9e0": {"t": 1785755674.8719482, "n": 19274, "h": "94431f55d10d74cb", "c": "package dev.langchain4j.service;\n\nimport dev.langchain4j.agent.tool.P;\nimport dev.langchain4j.agent.tool.Tool;\nimport dev.langchain4j.agent.tool.ToolExecutionRequest;\nimport dev.langchain4j.data.message.AiMessage;\nimport dev.langchain4j.data.message.ChatMessage;\nimport dev.langchain4j.data.message.ToolExecutionResultMessage;\nimport dev.langchain4j.data.message.UserMessage;\nimport dev.langchain4j.memory.ChatMemory;\nimport dev.langchain4j.memory.chat.MessageWindowChatMemory;\nimport dev.langchain4j.model.azure.AzureOpenAiStreamingChatModel;\nimport dev.langchain4j.model.chat.StreamingChatLanguageModel;\nimport dev.langchain4j.model.mistralai.MistralAiStreamingChatModel;\nimport dev.langchain4j.model.openai.OpenAiStreamingChatModel;\nimport dev.langchain4j.model.output.Response;\nimport dev.langchain4j.model.output.TokenUsage;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.MethodSource;\n\nimport java.util.List;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.stream.Stream;\n\nimport static dev.langchain4j.model.openai.OpenAiChatModelName.GPT_3_5_TURBO_0613;\nimport static dev.langchain4j.model.output.FinishReason.STOP;\nimport static java.util.concurrent.TimeUnit.SECONDS;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.Mockito.*;\n\npublic class StreamingAiServicesIT {\n\n static Stream<StreamingChatLanguageModel> models() {\n return Stream.of(\n OpenAiStreamingChatModel.builder()\n .baseUrl(System.getenv(\"OPENAI_BASE_URL\"))\n .apiKey(System.getenv(\"OPENAI_API_KEY\"))\n .organizationId(System.getenv(\"OPENAI_ORGANIZATION_ID\"))\n .logRequests(true)\n .logResponses(true)\n .build(),\n AzureOpenAiStreamingChatModel.builder()\n .endpoint(System.getenv(\"AZURE_OPENAI_ENDPOINT\"))\n .apiKey(System.getenv(\"AZURE_OPENAI_KEY\"))\n .logRequestsAndResponses(true)\n .build(),\n MistralAiStreamingChatModel.builder()\n .apiKey(System.getenv(\"MISTRAL_AI_API_KEY\"))\n .logRequests(true)\n .logResponses(true)\n .build()\n // TODO add more models\n );\n }\n\n interface Assistant {\n\n TokenStream chat(String userMessage);\n }\n\n @ParameterizedTest\n @MethodSource(\"models\")\n void should_stream_answer(StreamingChatLanguageModel model) throws Exception {\n\n Assistant assistant = AiServices.create(Assistant.class, model);\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<Response<AiMessage>> futureResponse = new CompletableFuture<>();\n\n assistant.chat(\"What is the capital of Germany?\")\n .onNext(answerBuilder::append)\n .onComplete(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n Response<AiMessage> response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"Berlin\");\n assertThat(response.content().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n if (!(model instanceof AzureOpenAiStreamingChatModel)) { // TODO\n assertThat(tokenUsage.inputTokenCount()).isGreaterThan(0);\n assertThat(tokenUsage.outputTokenCount()).isGreaterThan(0);\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n }\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n }\n\n @ParameterizedTest\n @MethodSource(\"models\")\n void should_stream_answers_with_memory(StreamingChatLanguageModel model) throws Exception {\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(model)\n .chatMemory(chatMemory)\n .build();\n\n\n String firstUserMessage = \"Hi, my name is Klaus\";\n CompletableFuture<Response<AiMessage>> firstResultFuture = new CompletableFuture<>();\n\n assistant.chat(firstUserMessage)\n .onNext(System.out::println)\n .onComplete(firstResultFuture::complete)\n .onError(firstResultFuture::completeExceptionally)\n .start();\n\n Response<AiMessage> firstResponse = firstResultFuture.get(30, SECONDS);\n assertThat(firstResponse.content().text()).contains(\"Klaus\");\n\n\n String secondUserMessage = \"What is my name?\";\n CompletableFuture<Response<AiMessage>> secondResultFuture = new CompletableFuture<>();\n\n assistant.chat(secondUserMessage)\n .onNext(System.out::println)\n .onComplete(secondResultFuture::complete)\n .onError(secondResultFuture::completeExceptionally)\n .start();\n\n Response<AiMessage> secondResponse = secondResultFuture.get(30, SECONDS);\n assertThat(secondResponse.content().text()).contains(\"Klaus\");\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(4);\n\n assertThat(messages.get(0)).isInstanceOf(UserMessage.class);\n assertThat(messages.get(0).text()).isEqualTo(firstUserMessage);\n\n assertThat(messages.get(1)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(1)).isEqualTo(firstResponse.content());\n\n assertThat(messages.get(2)).isInstanceOf(UserMessage.class);\n assertThat(messages.get(2).text()).isEqualTo(secondUserMessage);\n\n assertThat(messages.get(3)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(3)).isEqualTo(secondResponse.content());\n }\n\n static class Calculator {\n\n @Tool(\"calculates the square root of the provided number\")\n double squareRoot(@P(\"number to operate on\") double number) {\n return Math.sqrt(number);\n }\n }\n\n @ParameterizedTest\n @MethodSource(\"models\")\n void should_execute_a_tool_then_stream_answer(StreamingChatLanguageModel model) throws Exception {\n\n Calculator calculator = spy(new Calculator());\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(model)\n .chatMemory(chatMemory)\n .tools(calculator)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<Response<AiMessage>> futureResponse = new CompletableFuture<>();\n\n String userMessage = \"What is the square root of 485906798473894056 in scientific notation?\";\n\n assistant.chat(userMessage)\n .onNext(answerBuilder::append)\n .onComplete(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n Response<AiMessage> response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"6.97\");\n assertThat(response.content().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n if (!(model instanceof AzureOpenAiStreamingChatModel)) { // TODO\n assertThat(tokenUsage.inputTokenCount()).isGreaterThan(0);\n assertThat(tokenUsage.outputTokenCount()).isGreaterThan(0);\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n }\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n\n verify(calculator).squareRoot(485906798473894056.0);\n verifyNoMoreInteractions(calculator);\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(4);\n\n assertThat(messages.get(0)).isInstanceOf(UserMessage.class);\n assertThat(messages.get(0).text()).isEqualTo(userMessage);\n\n AiMessage aiMessage = (AiMessage) messages.get(1);\n assertThat(aiMessage.text()).isNull();\n assertThat(aiMessage.toolExecutionRequests()).hasSize(1);\n ToolExecutionRequest toolExecutionRequest = aiMessage.toolExecutionRequests().get(0);\n if (!(model instanceof AzureOpenAiStreamingChatModel)) { // TODO\n assertThat(toolExecutionRequest.id()).isNotBlank();\n }\n\n assertThat(toolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 485906798473894056}\");\n\n ToolExecutionResultMessage toolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(2);\n assertThat(toolExecutionResultMessage.id()).isEqualTo(toolExecutionRequest.id());\n assertThat(toolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionResultMessage.text()).isEqualTo(\"6.97070153193991E8\");\n\n assertThat(messages.get(3)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(3).text()).contains(\"6.97\");\n }\n\n @Test\n void should_execute_multiple_tools_sequentially_then_answer() throws Exception {\n\n // TODO test more models\n StreamingChatLanguageModel streamingChatModel = OpenAiStreamingChatModel.builder()\n .baseUrl(System.getenv(\"OPENAI_BASE_URL\"))\n .apiKey(System.getenv(\"OPENAI_API_KEY\"))\n .organizationId(System.getenv(\"OPENAI_ORGANIZATION_ID\"))\n .modelName(GPT_3_5_TURBO_0613) // this model can only call tools sequentially\n .temperature(0.0)\n .logRequests(true)\n .logResponses(true)\n .build();\n\n Calculator calculator = spy(new Calculator());\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(streamingChatModel)\n .chatMemory(chatMemory)\n .tools(calculator)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<Response<AiMessage>> futureResponse = new CompletableFuture<>();\n\n String userMessage = \"What is the square root of 485906798473894056 and 97866249624785 in scientific notation?\";\n\n assistant.chat(userMessage)\n .onNext(answerBuilder::append)\n .onComplete(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n Response<AiMessage> response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"6.97\", \"9.89\");\n assertThat(response.content().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isGreaterThan(0); // TODO\n assertThat(tokenUsage.outputTokenCount()).isGreaterThan(0);\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n\n verify(calculator).squareRoot(485906798473894056.0);\n verify(calculator).squareRoot(97866249624785.0);\n verifyNoMoreInteractions(calculator);\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(6);\n\n assertThat(messages.get(0)).isInstanceOf(dev.langchain4j.data.message.UserMessage.class);\n assertThat(messages.get(0).text()).isEqualTo(userMessage);\n\n AiMessage aiMessage = (AiMessage) messages.get(1);\n assertThat(aiMessage.text()).isNull();\n assertThat(aiMessage.toolExecutionRequests()).hasSize(1);\n ToolExecutionRequest toolExecutionRequest = aiMessage.toolExecutionRequests().get(0);\n assertThat(toolExecutionRequest.id()).isNotBlank();\n assertThat(toolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 485906798473894056}\");\n\n ToolExecutionResultMessage toolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(2);\n assertThat(toolExecutionResultMessage.id()).isEqualTo(toolExecutionRequest.id());\n assertThat(toolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionResultMessage.text()).isEqualTo(\"6.97070153193991E8\");\n\n AiMessage secondAiMessage = (AiMessage) messages.get(3);\n assertThat(secondAiMessage.text()).isNull();\n assertThat(secondAiMessage.toolExecutionRequests()).hasSize(1);\n ToolExecutionRequest secondToolExecutionRequest = secondAiMessage.toolExecutionRequests().get(0);\n assertThat(secondToolExecutionRequest.id()).isNotBlank();\n assertThat(secondToolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 97866249624785}\");\n\n ToolExecutionResultMessage secondToolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(4);\n assertThat(secondToolExecutionResultMessage.id()).isEqualTo(secondToolExecutionRequest.id());\n assertThat(secondToolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionResultMessage.text()).isEqualTo(\"9892737.215997653\");\n\n assertThat(messages.get(5)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(5).text()).contains(\"6.97\", \"9.89\");\n }\n\n @Test\n void should_execute_multiple_tools_in_parallel_then_answer() throws Exception {\n\n Calculator calculator = spy(new Calculator());\n\n // TODO test more models\n StreamingChatLanguageModel streamingChatModel = OpenAiStreamingChatModel.builder()\n .baseUrl(System.getenv(\"OPENAI_BASE_URL\"))\n .apiKey(System.getenv(\"OPENAI_API_KEY\"))\n .organizationId(System.getenv(\"OPENAI_ORGANIZATION_ID\"))\n .temperature(0.0)\n .logRequests(true)\n .logResponses(true)\n .build();\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(streamingChatModel)\n .chatMemory(chatMemory)\n .tools(calculator)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<Response<AiMessage>> futureResponse = new CompletableFuture<>();\n\n String userMessage = \"What is the square root of 485906798473894056 and 97866249624785 in scientific notation?\";\n\n assistant.chat(userMessage)\n .onNext(answerBuilder::append)\n .onComplete(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n Response<AiMessage> response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"6.97\", \"9.89\");\n assertThat(response.content().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isGreaterThan(0); // TODO\n assertThat(tokenUsage.outputTokenCount()).isGreaterThan(0);\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n\n verify(calculator).squareRoot(485906798473894056.0);\n verify(calculator).squareRoot(97866249624785.0);\n verifyNoMoreInteractions(calculator);\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(5);\n\n assertThat(messages.get(0)).isInstanceOf(dev.langchain4j.data.message.UserMessage.class);\n assertThat(messages.get(0).text()).isEqualTo(userMessage);\n\n AiMessage aiMessage = (AiMessage) messages.get(1);\n assertThat(aiMessage.text()).isNull();\n assertThat(aiMessage.toolExecutionRequests()).hasSize(2);\n\n ToolExecutionRequest firstToolExecutionRequest = aiMessage.toolExecutionRequests().get(0);\n assertThat(firstToolExecutionRequest.id()).isNotBlank();\n assertThat(firstToolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(firstToolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 485906798473894056}\");\n\n ToolExecutionRequest secondToolExecutionRequest = aiMessage.toolExecutionRequests().get(1);\n assertThat(secondToolExecutionRequest.id()).isNotBlank();\n assertThat(secondToolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 97866249624785}\");\n\n ToolExecutionResultMessage firstToolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(2);\n assertThat(firstToolExecutionResultMessage.id()).isEqualTo(firstToolExecutionRequest.id());\n assertThat(firstToolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(firstToolExecutionResultMessage.text()).isEqualTo(\"6.97070153193991E8\");\n\n ToolExecutionResultMessage secondToolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(3);\n assertThat(secondToolExecutionResultMessage.id()).isEqualTo(secondToolExecutionRequest.id());\n assertThat(secondToolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionResultMessage.text()).isEqualTo(\"9892737.215997653\");\n\n assertThat(messages.get(4)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(4).text()).contains(\"6.97\", \"9.89\");\n }\n}\n"}, "2ce9f592240eaeada8f626650928c4c2e74fbf6c": {"t": 1785755676.378204, "n": 18962, "h": "da242f14586df770", "c": "package dev.langchain4j.service;\n\nimport dev.langchain4j.agent.tool.P;\nimport dev.langchain4j.agent.tool.Tool;\nimport dev.langchain4j.agent.tool.ToolExecutionRequest;\nimport dev.langchain4j.data.message.AiMessage;\nimport dev.langchain4j.data.message.ChatMessage;\nimport dev.langchain4j.data.message.ToolExecutionResultMessage;\nimport dev.langchain4j.data.message.UserMessage;\nimport dev.langchain4j.memory.ChatMemory;\nimport dev.langchain4j.memory.chat.MessageWindowChatMemory;\nimport dev.langchain4j.model.azure.AzureOpenAiStreamingChatModel;\nimport dev.langchain4j.model.chat.StreamingChatLanguageModel;\nimport dev.langchain4j.model.openai.OpenAiStreamingChatModel;\nimport dev.langchain4j.model.output.Response;\nimport dev.langchain4j.model.output.TokenUsage;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.MethodSource;\n\nimport java.util.List;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.stream.Stream;\n\nimport static dev.langchain4j.model.openai.OpenAiChatModelName.GPT_3_5_TURBO_0613;\nimport static dev.langchain4j.model.output.FinishReason.STOP;\nimport static java.util.concurrent.TimeUnit.SECONDS;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.Mockito.*;\n\npublic class StreamingAiServicesIT {\n\n static Stream<StreamingChatLanguageModel> models() {\n return Stream.of(\n OpenAiStreamingChatModel.builder()\n .baseUrl(System.getenv(\"OPENAI_BASE_URL\"))\n .apiKey(System.getenv(\"OPENAI_API_KEY\"))\n .organizationId(System.getenv(\"OPENAI_ORGANIZATION_ID\"))\n .logRequests(true)\n .logResponses(true)\n .build(),\n AzureOpenAiStreamingChatModel.builder()\n .endpoint(System.getenv(\"AZURE_OPENAI_ENDPOINT\"))\n .apiKey(System.getenv(\"AZURE_OPENAI_KEY\"))\n .logRequestsAndResponses(true)\n .build()\n // TODO add more models\n );\n }\n\n interface Assistant {\n\n TokenStream chat(String userMessage);\n }\n\n @ParameterizedTest\n @MethodSource(\"models\")\n void should_stream_answer(StreamingChatLanguageModel model) throws Exception {\n\n Assistant assistant = AiServices.create(Assistant.class, model);\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<Response<AiMessage>> futureResponse = new CompletableFuture<>();\n\n assistant.chat(\"What is the capital of Germany?\")\n .onNext(answerBuilder::append)\n .onComplete(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n Response<AiMessage> response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"Berlin\");\n assertThat(response.content().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n if (!(model instanceof AzureOpenAiStreamingChatModel)) { // TODO\n assertThat(tokenUsage.inputTokenCount()).isGreaterThan(0);\n assertThat(tokenUsage.outputTokenCount()).isGreaterThan(0);\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n }\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n }\n\n @ParameterizedTest\n @MethodSource(\"models\")\n void should_stream_answers_with_memory(StreamingChatLanguageModel model) throws Exception {\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(model)\n .chatMemory(chatMemory)\n .build();\n\n\n String firstUserMessage = \"Hi, my name is Klaus\";\n CompletableFuture<Response<AiMessage>> firstResultFuture = new CompletableFuture<>();\n\n assistant.chat(firstUserMessage)\n .onNext(System.out::println)\n .onComplete(firstResultFuture::complete)\n .onError(firstResultFuture::completeExceptionally)\n .start();\n\n Response<AiMessage> firstResponse = firstResultFuture.get(30, SECONDS);\n assertThat(firstResponse.content().text()).contains(\"Klaus\");\n\n\n String secondUserMessage = \"What is my name?\";\n CompletableFuture<Response<AiMessage>> secondResultFuture = new CompletableFuture<>();\n\n assistant.chat(secondUserMessage)\n .onNext(System.out::println)\n .onComplete(secondResultFuture::complete)\n .onError(secondResultFuture::completeExceptionally)\n .start();\n\n Response<AiMessage> secondResponse = secondResultFuture.get(30, SECONDS);\n assertThat(secondResponse.content().text()).contains(\"Klaus\");\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(4);\n\n assertThat(messages.get(0)).isInstanceOf(UserMessage.class);\n assertThat(messages.get(0).text()).isEqualTo(firstUserMessage);\n\n assertThat(messages.get(1)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(1)).isEqualTo(firstResponse.content());\n\n assertThat(messages.get(2)).isInstanceOf(UserMessage.class);\n assertThat(messages.get(2).text()).isEqualTo(secondUserMessage);\n\n assertThat(messages.get(3)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(3)).isEqualTo(secondResponse.content());\n }\n\n static class Calculator {\n\n @Tool(\"calculates the square root of the provided number\")\n double squareRoot(@P(\"number to operate on\") double number) {\n return Math.sqrt(number);\n }\n }\n\n @ParameterizedTest\n @MethodSource(\"models\")\n void should_execute_a_tool_then_stream_answer(StreamingChatLanguageModel model) throws Exception {\n\n Calculator calculator = spy(new Calculator());\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(model)\n .chatMemory(chatMemory)\n .tools(calculator)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<Response<AiMessage>> futureResponse = new CompletableFuture<>();\n\n String userMessage = \"What is the square root of 485906798473894056 in scientific notation?\";\n\n assistant.chat(userMessage)\n .onNext(answerBuilder::append)\n .onComplete(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n Response<AiMessage> response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"6.97\");\n assertThat(response.content().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n if (!(model instanceof AzureOpenAiStreamingChatModel)) { // TODO\n assertThat(tokenUsage.inputTokenCount()).isGreaterThan(0);\n assertThat(tokenUsage.outputTokenCount()).isGreaterThan(0);\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n }\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n\n verify(calculator).squareRoot(485906798473894056.0);\n verifyNoMoreInteractions(calculator);\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(4);\n\n assertThat(messages.get(0)).isInstanceOf(UserMessage.class);\n assertThat(messages.get(0).text()).isEqualTo(userMessage);\n\n AiMessage aiMessage = (AiMessage) messages.get(1);\n assertThat(aiMessage.text()).isNull();\n assertThat(aiMessage.toolExecutionRequests()).hasSize(1);\n ToolExecutionRequest toolExecutionRequest = aiMessage.toolExecutionRequests().get(0);\n if (!(model instanceof AzureOpenAiStreamingChatModel)) { // TODO\n assertThat(toolExecutionRequest.id()).isNotBlank();\n }\n\n assertThat(toolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 485906798473894056}\");\n\n ToolExecutionResultMessage toolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(2);\n assertThat(toolExecutionResultMessage.id()).isEqualTo(toolExecutionRequest.id());\n assertThat(toolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionResultMessage.text()).isEqualTo(\"6.97070153193991E8\");\n\n assertThat(messages.get(3)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(3).text()).contains(\"6.97\");\n }\n\n @Test\n void should_execute_multiple_tools_sequentially_then_answer() throws Exception {\n\n // TODO test more models\n StreamingChatLanguageModel streamingChatModel = OpenAiStreamingChatModel.builder()\n .baseUrl(System.getenv(\"OPENAI_BASE_URL\"))\n .apiKey(System.getenv(\"OPENAI_API_KEY\"))\n .organizationId(System.getenv(\"OPENAI_ORGANIZATION_ID\"))\n .modelName(GPT_3_5_TURBO_0613) // this model can only call tools sequentially\n .temperature(0.0)\n .logRequests(true)\n .logResponses(true)\n .build();\n\n Calculator calculator = spy(new Calculator());\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(streamingChatModel)\n .chatMemory(chatMemory)\n .tools(calculator)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<Response<AiMessage>> futureResponse = new CompletableFuture<>();\n\n String userMessage = \"What is the square root of 485906798473894056 and 97866249624785 in scientific notation?\";\n\n assistant.chat(userMessage)\n .onNext(answerBuilder::append)\n .onComplete(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n Response<AiMessage> response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"6.97\", \"9.89\");\n assertThat(response.content().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isGreaterThan(0); // TODO\n assertThat(tokenUsage.outputTokenCount()).isGreaterThan(0);\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n\n verify(calculator).squareRoot(485906798473894056.0);\n verify(calculator).squareRoot(97866249624785.0);\n verifyNoMoreInteractions(calculator);\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(6);\n\n assertThat(messages.get(0)).isInstanceOf(dev.langchain4j.data.message.UserMessage.class);\n assertThat(messages.get(0).text()).isEqualTo(userMessage);\n\n AiMessage aiMessage = (AiMessage) messages.get(1);\n assertThat(aiMessage.text()).isNull();\n assertThat(aiMessage.toolExecutionRequests()).hasSize(1);\n ToolExecutionRequest toolExecutionRequest = aiMessage.toolExecutionRequests().get(0);\n assertThat(toolExecutionRequest.id()).isNotBlank();\n assertThat(toolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 485906798473894056}\");\n\n ToolExecutionResultMessage toolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(2);\n assertThat(toolExecutionResultMessage.id()).isEqualTo(toolExecutionRequest.id());\n assertThat(toolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionResultMessage.text()).isEqualTo(\"6.97070153193991E8\");\n\n AiMessage secondAiMessage = (AiMessage) messages.get(3);\n assertThat(secondAiMessage.text()).isNull();\n assertThat(secondAiMessage.toolExecutionRequests()).hasSize(1);\n ToolExecutionRequest secondToolExecutionRequest = secondAiMessage.toolExecutionRequests().get(0);\n assertThat(secondToolExecutionRequest.id()).isNotBlank();\n assertThat(secondToolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 97866249624785}\");\n\n ToolExecutionResultMessage secondToolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(4);\n assertThat(secondToolExecutionResultMessage.id()).isEqualTo(secondToolExecutionRequest.id());\n assertThat(secondToolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionResultMessage.text()).isEqualTo(\"9892737.215997653\");\n\n assertThat(messages.get(5)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(5).text()).contains(\"6.97\", \"9.89\");\n }\n\n @Test\n void should_execute_multiple_tools_in_parallel_then_answer() throws Exception {\n\n Calculator calculator = spy(new Calculator());\n\n // TODO test more models\n StreamingChatLanguageModel streamingChatModel = OpenAiStreamingChatModel.builder()\n .baseUrl(System.getenv(\"OPENAI_BASE_URL\"))\n .apiKey(System.getenv(\"OPENAI_API_KEY\"))\n .organizationId(System.getenv(\"OPENAI_ORGANIZATION_ID\"))\n .temperature(0.0)\n .logRequests(true)\n .logResponses(true)\n .build();\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(streamingChatModel)\n .chatMemory(chatMemory)\n .tools(calculator)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<Response<AiMessage>> futureResponse = new CompletableFuture<>();\n\n String userMessage = \"What is the square root of 485906798473894056 and 97866249624785 in scientific notation?\";\n\n assistant.chat(userMessage)\n .onNext(answerBuilder::append)\n .onComplete(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n Response<AiMessage> response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"6.97\", \"9.89\");\n assertThat(response.content().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isGreaterThan(0); // TODO\n assertThat(tokenUsage.outputTokenCount()).isGreaterThan(0);\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n\n verify(calculator).squareRoot(485906798473894056.0);\n verify(calculator).squareRoot(97866249624785.0);\n verifyNoMoreInteractions(calculator);\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(5);\n\n assertThat(messages.get(0)).isInstanceOf(dev.langchain4j.data.message.UserMessage.class);\n assertThat(messages.get(0).text()).isEqualTo(userMessage);\n\n AiMessage aiMessage = (AiMessage) messages.get(1);\n assertThat(aiMessage.text()).isNull();\n assertThat(aiMessage.toolExecutionRequests()).hasSize(2);\n\n ToolExecutionRequest firstToolExecutionRequest = aiMessage.toolExecutionRequests().get(0);\n assertThat(firstToolExecutionRequest.id()).isNotBlank();\n assertThat(firstToolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(firstToolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 485906798473894056}\");\n\n ToolExecutionRequest secondToolExecutionRequest = aiMessage.toolExecutionRequests().get(1);\n assertThat(secondToolExecutionRequest.id()).isNotBlank();\n assertThat(secondToolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 97866249624785}\");\n\n ToolExecutionResultMessage firstToolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(2);\n assertThat(firstToolExecutionResultMessage.id()).isEqualTo(firstToolExecutionRequest.id());\n assertThat(firstToolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(firstToolExecutionResultMessage.text()).isEqualTo(\"6.97070153193991E8\");\n\n ToolExecutionResultMessage secondToolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(3);\n assertThat(secondToolExecutionResultMessage.id()).isEqualTo(secondToolExecutionRequest.id());\n assertThat(secondToolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionResultMessage.text()).isEqualTo(\"9892737.215997653\");\n\n assertThat(messages.get(4)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(4).text()).contains(\"6.97\", \"9.89\");\n }\n}\n"}, "7aabdcb71905d7c64f034a44783850df8f797550": {"t": 1785755678.348952, "n": 19359, "h": "f6b1bbbb62c8529b", "c": "package dev.langchain4j.service;\n\nimport dev.langchain4j.agent.tool.P;\nimport dev.langchain4j.agent.tool.Tool;\nimport dev.langchain4j.agent.tool.ToolExecutionRequest;\nimport dev.langchain4j.data.message.AiMessage;\nimport dev.langchain4j.data.message.ChatMessage;\nimport dev.langchain4j.data.message.ToolExecutionResultMessage;\nimport dev.langchain4j.data.message.UserMessage;\nimport dev.langchain4j.memory.ChatMemory;\nimport dev.langchain4j.memory.chat.MessageWindowChatMemory;\nimport dev.langchain4j.model.azure.AzureOpenAiStreamingChatModel;\nimport dev.langchain4j.model.chat.StreamingChatLanguageModel;\nimport dev.langchain4j.model.openai.OpenAiStreamingChatModel;\nimport dev.langchain4j.model.output.Response;\nimport dev.langchain4j.model.output.TokenUsage;\nimport org.assertj.core.data.Percentage;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.MethodSource;\n\nimport java.util.List;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.stream.Stream;\n\nimport static dev.langchain4j.model.openai.OpenAiChatModelName.GPT_3_5_TURBO_0613;\nimport static dev.langchain4j.model.openai.OpenAiModelName.GPT_3_5_TURBO_1106;\nimport static dev.langchain4j.model.output.FinishReason.STOP;\nimport static java.util.concurrent.TimeUnit.SECONDS;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.data.Percentage.withPercentage;\nimport static org.mockito.Mockito.*;\n\npublic class StreamingAiServicesIT {\n\n private static final Percentage TOKENIZER_PRECISION = withPercentage(5);\n\n static Stream<StreamingChatLanguageModel> models() {\n return Stream.of(\n OpenAiStreamingChatModel.builder()\n .baseUrl(System.getenv(\"OPENAI_BASE_URL\"))\n .apiKey(System.getenv(\"OPENAI_API_KEY\"))\n .organizationId(System.getenv(\"OPENAI_ORGANIZATION_ID\"))\n .logRequests(true)\n .logResponses(true)\n .build(),\n AzureOpenAiStreamingChatModel.builder()\n .endpoint(System.getenv(\"AZURE_OPENAI_ENDPOINT\"))\n .apiKey(System.getenv(\"AZURE_OPENAI_KEY\"))\n .logRequestsAndResponses(true)\n .build()\n // TODO add more models\n );\n }\n\n interface Assistant {\n\n TokenStream chat(String userMessage);\n }\n\n @ParameterizedTest\n @MethodSource(\"models\")\n void should_stream_answer(StreamingChatLanguageModel model) throws Exception {\n\n Assistant assistant = AiServices.create(Assistant.class, model);\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<Response<AiMessage>> futureResponse = new CompletableFuture<>();\n\n assistant.chat(\"What is the capital of Germany?\")\n .onNext(answerBuilder::append)\n .onComplete(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n Response<AiMessage> response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"Berlin\");\n assertThat(response.content().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n if (!(model instanceof AzureOpenAiStreamingChatModel)) { // TODO\n assertThat(tokenUsage.inputTokenCount()).isGreaterThan(0);\n assertThat(tokenUsage.outputTokenCount()).isGreaterThan(0);\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n }\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n }\n\n @ParameterizedTest\n @MethodSource(\"models\")\n void should_stream_answers_with_memory(StreamingChatLanguageModel model) throws Exception {\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(model)\n .chatMemory(chatMemory)\n .build();\n\n\n String firstUserMessage = \"Hi, my name is Klaus\";\n CompletableFuture<Response<AiMessage>> firstResultFuture = new CompletableFuture<>();\n\n assistant.chat(firstUserMessage)\n .onNext(System.out::println)\n .onComplete(firstResultFuture::complete)\n .onError(firstResultFuture::completeExceptionally)\n .start();\n\n Response<AiMessage> firstResponse = firstResultFuture.get(30, SECONDS);\n assertThat(firstResponse.content().text()).contains(\"Klaus\");\n\n\n String secondUserMessage = \"What is my name?\";\n CompletableFuture<Response<AiMessage>> secondResultFuture = new CompletableFuture<>();\n\n assistant.chat(secondUserMessage)\n .onNext(System.out::println)\n .onComplete(secondResultFuture::complete)\n .onError(secondResultFuture::completeExceptionally)\n .start();\n\n Response<AiMessage> secondResponse = secondResultFuture.get(30, SECONDS);\n assertThat(secondResponse.content().text()).contains(\"Klaus\");\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(4);\n\n assertThat(messages.get(0)).isInstanceOf(UserMessage.class);\n assertThat(messages.get(0).text()).isEqualTo(firstUserMessage);\n\n assertThat(messages.get(1)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(1)).isEqualTo(firstResponse.content());\n\n assertThat(messages.get(2)).isInstanceOf(UserMessage.class);\n assertThat(messages.get(2).text()).isEqualTo(secondUserMessage);\n\n assertThat(messages.get(3)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(3)).isEqualTo(secondResponse.content());\n }\n\n static class Calculator {\n\n @Tool(\"calculates the square root of the provided number\")\n double squareRoot(@P(\"number to operate on\") double number) {\n return Math.sqrt(number);\n }\n }\n\n @ParameterizedTest\n @MethodSource(\"models\")\n void should_execute_a_tool_then_stream_answer(StreamingChatLanguageModel model) throws Exception {\n\n Calculator calculator = spy(new Calculator());\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(model)\n .chatMemory(chatMemory)\n .tools(calculator)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<Response<AiMessage>> futureResponse = new CompletableFuture<>();\n\n String userMessage = \"What is the square root of 485906798473894056 in scientific notation?\";\n\n assistant.chat(userMessage)\n .onNext(answerBuilder::append)\n .onComplete(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n Response<AiMessage> response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"6.97\");\n assertThat(response.content().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n if (!(model instanceof AzureOpenAiStreamingChatModel)) { // TODO\n assertThat(tokenUsage.inputTokenCount()).isGreaterThan(0);\n assertThat(tokenUsage.outputTokenCount()).isGreaterThan(0);\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n }\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n\n verify(calculator).squareRoot(485906798473894056.0);\n verifyNoMoreInteractions(calculator);\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(4);\n\n assertThat(messages.get(0)).isInstanceOf(UserMessage.class);\n assertThat(messages.get(0).text()).isEqualTo(userMessage);\n\n AiMessage aiMessage = (AiMessage) messages.get(1);\n assertThat(aiMessage.text()).isNull();\n assertThat(aiMessage.toolExecutionRequests()).hasSize(1);\n ToolExecutionRequest toolExecutionRequest = aiMessage.toolExecutionRequests().get(0);\n if (!(model instanceof AzureOpenAiStreamingChatModel)) { // TODO\n assertThat(toolExecutionRequest.id()).isNotBlank();\n }\n\n assertThat(toolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 485906798473894056}\");\n\n ToolExecutionResultMessage toolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(2);\n assertThat(toolExecutionResultMessage.id()).isEqualTo(toolExecutionRequest.id());\n assertThat(toolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionResultMessage.text()).isEqualTo(\"6.97070153193991E8\");\n\n assertThat(messages.get(3)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(3).text()).contains(\"6.97\");\n }\n\n @Test\n void should_execute_multiple_tools_sequentially_then_answer() throws Exception {\n\n // TODO test more models\n StreamingChatLanguageModel streamingChatModel = OpenAiStreamingChatModel.builder()\n .baseUrl(System.getenv(\"OPENAI_BASE_URL\"))\n .apiKey(System.getenv(\"OPENAI_API_KEY\"))\n .organizationId(System.getenv(\"OPENAI_ORGANIZATION_ID\"))\n .modelName(GPT_3_5_TURBO_0613) // this model can only call tools sequentially\n .temperature(0.0)\n .logRequests(true)\n .logResponses(true)\n .build();\n\n Calculator calculator = spy(new Calculator());\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(streamingChatModel)\n .chatMemory(chatMemory)\n .tools(calculator)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<Response<AiMessage>> futureResponse = new CompletableFuture<>();\n\n String userMessage = \"What is the square root of 485906798473894056 and 97866249624785 in scientific notation?\";\n\n assistant.chat(userMessage)\n .onNext(answerBuilder::append)\n .onComplete(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n Response<AiMessage> response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"6.97\", \"9.89\");\n assertThat(response.content().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isCloseTo(79 + 117 + 152, TOKENIZER_PRECISION);\n assertThat(tokenUsage.outputTokenCount()).isCloseTo(21 + 20 + 53, TOKENIZER_PRECISION);\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n\n verify(calculator).squareRoot(485906798473894056.0);\n verify(calculator).squareRoot(97866249624785.0);\n verifyNoMoreInteractions(calculator);\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(6);\n\n assertThat(messages.get(0)).isInstanceOf(dev.langchain4j.data.message.UserMessage.class);\n assertThat(messages.get(0).text()).isEqualTo(userMessage);\n\n AiMessage aiMessage = (AiMessage) messages.get(1);\n assertThat(aiMessage.text()).isNull();\n assertThat(aiMessage.toolExecutionRequests()).hasSize(1);\n ToolExecutionRequest toolExecutionRequest = aiMessage.toolExecutionRequests().get(0);\n assertThat(toolExecutionRequest.id()).isNotBlank();\n assertThat(toolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 485906798473894056}\");\n\n ToolExecutionResultMessage toolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(2);\n assertThat(toolExecutionResultMessage.id()).isEqualTo(toolExecutionRequest.id());\n assertThat(toolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionResultMessage.text()).isEqualTo(\"6.97070153193991E8\");\n\n AiMessage secondAiMessage = (AiMessage) messages.get(3);\n assertThat(secondAiMessage.text()).isNull();\n assertThat(secondAiMessage.toolExecutionRequests()).hasSize(1);\n ToolExecutionRequest secondToolExecutionRequest = secondAiMessage.toolExecutionRequests().get(0);\n assertThat(secondToolExecutionRequest.id()).isNotBlank();\n assertThat(secondToolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 97866249624785}\");\n\n ToolExecutionResultMessage secondToolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(4);\n assertThat(secondToolExecutionResultMessage.id()).isEqualTo(secondToolExecutionRequest.id());\n assertThat(secondToolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionResultMessage.text()).isEqualTo(\"9892737.215997653\");\n\n assertThat(messages.get(5)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(5).text()).contains(\"6.97\", \"9.89\");\n }\n\n @Test\n void should_execute_multiple_tools_in_parallel_then_answer() throws Exception {\n\n Calculator calculator = spy(new Calculator());\n\n // TODO test more models\n StreamingChatLanguageModel streamingChatModel = OpenAiStreamingChatModel.builder()\n .baseUrl(System.getenv(\"OPENAI_BASE_URL\"))\n .apiKey(System.getenv(\"OPENAI_API_KEY\"))\n .organizationId(System.getenv(\"OPENAI_ORGANIZATION_ID\"))\n .modelName(GPT_3_5_TURBO_1106)\n .temperature(0.0)\n .logRequests(true)\n .logResponses(true)\n .build();\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(streamingChatModel)\n .chatMemory(chatMemory)\n .tools(calculator)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<Response<AiMessage>> futureResponse = new CompletableFuture<>();\n\n String userMessage = \"What is the square root of 485906798473894056 and 97866249624785 in scientific notation?\";\n\n assistant.chat(userMessage)\n .onNext(answerBuilder::append)\n .onComplete(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n Response<AiMessage> response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"6.97\", \"9.89\");\n assertThat(response.content().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isCloseTo(79 + 160, TOKENIZER_PRECISION);\n assertThat(tokenUsage.outputTokenCount()).isCloseTo(54 + 58, TOKENIZER_PRECISION);\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n\n verify(calculator).squareRoot(485906798473894056.0);\n verify(calculator).squareRoot(97866249624785.0);\n verifyNoMoreInteractions(calculator);\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(5);\n\n assertThat(messages.get(0)).isInstanceOf(dev.langchain4j.data.message.UserMessage.class);\n assertThat(messages.get(0).text()).isEqualTo(userMessage);\n\n AiMessage aiMessage = (AiMessage) messages.get(1);\n assertThat(aiMessage.text()).isNull();\n assertThat(aiMessage.toolExecutionRequests()).hasSize(2);\n\n ToolExecutionRequest firstToolExecutionRequest = aiMessage.toolExecutionRequests().get(0);\n assertThat(firstToolExecutionRequest.id()).isNotBlank();\n assertThat(firstToolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(firstToolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 485906798473894056}\");\n\n ToolExecutionRequest secondToolExecutionRequest = aiMessage.toolExecutionRequests().get(1);\n assertThat(secondToolExecutionRequest.id()).isNotBlank();\n assertThat(secondToolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 97866249624785}\");\n\n ToolExecutionResultMessage firstToolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(2);\n assertThat(firstToolExecutionResultMessage.id()).isEqualTo(firstToolExecutionRequest.id());\n assertThat(firstToolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(firstToolExecutionResultMessage.text()).isEqualTo(\"6.97070153193991E8\");\n\n ToolExecutionResultMessage secondToolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(3);\n assertThat(secondToolExecutionResultMessage.id()).isEqualTo(secondToolExecutionRequest.id());\n assertThat(secondToolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionResultMessage.text()).isEqualTo(\"9892737.215997653\");\n\n assertThat(messages.get(4)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(4).text()).contains(\"6.97\", \"9.89\");\n }\n}\n"}, "49eadc2801bc1fde07ca58820c8cbe7fdd8dd8fc": {"t": 1785755679.707926, "n": 18115, "h": "b8048a575c7ba97e", "c": "package dev.langchain4j.service;\n\nimport dev.langchain4j.agent.tool.P;\nimport dev.langchain4j.agent.tool.Tool;\nimport dev.langchain4j.agent.tool.ToolExecutionRequest;\nimport dev.langchain4j.data.message.AiMessage;\nimport dev.langchain4j.data.message.ChatMessage;\nimport dev.langchain4j.data.message.ToolExecutionResultMessage;\nimport dev.langchain4j.data.message.UserMessage;\nimport dev.langchain4j.memory.ChatMemory;\nimport dev.langchain4j.memory.chat.MessageWindowChatMemory;\nimport dev.langchain4j.model.chat.StreamingChatLanguageModel;\nimport dev.langchain4j.model.openai.OpenAiStreamingChatModel;\nimport dev.langchain4j.model.output.Response;\nimport dev.langchain4j.model.output.TokenUsage;\nimport org.assertj.core.data.Percentage;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.List;\nimport java.util.concurrent.CompletableFuture;\n\nimport static dev.langchain4j.model.openai.OpenAiChatModelName.GPT_3_5_TURBO_0613;\nimport static dev.langchain4j.model.openai.OpenAiModelName.GPT_3_5_TURBO_1106;\nimport static dev.langchain4j.model.output.FinishReason.STOP;\nimport static java.util.concurrent.TimeUnit.SECONDS;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.data.Percentage.withPercentage;\nimport static org.mockito.Mockito.*;\n\npublic class StreamingAiServicesIT {\n\n StreamingChatLanguageModel streamingChatModel = OpenAiStreamingChatModel.builder()\n .baseUrl(System.getenv(\"OPENAI_BASE_URL\"))\n .apiKey(System.getenv(\"OPENAI_API_KEY\"))\n .organizationId(System.getenv(\"OPENAI_ORGANIZATION_ID\"))\n .temperature(0.0)\n .logRequests(true)\n .logResponses(true)\n .build();\n\n Percentage tokenizerPrecision = withPercentage(5);\n\n interface Assistant {\n\n TokenStream chat(String userMessage);\n }\n\n @Test\n void should_stream_answer() throws Exception {\n\n Assistant assistant = AiServices.create(Assistant.class, streamingChatModel);\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<Response<AiMessage>> futureResponse = new CompletableFuture<>();\n\n assistant.chat(\"What is the capital of Germany?\")\n .onNext(answerBuilder::append)\n .onComplete(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n Response<AiMessage> response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"Berlin\");\n assertThat(response.content().text()).isEqualTo(answer);\n\n assertThat(response.tokenUsage().inputTokenCount()).isEqualTo(14);\n assertThat(response.tokenUsage().outputTokenCount()).isGreaterThan(1);\n assertThat(response.tokenUsage().totalTokenCount()).isGreaterThan(15);\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n }\n\n @Test\n void should_stream_answers_with_memory() throws Exception {\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(streamingChatModel)\n .chatMemory(chatMemory)\n .build();\n\n\n String firstUserMessage = \"Hi, my name is Klaus\";\n CompletableFuture<Response<AiMessage>> firstResultFuture = new CompletableFuture<>();\n\n assistant.chat(firstUserMessage)\n .onNext(System.out::println)\n .onComplete(firstResultFuture::complete)\n .onError(firstResultFuture::completeExceptionally)\n .start();\n\n Response<AiMessage> firstResponse = firstResultFuture.get(30, SECONDS);\n assertThat(firstResponse.content().text()).contains(\"Klaus\");\n\n\n String secondUserMessage = \"What is my name?\";\n CompletableFuture<Response<AiMessage>> secondResultFuture = new CompletableFuture<>();\n\n assistant.chat(secondUserMessage)\n .onNext(System.out::println)\n .onComplete(secondResultFuture::complete)\n .onError(secondResultFuture::completeExceptionally)\n .start();\n\n Response<AiMessage> secondResponse = secondResultFuture.get(30, SECONDS);\n assertThat(secondResponse.content().text()).contains(\"Klaus\");\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(4);\n\n assertThat(messages.get(0)).isInstanceOf(UserMessage.class);\n assertThat(messages.get(0).text()).isEqualTo(firstUserMessage);\n\n assertThat(messages.get(1)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(1)).isEqualTo(firstResponse.content());\n\n assertThat(messages.get(2)).isInstanceOf(UserMessage.class);\n assertThat(messages.get(2).text()).isEqualTo(secondUserMessage);\n\n assertThat(messages.get(3)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(3)).isEqualTo(secondResponse.content());\n }\n\n static class Calculator {\n\n @Tool(\"calculates the square root of the provided number\")\n double squareRoot(@P(\"number to operate on\") double number) {\n return Math.sqrt(number);\n }\n }\n\n @Test\n void should_execute_a_tool_then_stream_answer() throws Exception {\n\n Calculator calculator = spy(new Calculator());\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(streamingChatModel)\n .chatMemory(chatMemory)\n .tools(calculator)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<Response<AiMessage>> futureResponse = new CompletableFuture<>();\n\n String userMessage = \"What is the square root of 485906798473894056 in scientific notation?\";\n\n assistant.chat(userMessage)\n .onNext(answerBuilder::append)\n .onComplete(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n Response<AiMessage> response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"6.97\");\n assertThat(response.content().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isCloseTo(72 + 110, tokenizerPrecision);\n assertThat(tokenUsage.outputTokenCount()).isCloseTo(21 + 28, tokenizerPrecision);\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n\n verify(calculator).squareRoot(485906798473894056.0);\n verifyNoMoreInteractions(calculator);\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(4);\n\n assertThat(messages.get(0)).isInstanceOf(UserMessage.class);\n assertThat(messages.get(0).text()).isEqualTo(userMessage);\n\n AiMessage aiMessage = (AiMessage) messages.get(1);\n assertThat(aiMessage.text()).isNull();\n assertThat(aiMessage.toolExecutionRequests()).hasSize(1);\n ToolExecutionRequest toolExecutionRequest = aiMessage.toolExecutionRequests().get(0);\n assertThat(toolExecutionRequest.id()).isNotBlank();\n assertThat(toolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 485906798473894056}\");\n\n ToolExecutionResultMessage toolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(2);\n assertThat(toolExecutionResultMessage.id()).isEqualTo(toolExecutionRequest.id());\n assertThat(toolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionResultMessage.text()).isEqualTo(\"6.97070153193991E8\");\n\n assertThat(messages.get(3)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(3).text()).contains(\"6.97\");\n }\n\n @Test\n void should_execute_multiple_tools_sequentially_then_answer() throws Exception {\n\n StreamingChatLanguageModel streamingChatModel = OpenAiStreamingChatModel.builder()\n .baseUrl(System.getenv(\"OPENAI_BASE_URL\"))\n .apiKey(System.getenv(\"OPENAI_API_KEY\"))\n .organizationId(System.getenv(\"OPENAI_ORGANIZATION_ID\"))\n .modelName(GPT_3_5_TURBO_0613) // this model can only call tools sequentially\n .temperature(0.0)\n .logRequests(true)\n .logResponses(true)\n .build();\n\n Calculator calculator = spy(new Calculator());\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(streamingChatModel)\n .chatMemory(chatMemory)\n .tools(calculator)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<Response<AiMessage>> futureResponse = new CompletableFuture<>();\n\n String userMessage = \"What is the square root of 485906798473894056 and 97866249624785 in scientific notation?\";\n\n assistant.chat(userMessage)\n .onNext(answerBuilder::append)\n .onComplete(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n Response<AiMessage> response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"6.97\", \"9.89\");\n assertThat(response.content().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isCloseTo(79 + 117 + 152, tokenizerPrecision);\n assertThat(tokenUsage.outputTokenCount()).isCloseTo(21 + 20 + 53, tokenizerPrecision);\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n\n verify(calculator).squareRoot(485906798473894056.0);\n verify(calculator).squareRoot(97866249624785.0);\n verifyNoMoreInteractions(calculator);\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(6);\n\n assertThat(messages.get(0)).isInstanceOf(dev.langchain4j.data.message.UserMessage.class);\n assertThat(messages.get(0).text()).isEqualTo(userMessage);\n\n AiMessage aiMessage = (AiMessage) messages.get(1);\n assertThat(aiMessage.text()).isNull();\n assertThat(aiMessage.toolExecutionRequests()).hasSize(1);\n ToolExecutionRequest toolExecutionRequest = aiMessage.toolExecutionRequests().get(0);\n assertThat(toolExecutionRequest.id()).isNotBlank();\n assertThat(toolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 485906798473894056}\");\n\n ToolExecutionResultMessage toolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(2);\n assertThat(toolExecutionResultMessage.id()).isEqualTo(toolExecutionRequest.id());\n assertThat(toolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionResultMessage.text()).isEqualTo(\"6.97070153193991E8\");\n\n AiMessage secondAiMessage = (AiMessage) messages.get(3);\n assertThat(secondAiMessage.text()).isNull();\n assertThat(secondAiMessage.toolExecutionRequests()).hasSize(1);\n ToolExecutionRequest secondToolExecutionRequest = secondAiMessage.toolExecutionRequests().get(0);\n assertThat(secondToolExecutionRequest.id()).isNotBlank();\n assertThat(secondToolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 97866249624785}\");\n\n ToolExecutionResultMessage secondToolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(4);\n assertThat(secondToolExecutionResultMessage.id()).isEqualTo(secondToolExecutionRequest.id());\n assertThat(secondToolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionResultMessage.text()).isEqualTo(\"9892737.215997653\");\n\n assertThat(messages.get(5)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(5).text()).contains(\"6.97\", \"9.89\");\n }\n\n @Test\n void should_execute_multiple_tools_in_parallel_then_answer() throws Exception {\n\n Calculator calculator = spy(new Calculator());\n\n StreamingChatLanguageModel streamingChatModel = OpenAiStreamingChatModel.builder()\n .baseUrl(System.getenv(\"OPENAI_BASE_URL\"))\n .apiKey(System.getenv(\"OPENAI_API_KEY\"))\n .organizationId(System.getenv(\"OPENAI_ORGANIZATION_ID\"))\n .modelName(GPT_3_5_TURBO_1106)\n .temperature(0.0)\n .logRequests(true)\n .logResponses(true)\n .build();\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(streamingChatModel)\n .chatMemory(chatMemory)\n .tools(calculator)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<Response<AiMessage>> futureResponse = new CompletableFuture<>();\n\n String userMessage = \"What is the square root of 485906798473894056 and 97866249624785 in scientific notation?\";\n\n assistant.chat(userMessage)\n .onNext(answerBuilder::append)\n .onComplete(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n Response<AiMessage> response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"6.97\", \"9.89\");\n assertThat(response.content().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isCloseTo(79 + 160, tokenizerPrecision);\n assertThat(tokenUsage.outputTokenCount()).isCloseTo(54 + 58, tokenizerPrecision);\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n\n verify(calculator).squareRoot(485906798473894056.0);\n verify(calculator).squareRoot(97866249624785.0);\n verifyNoMoreInteractions(calculator);\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(5);\n\n assertThat(messages.get(0)).isInstanceOf(dev.langchain4j.data.message.UserMessage.class);\n assertThat(messages.get(0).text()).isEqualTo(userMessage);\n\n AiMessage aiMessage = (AiMessage) messages.get(1);\n assertThat(aiMessage.text()).isNull();\n assertThat(aiMessage.toolExecutionRequests()).hasSize(2);\n\n ToolExecutionRequest firstToolExecutionRequest = aiMessage.toolExecutionRequests().get(0);\n assertThat(firstToolExecutionRequest.id()).isNotBlank();\n assertThat(firstToolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(firstToolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 485906798473894056}\");\n\n ToolExecutionRequest secondToolExecutionRequest = aiMessage.toolExecutionRequests().get(1);\n assertThat(secondToolExecutionRequest.id()).isNotBlank();\n assertThat(secondToolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 97866249624785}\");\n\n ToolExecutionResultMessage firstToolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(2);\n assertThat(firstToolExecutionResultMessage.id()).isEqualTo(firstToolExecutionRequest.id());\n assertThat(firstToolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(firstToolExecutionResultMessage.text()).isEqualTo(\"6.97070153193991E8\");\n\n ToolExecutionResultMessage secondToolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(3);\n assertThat(secondToolExecutionResultMessage.id()).isEqualTo(secondToolExecutionRequest.id());\n assertThat(secondToolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionResultMessage.text()).isEqualTo(\"9892737.215997653\");\n\n assertThat(messages.get(4)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(4).text()).contains(\"6.97\", \"9.89\");\n }\n}\n"}, "a8ad9e48d9e473164ff6f7c4219bea777f8e5e8e": {"t": 1785755681.3332105, "n": 17526, "h": "dc5f9647a3b4318a", "c": "package dev.langchain4j.service;\n\nimport dev.langchain4j.agent.tool.P;\nimport dev.langchain4j.agent.tool.Tool;\nimport dev.langchain4j.agent.tool.ToolExecutionRequest;\nimport dev.langchain4j.data.message.AiMessage;\nimport dev.langchain4j.data.message.ChatMessage;\nimport dev.langchain4j.data.message.ToolExecutionResultMessage;\nimport dev.langchain4j.data.message.UserMessage;\nimport dev.langchain4j.memory.ChatMemory;\nimport dev.langchain4j.memory.chat.MessageWindowChatMemory;\nimport dev.langchain4j.model.chat.StreamingChatLanguageModel;\nimport dev.langchain4j.model.openai.OpenAiStreamingChatModel;\nimport dev.langchain4j.model.output.Response;\nimport dev.langchain4j.model.output.TokenUsage;\nimport org.assertj.core.data.Percentage;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.List;\nimport java.util.concurrent.CompletableFuture;\n\nimport static dev.langchain4j.model.openai.OpenAiModelName.GPT_3_5_TURBO_1106;\nimport static dev.langchain4j.model.output.FinishReason.STOP;\nimport static java.util.concurrent.TimeUnit.SECONDS;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.data.Percentage.withPercentage;\nimport static org.mockito.Mockito.*;\n\npublic class StreamingAiServicesIT {\n\n StreamingChatLanguageModel streamingChatModel = OpenAiStreamingChatModel.builder()\n .baseUrl(System.getenv(\"OPENAI_BASE_URL\"))\n .apiKey(System.getenv(\"OPENAI_API_KEY\"))\n .organizationId(System.getenv(\"OPENAI_ORGANIZATION_ID\"))\n .temperature(0.0)\n .logRequests(true)\n .logResponses(true)\n .build();\n\n Percentage tokenizerPrecision = withPercentage(5);\n\n interface Assistant {\n\n TokenStream chat(String userMessage);\n }\n\n @Test\n void should_stream_answer() throws Exception {\n\n Assistant assistant = AiServices.create(Assistant.class, streamingChatModel);\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<Response<AiMessage>> futureResponse = new CompletableFuture<>();\n\n assistant.chat(\"What is the capital of Germany?\")\n .onNext(answerBuilder::append)\n .onComplete(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n Response<AiMessage> response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"Berlin\");\n assertThat(response.content().text()).isEqualTo(answer);\n\n assertThat(response.tokenUsage().inputTokenCount()).isEqualTo(14);\n assertThat(response.tokenUsage().outputTokenCount()).isGreaterThan(1);\n assertThat(response.tokenUsage().totalTokenCount()).isGreaterThan(15);\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n }\n\n @Test\n void should_stream_answers_with_memory() throws Exception {\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(streamingChatModel)\n .chatMemory(chatMemory)\n .build();\n\n\n String firstUserMessage = \"Hi, my name is Klaus\";\n CompletableFuture<Response<AiMessage>> firstResultFuture = new CompletableFuture<>();\n\n assistant.chat(firstUserMessage)\n .onNext(System.out::println)\n .onComplete(firstResultFuture::complete)\n .onError(firstResultFuture::completeExceptionally)\n .start();\n\n Response<AiMessage> firstResponse = firstResultFuture.get(30, SECONDS);\n assertThat(firstResponse.content().text()).contains(\"Klaus\");\n\n\n String secondUserMessage = \"What is my name?\";\n CompletableFuture<Response<AiMessage>> secondResultFuture = new CompletableFuture<>();\n\n assistant.chat(secondUserMessage)\n .onNext(System.out::println)\n .onComplete(secondResultFuture::complete)\n .onError(secondResultFuture::completeExceptionally)\n .start();\n\n Response<AiMessage> secondResponse = secondResultFuture.get(30, SECONDS);\n assertThat(secondResponse.content().text()).contains(\"Klaus\");\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(4);\n\n assertThat(messages.get(0)).isInstanceOf(UserMessage.class);\n assertThat(messages.get(0).text()).isEqualTo(firstUserMessage);\n\n assertThat(messages.get(1)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(1)).isEqualTo(firstResponse.content());\n\n assertThat(messages.get(2)).isInstanceOf(UserMessage.class);\n assertThat(messages.get(2).text()).isEqualTo(secondUserMessage);\n\n assertThat(messages.get(3)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(3)).isEqualTo(secondResponse.content());\n }\n\n static class Calculator {\n\n @Tool(\"calculates the square root of the provided number\")\n double squareRoot(@P(\"number to operate on\") double number) {\n return Math.sqrt(number);\n }\n }\n\n @Test\n void should_execute_a_tool_then_stream_answer() throws Exception {\n\n Calculator calculator = spy(new Calculator());\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(streamingChatModel)\n .chatMemory(chatMemory)\n .tools(calculator)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<Response<AiMessage>> futureResponse = new CompletableFuture<>();\n\n String userMessage = \"What is the square root of 485906798473894056 in scientific notation?\";\n\n assistant.chat(userMessage)\n .onNext(answerBuilder::append)\n .onComplete(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n Response<AiMessage> response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"6.97\");\n assertThat(response.content().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isCloseTo(72 + 110, tokenizerPrecision);\n assertThat(tokenUsage.outputTokenCount()).isCloseTo(21 + 28, tokenizerPrecision);\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n\n verify(calculator).squareRoot(485906798473894056.0);\n verifyNoMoreInteractions(calculator);\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(4);\n\n assertThat(messages.get(0)).isInstanceOf(UserMessage.class);\n assertThat(messages.get(0).text()).isEqualTo(userMessage);\n\n AiMessage aiMessage = (AiMessage) messages.get(1);\n assertThat(aiMessage.text()).isNull();\n assertThat(aiMessage.toolExecutionRequests()).hasSize(1);\n ToolExecutionRequest toolExecutionRequest = aiMessage.toolExecutionRequests().get(0);\n assertThat(toolExecutionRequest.id()).isNotBlank();\n assertThat(toolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 485906798473894056}\");\n\n ToolExecutionResultMessage toolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(2);\n assertThat(toolExecutionResultMessage.id()).isEqualTo(toolExecutionRequest.id());\n assertThat(toolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionResultMessage.text()).isEqualTo(\"6.97070153193991E8\");\n\n assertThat(messages.get(3)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(3).text()).contains(\"6.97\");\n }\n\n @Test\n void should_execute_multiple_tools_sequentially_then_answer() throws Exception {\n\n Calculator calculator = spy(new Calculator());\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(streamingChatModel)\n .chatMemory(chatMemory)\n .tools(calculator)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<Response<AiMessage>> futureResponse = new CompletableFuture<>();\n\n String userMessage = \"What is the square root of 485906798473894056 and 97866249624785 in scientific notation?\";\n\n assistant.chat(userMessage)\n .onNext(answerBuilder::append)\n .onComplete(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n Response<AiMessage> response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"6.97\", \"9.89\");\n assertThat(response.content().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isCloseTo(79 + 117 + 152, tokenizerPrecision);\n assertThat(tokenUsage.outputTokenCount()).isCloseTo(21 + 20 + 53, tokenizerPrecision);\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n\n verify(calculator).squareRoot(485906798473894056.0);\n verify(calculator).squareRoot(97866249624785.0);\n verifyNoMoreInteractions(calculator);\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(6);\n\n assertThat(messages.get(0)).isInstanceOf(dev.langchain4j.data.message.UserMessage.class);\n assertThat(messages.get(0).text()).isEqualTo(userMessage);\n\n AiMessage aiMessage = (AiMessage) messages.get(1);\n assertThat(aiMessage.text()).isNull();\n assertThat(aiMessage.toolExecutionRequests()).hasSize(1);\n ToolExecutionRequest toolExecutionRequest = aiMessage.toolExecutionRequests().get(0);\n assertThat(toolExecutionRequest.id()).isNotBlank();\n assertThat(toolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 485906798473894056}\");\n\n ToolExecutionResultMessage toolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(2);\n assertThat(toolExecutionResultMessage.id()).isEqualTo(toolExecutionRequest.id());\n assertThat(toolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionResultMessage.text()).isEqualTo(\"6.97070153193991E8\");\n\n AiMessage secondAiMessage = (AiMessage) messages.get(3);\n assertThat(secondAiMessage.text()).isNull();\n assertThat(secondAiMessage.toolExecutionRequests()).hasSize(1);\n ToolExecutionRequest secondToolExecutionRequest = secondAiMessage.toolExecutionRequests().get(0);\n assertThat(secondToolExecutionRequest.id()).isNotBlank();\n assertThat(secondToolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 97866249624785}\");\n\n ToolExecutionResultMessage secondToolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(4);\n assertThat(secondToolExecutionResultMessage.id()).isEqualTo(secondToolExecutionRequest.id());\n assertThat(secondToolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionResultMessage.text()).isEqualTo(\"9892737.215997653\");\n\n assertThat(messages.get(5)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(5).text()).contains(\"6.97\", \"9.89\");\n }\n\n @Test\n void should_execute_multiple_tools_in_parallel_then_answer() throws Exception {\n\n Calculator calculator = spy(new Calculator());\n\n StreamingChatLanguageModel streamingChatModel = OpenAiStreamingChatModel.builder()\n .baseUrl(System.getenv(\"OPENAI_BASE_URL\"))\n .apiKey(System.getenv(\"OPENAI_API_KEY\"))\n .organizationId(System.getenv(\"OPENAI_ORGANIZATION_ID\"))\n .modelName(GPT_3_5_TURBO_1106)\n .temperature(0.0)\n .logRequests(true)\n .logResponses(true)\n .build();\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(streamingChatModel)\n .chatMemory(chatMemory)\n .tools(calculator)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<Response<AiMessage>> futureResponse = new CompletableFuture<>();\n\n String userMessage = \"What is the square root of 485906798473894056 and 97866249624785 in scientific notation?\";\n\n assistant.chat(userMessage)\n .onNext(answerBuilder::append)\n .onComplete(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n Response<AiMessage> response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"6.97\", \"9.89\");\n assertThat(response.content().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isCloseTo(79 + 160, tokenizerPrecision);\n assertThat(tokenUsage.outputTokenCount()).isCloseTo(54 + 58, tokenizerPrecision);\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n\n verify(calculator).squareRoot(485906798473894056.0);\n verify(calculator).squareRoot(97866249624785.0);\n verifyNoMoreInteractions(calculator);\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(5);\n\n assertThat(messages.get(0)).isInstanceOf(dev.langchain4j.data.message.UserMessage.class);\n assertThat(messages.get(0).text()).isEqualTo(userMessage);\n\n AiMessage aiMessage = (AiMessage) messages.get(1);\n assertThat(aiMessage.text()).isNull();\n assertThat(aiMessage.toolExecutionRequests()).hasSize(2);\n\n ToolExecutionRequest firstToolExecutionRequest = aiMessage.toolExecutionRequests().get(0);\n assertThat(firstToolExecutionRequest.id()).isNotBlank();\n assertThat(firstToolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(firstToolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 485906798473894056}\");\n\n ToolExecutionRequest secondToolExecutionRequest = aiMessage.toolExecutionRequests().get(1);\n assertThat(secondToolExecutionRequest.id()).isNotBlank();\n assertThat(secondToolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 97866249624785}\");\n\n ToolExecutionResultMessage firstToolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(2);\n assertThat(firstToolExecutionResultMessage.id()).isEqualTo(firstToolExecutionRequest.id());\n assertThat(firstToolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(firstToolExecutionResultMessage.text()).isEqualTo(\"6.97070153193991E8\");\n\n ToolExecutionResultMessage secondToolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(3);\n assertThat(secondToolExecutionResultMessage.id()).isEqualTo(secondToolExecutionRequest.id());\n assertThat(secondToolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionResultMessage.text()).isEqualTo(\"9892737.215997653\");\n\n assertThat(messages.get(4)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(4).text()).contains(\"6.97\", \"9.89\");\n }\n}\n"}, "c2ab781adbc3a3e668d0f3e7a948eb599b2b3063": {"t": 1785755682.6610153, "n": 17412, "h": "46a7edde275e55f7", "c": "package dev.langchain4j.service;\n\nimport dev.langchain4j.agent.tool.P;\nimport dev.langchain4j.agent.tool.Tool;\nimport dev.langchain4j.agent.tool.ToolExecutionRequest;\nimport dev.langchain4j.data.message.AiMessage;\nimport dev.langchain4j.data.message.ChatMessage;\nimport dev.langchain4j.data.message.ToolExecutionResultMessage;\nimport dev.langchain4j.data.message.UserMessage;\nimport dev.langchain4j.memory.ChatMemory;\nimport dev.langchain4j.memory.chat.MessageWindowChatMemory;\nimport dev.langchain4j.model.chat.StreamingChatLanguageModel;\nimport dev.langchain4j.model.openai.OpenAiStreamingChatModel;\nimport dev.langchain4j.model.output.Response;\nimport dev.langchain4j.model.output.TokenUsage;\nimport org.assertj.core.data.Percentage;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.List;\nimport java.util.concurrent.CompletableFuture;\n\nimport static dev.langchain4j.model.openai.OpenAiModelName.GPT_3_5_TURBO_1106;\nimport static dev.langchain4j.model.output.FinishReason.STOP;\nimport static java.util.concurrent.TimeUnit.SECONDS;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.data.Percentage.withPercentage;\nimport static org.mockito.Mockito.*;\n\npublic class StreamingAiServicesIT {\n\n StreamingChatLanguageModel streamingChatModel = OpenAiStreamingChatModel.builder()\n .apiKey(System.getenv(\"OPENAI_API_KEY\"))\n .organizationId(System.getenv(\"OPENAI_ORGANIZATION_ID\"))\n .temperature(0.0)\n .logRequests(true)\n .logResponses(true)\n .build();\n\n Percentage tokenizerPrecision = withPercentage(5);\n\n interface Assistant {\n\n TokenStream chat(String userMessage);\n }\n\n @Test\n void should_stream_answer() throws Exception {\n\n Assistant assistant = AiServices.create(Assistant.class, streamingChatModel);\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<Response<AiMessage>> futureResponse = new CompletableFuture<>();\n\n assistant.chat(\"What is the capital of Germany?\")\n .onNext(answerBuilder::append)\n .onComplete(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n Response<AiMessage> response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"Berlin\");\n assertThat(response.content().text()).isEqualTo(answer);\n\n assertThat(response.tokenUsage().inputTokenCount()).isEqualTo(14);\n assertThat(response.tokenUsage().outputTokenCount()).isGreaterThan(1);\n assertThat(response.tokenUsage().totalTokenCount()).isGreaterThan(15);\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n }\n\n @Test\n void should_stream_answers_with_memory() throws Exception {\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(streamingChatModel)\n .chatMemory(chatMemory)\n .build();\n\n\n String firstUserMessage = \"Hi, my name is Klaus\";\n CompletableFuture<Response<AiMessage>> firstResultFuture = new CompletableFuture<>();\n\n assistant.chat(firstUserMessage)\n .onNext(System.out::println)\n .onComplete(firstResultFuture::complete)\n .onError(firstResultFuture::completeExceptionally)\n .start();\n\n Response<AiMessage> firstResponse = firstResultFuture.get(30, SECONDS);\n assertThat(firstResponse.content().text()).contains(\"Klaus\");\n\n\n String secondUserMessage = \"What is my name?\";\n CompletableFuture<Response<AiMessage>> secondResultFuture = new CompletableFuture<>();\n\n assistant.chat(secondUserMessage)\n .onNext(System.out::println)\n .onComplete(secondResultFuture::complete)\n .onError(secondResultFuture::completeExceptionally)\n .start();\n\n Response<AiMessage> secondResponse = secondResultFuture.get(30, SECONDS);\n assertThat(secondResponse.content().text()).contains(\"Klaus\");\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(4);\n\n assertThat(messages.get(0)).isInstanceOf(UserMessage.class);\n assertThat(messages.get(0).text()).isEqualTo(firstUserMessage);\n\n assertThat(messages.get(1)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(1)).isEqualTo(firstResponse.content());\n\n assertThat(messages.get(2)).isInstanceOf(UserMessage.class);\n assertThat(messages.get(2).text()).isEqualTo(secondUserMessage);\n\n assertThat(messages.get(3)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(3)).isEqualTo(secondResponse.content());\n }\n\n static class Calculator {\n\n @Tool(\"calculates the square root of the provided number\")\n double squareRoot(@P(\"number to operate on\") double number) {\n return Math.sqrt(number);\n }\n }\n\n @Test\n void should_execute_a_tool_then_stream_answer() throws Exception {\n\n Calculator calculator = spy(new Calculator());\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(streamingChatModel)\n .chatMemory(chatMemory)\n .tools(calculator)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<Response<AiMessage>> futureResponse = new CompletableFuture<>();\n\n String userMessage = \"What is the square root of 485906798473894056 in scientific notation?\";\n\n assistant.chat(userMessage)\n .onNext(answerBuilder::append)\n .onComplete(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n Response<AiMessage> response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"6.97\");\n assertThat(response.content().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isCloseTo(72 + 110, tokenizerPrecision);\n assertThat(tokenUsage.outputTokenCount()).isCloseTo(21 + 28, tokenizerPrecision);\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n\n verify(calculator).squareRoot(485906798473894056.0);\n verifyNoMoreInteractions(calculator);\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(4);\n\n assertThat(messages.get(0)).isInstanceOf(UserMessage.class);\n assertThat(messages.get(0).text()).isEqualTo(userMessage);\n\n AiMessage aiMessage = (AiMessage) messages.get(1);\n assertThat(aiMessage.text()).isNull();\n assertThat(aiMessage.toolExecutionRequests()).hasSize(1);\n ToolExecutionRequest toolExecutionRequest = aiMessage.toolExecutionRequests().get(0);\n assertThat(toolExecutionRequest.id()).isNotBlank();\n assertThat(toolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 485906798473894056}\");\n\n ToolExecutionResultMessage toolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(2);\n assertThat(toolExecutionResultMessage.id()).isEqualTo(toolExecutionRequest.id());\n assertThat(toolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionResultMessage.text()).isEqualTo(\"6.97070153193991E8\");\n\n assertThat(messages.get(3)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(3).text()).contains(\"6.97\");\n }\n\n @Test\n void should_execute_multiple_tools_sequentially_then_answer() throws Exception {\n\n Calculator calculator = spy(new Calculator());\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(streamingChatModel)\n .chatMemory(chatMemory)\n .tools(calculator)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<Response<AiMessage>> futureResponse = new CompletableFuture<>();\n\n String userMessage = \"What is the square root of 485906798473894056 and 97866249624785 in scientific notation?\";\n\n assistant.chat(userMessage)\n .onNext(answerBuilder::append)\n .onComplete(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n Response<AiMessage> response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"6.97\", \"9.89\");\n assertThat(response.content().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isCloseTo(79 + 117 + 152, tokenizerPrecision);\n assertThat(tokenUsage.outputTokenCount()).isCloseTo(21 + 20 + 53, tokenizerPrecision);\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n\n verify(calculator).squareRoot(485906798473894056.0);\n verify(calculator).squareRoot(97866249624785.0);\n verifyNoMoreInteractions(calculator);\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(6);\n\n assertThat(messages.get(0)).isInstanceOf(dev.langchain4j.data.message.UserMessage.class);\n assertThat(messages.get(0).text()).isEqualTo(userMessage);\n\n AiMessage aiMessage = (AiMessage) messages.get(1);\n assertThat(aiMessage.text()).isNull();\n assertThat(aiMessage.toolExecutionRequests()).hasSize(1);\n ToolExecutionRequest toolExecutionRequest = aiMessage.toolExecutionRequests().get(0);\n assertThat(toolExecutionRequest.id()).isNotBlank();\n assertThat(toolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 485906798473894056}\");\n\n ToolExecutionResultMessage toolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(2);\n assertThat(toolExecutionResultMessage.id()).isEqualTo(toolExecutionRequest.id());\n assertThat(toolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionResultMessage.text()).isEqualTo(\"6.97070153193991E8\");\n\n AiMessage secondAiMessage = (AiMessage) messages.get(3);\n assertThat(secondAiMessage.text()).isNull();\n assertThat(secondAiMessage.toolExecutionRequests()).hasSize(1);\n ToolExecutionRequest secondToolExecutionRequest = secondAiMessage.toolExecutionRequests().get(0);\n assertThat(secondToolExecutionRequest.id()).isNotBlank();\n assertThat(secondToolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 97866249624785}\");\n\n ToolExecutionResultMessage secondToolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(4);\n assertThat(secondToolExecutionResultMessage.id()).isEqualTo(secondToolExecutionRequest.id());\n assertThat(secondToolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionResultMessage.text()).isEqualTo(\"9892737.215997653\");\n\n assertThat(messages.get(5)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(5).text()).contains(\"6.97\", \"9.89\");\n }\n\n @Test\n void should_execute_multiple_tools_in_parallel_then_answer() throws Exception {\n\n Calculator calculator = spy(new Calculator());\n\n StreamingChatLanguageModel streamingChatModel = OpenAiStreamingChatModel.builder()\n .apiKey(System.getenv(\"OPENAI_API_KEY\"))\n .organizationId(System.getenv(\"OPENAI_ORGANIZATION_ID\"))\n .modelName(GPT_3_5_TURBO_1106)\n .temperature(0.0)\n .logRequests(true)\n .logResponses(true)\n .build();\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(streamingChatModel)\n .chatMemory(chatMemory)\n .tools(calculator)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<Response<AiMessage>> futureResponse = new CompletableFuture<>();\n\n String userMessage = \"What is the square root of 485906798473894056 and 97866249624785 in scientific notation?\";\n\n assistant.chat(userMessage)\n .onNext(answerBuilder::append)\n .onComplete(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n Response<AiMessage> response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"6.97\", \"9.89\");\n assertThat(response.content().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isCloseTo(79 + 160, tokenizerPrecision);\n assertThat(tokenUsage.outputTokenCount()).isCloseTo(54 + 58, tokenizerPrecision);\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n\n verify(calculator).squareRoot(485906798473894056.0);\n verify(calculator).squareRoot(97866249624785.0);\n verifyNoMoreInteractions(calculator);\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(5);\n\n assertThat(messages.get(0)).isInstanceOf(dev.langchain4j.data.message.UserMessage.class);\n assertThat(messages.get(0).text()).isEqualTo(userMessage);\n\n AiMessage aiMessage = (AiMessage) messages.get(1);\n assertThat(aiMessage.text()).isNull();\n assertThat(aiMessage.toolExecutionRequests()).hasSize(2);\n\n ToolExecutionRequest firstToolExecutionRequest = aiMessage.toolExecutionRequests().get(0);\n assertThat(firstToolExecutionRequest.id()).isNotBlank();\n assertThat(firstToolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(firstToolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 485906798473894056}\");\n\n ToolExecutionRequest secondToolExecutionRequest = aiMessage.toolExecutionRequests().get(1);\n assertThat(secondToolExecutionRequest.id()).isNotBlank();\n assertThat(secondToolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 97866249624785}\");\n\n ToolExecutionResultMessage firstToolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(2);\n assertThat(firstToolExecutionResultMessage.id()).isEqualTo(firstToolExecutionRequest.id());\n assertThat(firstToolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(firstToolExecutionResultMessage.text()).isEqualTo(\"6.97070153193991E8\");\n\n ToolExecutionResultMessage secondToolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(3);\n assertThat(secondToolExecutionResultMessage.id()).isEqualTo(secondToolExecutionRequest.id());\n assertThat(secondToolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionResultMessage.text()).isEqualTo(\"9892737.215997653\");\n\n assertThat(messages.get(4)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(4).text()).contains(\"6.97\", \"9.89\");\n }\n}\n"}, "8e4254fc20606fd78f9ebbfc5c08bbdd06e3d8b6": {"t": 1785755684.199113, "n": 17270, "h": "da056a9aa176799f", "c": "package dev.langchain4j.service;\n\nimport dev.langchain4j.agent.tool.P;\nimport dev.langchain4j.agent.tool.Tool;\nimport dev.langchain4j.agent.tool.ToolExecutionRequest;\nimport dev.langchain4j.data.message.AiMessage;\nimport dev.langchain4j.data.message.ChatMessage;\nimport dev.langchain4j.data.message.ToolExecutionResultMessage;\nimport dev.langchain4j.data.message.UserMessage;\nimport dev.langchain4j.memory.ChatMemory;\nimport dev.langchain4j.memory.chat.MessageWindowChatMemory;\nimport dev.langchain4j.model.chat.StreamingChatLanguageModel;\nimport dev.langchain4j.model.openai.OpenAiStreamingChatModel;\nimport dev.langchain4j.model.output.Response;\nimport dev.langchain4j.model.output.TokenUsage;\nimport org.assertj.core.data.Percentage;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.List;\nimport java.util.concurrent.CompletableFuture;\n\nimport static dev.langchain4j.model.openai.OpenAiModelName.GPT_3_5_TURBO_1106;\nimport static dev.langchain4j.model.output.FinishReason.STOP;\nimport static java.util.concurrent.TimeUnit.SECONDS;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.data.Percentage.withPercentage;\nimport static org.mockito.Mockito.*;\n\npublic class StreamingAiServicesIT {\n\n StreamingChatLanguageModel streamingChatModel = OpenAiStreamingChatModel.builder()\n .apiKey(System.getenv(\"OPENAI_API_KEY\"))\n .temperature(0.0)\n .logRequests(true)\n .logResponses(true)\n .build();\n\n Percentage tokenizerPrecision = withPercentage(5);\n\n interface Assistant {\n\n TokenStream chat(String userMessage);\n }\n\n @Test\n void should_stream_answer() throws Exception {\n\n Assistant assistant = AiServices.create(Assistant.class, streamingChatModel);\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<Response<AiMessage>> futureResponse = new CompletableFuture<>();\n\n assistant.chat(\"What is the capital of Germany?\")\n .onNext(answerBuilder::append)\n .onComplete(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n Response<AiMessage> response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"Berlin\");\n assertThat(response.content().text()).isEqualTo(answer);\n\n assertThat(response.tokenUsage().inputTokenCount()).isEqualTo(14);\n assertThat(response.tokenUsage().outputTokenCount()).isGreaterThan(1);\n assertThat(response.tokenUsage().totalTokenCount()).isGreaterThan(15);\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n }\n\n @Test\n void should_stream_answers_with_memory() throws Exception {\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(streamingChatModel)\n .chatMemory(chatMemory)\n .build();\n\n\n String firstUserMessage = \"Hi, my name is Klaus\";\n CompletableFuture<Response<AiMessage>> firstResultFuture = new CompletableFuture<>();\n\n assistant.chat(firstUserMessage)\n .onNext(System.out::println)\n .onComplete(firstResultFuture::complete)\n .onError(firstResultFuture::completeExceptionally)\n .start();\n\n Response<AiMessage> firstResponse = firstResultFuture.get(30, SECONDS);\n assertThat(firstResponse.content().text()).contains(\"Klaus\");\n\n\n String secondUserMessage = \"What is my name?\";\n CompletableFuture<Response<AiMessage>> secondResultFuture = new CompletableFuture<>();\n\n assistant.chat(secondUserMessage)\n .onNext(System.out::println)\n .onComplete(secondResultFuture::complete)\n .onError(secondResultFuture::completeExceptionally)\n .start();\n\n Response<AiMessage> secondResponse = secondResultFuture.get(30, SECONDS);\n assertThat(secondResponse.content().text()).contains(\"Klaus\");\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(4);\n\n assertThat(messages.get(0)).isInstanceOf(UserMessage.class);\n assertThat(messages.get(0).text()).isEqualTo(firstUserMessage);\n\n assertThat(messages.get(1)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(1)).isEqualTo(firstResponse.content());\n\n assertThat(messages.get(2)).isInstanceOf(UserMessage.class);\n assertThat(messages.get(2).text()).isEqualTo(secondUserMessage);\n\n assertThat(messages.get(3)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(3)).isEqualTo(secondResponse.content());\n }\n\n static class Calculator {\n\n @Tool(\"calculates the square root of the provided number\")\n double squareRoot(@P(\"number to operate on\") double number) {\n return Math.sqrt(number);\n }\n }\n\n @Test\n void should_execute_a_tool_then_stream_answer() throws Exception {\n\n Calculator calculator = spy(new Calculator());\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(streamingChatModel)\n .chatMemory(chatMemory)\n .tools(calculator)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<Response<AiMessage>> futureResponse = new CompletableFuture<>();\n\n String userMessage = \"What is the square root of 485906798473894056 in scientific notation?\";\n\n assistant.chat(userMessage)\n .onNext(answerBuilder::append)\n .onComplete(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n Response<AiMessage> response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"6.97\");\n assertThat(response.content().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isCloseTo(72 + 110, tokenizerPrecision);\n assertThat(tokenUsage.outputTokenCount()).isCloseTo(21 + 28, tokenizerPrecision);\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n\n verify(calculator).squareRoot(485906798473894056.0);\n verifyNoMoreInteractions(calculator);\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(4);\n\n assertThat(messages.get(0)).isInstanceOf(UserMessage.class);\n assertThat(messages.get(0).text()).isEqualTo(userMessage);\n\n AiMessage aiMessage = (AiMessage) messages.get(1);\n assertThat(aiMessage.text()).isNull();\n assertThat(aiMessage.toolExecutionRequests()).hasSize(1);\n ToolExecutionRequest toolExecutionRequest = aiMessage.toolExecutionRequests().get(0);\n assertThat(toolExecutionRequest.id()).isNotBlank();\n assertThat(toolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 485906798473894056}\");\n\n ToolExecutionResultMessage toolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(2);\n assertThat(toolExecutionResultMessage.id()).isEqualTo(toolExecutionRequest.id());\n assertThat(toolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionResultMessage.text()).isEqualTo(\"6.97070153193991E8\");\n\n assertThat(messages.get(3)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(3).text()).contains(\"6.97\");\n }\n\n @Test\n void should_execute_multiple_tools_sequentially_then_answer() throws Exception {\n\n Calculator calculator = spy(new Calculator());\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(streamingChatModel)\n .chatMemory(chatMemory)\n .tools(calculator)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<Response<AiMessage>> futureResponse = new CompletableFuture<>();\n\n String userMessage = \"What is the square root of 485906798473894056 and 97866249624785 in scientific notation?\";\n\n assistant.chat(userMessage)\n .onNext(answerBuilder::append)\n .onComplete(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n Response<AiMessage> response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"6.97\", \"9.89\");\n assertThat(response.content().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isCloseTo(79 + 117 + 152, tokenizerPrecision);\n assertThat(tokenUsage.outputTokenCount()).isCloseTo(21 + 20 + 53, tokenizerPrecision);\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n\n verify(calculator).squareRoot(485906798473894056.0);\n verify(calculator).squareRoot(97866249624785.0);\n verifyNoMoreInteractions(calculator);\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(6);\n\n assertThat(messages.get(0)).isInstanceOf(dev.langchain4j.data.message.UserMessage.class);\n assertThat(messages.get(0).text()).isEqualTo(userMessage);\n\n AiMessage aiMessage = (AiMessage) messages.get(1);\n assertThat(aiMessage.text()).isNull();\n assertThat(aiMessage.toolExecutionRequests()).hasSize(1);\n ToolExecutionRequest toolExecutionRequest = aiMessage.toolExecutionRequests().get(0);\n assertThat(toolExecutionRequest.id()).isNotBlank();\n assertThat(toolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 485906798473894056}\");\n\n ToolExecutionResultMessage toolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(2);\n assertThat(toolExecutionResultMessage.id()).isEqualTo(toolExecutionRequest.id());\n assertThat(toolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionResultMessage.text()).isEqualTo(\"6.97070153193991E8\");\n\n AiMessage secondAiMessage = (AiMessage) messages.get(3);\n assertThat(secondAiMessage.text()).isNull();\n assertThat(secondAiMessage.toolExecutionRequests()).hasSize(1);\n ToolExecutionRequest secondToolExecutionRequest = secondAiMessage.toolExecutionRequests().get(0);\n assertThat(secondToolExecutionRequest.id()).isNotBlank();\n assertThat(secondToolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 97866249624785}\");\n\n ToolExecutionResultMessage secondToolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(4);\n assertThat(secondToolExecutionResultMessage.id()).isEqualTo(secondToolExecutionRequest.id());\n assertThat(secondToolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionResultMessage.text()).isEqualTo(\"9892737.215997653\");\n\n assertThat(messages.get(5)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(5).text()).contains(\"6.97\", \"9.89\");\n }\n\n @Test\n void should_execute_multiple_tools_in_parallel_then_answer() throws Exception {\n\n Calculator calculator = spy(new Calculator());\n\n StreamingChatLanguageModel streamingChatModel = OpenAiStreamingChatModel.builder()\n .apiKey(System.getenv(\"OPENAI_API_KEY\"))\n .modelName(GPT_3_5_TURBO_1106)\n .temperature(0.0)\n .logRequests(true)\n .logResponses(true)\n .build();\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(streamingChatModel)\n .chatMemory(chatMemory)\n .tools(calculator)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<Response<AiMessage>> futureResponse = new CompletableFuture<>();\n\n String userMessage = \"What is the square root of 485906798473894056 and 97866249624785 in scientific notation?\";\n\n assistant.chat(userMessage)\n .onNext(answerBuilder::append)\n .onComplete(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n Response<AiMessage> response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"6.97\", \"9.89\");\n assertThat(response.content().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isCloseTo(79 + 160, tokenizerPrecision);\n assertThat(tokenUsage.outputTokenCount()).isCloseTo(54 + 58, tokenizerPrecision);\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n\n verify(calculator).squareRoot(485906798473894056.0);\n verify(calculator).squareRoot(97866249624785.0);\n verifyNoMoreInteractions(calculator);\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(5);\n\n assertThat(messages.get(0)).isInstanceOf(dev.langchain4j.data.message.UserMessage.class);\n assertThat(messages.get(0).text()).isEqualTo(userMessage);\n\n AiMessage aiMessage = (AiMessage) messages.get(1);\n assertThat(aiMessage.text()).isNull();\n assertThat(aiMessage.toolExecutionRequests()).hasSize(2);\n\n ToolExecutionRequest firstToolExecutionRequest = aiMessage.toolExecutionRequests().get(0);\n assertThat(firstToolExecutionRequest.id()).isNotBlank();\n assertThat(firstToolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(firstToolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 485906798473894056}\");\n\n ToolExecutionRequest secondToolExecutionRequest = aiMessage.toolExecutionRequests().get(1);\n assertThat(secondToolExecutionRequest.id()).isNotBlank();\n assertThat(secondToolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 97866249624785}\");\n\n ToolExecutionResultMessage firstToolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(2);\n assertThat(firstToolExecutionResultMessage.id()).isEqualTo(firstToolExecutionRequest.id());\n assertThat(firstToolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(firstToolExecutionResultMessage.text()).isEqualTo(\"6.97070153193991E8\");\n\n ToolExecutionResultMessage secondToolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(3);\n assertThat(secondToolExecutionResultMessage.id()).isEqualTo(secondToolExecutionRequest.id());\n assertThat(secondToolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionResultMessage.text()).isEqualTo(\"9892737.215997653\");\n\n assertThat(messages.get(4)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(4).text()).contains(\"6.97\", \"9.89\");\n }\n}\n"}, "303b2ab7b5cfb1ae890c39675bdbf9b662f52771": {"t": 1785755685.6906922, "n": 17183, "h": "7a142a4029ea95d5", "c": "package dev.langchain4j.service;\n\nimport dev.langchain4j.agent.tool.P;\nimport dev.langchain4j.agent.tool.Tool;\nimport dev.langchain4j.agent.tool.ToolExecutionRequest;\nimport dev.langchain4j.data.message.AiMessage;\nimport dev.langchain4j.data.message.ChatMessage;\nimport dev.langchain4j.data.message.ToolExecutionResultMessage;\nimport dev.langchain4j.data.message.UserMessage;\nimport dev.langchain4j.memory.ChatMemory;\nimport dev.langchain4j.memory.chat.MessageWindowChatMemory;\nimport dev.langchain4j.model.chat.StreamingChatLanguageModel;\nimport dev.langchain4j.model.openai.OpenAiStreamingChatModel;\nimport dev.langchain4j.model.output.Response;\nimport dev.langchain4j.model.output.TokenUsage;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.List;\nimport java.util.concurrent.CompletableFuture;\n\nimport static dev.langchain4j.model.openai.OpenAiModelName.GPT_3_5_TURBO_1106;\nimport static dev.langchain4j.model.output.FinishReason.STOP;\nimport static java.util.concurrent.TimeUnit.SECONDS;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.data.Percentage.withPercentage;\nimport static org.mockito.Mockito.*;\n\npublic class StreamingAiServicesIT {\n\n StreamingChatLanguageModel streamingChatModel = OpenAiStreamingChatModel.builder()\n .apiKey(System.getenv(\"OPENAI_API_KEY\"))\n .temperature(0.0)\n .logRequests(true)\n .logResponses(true)\n .build();\n\n interface Assistant {\n\n TokenStream chat(String userMessage);\n }\n\n @Test\n void should_stream_answer() throws Exception {\n\n Assistant assistant = AiServices.create(Assistant.class, streamingChatModel);\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<Response<AiMessage>> futureResponse = new CompletableFuture<>();\n\n assistant.chat(\"What is the capital of Germany?\")\n .onNext(answerBuilder::append)\n .onComplete(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n Response<AiMessage> response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"Berlin\");\n assertThat(response.content().text()).isEqualTo(answer);\n\n assertThat(response.tokenUsage().inputTokenCount()).isEqualTo(14);\n assertThat(response.tokenUsage().outputTokenCount()).isGreaterThan(1);\n assertThat(response.tokenUsage().totalTokenCount()).isGreaterThan(15);\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n }\n\n @Test\n void should_stream_answers_with_memory() throws Exception {\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(streamingChatModel)\n .chatMemory(chatMemory)\n .build();\n\n\n String firstUserMessage = \"Hi, my name is Klaus\";\n CompletableFuture<Response<AiMessage>> firstResultFuture = new CompletableFuture<>();\n\n assistant.chat(firstUserMessage)\n .onNext(System.out::println)\n .onComplete(firstResultFuture::complete)\n .onError(firstResultFuture::completeExceptionally)\n .start();\n\n Response<AiMessage> firstResponse = firstResultFuture.get(30, SECONDS);\n assertThat(firstResponse.content().text()).contains(\"Klaus\");\n\n\n String secondUserMessage = \"What is my name?\";\n CompletableFuture<Response<AiMessage>> secondResultFuture = new CompletableFuture<>();\n\n assistant.chat(secondUserMessage)\n .onNext(System.out::println)\n .onComplete(secondResultFuture::complete)\n .onError(secondResultFuture::completeExceptionally)\n .start();\n\n Response<AiMessage> secondResponse = secondResultFuture.get(30, SECONDS);\n assertThat(secondResponse.content().text()).contains(\"Klaus\");\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(4);\n\n assertThat(messages.get(0)).isInstanceOf(UserMessage.class);\n assertThat(messages.get(0).text()).isEqualTo(firstUserMessage);\n\n assertThat(messages.get(1)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(1)).isEqualTo(firstResponse.content());\n\n assertThat(messages.get(2)).isInstanceOf(UserMessage.class);\n assertThat(messages.get(2).text()).isEqualTo(secondUserMessage);\n\n assertThat(messages.get(3)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(3)).isEqualTo(secondResponse.content());\n }\n\n static class Calculator {\n\n @Tool(\"calculates the square root of the provided number\")\n double squareRoot(@P(\"number to operate on\") double number) {\n return Math.sqrt(number);\n }\n }\n\n @Test\n void should_execute_a_tool_then_stream_answer() throws Exception {\n\n Calculator calculator = spy(new Calculator());\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(streamingChatModel)\n .chatMemory(chatMemory)\n .tools(calculator)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<Response<AiMessage>> futureResponse = new CompletableFuture<>();\n\n String userMessage = \"What is the square root of 485906798473894056 in scientific notation?\";\n\n assistant.chat(userMessage)\n .onNext(answerBuilder::append)\n .onComplete(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n Response<AiMessage> response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"6.97\");\n assertThat(response.content().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isEqualTo(181); // TODO should be around 182?\n assertThat(tokenUsage.outputTokenCount()).isCloseTo(27, withPercentage(5)); // TODO\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n\n verify(calculator).squareRoot(485906798473894056.0);\n verifyNoMoreInteractions(calculator);\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(4);\n\n assertThat(messages.get(0)).isInstanceOf(UserMessage.class);\n assertThat(messages.get(0).text()).isEqualTo(userMessage);\n\n AiMessage aiMessage = (AiMessage) messages.get(1);\n assertThat(aiMessage.text()).isNull();\n assertThat(aiMessage.toolExecutionRequests()).hasSize(1);\n ToolExecutionRequest toolExecutionRequest = aiMessage.toolExecutionRequests().get(0);\n assertThat(toolExecutionRequest.id()).isNotBlank();\n assertThat(toolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 485906798473894056}\");\n\n ToolExecutionResultMessage toolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(2);\n assertThat(toolExecutionResultMessage.id()).isEqualTo(toolExecutionRequest.id());\n assertThat(toolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionResultMessage.text()).isEqualTo(\"6.97070153193991E8\");\n\n assertThat(messages.get(3)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(3).text()).contains(\"6.97\");\n }\n\n @Test\n void should_execute_multiple_tools_sequentially_then_answer() throws Exception {\n\n Calculator calculator = spy(new Calculator());\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(streamingChatModel)\n .chatMemory(chatMemory)\n .tools(calculator)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<Response<AiMessage>> futureResponse = new CompletableFuture<>();\n\n String userMessage = \"What is the square root of 485906798473894056 and 97866249624785 in scientific notation?\";\n\n assistant.chat(userMessage)\n .onNext(answerBuilder::append)\n .onComplete(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n Response<AiMessage> response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"6.97\", \"9.89\");\n assertThat(response.content().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isEqualTo(351); // TODO should be around 348?\n assertThat(tokenUsage.outputTokenCount()).isCloseTo(52, withPercentage(5)); // TODO\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n\n verify(calculator).squareRoot(485906798473894056.0);\n verify(calculator).squareRoot(97866249624785.0);\n verifyNoMoreInteractions(calculator);\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(6);\n\n assertThat(messages.get(0)).isInstanceOf(dev.langchain4j.data.message.UserMessage.class);\n assertThat(messages.get(0).text()).isEqualTo(userMessage);\n\n AiMessage aiMessage = (AiMessage) messages.get(1);\n assertThat(aiMessage.text()).isNull();\n assertThat(aiMessage.toolExecutionRequests()).hasSize(1);\n ToolExecutionRequest toolExecutionRequest = aiMessage.toolExecutionRequests().get(0);\n assertThat(toolExecutionRequest.id()).isNotBlank();\n assertThat(toolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 485906798473894056}\");\n\n ToolExecutionResultMessage toolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(2);\n assertThat(toolExecutionResultMessage.id()).isEqualTo(toolExecutionRequest.id());\n assertThat(toolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(toolExecutionResultMessage.text()).isEqualTo(\"6.97070153193991E8\");\n\n AiMessage secondAiMessage = (AiMessage) messages.get(3);\n assertThat(secondAiMessage.text()).isNull();\n assertThat(secondAiMessage.toolExecutionRequests()).hasSize(1);\n ToolExecutionRequest secondToolExecutionRequest = secondAiMessage.toolExecutionRequests().get(0);\n assertThat(secondToolExecutionRequest.id()).isNotBlank();\n assertThat(secondToolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 97866249624785}\");\n\n ToolExecutionResultMessage secondToolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(4);\n assertThat(secondToolExecutionResultMessage.id()).isEqualTo(secondToolExecutionRequest.id());\n assertThat(secondToolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionResultMessage.text()).isEqualTo(\"9892737.215997653\");\n\n assertThat(messages.get(5)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(5).text()).contains(\"6.97\", \"9.89\");\n }\n\n @Test\n void should_execute_multiple_tools_in_parallel_then_answer() throws Exception {\n\n Calculator calculator = spy(new Calculator());\n\n StreamingChatLanguageModel streamingChatModel = OpenAiStreamingChatModel.builder()\n .apiKey(System.getenv(\"OPENAI_API_KEY\"))\n .modelName(GPT_3_5_TURBO_1106)\n .temperature(0.0)\n .logRequests(true)\n .logResponses(true)\n .build();\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(streamingChatModel)\n .chatMemory(chatMemory)\n .tools(calculator)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<Response<AiMessage>> futureResponse = new CompletableFuture<>();\n\n String userMessage = \"What is the square root of 485906798473894056 and 97866249624785 in scientific notation?\";\n\n assistant.chat(userMessage)\n .onNext(answerBuilder::append)\n .onComplete(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n Response<AiMessage> response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"6.97\", \"9.89\");\n assertThat(response.content().text()).isEqualTo(answer);\n\n TokenUsage tokenUsage = response.tokenUsage();\n assertThat(tokenUsage.inputTokenCount()).isEqualTo(221); // TODO should be around 239?\n assertThat(tokenUsage.outputTokenCount()).isCloseTo(57, withPercentage(5)); // TODO\n assertThat(tokenUsage.totalTokenCount())\n .isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n\n verify(calculator).squareRoot(485906798473894056.0);\n verify(calculator).squareRoot(97866249624785.0);\n verifyNoMoreInteractions(calculator);\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(5);\n\n assertThat(messages.get(0)).isInstanceOf(dev.langchain4j.data.message.UserMessage.class);\n assertThat(messages.get(0).text()).isEqualTo(userMessage);\n\n AiMessage aiMessage = (AiMessage) messages.get(1);\n assertThat(aiMessage.text()).isNull();\n assertThat(aiMessage.toolExecutionRequests()).hasSize(2);\n\n ToolExecutionRequest firstToolExecutionRequest = aiMessage.toolExecutionRequests().get(0);\n assertThat(firstToolExecutionRequest.id()).isNotBlank();\n assertThat(firstToolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(firstToolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 485906798473894056}\");\n\n ToolExecutionRequest secondToolExecutionRequest = aiMessage.toolExecutionRequests().get(1);\n assertThat(secondToolExecutionRequest.id()).isNotBlank();\n assertThat(secondToolExecutionRequest.name()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionRequest.arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 97866249624785}\");\n\n ToolExecutionResultMessage firstToolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(2);\n assertThat(firstToolExecutionResultMessage.id()).isEqualTo(firstToolExecutionRequest.id());\n assertThat(firstToolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(firstToolExecutionResultMessage.text()).isEqualTo(\"6.97070153193991E8\");\n\n ToolExecutionResultMessage secondToolExecutionResultMessage = (ToolExecutionResultMessage) messages.get(3);\n assertThat(secondToolExecutionResultMessage.id()).isEqualTo(secondToolExecutionRequest.id());\n assertThat(secondToolExecutionResultMessage.toolName()).isEqualTo(\"squareRoot\");\n assertThat(secondToolExecutionResultMessage.text()).isEqualTo(\"9892737.215997653\");\n\n assertThat(messages.get(4)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(4).text()).contains(\"6.97\", \"9.89\");\n }\n}\n"}, "c0c2c2e1b72fd24833f5a3a832cf34ac72917c7a": {"t": 1785755686.9212034, "n": 7770, "h": "ca6eb7a7b0517009", "c": "package dev.langchain4j.service;\n\nimport dev.langchain4j.agent.tool.Tool;\nimport dev.langchain4j.data.message.AiMessage;\nimport dev.langchain4j.data.message.ChatMessage;\nimport dev.langchain4j.data.message.ToolExecutionResultMessage;\nimport dev.langchain4j.data.message.UserMessage;\nimport dev.langchain4j.memory.ChatMemory;\nimport dev.langchain4j.memory.chat.MessageWindowChatMemory;\nimport dev.langchain4j.model.chat.StreamingChatLanguageModel;\nimport dev.langchain4j.model.openai.OpenAiStreamingChatModel;\nimport dev.langchain4j.model.output.Response;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.List;\nimport java.util.concurrent.CompletableFuture;\n\nimport static dev.langchain4j.model.output.FinishReason.STOP;\nimport static java.time.Duration.ofSeconds;\nimport static java.util.concurrent.TimeUnit.SECONDS;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.Mockito.*;\n\npublic class StreamingAiServicesIT {\n\n StreamingChatLanguageModel streamingChatModel = OpenAiStreamingChatModel.builder()\n .apiKey(System.getenv(\"OPENAI_API_KEY\"))\n .timeout(ofSeconds(30))\n .logRequests(true)\n .logResponses(true)\n .build();\n\n interface Assistant {\n\n TokenStream chat(String userMessage);\n }\n\n @Test\n void should_stream_answer() throws Exception {\n\n Assistant assistant = AiServices.create(Assistant.class, streamingChatModel);\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<Response<AiMessage>> futureResponse = new CompletableFuture<>();\n\n assistant.chat(\"What is the capital of Germany?\")\n .onNext(answerBuilder::append)\n .onComplete(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n Response<AiMessage> response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"Berlin\");\n assertThat(response.content().text()).isEqualTo(answer);\n\n assertThat(response.tokenUsage().inputTokenCount()).isEqualTo(14);\n assertThat(response.tokenUsage().outputTokenCount()).isGreaterThan(1);\n assertThat(response.tokenUsage().totalTokenCount()).isGreaterThan(15);\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n }\n\n @Test\n void should_stream_answers_with_memory() throws Exception {\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(streamingChatModel)\n .chatMemory(chatMemory)\n .build();\n\n\n String firstUserMessage = \"Hi, my name is Klaus\";\n CompletableFuture<Response<AiMessage>> firstResultFuture = new CompletableFuture<>();\n\n assistant.chat(firstUserMessage)\n .onNext(System.out::println)\n .onComplete(firstResultFuture::complete)\n .onError(firstResultFuture::completeExceptionally)\n .start();\n\n Response<AiMessage> firstResponse = firstResultFuture.get(30, SECONDS);\n assertThat(firstResponse.content().text()).contains(\"Klaus\");\n\n\n String secondUserMessage = \"What is my name?\";\n CompletableFuture<Response<AiMessage>> secondResultFuture = new CompletableFuture<>();\n\n assistant.chat(secondUserMessage)\n .onNext(System.out::println)\n .onComplete(secondResultFuture::complete)\n .onError(secondResultFuture::completeExceptionally)\n .start();\n\n Response<AiMessage> secondResponse = secondResultFuture.get(30, SECONDS);\n assertThat(secondResponse.content().text()).contains(\"Klaus\");\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(4);\n\n assertThat(messages.get(0)).isInstanceOf(UserMessage.class);\n assertThat(messages.get(0).text()).isEqualTo(firstUserMessage);\n\n assertThat(messages.get(1)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(1)).isEqualTo(firstResponse.content());\n\n assertThat(messages.get(2)).isInstanceOf(UserMessage.class);\n assertThat(messages.get(2).text()).isEqualTo(secondUserMessage);\n\n assertThat(messages.get(3)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(3)).isEqualTo(secondResponse.content());\n }\n\n static class Calculator {\n\n @Tool\n double squareRoot(double number) {\n return Math.sqrt(number);\n }\n }\n\n @Test\n void should_execute_tool_then_stream_answer() throws Exception {\n\n Calculator calculator = spy(new Calculator());\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(streamingChatModel)\n .chatMemory(chatMemory)\n .tools(calculator)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<Response<AiMessage>> futureResponse = new CompletableFuture<>();\n\n String userMessage = \"What is the square root of 485906798473894056 in scientific notation?\";\n assistant.chat(userMessage)\n .onNext(answerBuilder::append)\n .onComplete(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n Response<AiMessage> response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"6.97\");\n assertThat(response.content().text()).isEqualTo(answer);\n\n // tool arguments provided by the model (which contribute to the input token count)\n // can have different formatting (extra spaces) which results in one extra/missing token\n assertThat(response.tokenUsage().inputTokenCount()).isBetween(146, 147);\n assertThat(response.tokenUsage().outputTokenCount()).isGreaterThan(1);\n assertThat(response.tokenUsage().totalTokenCount()).isGreaterThan(148);\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n\n verify(calculator).squareRoot(485906798473894056.0);\n verifyNoMoreInteractions(calculator);\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(4);\n\n assertThat(messages.get(0)).isInstanceOf(UserMessage.class);\n assertThat(messages.get(0).text()).isEqualTo(userMessage);\n\n assertThat(messages.get(1)).isInstanceOf(AiMessage.class);\n AiMessage aiMessage = (AiMessage) messages.get(1);\n assertThat(aiMessage.toolExecutionRequest().name()).isEqualTo(\"squareRoot\");\n assertThat(aiMessage.toolExecutionRequest().arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 485906798473894056}\");\n assertThat(messages.get(1).text()).isNull();\n\n assertThat(messages.get(2)).isInstanceOf(ToolExecutionResultMessage.class);\n assertThat(messages.get(2).text()).isEqualTo(\"6.97070153193991E8\");\n\n assertThat(messages.get(3)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(3).text()).contains(\"6.97\");\n }\n}\n"}, "810f2f3967904867707849fd0b6f3443bc251d4f": {"t": 1785755689.2754028, "n": 7513, "h": "40251bdfaeaf4a96", "c": "package dev.langchain4j.service;\n\nimport dev.langchain4j.agent.tool.Tool;\nimport dev.langchain4j.data.message.AiMessage;\nimport dev.langchain4j.data.message.ChatMessage;\nimport dev.langchain4j.data.message.ToolExecutionResultMessage;\nimport dev.langchain4j.data.message.UserMessage;\nimport dev.langchain4j.memory.ChatMemory;\nimport dev.langchain4j.memory.chat.MessageWindowChatMemory;\nimport dev.langchain4j.model.chat.StreamingChatLanguageModel;\nimport dev.langchain4j.model.openai.OpenAiStreamingChatModel;\nimport dev.langchain4j.model.output.Response;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.List;\nimport java.util.concurrent.CompletableFuture;\n\nimport static dev.langchain4j.model.output.FinishReason.STOP;\nimport static java.time.Duration.ofSeconds;\nimport static java.util.concurrent.TimeUnit.SECONDS;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.Mockito.*;\n\npublic class StreamingAiServicesIT {\n\n StreamingChatLanguageModel streamingChatModel = OpenAiStreamingChatModel.builder()\n .apiKey(System.getenv(\"OPENAI_API_KEY\"))\n .timeout(ofSeconds(30))\n .build();\n\n interface Assistant {\n\n TokenStream chat(String userMessage);\n }\n\n @Test\n void should_stream_answer() throws Exception {\n\n Assistant assistant = AiServices.create(Assistant.class, streamingChatModel);\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<Response<AiMessage>> futureResponse = new CompletableFuture<>();\n\n assistant.chat(\"What is the capital of Germany?\")\n .onNext(answerBuilder::append)\n .onComplete(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n Response<AiMessage> response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"Berlin\");\n assertThat(response.content().text()).isEqualTo(answer);\n\n assertThat(response.tokenUsage().inputTokenCount()).isEqualTo(14);\n assertThat(response.tokenUsage().outputTokenCount()).isGreaterThan(1);\n assertThat(response.tokenUsage().totalTokenCount()).isGreaterThan(15);\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n }\n\n @Test\n void should_stream_answers_with_memory() throws Exception {\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(streamingChatModel)\n .chatMemory(chatMemory)\n .build();\n\n\n String firstUserMessage = \"Hi, my name is Klaus\";\n CompletableFuture<Response<AiMessage>> firstResultFuture = new CompletableFuture<>();\n\n assistant.chat(firstUserMessage)\n .onNext(System.out::println)\n .onComplete(firstResultFuture::complete)\n .onError(firstResultFuture::completeExceptionally)\n .start();\n\n Response<AiMessage> firstResponse = firstResultFuture.get(30, SECONDS);\n assertThat(firstResponse.content().text()).contains(\"Klaus\");\n\n\n String secondUserMessage = \"What is my name?\";\n CompletableFuture<Response<AiMessage>> secondResultFuture = new CompletableFuture<>();\n\n assistant.chat(secondUserMessage)\n .onNext(System.out::println)\n .onComplete(secondResultFuture::complete)\n .onError(secondResultFuture::completeExceptionally)\n .start();\n\n Response<AiMessage> secondResponse = secondResultFuture.get(30, SECONDS);\n assertThat(secondResponse.content().text()).contains(\"Klaus\");\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(4);\n\n assertThat(messages.get(0)).isInstanceOf(UserMessage.class);\n assertThat(messages.get(0).text()).isEqualTo(firstUserMessage);\n\n assertThat(messages.get(1)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(1)).isEqualTo(firstResponse.content());\n\n assertThat(messages.get(2)).isInstanceOf(UserMessage.class);\n assertThat(messages.get(2).text()).isEqualTo(secondUserMessage);\n\n assertThat(messages.get(3)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(3)).isEqualTo(secondResponse.content());\n }\n\n static class Calculator {\n\n @Tool\n double squareRoot(double number) {\n return Math.sqrt(number);\n }\n }\n\n @Test\n void should_execute_tool_then_stream_answer() throws Exception {\n\n Calculator calculator = spy(new Calculator());\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(streamingChatModel)\n .chatMemory(chatMemory)\n .tools(calculator)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<Response<AiMessage>> futureResponse = new CompletableFuture<>();\n\n String userMessage = \"What is the square root of 485906798473894056 in scientific notation?\";\n assistant.chat(userMessage)\n .onNext(answerBuilder::append)\n .onComplete(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n Response<AiMessage> response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"6.97\");\n assertThat(response.content().text()).isEqualTo(answer);\n\n assertThat(response.tokenUsage().inputTokenCount()).isEqualTo(147);\n assertThat(response.tokenUsage().outputTokenCount()).isGreaterThan(1);\n assertThat(response.tokenUsage().totalTokenCount()).isGreaterThan(148);\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n\n verify(calculator).squareRoot(485906798473894056.0);\n verifyNoMoreInteractions(calculator);\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(4);\n\n assertThat(messages.get(0)).isInstanceOf(UserMessage.class);\n assertThat(messages.get(0).text()).isEqualTo(userMessage);\n\n assertThat(messages.get(1)).isInstanceOf(AiMessage.class);\n AiMessage aiMessage = (AiMessage) messages.get(1);\n assertThat(aiMessage.toolExecutionRequest().name()).isEqualTo(\"squareRoot\");\n assertThat(aiMessage.toolExecutionRequest().arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 485906798473894056}\");\n assertThat(messages.get(1).text()).isNull();\n\n assertThat(messages.get(2)).isInstanceOf(ToolExecutionResultMessage.class);\n assertThat(messages.get(2).text()).isEqualTo(\"6.97070153193991E8\");\n\n assertThat(messages.get(3)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(3).text()).contains(\"6.97\");\n }\n}\n"}, "21f744868b0edd7d293b3b0319452d02620064bf": {"t": 1785755690.5210078, "n": 7405, "h": "8bc245a2488348a1", "c": "package dev.langchain4j.service;\n\nimport dev.langchain4j.agent.tool.Tool;\nimport dev.langchain4j.data.message.AiMessage;\nimport dev.langchain4j.data.message.ChatMessage;\nimport dev.langchain4j.data.message.ToolExecutionResultMessage;\nimport dev.langchain4j.data.message.UserMessage;\nimport dev.langchain4j.memory.ChatMemory;\nimport dev.langchain4j.memory.chat.MessageWindowChatMemory;\nimport dev.langchain4j.model.chat.StreamingChatLanguageModel;\nimport dev.langchain4j.model.openai.OpenAiStreamingChatModel;\nimport dev.langchain4j.model.output.Response;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.List;\nimport java.util.concurrent.CompletableFuture;\n\nimport static dev.langchain4j.model.output.FinishReason.STOP;\nimport static java.util.concurrent.TimeUnit.SECONDS;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.Mockito.*;\n\npublic class StreamingAiServicesIT {\n\n StreamingChatLanguageModel streamingChatModel\n = OpenAiStreamingChatModel.withApiKey(System.getenv(\"OPENAI_API_KEY\"));\n\n interface Assistant {\n\n TokenStream chat(String userMessage);\n }\n\n @Test\n void should_stream_answer() throws Exception {\n\n Assistant assistant = AiServices.create(Assistant.class, streamingChatModel);\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<Response<AiMessage>> futureResponse = new CompletableFuture<>();\n\n assistant.chat(\"What is the capital of Germany?\")\n .onNext(answerBuilder::append)\n .onComplete(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n Response<AiMessage> response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"Berlin\");\n assertThat(response.content().text()).isEqualTo(answer);\n\n assertThat(response.tokenUsage().inputTokenCount()).isEqualTo(14);\n assertThat(response.tokenUsage().outputTokenCount()).isGreaterThan(1);\n assertThat(response.tokenUsage().totalTokenCount()).isGreaterThan(15);\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n }\n\n @Test\n void should_stream_answers_with_memory() throws Exception {\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(streamingChatModel)\n .chatMemory(chatMemory)\n .build();\n\n\n String firstUserMessage = \"Hi, my name is Klaus\";\n CompletableFuture<Response<AiMessage>> firstResultFuture = new CompletableFuture<>();\n\n assistant.chat(firstUserMessage)\n .onNext(System.out::println)\n .onComplete(firstResultFuture::complete)\n .onError(firstResultFuture::completeExceptionally)\n .start();\n\n Response<AiMessage> firstResponse = firstResultFuture.get(30, SECONDS);\n assertThat(firstResponse.content().text()).contains(\"Klaus\");\n\n\n String secondUserMessage = \"What is my name?\";\n CompletableFuture<Response<AiMessage>> secondResultFuture = new CompletableFuture<>();\n\n assistant.chat(secondUserMessage)\n .onNext(System.out::println)\n .onComplete(secondResultFuture::complete)\n .onError(secondResultFuture::completeExceptionally)\n .start();\n\n Response<AiMessage> secondResponse = secondResultFuture.get(30, SECONDS);\n assertThat(secondResponse.content().text()).contains(\"Klaus\");\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(4);\n\n assertThat(messages.get(0)).isInstanceOf(UserMessage.class);\n assertThat(messages.get(0).text()).isEqualTo(firstUserMessage);\n\n assertThat(messages.get(1)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(1)).isEqualTo(firstResponse.content());\n\n assertThat(messages.get(2)).isInstanceOf(UserMessage.class);\n assertThat(messages.get(2).text()).isEqualTo(secondUserMessage);\n\n assertThat(messages.get(3)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(3)).isEqualTo(secondResponse.content());\n }\n\n static class Calculator {\n\n @Tool\n double squareRoot(double number) {\n return Math.sqrt(number);\n }\n }\n\n @Test\n void should_execute_tool_then_stream_answer() throws Exception {\n\n Calculator calculator = spy(new Calculator());\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(streamingChatModel)\n .chatMemory(chatMemory)\n .tools(calculator)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> futureAnswer = new CompletableFuture<>();\n CompletableFuture<Response<AiMessage>> futureResponse = new CompletableFuture<>();\n\n String userMessage = \"What is the square root of 485906798473894056 in scientific notation?\";\n assistant.chat(userMessage)\n .onNext(answerBuilder::append)\n .onComplete(response -> {\n futureAnswer.complete(answerBuilder.toString());\n futureResponse.complete(response);\n })\n .onError(futureAnswer::completeExceptionally)\n .start();\n\n String answer = futureAnswer.get(30, SECONDS);\n Response<AiMessage> response = futureResponse.get(30, SECONDS);\n\n assertThat(answer).contains(\"6.97\");\n assertThat(response.content().text()).isEqualTo(answer);\n\n assertThat(response.tokenUsage().inputTokenCount()).isEqualTo(147);\n assertThat(response.tokenUsage().outputTokenCount()).isGreaterThan(1);\n assertThat(response.tokenUsage().totalTokenCount()).isGreaterThan(148);\n\n assertThat(response.finishReason()).isEqualTo(STOP);\n\n\n verify(calculator).squareRoot(485906798473894056.0);\n verifyNoMoreInteractions(calculator);\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(4);\n\n assertThat(messages.get(0)).isInstanceOf(UserMessage.class);\n assertThat(messages.get(0).text()).isEqualTo(userMessage);\n\n assertThat(messages.get(1)).isInstanceOf(AiMessage.class);\n AiMessage aiMessage = (AiMessage) messages.get(1);\n assertThat(aiMessage.toolExecutionRequest().name()).isEqualTo(\"squareRoot\");\n assertThat(aiMessage.toolExecutionRequest().arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 485906798473894056}\");\n assertThat(messages.get(1).text()).isNull();\n\n assertThat(messages.get(2)).isInstanceOf(ToolExecutionResultMessage.class);\n assertThat(messages.get(2).text()).isEqualTo(\"6.97070153193991E8\");\n\n assertThat(messages.get(3)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(3).text()).contains(\"6.97\");\n }\n}\n"}, "93173f20d7c39fcbcee9935c4550a5e65507f4f2": {"t": 1785755691.7482152, "n": 6060, "h": "aa32c63056112d41", "c": "package dev.langchain4j.service;\n\nimport dev.langchain4j.agent.tool.Tool;\nimport dev.langchain4j.data.message.AiMessage;\nimport dev.langchain4j.data.message.ChatMessage;\nimport dev.langchain4j.data.message.ToolExecutionResultMessage;\nimport dev.langchain4j.data.message.UserMessage;\nimport dev.langchain4j.memory.ChatMemory;\nimport dev.langchain4j.memory.chat.MessageWindowChatMemory;\nimport dev.langchain4j.model.chat.StreamingChatLanguageModel;\nimport dev.langchain4j.model.openai.OpenAiStreamingChatModel;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.List;\nimport java.util.concurrent.CompletableFuture;\n\nimport static java.util.concurrent.TimeUnit.SECONDS;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.Mockito.*;\n\npublic class StreamingAiServicesIT {\n\n StreamingChatLanguageModel streamingChatModel\n = OpenAiStreamingChatModel.withApiKey(System.getenv(\"OPENAI_API_KEY\"));\n\n interface Assistant {\n\n TokenStream chat(String userMessage);\n }\n\n @Test\n void should_stream_answer() throws Exception {\n\n Assistant assistant = AiServices.create(Assistant.class, streamingChatModel);\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> future = new CompletableFuture<>();\n\n assistant.chat(\"What is the capital of Germany?\")\n .onNext(answerBuilder::append)\n .onComplete(() -> future.complete(answerBuilder.toString()))\n .onError(future::completeExceptionally)\n .start();\n\n String answer = future.get(30, SECONDS);\n\n assertThat(answer).contains(\"Berlin\");\n }\n\n @Test\n void should_stream_answers_with_memory() throws Exception {\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(streamingChatModel)\n .chatMemory(chatMemory)\n .build();\n\n\n String firstUserMessage = \"Hi, my name is Klaus\";\n StringBuilder firstAnswerBuilder = new StringBuilder();\n CompletableFuture<String> firstFuture = new CompletableFuture<>();\n\n assistant.chat(firstUserMessage)\n .onNext(firstAnswerBuilder::append)\n .onComplete(() -> firstFuture.complete(firstAnswerBuilder.toString()))\n .onError(firstFuture::completeExceptionally)\n .start();\n\n String firstAnswer = firstFuture.get(30, SECONDS);\n assertThat(firstAnswer).contains(\"Klaus\");\n\n\n String secondUserMessage = \"What is my name?\";\n StringBuilder secondAnswerBuilder = new StringBuilder();\n CompletableFuture<String> secondFuture = new CompletableFuture<>();\n\n assistant.chat(secondUserMessage)\n .onNext(secondAnswerBuilder::append)\n .onComplete(() -> secondFuture.complete(secondAnswerBuilder.toString()))\n .onError(secondFuture::completeExceptionally)\n .start();\n\n String secondAnswer = secondFuture.get(30, SECONDS);\n assertThat(secondAnswer).contains(\"Klaus\");\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(4);\n\n assertThat(messages.get(0)).isInstanceOf(UserMessage.class);\n assertThat(messages.get(0).text()).isEqualTo(firstUserMessage);\n\n assertThat(messages.get(1)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(1).text()).isEqualTo(firstAnswer);\n\n assertThat(messages.get(2)).isInstanceOf(UserMessage.class);\n assertThat(messages.get(2).text()).isEqualTo(secondUserMessage);\n\n assertThat(messages.get(3)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(3).text()).isEqualTo(secondAnswer);\n }\n\n static class Calculator {\n\n @Tool\n double squareRoot(double number) {\n return Math.sqrt(number);\n }\n }\n\n @Test\n void should_execute_tool_then_stream_answer() throws Exception {\n\n Calculator calculator = spy(new Calculator());\n\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(streamingChatModel)\n .chatMemory(chatMemory)\n .tools(calculator)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> future = new CompletableFuture<>();\n\n String userMessage = \"What is the square root of 485906798473894056 in scientific notation?\";\n assistant.chat(userMessage)\n .onNext(answerBuilder::append)\n .onComplete(() -> future.complete(answerBuilder.toString()))\n .onError(future::completeExceptionally)\n .start();\n\n String answer = future.get(30, SECONDS);\n\n assertThat(answer).contains(\"6.97\");\n\n\n verify(calculator).squareRoot(485906798473894056.0);\n verifyNoMoreInteractions(calculator);\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(4);\n\n assertThat(messages.get(0)).isInstanceOf(UserMessage.class);\n assertThat(messages.get(0).text()).isEqualTo(userMessage);\n\n assertThat(messages.get(1)).isInstanceOf(AiMessage.class);\n AiMessage aiMessage = (AiMessage) messages.get(1);\n assertThat(aiMessage.toolExecutionRequest().name()).isEqualTo(\"squareRoot\");\n assertThat(aiMessage.toolExecutionRequest().arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 485906798473894056}\");\n assertThat(messages.get(1).text()).isNull();\n\n assertThat(messages.get(2)).isInstanceOf(ToolExecutionResultMessage.class);\n assertThat(messages.get(2).text()).isEqualTo(\"6.97070153193991E8\");\n\n assertThat(messages.get(3)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(3).text()).contains(\"6.97\");\n }\n}\n"}, "80f71feeb6a85706630f397f142533f2a532868d": {"t": 1785755693.2310095, "n": 6054, "h": "db2266204e4f4f27", "c": "package dev.langchain4j.service;\n\nimport dev.langchain4j.agent.tool.Tool;\nimport dev.langchain4j.data.message.AiMessage;\nimport dev.langchain4j.data.message.ChatMessage;\nimport dev.langchain4j.data.message.ToolExecutionResultMessage;\nimport dev.langchain4j.data.message.UserMessage;\nimport dev.langchain4j.memory.ChatMemory;\nimport dev.langchain4j.memory.chat.MessageWindowChatMemory;\nimport dev.langchain4j.model.chat.StreamingChatLanguageModel;\nimport dev.langchain4j.model.openai.OpenAiStreamingChatModel;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.List;\nimport java.util.concurrent.CompletableFuture;\n\nimport static java.util.concurrent.TimeUnit.SECONDS;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.Mockito.*;\n\npublic class StreamingAiServicesIT {\n\n StreamingChatLanguageModel streamingChatModel\n = OpenAiStreamingChatModel.withApiKey(System.getenv(\"OPENAI_API_KEY\"));\n\n interface Assistant {\n\n TokenStream chat(String userMessage);\n }\n\n @Test\n void should_stream_answer() throws Exception {\n\n Assistant assistant = AiServices.create(Assistant.class, streamingChatModel);\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> future = new CompletableFuture<>();\n\n assistant.chat(\"What is the capital of Germany?\")\n .onNext(answerBuilder::append)\n .onComplete(() -> future.complete(answerBuilder.toString()))\n .onError(future::completeExceptionally)\n .start();\n\n String answer = future.get(30, SECONDS);\n\n assertThat(answer).contains(\"Berlin\");\n }\n\n @Test\n void should_stream_answers_with_memory() throws Exception {\n\n ChatMemory chatMemory = MessageWindowChatMemory.withCapacity(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(streamingChatModel)\n .chatMemory(chatMemory)\n .build();\n\n\n String firstUserMessage = \"Hi, my name is Klaus\";\n StringBuilder firstAnswerBuilder = new StringBuilder();\n CompletableFuture<String> firstFuture = new CompletableFuture<>();\n\n assistant.chat(firstUserMessage)\n .onNext(firstAnswerBuilder::append)\n .onComplete(() -> firstFuture.complete(firstAnswerBuilder.toString()))\n .onError(firstFuture::completeExceptionally)\n .start();\n\n String firstAnswer = firstFuture.get(30, SECONDS);\n assertThat(firstAnswer).contains(\"Klaus\");\n\n\n String secondUserMessage = \"What is my name?\";\n StringBuilder secondAnswerBuilder = new StringBuilder();\n CompletableFuture<String> secondFuture = new CompletableFuture<>();\n\n assistant.chat(secondUserMessage)\n .onNext(secondAnswerBuilder::append)\n .onComplete(() -> secondFuture.complete(secondAnswerBuilder.toString()))\n .onError(secondFuture::completeExceptionally)\n .start();\n\n String secondAnswer = secondFuture.get(30, SECONDS);\n assertThat(secondAnswer).contains(\"Klaus\");\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(4);\n\n assertThat(messages.get(0)).isInstanceOf(UserMessage.class);\n assertThat(messages.get(0).text()).isEqualTo(firstUserMessage);\n\n assertThat(messages.get(1)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(1).text()).isEqualTo(firstAnswer);\n\n assertThat(messages.get(2)).isInstanceOf(UserMessage.class);\n assertThat(messages.get(2).text()).isEqualTo(secondUserMessage);\n\n assertThat(messages.get(3)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(3).text()).isEqualTo(secondAnswer);\n }\n\n static class Calculator {\n\n @Tool\n double squareRoot(double number) {\n return Math.sqrt(number);\n }\n }\n\n @Test\n void should_execute_tool_then_stream_answer() throws Exception {\n\n Calculator calculator = spy(new Calculator());\n\n ChatMemory chatMemory = MessageWindowChatMemory.withCapacity(10);\n\n Assistant assistant = AiServices.builder(Assistant.class)\n .streamingChatLanguageModel(streamingChatModel)\n .chatMemory(chatMemory)\n .tools(calculator)\n .build();\n\n StringBuilder answerBuilder = new StringBuilder();\n CompletableFuture<String> future = new CompletableFuture<>();\n\n String userMessage = \"What is the square root of 485906798473894056 in scientific notation?\";\n assistant.chat(userMessage)\n .onNext(answerBuilder::append)\n .onComplete(() -> future.complete(answerBuilder.toString()))\n .onError(future::completeExceptionally)\n .start();\n\n String answer = future.get(30, SECONDS);\n\n assertThat(answer).contains(\"6.97\");\n\n\n verify(calculator).squareRoot(485906798473894056.0);\n verifyNoMoreInteractions(calculator);\n\n\n List<ChatMessage> messages = chatMemory.messages();\n assertThat(messages).hasSize(4);\n\n assertThat(messages.get(0)).isInstanceOf(UserMessage.class);\n assertThat(messages.get(0).text()).isEqualTo(userMessage);\n\n assertThat(messages.get(1)).isInstanceOf(AiMessage.class);\n AiMessage aiMessage = (AiMessage) messages.get(1);\n assertThat(aiMessage.toolExecutionRequest().name()).isEqualTo(\"squareRoot\");\n assertThat(aiMessage.toolExecutionRequest().arguments())\n .isEqualToIgnoringWhitespace(\"{\\\"arg0\\\": 485906798473894056}\");\n assertThat(messages.get(1).text()).isNull();\n\n assertThat(messages.get(2)).isInstanceOf(ToolExecutionResultMessage.class);\n assertThat(messages.get(2).text()).isEqualTo(\"6.97070153193991E8\");\n\n assertThat(messages.get(3)).isInstanceOf(AiMessage.class);\n assertThat(messages.get(3).text()).contains(\"6.97\");\n }\n}\n"}}, "langchain4j-open-ai/src/test/java/dev/langchain4j/model/openai/compatible/deepseek/OpenAiStreamingChatModelDeepSeekThinkingIT.java": {"fcde6ad6baf89c5dea8e76999999aef66e26f037": {"t": 1785745684.012442, "n": 8949, "h": "de95f339783ae069", "c": "package dev.langchain4j.model.openai.compatible.deepseek;\n\nimport static dev.langchain4j.JsonTestUtils.jsonify;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.Mockito.atLeastOnce;\nimport static org.mockito.Mockito.inOrder;\nimport static org.mockito.Mockito.never;\nimport static org.mockito.Mockito.spy;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.verifyNoMoreInteractions;\n\nimport java.util.List;\nimport dev.langchain4j.data.message.AiMessage;\nimport dev.langchain4j.data.message.UserMessage;\nimport dev.langchain4j.http.client.HttpRequest;\nimport dev.langchain4j.http.client.MockHttpClientBuilder;\nimport dev.langchain4j.http.client.SpyingHttpClient;\nimport dev.langchain4j.http.client.jdk.JdkHttpClient;\nimport dev.langchain4j.model.chat.StreamingChatModel;\nimport dev.langchain4j.model.chat.TestStreamingChatResponseHandler;\nimport dev.langchain4j.model.openai.OpenAiStreamingChatModel;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.NullSource;\nimport org.junit.jupiter.params.provider.ValueSource;\nimport org.mockito.InOrder;\n\n/**\n * See <a href=\"https://api-docs.deepseek.com/guides/reasoning_model\">DeepSeek API Docs</a> for more info.\n */\n@EnabledIfEnvironmentVariable(named = \"DEEPSEEK_API_KEY\", matches = \".+\")\nclass OpenAiStreamingChatModelDeepSeekThinkingIT {\n\n private final SpyingHttpClient spyingHttpClient = new SpyingHttpClient(JdkHttpClient.builder().build());\n\n @Test\n void should_return_thinking() {\n\n // given\n boolean returnThinking = true;\n\n SpyingHttpClient spyingHttpClient = new SpyingHttpClient(JdkHttpClient.builder().build());\n\n StreamingChatModel model = OpenAiStreamingChatModel.builder()\n .httpClientBuilder(new MockHttpClientBuilder(spyingHttpClient))\n .baseUrl(\"https://api.deepseek.com/v1\")\n .apiKey(System.getenv(\"DEEPSEEK_API_KEY\"))\n .modelName(\"deepseek-reasoner\")\n\n .returnThinking(returnThinking)\n\n .logRequests(true)\n .logResponses(true)\n .build();\n\n UserMessage userMessage1 = UserMessage.from(\"What is the capital of Germany?\");\n\n // when\n TestStreamingChatResponseHandler spyHandler1 = spy(new TestStreamingChatResponseHandler());\n model.chat(List.of(userMessage1), spyHandler1);\n\n // then\n AiMessage aiMessage1 = spyHandler1.get().aiMessage();\n assertThat(aiMessage1.text()).containsIgnoringCase(\"Berlin\");\n assertThat(aiMessage1.thinking())\n .containsIgnoringCase(\"Berlin\")\n .isEqualTo(spyHandler1.getThinking());\n\n InOrder inOrder1 = inOrder(spyHandler1);\n inOrder1.verify(spyHandler1).get();\n inOrder1.verify(spyHandler1, atLeastOnce()).onPartialThinking(any(), any());\n inOrder1.verify(spyHandler1, atLeastOnce()).onPartialResponse(any(), any());\n inOrder1.verify(spyHandler1).onCompleteResponse(any());\n inOrder1.verify(spyHandler1).getThinking();\n inOrder1.verifyNoMoreInteractions();\n verifyNoMoreInteractions(spyHandler1);\n\n // given\n UserMessage userMessage2 = UserMessage.from(\"What is the capital of France?\");\n\n // when\n TestStreamingChatResponseHandler handler2 = new TestStreamingChatResponseHandler();\n model.chat(List.of(userMessage1, aiMessage1, userMessage2), handler2);\n\n // then\n AiMessage aiMessage2 = handler2.get().aiMessage();\n assertThat(aiMessage2.text()).containsIgnoringCase(\"Paris\");\n assertThat(aiMessage2.thinking()).isNotBlank();\n\n // should NOT send thinking in the follow-up request\n List<HttpRequest> httpRequests = spyingHttpClient.requests();\n assertThat(httpRequests).hasSize(2);\n assertThat(httpRequests.get(1).body())\n .contains(jsonify(aiMessage1.text()))\n .doesNotContain(jsonify(aiMessage1.thinking()));\n }\n\n @Test\n void should_send_thinking_in_follow_up_request_when_enabled() {\n\n // given\n boolean returnThinking = true;\n\n SpyingHttpClient spyingHttpClient = new SpyingHttpClient(JdkHttpClient.builder().build());\n\n StreamingChatModel model = OpenAiStreamingChatModel.builder()\n .httpClientBuilder(new MockHttpClientBuilder(spyingHttpClient))\n .baseUrl(\"https://api.deepseek.com/v1\")\n .apiKey(System.getenv(\"DEEPSEEK_API_KEY\"))\n .modelName(\"deepseek-reasoner\")\n\n .returnThinking(returnThinking)\n .sendThinking(true)\n .logRequests(true)\n .logResponses(true)\n .build();\n\n UserMessage userMessage1 = UserMessage.from(\"What is the capital of Germany?\");\n\n // when\n TestStreamingChatResponseHandler spyHandler1 = spy(new TestStreamingChatResponseHandler());\n model.chat(List.of(userMessage1), spyHandler1);\n\n // then\n AiMessage aiMessage1 = spyHandler1.get().aiMessage();\n assertThat(aiMessage1.text()).containsIgnoringCase(\"Berlin\");\n assertThat(aiMessage1.thinking()).isNotBlank();\n\n // given\n UserMessage userMessage2 = UserMessage.from(\"What is the capital of France?\");\n\n // when\n TestStreamingChatResponseHandler handler2 = new TestStreamingChatResponseHandler();\n model.chat(List.of(userMessage1, aiMessage1, userMessage2), handler2);\n\n // then\n List<HttpRequest> httpRequests = spyingHttpClient.requests();\n assertThat(httpRequests).hasSize(2);\n String secondBody = httpRequests.get(1).body();\n assertThat(secondBody)\n .contains(\"\\\"reasoning_content\\\"\")\n .contains(jsonify(aiMessage1.thinking()));\n }\n\n @Test\n void should_use_custom_reasoning_content_field_name_in_follow_up_request() {\n\n // given\n boolean returnThinking = true;\n\n SpyingHttpClient spyingHttpClient = new SpyingHttpClient(JdkHttpClient.builder().build());\n\n StreamingChatModel model = OpenAiStreamingChatModel.builder()\n .httpClientBuilder(new MockHttpClientBuilder(spyingHttpClient))\n .baseUrl(\"https://api.deepseek.com/v1\")\n .apiKey(System.getenv(\"DEEPSEEK_API_KEY\"))\n .modelName(\"deepseek-reasoner\")\n\n .returnThinking(returnThinking)\n .sendThinking(true, \"my_reason\")\n .logRequests(true)\n .logResponses(true)\n .build();\n\n UserMessage userMessage1 = UserMessage.from(\"What is the capital of Germany?\");\n\n // when\n TestStreamingChatResponseHandler spyHandler1 = spy(new TestStreamingChatResponseHandler());\n model.chat(List.of(userMessage1), spyHandler1);\n\n // then\n AiMessage aiMessage1 = spyHandler1.get().aiMessage();\n assertThat(aiMessage1.text()).containsIgnoringCase(\"Berlin\");\n assertThat(aiMessage1.thinking()).isNotBlank();\n\n // given\n UserMessage userMessage2 = UserMessage.from(\"What is the capital of France?\");\n\n // when\n TestStreamingChatResponseHandler handler2 = new TestStreamingChatResponseHandler();\n model.chat(List.of(userMessage1, aiMessage1, userMessage2), handler2);\n\n // then\n List<HttpRequest> httpRequests = spyingHttpClient.requests();\n assertThat(httpRequests).hasSize(2);\n String secondBody = httpRequests.get(1).body();\n assertThat(secondBody)\n .contains(\"\\\"my_reason\\\"\")\n .contains(jsonify(aiMessage1.thinking()))\n .doesNotContain(\"\\\"reasoning_content\\\"\");\n }\n\n @ParameterizedTest\n @NullSource\n @ValueSource(booleans = false)\n void should_NOT_return_thinking(Boolean returnThinking) {\n\n // given\n StreamingChatModel model = OpenAiStreamingChatModel.builder()\n .baseUrl(\"https://api.deepseek.com/v1\")\n .apiKey(System.getenv(\"DEEPSEEK_API_KEY\"))\n .modelName(\"deepseek-reasoner\")\n\n .returnThinking(returnThinking)\n\n .logRequests(true)\n .logResponses(true)\n .build();\n\n String userMessage = \"What is the capital of Germany?\";\n\n // when\n TestStreamingChatResponseHandler spyHandler = spy(new TestStreamingChatResponseHandler());\n model.chat(userMessage, spyHandler);\n\n // then\n AiMessage aiMessage = spyHandler.get().aiMessage();\n assertThat(aiMessage.text()).containsIgnoringCase(\"Berlin\");\n assertThat(aiMessage.thinking()).isNull();\n\n verify(spyHandler, never()).onPartialThinking(any(), any());\n }\n}\n"}, "00dbfa28d3599cd38ddad9a3596604da2229702f": {"t": 1785755651.2126746, "n": 8949, "h": "de95f339783ae069", "c": "package dev.langchain4j.model.openai.compatible.deepseek;\n\nimport static dev.langchain4j.JsonTestUtils.jsonify;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.Mockito.atLeastOnce;\nimport static org.mockito.Mockito.inOrder;\nimport static org.mockito.Mockito.never;\nimport static org.mockito.Mockito.spy;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.verifyNoMoreInteractions;\n\nimport java.util.List;\nimport dev.langchain4j.data.message.AiMessage;\nimport dev.langchain4j.data.message.UserMessage;\nimport dev.langchain4j.http.client.HttpRequest;\nimport dev.langchain4j.http.client.MockHttpClientBuilder;\nimport dev.langchain4j.http.client.SpyingHttpClient;\nimport dev.langchain4j.http.client.jdk.JdkHttpClient;\nimport dev.langchain4j.model.chat.StreamingChatModel;\nimport dev.langchain4j.model.chat.TestStreamingChatResponseHandler;\nimport dev.langchain4j.model.openai.OpenAiStreamingChatModel;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.NullSource;\nimport org.junit.jupiter.params.provider.ValueSource;\nimport org.mockito.InOrder;\n\n/**\n * See <a href=\"https://api-docs.deepseek.com/guides/reasoning_model\">DeepSeek API Docs</a> for more info.\n */\n@EnabledIfEnvironmentVariable(named = \"DEEPSEEK_API_KEY\", matches = \".+\")\nclass OpenAiStreamingChatModelDeepSeekThinkingIT {\n\n private final SpyingHttpClient spyingHttpClient = new SpyingHttpClient(JdkHttpClient.builder().build());\n\n @Test\n void should_return_thinking() {\n\n // given\n boolean returnThinking = true;\n\n SpyingHttpClient spyingHttpClient = new SpyingHttpClient(JdkHttpClient.builder().build());\n\n StreamingChatModel model = OpenAiStreamingChatModel.builder()\n .httpClientBuilder(new MockHttpClientBuilder(spyingHttpClient))\n .baseUrl(\"https://api.deepseek.com/v1\")\n .apiKey(System.getenv(\"DEEPSEEK_API_KEY\"))\n .modelName(\"deepseek-reasoner\")\n\n .returnThinking(returnThinking)\n\n .logRequests(true)\n .logResponses(true)\n .build();\n\n UserMessage userMessage1 = UserMessage.from(\"What is the capital of Germany?\");\n\n // when\n TestStreamingChatResponseHandler spyHandler1 = spy(new TestStreamingChatResponseHandler());\n model.chat(List.of(userMessage1), spyHandler1);\n\n // then\n AiMessage aiMessage1 = spyHandler1.get().aiMessage();\n assertThat(aiMessage1.text()).containsIgnoringCase(\"Berlin\");\n assertThat(aiMessage1.thinking())\n .containsIgnoringCase(\"Berlin\")\n .isEqualTo(spyHandler1.getThinking());\n\n InOrder inOrder1 = inOrder(spyHandler1);\n inOrder1.verify(spyHandler1).get();\n inOrder1.verify(spyHandler1, atLeastOnce()).onPartialThinking(any(), any());\n inOrder1.verify(spyHandler1, atLeastOnce()).onPartialResponse(any(), any());\n inOrder1.verify(spyHandler1).onCompleteResponse(any());\n inOrder1.verify(spyHandler1).getThinking();\n inOrder1.verifyNoMoreInteractions();\n verifyNoMoreInteractions(spyHandler1);\n\n // given\n UserMessage userMessage2 = UserMessage.from(\"What is the capital of France?\");\n\n // when\n TestStreamingChatResponseHandler handler2 = new TestStreamingChatResponseHandler();\n model.chat(List.of(userMessage1, aiMessage1, userMessage2), handler2);\n\n // then\n AiMessage aiMessage2 = handler2.get().aiMessage();\n assertThat(aiMessage2.text()).containsIgnoringCase(\"Paris\");\n assertThat(aiMessage2.thinking()).isNotBlank();\n\n // should NOT send thinking in the follow-up request\n List<HttpRequest> httpRequests = spyingHttpClient.requests();\n assertThat(httpRequests).hasSize(2);\n assertThat(httpRequests.get(1).body())\n .contains(jsonify(aiMessage1.text()))\n .doesNotContain(jsonify(aiMessage1.thinking()));\n }\n\n @Test\n void should_send_thinking_in_follow_up_request_when_enabled() {\n\n // given\n boolean returnThinking = true;\n\n SpyingHttpClient spyingHttpClient = new SpyingHttpClient(JdkHttpClient.builder().build());\n\n StreamingChatModel model = OpenAiStreamingChatModel.builder()\n .httpClientBuilder(new MockHttpClientBuilder(spyingHttpClient))\n .baseUrl(\"https://api.deepseek.com/v1\")\n .apiKey(System.getenv(\"DEEPSEEK_API_KEY\"))\n .modelName(\"deepseek-reasoner\")\n\n .returnThinking(returnThinking)\n .sendThinking(true)\n .logRequests(true)\n .logResponses(true)\n .build();\n\n UserMessage userMessage1 = UserMessage.from(\"What is the capital of Germany?\");\n\n // when\n TestStreamingChatResponseHandler spyHandler1 = spy(new TestStreamingChatResponseHandler());\n model.chat(List.of(userMessage1), spyHandler1);\n\n // then\n AiMessage aiMessage1 = spyHandler1.get().aiMessage();\n assertThat(aiMessage1.text()).containsIgnoringCase(\"Berlin\");\n assertThat(aiMessage1.thinking()).isNotBlank();\n\n // given\n UserMessage userMessage2 = UserMessage.from(\"What is the capital of France?\");\n\n // when\n TestStreamingChatResponseHandler handler2 = new TestStreamingChatResponseHandler();\n model.chat(List.of(userMessage1, aiMessage1, userMessage2), handler2);\n\n // then\n List<HttpRequest> httpRequests = spyingHttpClient.requests();\n assertThat(httpRequests).hasSize(2);\n String secondBody = httpRequests.get(1).body();\n assertThat(secondBody)\n .contains(\"\\\"reasoning_content\\\"\")\n .contains(jsonify(aiMessage1.thinking()));\n }\n\n @Test\n void should_use_custom_reasoning_content_field_name_in_follow_up_request() {\n\n // given\n boolean returnThinking = true;\n\n SpyingHttpClient spyingHttpClient = new SpyingHttpClient(JdkHttpClient.builder().build());\n\n StreamingChatModel model = OpenAiStreamingChatModel.builder()\n .httpClientBuilder(new MockHttpClientBuilder(spyingHttpClient))\n .baseUrl(\"https://api.deepseek.com/v1\")\n .apiKey(System.getenv(\"DEEPSEEK_API_KEY\"))\n .modelName(\"deepseek-reasoner\")\n\n .returnThinking(returnThinking)\n .sendThinking(true, \"my_reason\")\n .logRequests(true)\n .logResponses(true)\n .build();\n\n UserMessage userMessage1 = UserMessage.from(\"What is the capital of Germany?\");\n\n // when\n TestStreamingChatResponseHandler spyHandler1 = spy(new TestStreamingChatResponseHandler());\n model.chat(List.of(userMessage1), spyHandler1);\n\n // then\n AiMessage aiMessage1 = spyHandler1.get().aiMessage();\n assertThat(aiMessage1.text()).containsIgnoringCase(\"Berlin\");\n assertThat(aiMessage1.thinking()).isNotBlank();\n\n // given\n UserMessage userMessage2 = UserMessage.from(\"What is the capital of France?\");\n\n // when\n TestStreamingChatResponseHandler handler2 = new TestStreamingChatResponseHandler();\n model.chat(List.of(userMessage1, aiMessage1, userMessage2), handler2);\n\n // then\n List<HttpRequest> httpRequests = spyingHttpClient.requests();\n assertThat(httpRequests).hasSize(2);\n String secondBody = httpRequests.get(1).body();\n assertThat(secondBody)\n .contains(\"\\\"my_reason\\\"\")\n .contains(jsonify(aiMessage1.thinking()))\n .doesNotContain(\"\\\"reasoning_content\\\"\");\n }\n\n @ParameterizedTest\n @NullSource\n @ValueSource(booleans = false)\n void should_NOT_return_thinking(Boolean returnThinking) {\n\n // given\n StreamingChatModel model = OpenAiStreamingChatModel.builder()\n .baseUrl(\"https://api.deepseek.com/v1\")\n .apiKey(System.getenv(\"DEEPSEEK_API_KEY\"))\n .modelName(\"deepseek-reasoner\")\n\n .returnThinking(returnThinking)\n\n .logRequests(true)\n .logResponses(true)\n .build();\n\n String userMessage = \"What is the capital of Germany?\";\n\n // when\n TestStreamingChatResponseHandler spyHandler = spy(new TestStreamingChatResponseHandler());\n model.chat(userMessage, spyHandler);\n\n // then\n AiMessage aiMessage = spyHandler.get().aiMessage();\n assertThat(aiMessage.text()).containsIgnoringCase(\"Berlin\");\n assertThat(aiMessage.thinking()).isNull();\n\n verify(spyHandler, never()).onPartialThinking(any(), any());\n }\n}\n"}, "57c49ade5ab64b3c5181a935551f9da6c7d8cbbf": {"t": 1785755652.6578734, "n": 8649, "h": "32f69ee556940b12", "c": "package dev.langchain4j.model.openai.compatible.deepseek;\n\nimport static dev.langchain4j.JsonTestUtils.jsonify;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.Mockito.atLeastOnce;\nimport static org.mockito.Mockito.inOrder;\nimport static org.mockito.Mockito.never;\nimport static org.mockito.Mockito.spy;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.verifyNoMoreInteractions;\n\nimport java.util.List;\nimport dev.langchain4j.data.message.AiMessage;\nimport dev.langchain4j.data.message.UserMessage;\nimport dev.langchain4j.http.client.HttpRequest;\nimport dev.langchain4j.http.client.MockHttpClientBuilder;\nimport dev.langchain4j.http.client.SpyingHttpClient;\nimport dev.langchain4j.http.client.jdk.JdkHttpClient;\nimport dev.langchain4j.model.chat.StreamingChatModel;\nimport dev.langchain4j.model.chat.TestStreamingChatResponseHandler;\nimport dev.langchain4j.model.openai.OpenAiStreamingChatModel;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.NullSource;\nimport org.junit.jupiter.params.provider.ValueSource;\nimport org.mockito.InOrder;\n\n/**\n * See <a href=\"https://api-docs.deepseek.com/guides/reasoning_model\">DeepSeek API Docs</a> for more info.\n */\n@EnabledIfEnvironmentVariable(named = \"DEEPSEEK_API_KEY\", matches = \".+\")\nclass OpenAiStreamingChatModelDeepSeekThinkingIT {\n\n private final SpyingHttpClient spyingHttpClient = new SpyingHttpClient(JdkHttpClient.builder().build());\n\n @Test\n void should_return_thinking() {\n\n // given\n boolean returnThinking = true;\n\n StreamingChatModel model = OpenAiStreamingChatModel.builder()\n .httpClientBuilder(new MockHttpClientBuilder(spyingHttpClient))\n .baseUrl(\"https://api.deepseek.com/v1\")\n .apiKey(System.getenv(\"DEEPSEEK_API_KEY\"))\n .modelName(\"deepseek-reasoner\")\n\n .returnThinking(returnThinking)\n\n .logRequests(true)\n .logResponses(true)\n .build();\n\n UserMessage userMessage1 = UserMessage.from(\"What is the capital of Germany?\");\n\n // when\n TestStreamingChatResponseHandler spyHandler1 = spy(new TestStreamingChatResponseHandler());\n model.chat(List.of(userMessage1), spyHandler1);\n\n // then\n AiMessage aiMessage1 = spyHandler1.get().aiMessage();\n assertThat(aiMessage1.text()).containsIgnoringCase(\"Berlin\");\n assertThat(aiMessage1.thinking())\n .containsIgnoringCase(\"Berlin\")\n .isEqualTo(spyHandler1.getThinking());\n\n InOrder inOrder1 = inOrder(spyHandler1);\n inOrder1.verify(spyHandler1).get();\n inOrder1.verify(spyHandler1, atLeastOnce()).onPartialThinking(any(), any());\n inOrder1.verify(spyHandler1, atLeastOnce()).onPartialResponse(any(), any());\n inOrder1.verify(spyHandler1).onCompleteResponse(any());\n inOrder1.verify(spyHandler1).getThinking();\n inOrder1.verifyNoMoreInteractions();\n verifyNoMoreInteractions(spyHandler1);\n\n // given\n UserMessage userMessage2 = UserMessage.from(\"What is the capital of France?\");\n\n // when\n TestStreamingChatResponseHandler handler2 = new TestStreamingChatResponseHandler();\n model.chat(List.of(userMessage1, aiMessage1, userMessage2), handler2);\n\n // then\n AiMessage aiMessage2 = handler2.get().aiMessage();\n assertThat(aiMessage2.text()).containsIgnoringCase(\"Paris\");\n assertThat(aiMessage2.thinking()).isNotBlank();\n\n // should NOT send thinking in the follow-up request\n List<HttpRequest> httpRequests = spyingHttpClient.requests();\n assertThat(httpRequests).hasSize(2);\n assertThat(httpRequests.get(1).body())\n .contains(jsonify(aiMessage1.text()))\n .doesNotContain(jsonify(aiMessage1.thinking()));\n }\n\n @Test\n void should_send_thinking_in_follow_up_request_when_enabled() {\n\n // given\n boolean returnThinking = true;\n\n StreamingChatModel model = OpenAiStreamingChatModel.builder()\n .httpClientBuilder(new MockHttpClientBuilder(spyingHttpClient))\n .baseUrl(\"https://api.deepseek.com/v1\")\n .apiKey(System.getenv(\"DEEPSEEK_API_KEY\"))\n .modelName(\"deepseek-reasoner\")\n\n .returnThinking(returnThinking)\n .sendThinking(true)\n .logRequests(true)\n .logResponses(true)\n .build();\n\n UserMessage userMessage1 = UserMessage.from(\"What is the capital of Germany?\");\n\n // when\n TestStreamingChatResponseHandler spyHandler1 = spy(new TestStreamingChatResponseHandler());\n model.chat(List.of(userMessage1), spyHandler1);\n\n // then\n AiMessage aiMessage1 = spyHandler1.get().aiMessage();\n assertThat(aiMessage1.text()).containsIgnoringCase(\"Berlin\");\n assertThat(aiMessage1.thinking()).isNotBlank();\n\n // given\n UserMessage userMessage2 = UserMessage.from(\"What is the capital of France?\");\n\n // when\n TestStreamingChatResponseHandler handler2 = new TestStreamingChatResponseHandler();\n model.chat(List.of(userMessage1, aiMessage1, userMessage2), handler2);\n\n // then\n List<HttpRequest> httpRequests = spyingHttpClient.requests();\n assertThat(httpRequests).hasSize(2);\n String secondBody = httpRequests.get(1).body();\n assertThat(secondBody)\n .contains(\"\\\"reasoning_content\\\"\")\n .contains(jsonify(aiMessage1.thinking()));\n }\n\n @Test\n void should_use_custom_reasoning_content_field_name_in_follow_up_request() {\n\n // given\n boolean returnThinking = true;\n\n StreamingChatModel model = OpenAiStreamingChatModel.builder()\n .httpClientBuilder(new MockHttpClientBuilder(spyingHttpClient))\n .baseUrl(\"https://api.deepseek.com/v1\")\n .apiKey(System.getenv(\"DEEPSEEK_API_KEY\"))\n .modelName(\"deepseek-reasoner\")\n\n .returnThinking(returnThinking)\n .sendThinking(true, \"my_reason\")\n .logRequests(true)\n .logResponses(true)\n .build();\n\n UserMessage userMessage1 = UserMessage.from(\"What is the capital of Germany?\");\n\n // when\n TestStreamingChatResponseHandler spyHandler1 = spy(new TestStreamingChatResponseHandler());\n model.chat(List.of(userMessage1), spyHandler1);\n\n // then\n AiMessage aiMessage1 = spyHandler1.get().aiMessage();\n assertThat(aiMessage1.text()).containsIgnoringCase(\"Berlin\");\n assertThat(aiMessage1.thinking()).isNotBlank();\n\n // given\n UserMessage userMessage2 = UserMessage.from(\"What is the capital of France?\");\n\n // when\n TestStreamingChatResponseHandler handler2 = new TestStreamingChatResponseHandler();\n model.chat(List.of(userMessage1, aiMessage1, userMessage2), handler2);\n\n // then\n List<HttpRequest> httpRequests = spyingHttpClient.requests();\n assertThat(httpRequests).hasSize(2);\n String secondBody = httpRequests.get(1).body();\n assertThat(secondBody)\n .contains(\"\\\"my_reason\\\"\")\n .contains(jsonify(aiMessage1.thinking()))\n .doesNotContain(\"\\\"reasoning_content\\\"\");\n }\n\n @ParameterizedTest\n @NullSource\n @ValueSource(booleans = false)\n void should_NOT_return_thinking(Boolean returnThinking) {\n\n // given\n StreamingChatModel model = OpenAiStreamingChatModel.builder()\n .baseUrl(\"https://api.deepseek.com/v1\")\n .apiKey(System.getenv(\"DEEPSEEK_API_KEY\"))\n .modelName(\"deepseek-reasoner\")\n\n .returnThinking(returnThinking)\n\n .logRequests(true)\n .logResponses(true)\n .build();\n\n String userMessage = \"What is the capital of Germany?\";\n\n // when\n TestStreamingChatResponseHandler spyHandler = spy(new TestStreamingChatResponseHandler());\n model.chat(userMessage, spyHandler);\n\n // then\n AiMessage aiMessage = spyHandler.get().aiMessage();\n assertThat(aiMessage.text()).containsIgnoringCase(\"Berlin\");\n assertThat(aiMessage.thinking()).isNull();\n\n verify(spyHandler, never()).onPartialThinking(any(), any());\n }\n}\n"}, "ecc754ba1fce9231c88085a0e75b6f8a49142d4b": {"t": 1785755654.079322, "n": 5120, "h": "98b0b99d78b74a70", "c": "package dev.langchain4j.model.openai.compatible.deepseek;\n\nimport static dev.langchain4j.JsonTestUtils.jsonify;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.Mockito.atLeastOnce;\nimport static org.mockito.Mockito.inOrder;\nimport static org.mockito.Mockito.never;\nimport static org.mockito.Mockito.spy;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.verifyNoMoreInteractions;\n\nimport java.util.List;\nimport dev.langchain4j.data.message.AiMessage;\nimport dev.langchain4j.data.message.UserMessage;\nimport dev.langchain4j.http.client.HttpRequest;\nimport dev.langchain4j.http.client.MockHttpClientBuilder;\nimport dev.langchain4j.http.client.SpyingHttpClient;\nimport dev.langchain4j.http.client.jdk.JdkHttpClient;\nimport dev.langchain4j.model.chat.StreamingChatModel;\nimport dev.langchain4j.model.chat.TestStreamingChatResponseHandler;\nimport dev.langchain4j.model.openai.OpenAiStreamingChatModel;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.NullSource;\nimport org.junit.jupiter.params.provider.ValueSource;\nimport org.mockito.InOrder;\n\n/**\n * See <a href=\"https://api-docs.deepseek.com/guides/reasoning_model\">DeepSeek API Docs</a> for more info.\n */\n@EnabledIfEnvironmentVariable(named = \"DEEPSEEK_API_KEY\", matches = \".+\")\nclass OpenAiStreamingChatModelDeepSeekThinkingIT {\n\n private final SpyingHttpClient spyingHttpClient = new SpyingHttpClient(JdkHttpClient.builder().build());\n\n @Test\n void should_return_thinking() {\n\n // given\n boolean returnThinking = true;\n\n StreamingChatModel model = OpenAiStreamingChatModel.builder()\n .httpClientBuilder(new MockHttpClientBuilder(spyingHttpClient))\n .baseUrl(\"https://api.deepseek.com/v1\")\n .apiKey(System.getenv(\"DEEPSEEK_API_KEY\"))\n .modelName(\"deepseek-reasoner\")\n\n .returnThinking(returnThinking)\n\n .logRequests(true)\n .logResponses(true)\n .build();\n\n UserMessage userMessage1 = UserMessage.from(\"What is the capital of Germany?\");\n\n // when\n TestStreamingChatResponseHandler spyHandler1 = spy(new TestStreamingChatResponseHandler());\n model.chat(List.of(userMessage1), spyHandler1);\n\n // then\n AiMessage aiMessage1 = spyHandler1.get().aiMessage();\n assertThat(aiMessage1.text()).containsIgnoringCase(\"Berlin\");\n assertThat(aiMessage1.thinking())\n .containsIgnoringCase(\"Berlin\")\n .isEqualTo(spyHandler1.getThinking());\n\n InOrder inOrder1 = inOrder(spyHandler1);\n inOrder1.verify(spyHandler1).get();\n inOrder1.verify(spyHandler1, atLeastOnce()).onPartialThinking(any(), any());\n inOrder1.verify(spyHandler1, atLeastOnce()).onPartialResponse(any(), any());\n inOrder1.verify(spyHandler1).onCompleteResponse(any());\n inOrder1.verify(spyHandler1).getThinking();\n inOrder1.verifyNoMoreInteractions();\n verifyNoMoreInteractions(spyHandler1);\n\n // given\n UserMessage userMessage2 = UserMessage.from(\"What is the capital of France?\");\n\n // when\n TestStreamingChatResponseHandler handler2 = new TestStreamingChatResponseHandler();\n model.chat(List.of(userMessage1, aiMessage1, userMessage2), handler2);\n\n // then\n AiMessage aiMessage2 = handler2.get().aiMessage();\n assertThat(aiMessage2.text()).containsIgnoringCase(\"Paris\");\n assertThat(aiMessage2.thinking()).isNotBlank();\n\n // should NOT send thinking in the follow-up request\n List<HttpRequest> httpRequests = spyingHttpClient.requests();\n assertThat(httpRequests).hasSize(2);\n assertThat(httpRequests.get(1).body())\n .contains(jsonify(aiMessage1.text()))\n .doesNotContain(jsonify(aiMessage1.thinking()));\n }\n\n @ParameterizedTest\n @NullSource\n @ValueSource(booleans = false)\n void should_NOT_return_thinking(Boolean returnThinking) {\n\n // given\n StreamingChatModel model = OpenAiStreamingChatModel.builder()\n .baseUrl(\"https://api.deepseek.com/v1\")\n .apiKey(System.getenv(\"DEEPSEEK_API_KEY\"))\n .modelName(\"deepseek-reasoner\")\n\n .returnThinking(returnThinking)\n\n .logRequests(true)\n .logResponses(true)\n .build();\n\n String userMessage = \"What is the capital of Germany?\";\n\n // when\n TestStreamingChatResponseHandler spyHandler = spy(new TestStreamingChatResponseHandler());\n model.chat(userMessage, spyHandler);\n\n // then\n AiMessage aiMessage = spyHandler.get().aiMessage();\n assertThat(aiMessage.text()).containsIgnoringCase(\"Berlin\");\n assertThat(aiMessage.thinking()).isNull();\n\n verify(spyHandler, never()).onPartialThinking(any(), any());\n }\n}\n"}, "03930ba8500f888083f59a9f4165468cf84e0ec1": {"t": 1785755655.412239, "n": 5099, "h": "ce316a3427297c4d", "c": "package dev.langchain4j.model.openai.compatible.deepseek;\n\nimport static dev.langchain4j.JsonTestUtils.jsonify;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.Mockito.atLeastOnce;\nimport static org.mockito.Mockito.inOrder;\nimport static org.mockito.Mockito.never;\nimport static org.mockito.Mockito.spy;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.verifyNoMoreInteractions;\n\nimport java.util.List;\nimport dev.langchain4j.data.message.AiMessage;\nimport dev.langchain4j.data.message.UserMessage;\nimport dev.langchain4j.http.client.HttpRequest;\nimport dev.langchain4j.http.client.MockHttpClientBuilder;\nimport dev.langchain4j.http.client.SpyingHttpClient;\nimport dev.langchain4j.http.client.jdk.JdkHttpClient;\nimport dev.langchain4j.model.chat.StreamingChatModel;\nimport dev.langchain4j.model.chat.TestStreamingChatResponseHandler;\nimport dev.langchain4j.model.openai.OpenAiStreamingChatModel;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.NullSource;\nimport org.junit.jupiter.params.provider.ValueSource;\nimport org.mockito.InOrder;\n\n/**\n * See <a href=\"https://api-docs.deepseek.com/guides/reasoning_model\">DeepSeek API Docs</a> for more info.\n */\n@EnabledIfEnvironmentVariable(named = \"DEEPSEEK_API_KEY\", matches = \".+\")\nclass OpenAiStreamingChatModelDeepSeekThinkingIT {\n\n private final SpyingHttpClient spyingHttpClient = new SpyingHttpClient(JdkHttpClient.builder().build());\n\n @Test\n void should_return_thinking() {\n\n // given\n boolean returnThinking = true;\n\n StreamingChatModel model = OpenAiStreamingChatModel.builder()\n .httpClientBuilder(new MockHttpClientBuilder(spyingHttpClient))\n .baseUrl(\"https://api.deepseek.com/v1\")\n .apiKey(System.getenv(\"DEEPSEEK_API_KEY\"))\n .modelName(\"deepseek-reasoner\")\n\n .returnThinking(returnThinking)\n\n .logRequests(true)\n .logResponses(true)\n .build();\n\n UserMessage userMessage1 = UserMessage.from(\"What is the capital of Germany?\");\n\n // when\n TestStreamingChatResponseHandler spyHandler1 = spy(new TestStreamingChatResponseHandler());\n model.chat(List.of(userMessage1), spyHandler1);\n\n // then\n AiMessage aiMessage1 = spyHandler1.get().aiMessage();\n assertThat(aiMessage1.text()).containsIgnoringCase(\"Berlin\");\n assertThat(aiMessage1.thinking())\n .containsIgnoringCase(\"Berlin\")\n .isEqualTo(spyHandler1.getThinking());\n\n InOrder inOrder1 = inOrder(spyHandler1);\n inOrder1.verify(spyHandler1).get();\n inOrder1.verify(spyHandler1, atLeastOnce()).onPartialThinking(any());\n inOrder1.verify(spyHandler1, atLeastOnce()).onPartialResponse(any());\n inOrder1.verify(spyHandler1).onCompleteResponse(any());\n inOrder1.verify(spyHandler1).getThinking();\n inOrder1.verifyNoMoreInteractions();\n verifyNoMoreInteractions(spyHandler1);\n\n // given\n UserMessage userMessage2 = UserMessage.from(\"What is the capital of France?\");\n\n // when\n TestStreamingChatResponseHandler handler2 = new TestStreamingChatResponseHandler();\n model.chat(List.of(userMessage1, aiMessage1, userMessage2), handler2);\n\n // then\n AiMessage aiMessage2 = handler2.get().aiMessage();\n assertThat(aiMessage2.text()).containsIgnoringCase(\"Paris\");\n assertThat(aiMessage2.thinking()).isNotBlank();\n\n // should NOT send thinking in the follow-up request\n List<HttpRequest> httpRequests = spyingHttpClient.requests();\n assertThat(httpRequests).hasSize(2);\n assertThat(httpRequests.get(1).body())\n .contains(jsonify(aiMessage1.text()))\n .doesNotContain(jsonify(aiMessage1.thinking()));\n }\n\n @ParameterizedTest\n @NullSource\n @ValueSource(booleans = false)\n void should_NOT_return_thinking(Boolean returnThinking) {\n\n // given\n StreamingChatModel model = OpenAiStreamingChatModel.builder()\n .baseUrl(\"https://api.deepseek.com/v1\")\n .apiKey(System.getenv(\"DEEPSEEK_API_KEY\"))\n .modelName(\"deepseek-reasoner\")\n\n .returnThinking(returnThinking)\n\n .logRequests(true)\n .logResponses(true)\n .build();\n\n String userMessage = \"What is the capital of Germany?\";\n\n // when\n TestStreamingChatResponseHandler spyHandler = spy(new TestStreamingChatResponseHandler());\n model.chat(userMessage, spyHandler);\n\n // then\n AiMessage aiMessage = spyHandler.get().aiMessage();\n assertThat(aiMessage.text()).containsIgnoringCase(\"Berlin\");\n assertThat(aiMessage.thinking()).isNull();\n\n verify(spyHandler, never()).onPartialThinking(any());\n }\n}\n"}}, "langchain4j-open-ai/src/test/java/dev/langchain4j/model/openai/compatible/deepseek/OpenAiStreamingChatModelDeepSeekIT.java": {"fcde6ad6baf89c5dea8e76999999aef66e26f037": {"t": 1785745696.8121529, "n": 2950, "h": "3db5c6975e9a2e62", "c": "package dev.langchain4j.model.openai.compatible.deepseek;\n\nimport dev.langchain4j.data.message.AiMessage;\nimport dev.langchain4j.data.message.UserMessage;\nimport dev.langchain4j.model.chat.StreamingChatModel;\nimport dev.langchain4j.model.chat.common.AbstractStreamingChatModelIT;\nimport dev.langchain4j.model.chat.common.ChatResponseAndStreamingMetadata;\nimport dev.langchain4j.model.chat.common.StreamingMetadata;\nimport dev.langchain4j.model.chat.request.ChatRequest;\nimport dev.langchain4j.model.chat.response.ChatResponse;\nimport dev.langchain4j.model.chat.response.ChatResponseMetadata;\nimport dev.langchain4j.model.openai.OpenAiStreamingChatModel;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;\n\nimport static dev.langchain4j.model.output.FinishReason.STOP;\nimport static org.assertj.core.api.Assertions.assertThat;\n\n@EnabledIfEnvironmentVariable(named = \"DEEPSEEK_API_KEY\", matches = \".+\")\nclass OpenAiStreamingChatModelDeepSeekIT { // TODO extends AbstractStreamingChatModelIT\n\n @Test\n protected void should_respect_user_message() {\n\n // given\n StreamingChatModel model = OpenAiStreamingChatModel.builder()\n .baseUrl(\"https://api.deepseek.com/v1\")\n .apiKey(System.getenv(\"DEEPSEEK_API_KEY\"))\n .modelName(\"deepseek-chat\")\n .logRequests(false) // base64-encoded images are huge in logs\n .logResponses(true)\n .build();\n\n ChatRequest chatRequest = ChatRequest.builder()\n .messages(UserMessage.from(\"What is the capital of Germany?\"))\n .build();\n\n // when\n ChatResponseAndStreamingMetadata chatResponseAndStreamingMetadata =\n AbstractStreamingChatModelIT.chat(model, chatRequest, ignored -> {\n }, 60, true);\n ChatResponse chatResponse = chatResponseAndStreamingMetadata.chatResponse();\n\n // then\n AiMessage aiMessage = chatResponse.aiMessage();\n assertThat(aiMessage.text()).containsIgnoringCase(\"Berlin\");\n assertThat(aiMessage.toolExecutionRequests()).isEmpty();\n\n ChatResponseMetadata chatResponseMetadata = chatResponse.metadata();\n assertThat(chatResponseMetadata.id()).isNotBlank();\n assertThat(chatResponseMetadata.modelName()).isNotBlank();\n assertThat(chatResponseMetadata.finishReason()).isEqualTo(STOP);\n\n StreamingMetadata streamingMetadata = chatResponseAndStreamingMetadata.streamingMetadata();\n assertThat(streamingMetadata.concatenatedPartialResponses()).isEqualTo(aiMessage.text());\n assertThat(streamingMetadata.timesOnPartialResponseWasCalled()).isGreaterThan(1);\n assertThat(streamingMetadata.partialToolCalls()).isEmpty();\n assertThat(streamingMetadata.completeToolCalls()).isEmpty();\n assertThat(streamingMetadata.timesOnCompleteResponseWasCalled()).isEqualTo(1);\n }\n}\n"}, "3b6e111fcd6040d622e89fee8b7e741984699f48": {"t": 1785755650.042161, "n": 2950, "h": "3db5c6975e9a2e62", "c": "package dev.langchain4j.model.openai.compatible.deepseek;\n\nimport dev.langchain4j.data.message.AiMessage;\nimport dev.langchain4j.data.message.UserMessage;\nimport dev.langchain4j.model.chat.StreamingChatModel;\nimport dev.langchain4j.model.chat.common.AbstractStreamingChatModelIT;\nimport dev.langchain4j.model.chat.common.ChatResponseAndStreamingMetadata;\nimport dev.langchain4j.model.chat.common.StreamingMetadata;\nimport dev.langchain4j.model.chat.request.ChatRequest;\nimport dev.langchain4j.model.chat.response.ChatResponse;\nimport dev.langchain4j.model.chat.response.ChatResponseMetadata;\nimport dev.langchain4j.model.openai.OpenAiStreamingChatModel;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;\n\nimport static dev.langchain4j.model.output.FinishReason.STOP;\nimport static org.assertj.core.api.Assertions.assertThat;\n\n@EnabledIfEnvironmentVariable(named = \"DEEPSEEK_API_KEY\", matches = \".+\")\nclass OpenAiStreamingChatModelDeepSeekIT { // TODO extends AbstractStreamingChatModelIT\n\n @Test\n protected void should_respect_user_message() {\n\n // given\n StreamingChatModel model = OpenAiStreamingChatModel.builder()\n .baseUrl(\"https://api.deepseek.com/v1\")\n .apiKey(System.getenv(\"DEEPSEEK_API_KEY\"))\n .modelName(\"deepseek-chat\")\n .logRequests(false) // base64-encoded images are huge in logs\n .logResponses(true)\n .build();\n\n ChatRequest chatRequest = ChatRequest.builder()\n .messages(UserMessage.from(\"What is the capital of Germany?\"))\n .build();\n\n // when\n ChatResponseAndStreamingMetadata chatResponseAndStreamingMetadata =\n AbstractStreamingChatModelIT.chat(model, chatRequest, ignored -> {\n }, 60, true);\n ChatResponse chatResponse = chatResponseAndStreamingMetadata.chatResponse();\n\n // then\n AiMessage aiMessage = chatResponse.aiMessage();\n assertThat(aiMessage.text()).containsIgnoringCase(\"Berlin\");\n assertThat(aiMessage.toolExecutionRequests()).isEmpty();\n\n ChatResponseMetadata chatResponseMetadata = chatResponse.metadata();\n assertThat(chatResponseMetadata.id()).isNotBlank();\n assertThat(chatResponseMetadata.modelName()).isNotBlank();\n assertThat(chatResponseMetadata.finishReason()).isEqualTo(STOP);\n\n StreamingMetadata streamingMetadata = chatResponseAndStreamingMetadata.streamingMetadata();\n assertThat(streamingMetadata.concatenatedPartialResponses()).isEqualTo(aiMessage.text());\n assertThat(streamingMetadata.timesOnPartialResponseWasCalled()).isGreaterThan(1);\n assertThat(streamingMetadata.partialToolCalls()).isEmpty();\n assertThat(streamingMetadata.completeToolCalls()).isEmpty();\n assertThat(streamingMetadata.timesOnCompleteResponseWasCalled()).isEqualTo(1);\n }\n}\n"}}}} |