Skip to content

Commit beaa244

Browse files
authored
QQL GenAi improvements (#25)
* QQL AI generation improvements * GenAi: fix launch when genai is disabled * GenAi: fix merge conflicts * GenAi: add genai configuration docs
1 parent 9a55954 commit beaa244

File tree

73 files changed

+7940
-14
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

73 files changed

+7940
-14
lines changed

build.gradle

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,10 @@ configure(javaProjects) {
194194
// entry 'jackson-module-parameter-names'
195195
// }
196196

197-
dependency 'com.azure:azure-security-keyvault-secrets:4.8.4'
197+
dependency 'com.azure:azure-core:1.55.3'
198+
dependency 'com.azure:azure-core-http-netty:1.15.11'
199+
dependency 'com.azure:azure-identity:1.15.4'
200+
dependency 'com.azure:azure-security-keyvault-secrets:4.9.4'
198201
dependency 'net.java.dev.jna:jna-platform:5.6.0' // azure sdk requires jna 5.6.0
199202
dependency 'com.microsoft.azure:msal4j:1.15.1'
200203

@@ -284,6 +287,11 @@ configure(javaProjects) {
284287

285288
dependency 'org.apache.bcel:bcel:6.6.0'
286289

290+
// AI integration
291+
dependency 'dev.langchain4j:langchain4j:1.7.1'
292+
dependency 'dev.langchain4j:langchain4j-azure-open-ai-spring-boot-starter:1.7.1-beta14'
293+
dependency 'dev.langchain4j:langchain4j-spring-boot-starter:1.7.1-beta14'
294+
287295
dependency 'org.reactivestreams:reactive-streams:1.0.4'
288296

289297
dependencySet(group: 'io.netty', version: "$nettyVersion") {

java/ws-server/build.gradle

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,10 @@ dependencies {
8080

8181
// implementation 'deltix:deltix-spring-api-keys'
8282

83-
// implementation 'io.reactivex.rxjava2:rxjava'
84-
// implementation 'deltix:deltix-timebase-api-rx'
85-
// implementation 'deltix:transformation-api'
83+
// AI integration
84+
implementation 'dev.langchain4j:langchain4j'
85+
implementation 'dev.langchain4j:langchain4j-azure-open-ai-spring-boot-starter'
86+
implementation 'dev.langchain4j:langchain4j-spring-boot-starter'
8687

8788
compileOnly 'com.webcohesion.enunciate:enunciate-core-annotations'
8889

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
/*
2+
* Copyright 2025 EPAM Systems, Inc
3+
*
4+
* See the NOTICE file distributed with this work for additional information
5+
* regarding copyright ownership. Licensed under the Apache License,
6+
* Version 2.0 (the "License"); you may not use this file except in compliance
7+
* with the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14+
* License for the specific language governing permissions and limitations under
15+
* the License.
16+
*/
17+
package com.epam.deltix.tbwg.webapp.config;
18+
19+
import com.epam.deltix.tbwg.webapp.services.genai.GenAiHelperService;
20+
import com.epam.deltix.tbwg.webapp.services.genai.models.*;
21+
import com.epam.deltix.tbwg.webapp.services.genai.docsloader.MarkdownCodeAwareSplitter;
22+
import com.epam.deltix.tbwg.webapp.services.genai.docsloader.MarkdownDocsLoader;
23+
import com.epam.deltix.tbwg.webapp.settings.AiApiSettings;
24+
import dev.langchain4j.data.document.Document;
25+
import dev.langchain4j.data.document.DocumentSplitter;
26+
import dev.langchain4j.data.segment.TextSegment;
27+
import dev.langchain4j.model.chat.ChatModel;
28+
import dev.langchain4j.model.chat.StreamingChatModel;
29+
import dev.langchain4j.model.embedding.EmbeddingModel;
30+
import dev.langchain4j.rag.content.retriever.ContentRetriever;
31+
import dev.langchain4j.rag.content.retriever.EmbeddingStoreContentRetriever;
32+
import dev.langchain4j.service.AiServices;
33+
import dev.langchain4j.store.embedding.EmbeddingStore;
34+
import dev.langchain4j.store.embedding.EmbeddingStoreIngestor;
35+
import dev.langchain4j.store.embedding.inmemory.InMemoryEmbeddingStore;
36+
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
37+
import org.springframework.context.annotation.Bean;
38+
import org.springframework.context.annotation.Configuration;
39+
import org.springframework.core.io.Resource;
40+
import org.springframework.core.io.ResourceLoader;
41+
import org.springframework.core.io.support.ResourcePatternResolver;
42+
43+
import java.io.IOException;
44+
import java.io.InputStream;
45+
import java.nio.file.Files;
46+
import java.nio.file.StandardCopyOption;
47+
import java.util.List;
48+
49+
@Configuration
50+
@ConditionalOnBean(AiApiSettings.class)
51+
public class GenAiConfig {
52+
53+
private static final String CLASSPATH_EMB_PATH = "classpath:qql_gen/qql-docs-embeddings.json";
54+
private static final String FALLBACK_EMB_FILE = "qql-docs-embeddings.json";
55+
56+
@Bean
57+
public PerUserAzureChatModel perUserAzureChatModel(AiApiSettings settings,
58+
UserAiApiKeyProvider keyProvider) {
59+
return new PerUserAzureChatModel(settings.getEndpointUrl(),
60+
settings.getDeploymentName(),
61+
keyProvider);
62+
}
63+
64+
@Bean
65+
public PerUserAzureStreamingChatModel perUserAzureStreamingChatModel(AiApiSettings settings,
66+
UserAiApiKeyProvider keyProvider) {
67+
return new PerUserAzureStreamingChatModel(settings.getEndpointUrl(),
68+
settings.getDeploymentName(),
69+
keyProvider);
70+
}
71+
72+
@Bean
73+
public PerUserAzureEmbeddingModel perUserAzureEmbeddingModel(AiApiSettings settings,
74+
UserAiApiKeyProvider keyProvider) {
75+
return new PerUserAzureEmbeddingModel(settings.getEndpointUrl(),
76+
settings.getEmbeddingDeploymentName(),
77+
keyProvider);
78+
}
79+
80+
@Bean
81+
public GenAiHelperService genAiHelperService(ChatModel model, StreamingChatModel streamingModel) {
82+
return AiServices.builder(GenAiHelperService.class)
83+
.chatModel(model)
84+
.streamingChatModel(streamingModel)
85+
.build();
86+
}
87+
88+
@Bean
89+
public MarkdownDocsLoader markdownDocsLoader(ResourcePatternResolver resolver) {
90+
return new MarkdownDocsLoader(resolver);
91+
}
92+
93+
@Bean
94+
public List<Document> qqlAllSectionDocs(MarkdownDocsLoader loader) throws IOException {
95+
return loader.load();
96+
}
97+
98+
@Bean
99+
EmbeddingStore<TextSegment> embeddingStore(EmbeddingModel embeddingModel,
100+
MarkdownDocsLoader loader,
101+
ResourceLoader resourceLoader) throws IOException {
102+
Resource res = resourceLoader.getResource(CLASSPATH_EMB_PATH);
103+
if (res.exists()) {
104+
try (InputStream in = res.getInputStream()) {
105+
var tmp = Files.createTempFile("qql-docs-embeddings", ".json");
106+
Files.copy(in, tmp, StandardCopyOption.REPLACE_EXISTING);
107+
return InMemoryEmbeddingStore.fromFile(tmp.toString());
108+
}
109+
}
110+
111+
try {
112+
return InMemoryEmbeddingStore.fromFile(FALLBACK_EMB_FILE);
113+
} catch (RuntimeException ignore) {
114+
// Rebuild the embeddings
115+
}
116+
117+
InMemoryEmbeddingStore<TextSegment> store = new InMemoryEmbeddingStore<>();
118+
List<Document> documents = loader.load();
119+
if (documents.isEmpty())
120+
return store;
121+
DocumentSplitter splitter = MarkdownCodeAwareSplitter.defaultSplitter();
122+
EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder()
123+
.documentSplitter(splitter)
124+
.embeddingModel(embeddingModel)
125+
.embeddingStore(store)
126+
.build();
127+
documents.set(0, Document.from(
128+
PerUserChatMaker.wrapUserInput("regenerateEmbeddings",
129+
documents.get(0).text())));
130+
ingestor.ingest(documents);
131+
store.serializeToFile(FALLBACK_EMB_FILE);
132+
133+
return store;
134+
}
135+
136+
@Bean
137+
ContentRetriever contentRetriever(EmbeddingStore<TextSegment> embeddingStore,
138+
EmbeddingModel embeddingModel) {
139+
return EmbeddingStoreContentRetriever.builder()
140+
.embeddingStore(embeddingStore)
141+
.embeddingModel(embeddingModel)
142+
.maxResults(25)
143+
.minScore(0.55)
144+
.build();
145+
}
146+
}

java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/config/WebSocketConfig.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
7474
public static final String IMPORT_QSMSG_TOPIC = IMPORT_TOPIC + "/qsmsg";
7575
public static final String INIT_IMPORT_QSMSG_TOPIC = INIT_IMPORT_TOPIC + "/qsmsg";
7676

77+
public static final String GENAI_QQL_TOPIC = TOPIC + "/genai-qql";
78+
7779
public static final String SUBSCRIPTIONS_METRIC = "websocket.subscriptions";
7880

7981
public static final String SEND_MESSAGES_METRIC = "websocket.messages";
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* Copyright 2025 EPAM Systems, Inc
3+
*
4+
* See the NOTICE file distributed with this work for additional information
5+
* regarding copyright ownership. Licensed under the Apache License,
6+
* Version 2.0 (the "License"); you may not use this file except in compliance
7+
* with the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14+
* License for the specific language governing permissions and limitations under
15+
* the License.
16+
*/
17+
package com.epam.deltix.tbwg.webapp.controllers;
18+
19+
import com.epam.deltix.tbwg.webapp.config.WebSocketConfig;
20+
import com.epam.deltix.tbwg.webapp.services.genai.GenAiService;
21+
import com.epam.deltix.tbwg.webapp.websockets.subscription.Subscription;
22+
import com.epam.deltix.tbwg.webapp.websockets.subscription.SubscriptionChannel;
23+
import com.epam.deltix.tbwg.webapp.websockets.subscription.SubscriptionController;
24+
import com.epam.deltix.tbwg.webapp.websockets.subscription.SubscriptionControllerRegistry;
25+
import org.jetbrains.annotations.Nullable;
26+
import org.springframework.beans.factory.annotation.Autowired;
27+
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
28+
import org.springframework.stereotype.Controller;
29+
import org.springframework.web.bind.annotation.CrossOrigin;
30+
31+
import java.security.Principal;
32+
33+
@Controller
34+
@CrossOrigin
35+
public class GenAiController implements SubscriptionController {
36+
37+
private final @Nullable GenAiService genAiService;
38+
39+
public GenAiController(SubscriptionControllerRegistry registry,
40+
@Autowired(required = false) @Nullable GenAiService genAiService) {
41+
registry.register(WebSocketConfig.GENAI_QQL_TOPIC, this);
42+
this.genAiService = genAiService;
43+
}
44+
45+
@Override
46+
public Subscription onSubscribe(SimpMessageHeaderAccessor headerAccessor, SubscriptionChannel channel) {
47+
if (genAiService == null) {
48+
channel.sendError("Gen AI service is disabled");
49+
return () -> {};
50+
}
51+
String userInput = headerAccessor.getFirstNativeHeader("userInput");
52+
String rawStreamKeys = headerAccessor.getFirstNativeHeader("streamKeys");
53+
if (userInput == null || userInput.isEmpty()) {
54+
channel.sendError(new IllegalArgumentException("userInput header is required"));
55+
return () -> {};
56+
}
57+
Principal user = headerAccessor.getUser();
58+
if (user == null) {
59+
channel.sendError(new IllegalArgumentException("User header is required"));
60+
return () -> {};
61+
}
62+
String username = user.getName();
63+
if (username == null || username.isEmpty()) {
64+
channel.sendError(new IllegalArgumentException("Username is required"));
65+
return () -> {};
66+
}
67+
68+
genAiService.subscribe(username, userInput, rawStreamKeys, channel);
69+
return () -> genAiService.unsubscribe(channel);
70+
}
71+
}

java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/model/authorization/UserDto.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ public class UserDto {
3131
@NotEmpty
3232
private List<String> authorities;
3333

34+
private String aiApiKey;
35+
3436
public String getUsername() {
3537
return username;
3638
}
@@ -54,4 +56,12 @@ public List<String> getAuthorities() {
5456
public void setAuthorities(List<String> authorities) {
5557
this.authorities = authorities;
5658
}
57-
}
59+
60+
public String getAiApiKey() {
61+
return aiApiKey;
62+
}
63+
64+
public void setAiApiKey(String aiApiKey) {
65+
this.aiApiKey = aiApiKey;
66+
}
67+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
* Copyright 2025 EPAM Systems, Inc
3+
*
4+
* See the NOTICE file distributed with this work for additional information
5+
* regarding copyright ownership. Licensed under the Apache License,
6+
* Version 2.0 (the "License"); you may not use this file except in compliance
7+
* with the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14+
* License for the specific language governing permissions and limitations under
15+
* the License.
16+
*/
17+
package com.epam.deltix.tbwg.webapp.model.genai;
18+
19+
import com.fasterxml.jackson.annotation.JsonProperty;
20+
21+
public class QqlGenMessage {
22+
23+
@JsonProperty
24+
private final String stage;
25+
@JsonProperty
26+
private final Integer attempt;
27+
@JsonProperty
28+
private final Integer maxAttempts;
29+
@JsonProperty
30+
private final String data;
31+
@JsonProperty
32+
private final String error;
33+
@JsonProperty
34+
private final Boolean finalEvent;
35+
36+
private QqlGenMessage(Builder b) {
37+
this.stage = b.stage;
38+
this.attempt = b.attempt;
39+
this.maxAttempts = b.maxAttempts;
40+
this.data = b.data;
41+
this.error = b.error;
42+
this.finalEvent = b.finalEvent;
43+
}
44+
45+
public String getStage() { return stage; }
46+
public Integer getAttempt() { return attempt; }
47+
public Integer getMaxAttempts() { return maxAttempts; }
48+
public String getData() { return data; }
49+
public String getError() { return error; }
50+
public Boolean getFinalEvent() { return finalEvent; }
51+
52+
public static Builder builder(String stage) { return new Builder(stage); }
53+
54+
public static class Builder {
55+
private final String stage;
56+
private Integer attempt;
57+
private Integer maxAttempts;
58+
private String data;
59+
private String error;
60+
private Boolean finalEvent;
61+
62+
public Builder(String stage) { this.stage = stage; }
63+
public Builder attempt(Integer v) { this.attempt = v; return this; }
64+
public Builder maxAttempts(Integer v) { this.maxAttempts = v; return this; }
65+
public Builder data(String v) { this.data = v; return this; }
66+
public Builder error(String v) { this.error = v; return this; }
67+
public Builder finalEvent(Boolean v) { this.finalEvent = v; return this; }
68+
public QqlGenMessage build() { return new QqlGenMessage(this); }
69+
}
70+
}

java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/authorization/FileAuthorizationProvider.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,8 @@ private synchronized void reloadFile() {
9797
users.put(
9898
user.getUsername(),
9999
new TbwgUser(
100-
user.getUsername(), user.getPassword(), buildAuthorities(user.getAuthorities())
100+
user.getUsername(), user.getPassword(), buildAuthorities(user.getAuthorities()),
101+
user.getAiApiKey()
101102
)
102103
);
103104
});

java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/authorization/SettingsAuthorizationProvider.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@ public SettingsAuthorizationProvider(SecurityOauth2ProviderSettings providerSett
5959
new TbwgUser(
6060
user.getUsername(),
6161
providerType == ProviderType.BUILT_IN_OAUTH ? user.getPassword() : "",
62-
buildAuthorities(user.getAuthorities())
62+
buildAuthorities(user.getAuthorities()),
63+
user.getAiApiKey()
6364
)
6465
);
6566
});

0 commit comments

Comments
 (0)