Skip to content

Commit 5960046

Browse files
committed
GenAi: switch to OpenAI official SDK and improve UI
1 parent 6cd04cc commit 5960046

21 files changed

+551
-384
lines changed

build.gradle

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -288,9 +288,17 @@ configure(javaProjects) {
288288
dependency 'org.apache.bcel:bcel:6.6.0'
289289

290290
// 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'
291+
dependency 'dev.langchain4j:langchain4j:1.8.0'
292+
dependency 'dev.langchain4j:langchain4j-spring-boot-starter:1.8.0-beta15'
293+
dependency 'dev.langchain4j:langchain4j-open-ai-official:1.8.0-beta15'
294+
295+
// Conflict resolution
296+
dependency 'com.google.errorprone:error_prone_annotations:2.33.0'
297+
dependency 'org.jetbrains.kotlin:kotlin-stdlib:1.9.10'
298+
dependency 'org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.10'
299+
dependency 'com.fasterxml:classmate:1.7.0'
300+
dependency 'org.apache.httpcomponents.client5:httpclient5:5.5'
301+
dependency 'org.apache.httpcomponents.core5:httpcore5:5.3.4'
294302

295303
dependency 'org.reactivestreams:reactive-streams:1.0.4'
296304

guide/configuration.md

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -167,15 +167,16 @@ services:
167167
168168
## Gen AI Configuration
169169
170-
TimeBase Web Administrator can generate QQL queries with the help of an external Gen AI provider.
170+
TimeBase Web Administrator can generate QQL queries with the help of an external Gen AI provider.
171171
The feature reads its settings from the `ai-api` section of `application.yaml`.
172172

173173
Example config:
174174

175175
```yaml
176176
ai-api:
177177
enabled: true
178-
endpointUrl: "https://YOUR-RESOURCE-NAME.openai.azure.com" # endpoint must support Azure OpenAI API
178+
provider: AZURE_LEGACY # OPENAI | AZURE | AZURE_LEGACY | GITHUB
179+
endpointUrl: "https://YOUR-RESOURCE-NAME.openai.azure.com" # base url of the ai api, may be ommitted for OPENAI and GITHUB providers
179180
deploymentName: gpt-5-mini-2025-08-07 # deployment for QQL generation
180181
embeddingDeploymentName: text-embedding-3-small-1 # deployment for embeddings
181182
keys: # ai api keys per user
@@ -189,4 +190,17 @@ ai-api:
189190
If a user is not present in `ai-api.keys`, the system falls back to `security.oauth2.users[].aiApiKey` when it is defined.
190191

191192
> [!IMPORTANT]
192-
> The Gen AI endpoint must be compatible with the Azure OpenAI API.
193+
> The Gen AI endpoint must be compatible with the selected provider's API.
194+
195+
Supported AI providers:
196+
197+
1. AZURE_LEGACY includes the deployment name in the request URL. Example:
198+
`https://YOUR-RESOURCE-NAME.openai.azure.com/openai/deployments/gpt-5-mini-2025-08-07/chat/completions`
199+
where `https://YOUR-RESOURCE-NAME.openai.azure.com` is the `endpointUrl` and
200+
`gpt-5-mini-2025-08-07` is the `deploymentName`.
201+
2. AZURE uses the `endpointUrl` and provides the model name in the request body.
202+
3. OPENAI is the official OpenAI API. The endpoint defaults to
203+
`https://api.openai.com/v1`, but may be any compatible endpoint.
204+
4. GITHUB uses the GitHub Models compatible API. The endpoint defaults to
205+
`https://models.inference.ai.azure.com`. Use a GitHub personal access token
206+
as the key — see GitHub docs [docs](https://docs.github.com/en/github-models/about-github-models) for details.

java/ws-server/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,8 @@ dependencies {
8282

8383
// AI integration
8484
implementation 'dev.langchain4j:langchain4j'
85-
implementation 'dev.langchain4j:langchain4j-azure-open-ai-spring-boot-starter'
8685
implementation 'dev.langchain4j:langchain4j-spring-boot-starter'
86+
implementation 'dev.langchain4j:langchain4j-open-ai-official'
8787

8888
compileOnly 'com.webcohesion.enunciate:enunciate-core-annotations'
8989

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

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -54,27 +54,21 @@ public class GenAiConfig {
5454
private static final String FALLBACK_EMB_FILE = "qql-docs-embeddings.json";
5555

5656
@Bean
57-
public PerUserAzureChatModel perUserAzureChatModel(AiApiSettings settings,
58-
UserAiApiKeyProvider keyProvider) {
59-
return new PerUserAzureChatModel(settings.getEndpointUrl(),
60-
settings.getDeploymentName(),
61-
keyProvider);
57+
public ChatModel perUserChatModel(AiApiSettings settings,
58+
UserAiApiKeyProvider keyProvider) {
59+
return new PerUserOpenAiOfficialChatModel(settings, keyProvider);
6260
}
6361

6462
@Bean
65-
public PerUserAzureStreamingChatModel perUserAzureStreamingChatModel(AiApiSettings settings,
66-
UserAiApiKeyProvider keyProvider) {
67-
return new PerUserAzureStreamingChatModel(settings.getEndpointUrl(),
68-
settings.getDeploymentName(),
69-
keyProvider);
63+
public StreamingChatModel perUserStreamingChatModel(AiApiSettings settings,
64+
UserAiApiKeyProvider keyProvider) {
65+
return new PerUserOpenAiOfficialStreamingChatModel(settings, keyProvider);
7066
}
7167

7268
@Bean
73-
public PerUserAzureEmbeddingModel perUserAzureEmbeddingModel(AiApiSettings settings,
74-
UserAiApiKeyProvider keyProvider) {
75-
return new PerUserAzureEmbeddingModel(settings.getEndpointUrl(),
76-
settings.getEmbeddingDeploymentName(),
77-
keyProvider);
69+
public EmbeddingModel perUserEmbeddingModel(AiApiSettings settings,
70+
UserAiApiKeyProvider keyProvider) {
71+
return new PerUserOpenAiOfficialEmbeddingModel(settings, keyProvider);
7872
}
7973

8074
@Bean

java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/genai/GenAiService.java

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
import com.epam.deltix.tbwg.webapp.settings.AiApiSettings;
3131
import com.epam.deltix.tbwg.webapp.websockets.subscription.SubscriptionChannel;
3232
import dev.langchain4j.model.chat.response.ChatResponse;
33+
import dev.langchain4j.model.chat.response.PartialResponse;
34+
import dev.langchain4j.model.chat.response.PartialResponseContext;
3335
import dev.langchain4j.service.Result;
3436
import dev.langchain4j.service.TokenStream;
3537
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
@@ -158,8 +160,16 @@ private void iterativeGenerate(String username, String schema, String pinned,
158160

159161
String wrappedPrompt = PerUserChatMaker.wrapUserInput(username, prompt);
160162
TokenStream ts = genAiHelperService.genQql(schema, pinned, wrappedPrompt)
161-
.onPartialResponse((String part) -> {
162-
if (!ctx.stopped().get() && part != null && !part.isEmpty()) {
163+
.onPartialResponseWithContext((PartialResponse partResp, PartialResponseContext partCtx) -> {
164+
if (ctx.stopped().get()) {
165+
partCtx.streamingHandle().cancel();
166+
latch.countDown();
167+
LOG.info("Generation cancelled by user.");
168+
return;
169+
}
170+
171+
String part = partResp.text();
172+
if (part != null && !part.isEmpty()) {
163173
streamed.append(part);
164174
send(ch, QqlGenMessage.builder("PART")
165175
.data(part));
@@ -175,7 +185,6 @@ private void iterativeGenerate(String username, String schema, String pinned,
175185
.error(e.getMessage()).finalEvent(true));
176186
latch.countDown();
177187
});
178-
179188
ts.start();
180189
try {
181190
if (!latch.await(STREAM_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS)) {
@@ -223,7 +232,7 @@ private void send(SubscriptionChannel ch, QqlGenMessage.Builder b) {
223232
ch.sendMessage(b.build());
224233
}
225234

226-
private String loadOverview() {
235+
private static String loadOverview() {
227236
try {
228237
return Resources.toString(
229238
Resources.getResource("qql_gen/overview.md"),
@@ -273,19 +282,19 @@ private static void describeBasic(DXTickStream stream, StringBuilder b) {
273282
b.append("\n\n");
274283
}
275284

276-
private String repairPrompt(String intent, String prev, String err) {
285+
private static String repairPrompt(String intent, String prev, String err) {
277286
return "User intent:\n" + intent +
278287
"\n\nPrevious QQL (fix minimally):\n" + prev +
279288
"\n\nCompiler error:\n" + err +
280289
"\n\nOutput ONLY corrected QQL.";
281290
}
282291

283-
private String summaryPinned(String pinned) {
292+
private static String summaryPinned(String pinned) {
284293
if (pinned == null) return "No pinned docs.";
285294
return "Pinned size=" + pinned.length();
286295
}
287296

288-
private Set<String> parseStreamKeys(String raw) {
297+
private static Set<String> parseStreamKeys(String raw) {
289298
if (raw == null || raw.isBlank()) return Collections.emptySet();
290299
return Stream.of(raw.split(","))
291300
.map(String::trim)

java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/genai/models/PerUserAzureStreamingChatModel.java

Lines changed: 0 additions & 60 deletions
This file was deleted.

java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/genai/models/PerUserChatMaker.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import dev.langchain4j.data.message.UserMessage;
2121
import dev.langchain4j.model.chat.request.ChatRequest;
2222
import dev.langchain4j.model.chat.request.ChatRequestParameters;
23+
import dev.langchain4j.model.openaiofficial.OpenAiOfficialChatRequestParameters;
2324

2425
import java.util.ArrayList;
2526
import java.util.List;
@@ -105,4 +106,8 @@ public static ProcessResult prepare(ChatRequest chatRequest) {
105106
public static ChatRequestParameters defaultParams(String modelName) {
106107
return ChatRequestParameters.builder().modelName(modelName).build();
107108
}
109+
110+
public static OpenAiOfficialChatRequestParameters defaultOpenAiOfficialParams(String modelName) {
111+
return OpenAiOfficialChatRequestParameters.builder().modelName(modelName).build();
112+
}
108113
}

java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/genai/models/PerUserAzureChatModel.java renamed to java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/genai/models/PerUserOpenAiOfficialChatModel.java

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,26 +16,24 @@
1616
*/
1717
package com.epam.deltix.tbwg.webapp.services.genai.models;
1818

19-
import dev.langchain4j.model.azure.AzureOpenAiChatModel;
19+
import com.epam.deltix.tbwg.webapp.settings.AiApiSettings;
2020
import dev.langchain4j.model.chat.ChatModel;
2121
import dev.langchain4j.model.chat.request.ChatRequest;
2222
import dev.langchain4j.model.chat.request.ChatRequestParameters;
2323
import dev.langchain4j.model.chat.response.ChatResponse;
24+
import dev.langchain4j.model.openaiofficial.OpenAiOfficialChatModel;
2425

25-
public class PerUserAzureChatModel implements ChatModel {
26+
public class PerUserOpenAiOfficialChatModel implements ChatModel {
2627

27-
private final String endpoint;
28-
private final String deploymentName;
28+
private final AiApiSettings settings;
2929
private final UserAiApiKeyProvider keyProvider;
3030
private final ChatRequestParameters defaultParams;
3131

32-
public PerUserAzureChatModel(String endpoint,
33-
String deploymentName,
34-
UserAiApiKeyProvider keyProvider) {
35-
this.endpoint = endpoint;
36-
this.deploymentName = deploymentName;
32+
public PerUserOpenAiOfficialChatModel(AiApiSettings settings,
33+
UserAiApiKeyProvider keyProvider) {
34+
this.settings = settings;
3735
this.keyProvider = keyProvider;
38-
this.defaultParams = PerUserChatMaker.defaultParams(deploymentName);
36+
this.defaultParams = PerUserChatMaker.defaultOpenAiOfficialParams(settings.chatModelNameForParams());
3937
}
4038

4139
@Override
@@ -49,13 +47,33 @@ public ChatResponse doChat(ChatRequest chatRequest) {
4947
String username = pr.username();
5048
ChatRequest effectiveRequest = pr.request();
5149

52-
ChatModel model = AzureOpenAiChatModel.builder()
53-
.endpoint(endpoint)
54-
.apiKey(keyProvider.resolve(username))
55-
.deploymentName(deploymentName)
56-
.build();
50+
ChatModel model = createDelegate(username);
5751

5852
return model.doChat(effectiveRequest);
5953
}
6054

55+
private ChatModel createDelegate(String username) {
56+
OpenAiOfficialChatModel.Builder builder = OpenAiOfficialChatModel.builder()
57+
.apiKey(keyProvider.resolve(username));
58+
59+
String endpoint = settings.getEndpointUrl();
60+
if (endpoint != null && !endpoint.isBlank()) {
61+
builder.baseUrl(endpoint);
62+
}
63+
64+
String deployment = settings.requireDeploymentName();
65+
switch (settings.providerOrDefault()) {
66+
case OPENAI -> builder.modelName(deployment);
67+
case AZURE -> builder.isAzure(true)
68+
.modelName(deployment)
69+
.azureDeploymentName(deployment);
70+
case AZURE_LEGACY -> builder.isAzure(true)
71+
.azureDeploymentName(deployment)
72+
.modelName("");
73+
case GITHUB -> builder.isGitHubModels(true)
74+
.modelName(deployment);
75+
}
76+
77+
return builder.build();
78+
}
6179
}

java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/genai/models/PerUserAzureEmbeddingModel.java renamed to java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/genai/models/PerUserOpenAiOfficialEmbeddingModel.java

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,26 +16,24 @@
1616
*/
1717
package com.epam.deltix.tbwg.webapp.services.genai.models;
1818

19+
import com.epam.deltix.tbwg.webapp.settings.AiApiSettings;
1920
import dev.langchain4j.data.embedding.Embedding;
2021
import dev.langchain4j.data.segment.TextSegment;
21-
import dev.langchain4j.model.azure.AzureOpenAiEmbeddingModel;
2222
import dev.langchain4j.model.embedding.EmbeddingModel;
23+
import dev.langchain4j.model.openaiofficial.OpenAiOfficialEmbeddingModel;
2324
import dev.langchain4j.model.output.Response;
2425

2526
import java.util.ArrayList;
2627
import java.util.List;
2728

28-
public class PerUserAzureEmbeddingModel implements EmbeddingModel {
29+
public class PerUserOpenAiOfficialEmbeddingModel implements EmbeddingModel {
2930

30-
private final String endpoint;
31-
private final String deploymentName;
31+
private final AiApiSettings settings;
3232
private final UserAiApiKeyProvider keyProvider;
3333

34-
public PerUserAzureEmbeddingModel(String endpoint,
35-
String deploymentName,
36-
UserAiApiKeyProvider keyProvider) {
37-
this.endpoint = endpoint;
38-
this.deploymentName = deploymentName;
34+
public PerUserOpenAiOfficialEmbeddingModel(AiApiSettings settings,
35+
UserAiApiKeyProvider keyProvider) {
36+
this.settings = settings;
3937
this.keyProvider = keyProvider;
4038
}
4139

@@ -60,11 +58,32 @@ public Response<List<Embedding>> embedAll(List<TextSegment> textSegments) {
6058
effectiveSegments.add(TextSegment.from(unwrappedText, segment.metadata()));
6159
}
6260

63-
EmbeddingModel model = AzureOpenAiEmbeddingModel.builder()
64-
.endpoint(endpoint)
65-
.deploymentName(deploymentName)
66-
.apiKey(keyProvider.resolve(username))
67-
.build();
61+
EmbeddingModel model = createDelegate(username);
6862
return model.embedAll(effectiveSegments);
6963
}
64+
65+
private EmbeddingModel createDelegate(String username) {
66+
OpenAiOfficialEmbeddingModel.Builder builder = OpenAiOfficialEmbeddingModel.builder()
67+
.apiKey(keyProvider.resolve(username));
68+
69+
String endpoint = settings.getEndpointUrl();
70+
if (endpoint != null && !endpoint.isBlank()) {
71+
builder.baseUrl(endpoint);
72+
}
73+
74+
String deployment = settings.requireEmbeddingDeploymentName();
75+
switch (settings.providerOrDefault()) {
76+
case OPENAI -> builder.modelName(deployment);
77+
case AZURE -> builder.isAzure(true)
78+
.modelName(deployment)
79+
.azureDeploymentName(deployment);
80+
case AZURE_LEGACY -> builder.isAzure(true)
81+
.azureDeploymentName(deployment)
82+
.modelName("");
83+
case GITHUB -> builder.isGitHubModels(true)
84+
.modelName(deployment);
85+
}
86+
87+
return builder.build();
88+
}
7089
}

0 commit comments

Comments
 (0)