Skip to content

Commit 9998a1c

Browse files
committed
Implement upstream LC4j auditing capabilities
Fixes #1844
1 parent 37a459a commit 9998a1c

File tree

13 files changed

+1434
-243
lines changed

13 files changed

+1434
-243
lines changed
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package io.quarkiverse.langchain4j.observability;
2+
3+
import dev.langchain4j.observability.api.event.AiServiceCompletedEvent;
4+
import dev.langchain4j.observability.api.event.AiServiceErrorEvent;
5+
import dev.langchain4j.observability.api.event.AiServiceEvent;
6+
import dev.langchain4j.observability.api.event.AiServiceResponseReceivedEvent;
7+
import dev.langchain4j.observability.api.event.AiServiceStartedEvent;
8+
import dev.langchain4j.observability.api.event.InputGuardrailExecutedEvent;
9+
import dev.langchain4j.observability.api.event.OutputGuardrailExecutedEvent;
10+
import dev.langchain4j.observability.api.event.ToolExecutedEvent;
11+
import io.quarkiverse.langchain4j.observability.listener.AiServiceListenerAdapter;
12+
13+
/**
14+
* Enum representing various events that can occur in an AI service. Each event type
15+
* is associated with a specific event class that extends {@link AiServiceEvent}.
16+
*/
17+
public enum AiServiceEvents {
18+
COMPLETED(AiServiceCompletedEvent.class),
19+
ERROR(AiServiceErrorEvent.class),
20+
INPUT_GUARDRAIL_EXECUTED(InputGuardrailExecutedEvent.class),
21+
OUTPUT_GUARDRAIL_EXECUTED(OutputGuardrailExecutedEvent.class),
22+
RESPONSE_RECEIVED(AiServiceResponseReceivedEvent.class),
23+
STARTED(AiServiceStartedEvent.class),
24+
TOOL_EXECUTED(ToolExecutedEvent.class);
25+
26+
private final Class<? extends AiServiceEvent> eventClass;
27+
28+
AiServiceEvents(Class<? extends AiServiceEvent> eventClass) {
29+
this.eventClass = eventClass;
30+
}
31+
32+
public <T extends AiServiceEvent> AiServiceListenerAdapter<T> createListener(Class<?> aiServiceClass) {
33+
return new AiServiceListenerAdapter<>(getEventClass(), aiServiceClass);
34+
}
35+
36+
public <T extends AiServiceEvent> Class<T> getEventClass() {
37+
return (Class<T>) this.eventClass;
38+
}
39+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package io.quarkiverse.langchain4j.observability;
2+
3+
import static java.lang.annotation.ElementType.FIELD;
4+
import static java.lang.annotation.ElementType.METHOD;
5+
import static java.lang.annotation.ElementType.PARAMETER;
6+
import static java.lang.annotation.ElementType.TYPE;
7+
import static java.lang.annotation.RetentionPolicy.RUNTIME;
8+
9+
import java.lang.annotation.Retention;
10+
import java.lang.annotation.Target;
11+
12+
import jakarta.inject.Qualifier;
13+
14+
/**
15+
* Annotation used to select a particular AiService interface. The interface specified in {@link #value()} must be annotated
16+
* with
17+
* {@link io.quarkiverse.langchain4j.RegisterAiService RegisterAiService}.
18+
*/
19+
@Qualifier
20+
@Target({ METHOD, FIELD, PARAMETER, TYPE })
21+
@Retention(RUNTIME)
22+
public @interface AiServiceSelector {
23+
/**
24+
* Specifies the interface class annotated with {@link io.quarkiverse.langchain4j.RegisterAiService RegisterAiService}.
25+
*/
26+
Class<?> value();
27+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package io.quarkiverse.langchain4j.observability;
2+
3+
import static dev.langchain4j.internal.ValidationUtils.ensureNotNull;
4+
5+
import jakarta.enterprise.util.AnnotationLiteral;
6+
7+
/**
8+
* A concrete implementation of {@link AiServiceSelector} and {@link AnnotationLiteral}, used to provide
9+
* a runtime version of the {@link AiServiceSelector} annotation. This class is primarily used as a literal
10+
* for contextual injection where an {@link AiServiceSelector} annotation is required, allowing the
11+
* specification of the AI service class at runtime.
12+
*/
13+
public class AiServiceSelectorLiteral extends AnnotationLiteral<AiServiceSelector> implements AiServiceSelector {
14+
private final Class<?> aiServiceClass;
15+
16+
protected AiServiceSelectorLiteral(Builder builder) {
17+
this.aiServiceClass = ensureNotNull(builder.aiServiceClass, "aiServiceClass");
18+
}
19+
20+
@Override
21+
public Class<?> value() {
22+
return aiServiceClass();
23+
}
24+
25+
/**
26+
* Retrieves the AI service class specified for this instance.
27+
*
28+
* @return the class object representing the AI service interface associated with this instance
29+
*/
30+
public Class<?> aiServiceClass() {
31+
return this.aiServiceClass;
32+
}
33+
34+
/**
35+
* Creates a new {@link Builder} initialized with the current state of this instance.
36+
*
37+
* @return a {@link Builder} instance pre-configured with the properties of this {@code AiServiceSelector}
38+
*/
39+
public Builder toBuilder() {
40+
return new Builder(this);
41+
}
42+
43+
/**
44+
* Creates and returns a new instance of the {@link Builder} class, which can be used
45+
* to construct an {@link AiServiceSelector} instance with specified properties.
46+
*
47+
* @return a new {@link Builder} instance
48+
*/
49+
public static Builder builder() {
50+
return new Builder();
51+
}
52+
53+
/**
54+
* Builder class for constructing instances of {@link AiServiceSelector}.
55+
* Provides a fluent API to configure its properties.
56+
*/
57+
public static class Builder {
58+
private Class<?> aiServiceClass;
59+
60+
private Builder() {
61+
}
62+
63+
private Builder(AiServiceSelector aiServiceSelector) {
64+
this.aiServiceClass = aiServiceSelector.value();
65+
}
66+
67+
/**
68+
* Sets the AI service class to be used in the construction of the {@link AiServiceSelector}.
69+
*
70+
* @param aiServiceClass the class object representing the AI service interface
71+
* @return the {@link Builder} instance for method chaining
72+
*/
73+
public Builder aiServiceClass(Class<?> aiServiceClass) {
74+
this.aiServiceClass = aiServiceClass;
75+
return this;
76+
}
77+
78+
/**
79+
* Constructs and returns a new instance of {@link AiServiceSelector}.
80+
* The returned instance represents a runtime version of the {@link AiServiceSelector} annotation,
81+
* based on the properties configured in the {@link Builder}.
82+
*
83+
* @return a new {@link AiServiceSelector} instance constructed with the current state of the {@link Builder}
84+
*/
85+
public AiServiceSelector build() {
86+
return new AiServiceSelectorLiteral(this);
87+
}
88+
}
89+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package io.quarkiverse.langchain4j.observability.listener;
2+
3+
import dev.langchain4j.observability.api.event.AiServiceEvent;
4+
import dev.langchain4j.observability.api.listener.AiServiceListener;
5+
import io.quarkiverse.langchain4j.observability.AiServiceSelectorLiteral;
6+
import io.quarkus.arc.Arc;
7+
8+
/**
9+
* An adapter class for implementing the {@link AiServiceListener} interface to CDI event firing.
10+
*
11+
* @param <T> the type of {@link AiServiceEvent} being observed, which extends {@link AiServiceEvent}
12+
*/
13+
public record AiServiceListenerAdapter<T extends AiServiceEvent>(Class<T> eventClass,
14+
Class<?> aiServiceClass) implements AiServiceListener<T> {
15+
@Override
16+
public Class<T> getEventClass() {
17+
return this.eventClass;
18+
}
19+
20+
@Override
21+
public void onEvent(T event) {
22+
Arc.container()
23+
.beanManager()
24+
.getEvent()
25+
.select(
26+
getEventClass(),
27+
AiServiceSelectorLiteral.builder()
28+
.aiServiceClass(this.aiServiceClass)
29+
.build())
30+
.fire(event);
31+
}
32+
}

core/runtime/src/main/java/io/quarkiverse/langchain4j/runtime/AiServicesRecorder.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import java.lang.reflect.InvocationTargetException;
66
import java.util.ArrayList;
7+
import java.util.Arrays;
78
import java.util.HashMap;
89
import java.util.List;
910
import java.util.Map;
@@ -23,6 +24,7 @@
2324
import dev.langchain4j.service.tool.ToolProvider;
2425
import io.quarkiverse.langchain4j.ModelName;
2526
import io.quarkiverse.langchain4j.RegisterAiService;
27+
import io.quarkiverse.langchain4j.observability.AiServiceEvents;
2628
import io.quarkiverse.langchain4j.runtime.aiservice.AiServiceClassCreateInfo;
2729
import io.quarkiverse.langchain4j.runtime.aiservice.AiServiceMethodCreateInfo;
2830
import io.quarkiverse.langchain4j.runtime.aiservice.ChatMemorySeeder;
@@ -123,6 +125,11 @@ public QuarkusAiServiceContext apply(SyntheticCreationalContext<QuarkusAiService
123125
// properly populates QuarkusAiServiceContext which is what we are trying to construct
124126
var quarkusAiServices = INSTANCE.create(aiServiceContext);
125127

128+
// Populate the AI service listeners
129+
Arrays.stream(AiServiceEvents.values())
130+
.map(event -> event.createListener(serviceClass))
131+
.forEach(quarkusAiServices::registerListener);
132+
126133
if (info.languageModelSupplierClassName() != null
127134
|| info.streamingChatLanguageModelSupplierClassName() != null) {
128135
if (info.languageModelSupplierClassName() != null) {

0 commit comments

Comments
 (0)