Class AiServiceCapture
import static dev.langchain4j.test.LangChain4jAssertions.assertThat;
import static dev.langchain4j.test.service.AiServiceCapture.capture;
AiServiceCapture capture = capture(() -> assistant.chat("Cancel booking MS-777"));
assertThat(capture)
.hasToolExecutedWith("cancelBooking", Map.of("bookingNumber", "MS-777"))
.hasNoFailedToolExecutions();
assertThat(capture.outputText()).contains("cancelled");
output() and outputText() are what the AI Service produced, whatever the lambda returned: for
example the object of a method with structured output (also when the method returns it in a
CompletableFuture, a Mono or a Uni), or the text of the final response of a streaming method:
AiServiceCapture capture = capture(() -> assistant.extractBookingAsync("..."));
Booking booking = capture.output(Booking.class);
lambdaOutput() is what the lambda returned, which can be something else, for example the result of a
controller called with MockMvc.
The top-level invocations (see invocations()) are those not started by another AI Service
invocation, in the order they started: the AI Services that the lambda called, directly or through your code, and
the AI Services that a framework uses internally, for example the planner of an agentic supervisor. The invocations
started by their work (for example by their tools, guardrails or content retrievers) are nested in them (see
AiServiceInvocation.nestedInvocations()): an AI Service called by a tool is found there, not in
invocations(). The shortcuts of this class, such as output(), outputText() and
lastRequest(), are those of the only top-level invocation.
Streaming and asynchronous methods
capture(...) returns when every captured invocation has finished, so streaming and asynchronous AI Service
methods need little extra code:
- a
TokenStreammust be started in the lambda: set the handlers it needs and callstart(), for examplecapture(() -> assistant.chatStream("Hello").ignoreErrors().start()). If the lambda returns aTokenStreamthat was not started,capture(...)fails; - a
Flow.Publisherreturned by the lambda (for example a MutinyMulti), or a Reactive Streams publisher (for example a ReactorFlux), is subscribed to and read to the end, and its items are captured (seelambdaOutputItems()). Do not return a publisher that the lambda already subscribed to: it would be subscribed to a second time, which may call the model again or fail; - a
CompletableFuture(or otherCompletionStage) returned by the lambda does not need to be waited for; - a Mutiny
Unireturned by the lambda is subscribed to and waited for.
AiServiceCapture capture = capture(() -> assistant.chatStream("Hello").ignoreErrors().start());
String answer = capture.outputText();
While an invocation is still running, a warning naming it is logged every 30 seconds. After 10 minutes,
capture(...) cancels the future or publisher returned by the lambda and fails, naming the invocations that
have not finished. Change this timeout with the JVM system property
langchain4j.test.capture.timeout-millis (in milliseconds, 0 for no timeout). capture(...)
also stops waiting when its thread is interrupted, for example by JUnit's @Timeout, and then cancels the
future or publisher returned by the lambda too. Call capture(...) on a thread that
may block, not on an event loop. If the future or publisher returned by the lambda has ended, but an invocation
still has not reported its end 5 seconds later, capture(...) fails, because some frameworks do not report
it in every case. If a TokenStream was created but is not started within 2 seconds, capture(...)
fails, because nothing would ever start it. Both waits can be changed, for example on a slow build machine, with
the JVM system properties langchain4j.test.capture.end-report-grace-millis and
langchain4j.test.capture.stream-start-grace-millis (in milliseconds), for example with -D... or the
systemPropertyVariables of the Maven Surefire plugin. These properties, and the timeout above, are not
Spring or Quarkus configuration properties: setting them in application.properties has no effect.
Failures
If an AI Service invocation fails,capture(...) throws its error once all invocations have finished, also
when the error was passed to an onError handler or to a returned CompletableFuture, or was caught by
the lambda. Any other exception thrown by the lambda is rethrown too, for example an AssertionError thrown
in a mock model, or by an assertion in the lambda (such as mockMvc.perform(...).andExpect(status().isOk())).
An Error is rethrown even when a failure is expected. An exception or AssertionError thrown by a
tool can be different: whether it fails the invocation or is sent to the model as the result of the tool depends on
the kind of AI Service method and on its tool execution error handler (see
AiServices.toolExecutionErrorHandler(ToolExecutionErrorHandler)). When it is sent to the model, it is not
thrown: check it with AiServiceInvocationAssert.hasToolExecutionFailed(String) or
AiServiceInvocationAssert.hasNoFailedToolExecutions(). A failed invocation makes capture(...) throw
even when the code recovered from it, for example an agentic workflow whose error handler retries the agent or
returns a fallback result. To check a failure that the test expects, use captureExpectingFailure(Callable):
AiServiceCapture capture = captureExpectingFailure(() -> assistant.chat("Cancel MS-777"));
assertThat(capture)
.hasFailedWithMessageContaining("Booking MS-777 not found")
.hasToolExecutionFailed("getBookingDetails");
A cancelled invocation does not make capture(...) throw, but every assertion on it fails, and
AiServiceInvocation.output() and AiServiceInvocation.outputText() throw. Invocations started by
another invocation (see AiServiceInvocation.nestedInvocations()) do not make capture(...) throw when
they fail, because the invocation that started them can handle their failure, for example a tool that reports the
error to the model. This relies on the AI Service reporting which invocation started another one (see
InvocationContext.parentInvocationId()).
Pay attention to what the invocation captured rather than to the side effects of your own handlers: for a
TokenStream, the invocation finishes before the onCompleteResponse handler you gave it has run, so
when capture(...) returns, that handler may still be running. AiServiceInvocation.outputText() has
the final text in any case.
Threads
An invocation is captured when it starts on the thread that runs the lambda, or when it is started while a captured invocation runs, for example by its tool, guardrail or content retriever (seeAiServiceInvocation.nestedInvocations()). Invocations started on other threads are not captured: for
example by an HTTP request to a running server, by an @Async method, by subscribeOn(...), by a
parallel stream, by an executor, or the agents of an agentic workflow that run on other threads, such as the agents
of a parallel workflow. Captures on different threads do not see each other's invocations, so tests can run in
parallel. langchain4j-test listens to AI Services with listeners that ignore all events while nothing is
being captured.
capture(...) blocks until the captured invocations have finished, also when the lambda returns a
CompletableFuture or a stream. So call it from the test thread, never from an event-loop thread (for
example on a Vert.x context): an invocation that needs that thread to finish could then not finish.
-
Nested Class Summary
Nested ClassesModifier and TypeClassDescriptionstatic interfaceA lambda to capture that returns nothing. -
Method Summary
Modifier and TypeMethodDescriptionstatic AiServiceCaptureRuns the given lambda and captures the AI Service invocations it starts, then waits until all of them have finished.static AiServiceCaptureRuns the given lambda and captures the AI Service invocations it starts.static AiServiceCaptureLikecapture(ThrowingRunnable), for a lambda that is expected to make an AI Service invocation fail: the failed invocation is captured instead of its error being thrown, so that what happened before the failure can be checked.static AiServiceCapturecaptureExpectingFailure(Callable<?> code) Likecapture(Callable), for a lambda that is expected to make an AI Service invocation fail: the failed invocation is captured instead of its error being thrown, so that what happened before the failure can be checked.Returns the first request sent to the chat model by the only top-level AI Service invocation.Returns the only top-level AI Service invocation (seeinvocations()).invocation(Class<?> aiService) Returns the only top-level AI Service invocation of the given AI Service interface (seeinvocations()).Returns the top-level AI Service invocations: those not started by another captured invocation, in the order they started.invocations(Class<?> aiService) Returns the top-level AI Service invocations of the given AI Service interface (seeinvocations()), in the order they started.Returns what the lambda returned.<T> TlambdaOutput(Class<T> type) Returns what the lambda returned, as the given type.Returns the items of the stream returned by the lambda, in the order they arrived. capture(...) subscribes to a returned publisher (such as aFlux, aMultior aFlow.Publisher) and keeps every item it receives, whatever its type: for example theStrings streamed by an AI Service method, or the server-sent events that a controller built from them.Returns the last request sent to the chat model by the only top-level AI Service invocation.Returns the last response of the chat model to the only top-level AI Service invocation.output()Returns what the only top-level AI Service invocation produced (seeAiServiceInvocation.output()): for example theStringanswer, the object of a method with structured output, the value inside aResult, aCompletableFuture, aMonoor aUni, or the text of the final response of a streaming method.<T> TReturns what a top-level AI Service invocation produced, as the given type: the output of the only top-level invocation whose output is of this type (seeAiServiceInvocation.output()).Returns the text produced by the only top-level AI Service invocation: the answer of a method that returns aString(also inside aResultor aCompletableFuture), or the text of the final response of a streaming method.toString()
-
Method Details
-
capture
Runs the given lambda and captures the AI Service invocations it starts. If the lambda returns a publisher, a future or aUni, it is subscribed to. Then waits until all captured invocations have finished.- Parameters:
code- the code to run, for example() -> assistant.chat("Hello")- Returns:
- the captured invocations, and what the lambda returned
-
capture
Runs the given lambda and captures the AI Service invocations it starts, then waits until all of them have finished.- Parameters:
code- the code to run, for example() -> assistant.chatStream("Hi").onPartialResponse(tokens::add).onError(errors::add).start()- Returns:
- the captured invocations
-
captureExpectingFailure
Likecapture(Callable), for a lambda that is expected to make an AI Service invocation fail: the failed invocation is captured instead of its error being thrown, so that what happened before the failure can be checked.- Parameters:
code- the code to run, for example() -> assistant.chat("Cancel booking MS-777")- Returns:
- the captured invocations, and what the lambda returned
- Throws:
AssertionError- if no AI Service invocation started by the lambda failed, also when nothing was captured while the lambda threw an exception (the exception is then the cause)
-
captureExpectingFailure
Likecapture(ThrowingRunnable), for a lambda that is expected to make an AI Service invocation fail: the failed invocation is captured instead of its error being thrown, so that what happened before the failure can be checked.- Parameters:
code- the code to run, for example() -> assistant.chatStream("Hi").onPartialResponse(tokens::add).onError(errors::add).start()- Returns:
- the captured invocations
- Throws:
AssertionError- if no AI Service invocation started by the lambda failed, also when nothing was captured while the lambda threw an exception (the exception is then the cause)
-
output
Returns what the only top-level AI Service invocation produced (seeAiServiceInvocation.output()): for example theStringanswer, the object of a method with structured output, the value inside aResult, aCompletableFuture, aMonoor aUni, or the text of the final response of a streaming method. It does not depend on what the lambda returned (seelambdaOutput()for that).
UseassertThat(capture.output()).isEqualTo(new Booking("MS-777"));output(Class)to get it as a given type, also when the lambda started several invocations.- Throws:
AssertionError- if no top-level invocation or more than one was captured, or if the invocation failed or was cancelled
-
output
Returns what a top-level AI Service invocation produced, as the given type: the output of the only top-level invocation whose output is of this type (seeAiServiceInvocation.output()). This works also when the lambda started several invocations, as long as only one of them produced a value of this type:
Invocations that failed or were cancelled produced nothing, so they never match.// the lambda extracts a Booking with one AI Service, then creates a Client with another one Booking booking = capture.output(Booking.class); Client client = capture.output(Client.class);- Type Parameters:
T- the type of the output- Parameters:
type- the type of the output, for exampleBooking.classorString.class- Throws:
AssertionError- if no top-level invocation or more than one produced a value of the given type
-
outputText
Returns the text produced by the only top-level AI Service invocation: the answer of a method that returns aString(also inside aResultor aCompletableFuture), or the text of the final response of a streaming method. A shortcut forinvocation().outputText(), seeAiServiceInvocation.outputText().- Throws:
AssertionError- if no top-level invocation or more than one was captured, if the invocation failed or was cancelled, or if the AI Service method does not produce text (seeAiServiceInvocation.outputText())
-
lambdaOutput
Returns what the lambda returned. When the lambda returns the result of an AI Service method, this is the same asoutput(), except for a method that returns aResult, a stream or an asynchronous value (such as aTokenStream, aFlux, aCompletableFutureor aUni): then this is thatResult(whose content isoutput()), or that stream or value, which has already been started and has ended. Do not start or subscribe to it again: this would call the model again, or fail. When the lambda calls something else, for example a controller withMockMvc, this is what that call returned:AiServiceCapture capture = AiServiceCapture.capture(() -> mockMvc.perform(get("/chat?message=Hi")).andReturn()); MvcResult mvcResult = capture.lambdaOutput(MvcResult.class);- Throws:
AssertionError- if the lambda returns nothing (for example() -> assistant.handle("...")with avoidmethod), or if it threw an exception (possible withcaptureExpectingFailure(Callable))
-
lambdaOutput
Returns what the lambda returned, as the given type. SeelambdaOutput().- Type Parameters:
T- the type of the value returned by the lambda- Parameters:
type- the type of the value returned by the lambda, for exampleMvcResult.class- Throws:
AssertionError- if the lambda threw an exception, or returnednullor a value of another type
-
lambdaOutputItems
Returns the items of the stream returned by the lambda, in the order they arrived. capture(...) subscribes to a returned publisher (such as aFlux, aMultior aFlow.Publisher) and keeps every item it receives, whatever its type: for example theStrings streamed by an AI Service method, or the server-sent events that a controller built from them.AiServiceCapture capture = AiServiceCapture.capture(() -> streamingController.chat("Hi")); assertThat(capture.lambdaOutputItems()).hasSizeGreaterThan(1);- Throws:
AssertionError- if the lambda did not return a stream, for example when it returned aTokenStream, aMonoor a value, or when it threw an exception
-
invocation
Returns the only top-level AI Service invocation (seeinvocations()).- Throws:
AssertionError- if no top-level invocation or more than one was captured: useinvocations()then
-
invocations
Returns the top-level AI Service invocations: those not started by another captured invocation, in the order they started. Empty if none was captured. The invocations started by their work (for example an AI Service called by a tool) are nested in them, at any depth: seeAiServiceInvocation.nestedInvocations(). -
invocation
Returns the only top-level AI Service invocation of the given AI Service interface (seeinvocations()). Useful when the lambda starts several invocations, for example an agentic workflow or supervisor, which calls each of its agents as a separate top-level invocation:AiServiceCapture capture = capture(() -> supervisor.invoke("Book a flight")); assertThat(capture.invocation(BookingAgent.class)).hasToolExecuted("bookFlight");- Parameters:
aiService- the AI Service interface, for exampleBookingAgent.class- Throws:
AssertionError- if no top-level invocation of this interface or more than one was captured: useinvocations(Class)then
-
invocations
Returns the top-level AI Service invocations of the given AI Service interface (seeinvocations()), in the order they started. Empty if none was captured.- Parameters:
aiService- the AI Service interface, for exampleBookingAgent.class
-
firstRequest
Returns the first request sent to the chat model by the only top-level AI Service invocation. A shortcut forinvocation().firstRequest(), seeAiServiceInvocation.firstRequest().assertThat(capture.firstRequest()).hasSystemMessageContaining("support agent");- Throws:
AssertionError- if no top-level invocation or more than one was captured, or if the model did not respond to any request of the invocation
-
lastRequest
Returns the last request sent to the chat model by the only top-level AI Service invocation. A shortcut forinvocation().lastRequest(), seeAiServiceInvocation.lastRequest(). Its messages are the conversation that led to the last response, which can be given to a judge:Judge.from(judgeModel) .assertThat(capture.outputText()) .given(capture.lastRequest().messages()) .satisfies("only uses facts from the tool results");- Throws:
AssertionError- if no top-level invocation or more than one was captured, or if the model did not respond to any request of the invocation
-
lastResponse
Returns the last response of the chat model to the only top-level AI Service invocation. A shortcut forinvocation().lastResponse(), seeAiServiceInvocation.lastResponse().- Throws:
AssertionError- if no top-level invocation or more than one was captured, or if the model did not respond
-
toString
-