Compare commits
103 Commits
SPIKE_LLM-
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f27f0cdf6e | ||
|
|
a02e9ed917 | ||
|
|
c607b1728c | ||
|
|
f6574deb61 | ||
|
|
456d9bc1f5 | ||
|
|
613e818c6a | ||
|
|
a9125c6a16 | ||
|
|
cb4cd51dba | ||
|
|
646f3ff862 | ||
|
|
1adb161c7b | ||
|
|
93b292bcff | ||
|
|
5093c2db82 | ||
|
|
694590768b | ||
|
|
c4143539a7 | ||
|
|
30ef0ec59f | ||
|
|
fb4e504b47 | ||
|
|
792808a03c | ||
|
|
52bf7b834b | ||
|
|
0a993ce053 | ||
|
|
a05a1a12b9 | ||
|
|
d980dbe853 | ||
|
|
d0b3f07aaa | ||
|
|
3b00f85e72 | ||
|
|
f14a4c9cb1 | ||
|
|
9380ddd7c7 | ||
|
|
d7b2d41653 | ||
|
|
ba0f8e681b | ||
|
|
51833879fa | ||
|
|
af9c6727b4 | ||
|
|
9fa2a90f8d | ||
|
|
45b5568e29 | ||
|
|
31b3717a2a | ||
|
|
51d8280ba8 | ||
|
|
316ebfdac0 | ||
|
|
a7e7d589b2 | ||
|
|
543b00713e | ||
|
|
f85c2b6df2 | ||
|
|
e5e2d9c33b | ||
|
|
c3b8a7bb1f | ||
|
|
cccc10c15e | ||
|
|
27652bfefa | ||
|
|
32995327ff | ||
|
|
f199b42490 | ||
|
|
415a2aca72 | ||
|
|
66fcf88535 | ||
|
|
223f87400a | ||
|
|
187ed3ec4f | ||
|
|
1d02bddb63 | ||
|
|
449515ed53 | ||
|
|
bbe2ee71ca | ||
|
|
c454b86776 | ||
|
|
2d5ab7045f | ||
|
|
7070c8847d | ||
|
|
cad4f9ab09 | ||
|
|
c70637eae7 | ||
|
|
f8b50f8eb0 | ||
|
|
e7e91547a2 | ||
|
|
5011707319 | ||
|
|
4456b7144d | ||
|
|
b1603b450d | ||
|
|
45e959d112 | ||
|
|
9b62571172 | ||
|
|
0de27df114 | ||
|
|
ed7ed6539b | ||
|
|
65b8c67844 | ||
|
|
685df45f2e | ||
|
|
b96881baa5 | ||
|
|
d2716f31fa | ||
|
|
6b1399daae | ||
|
|
66561cf8dd | ||
|
|
1bb1b02c83 | ||
|
|
9a63ee5351 | ||
|
|
58ab93cffc | ||
|
|
d4ec66f762 | ||
|
|
032fa06c42 | ||
|
|
1c902d0592 | ||
|
|
2026316694 | ||
|
|
fab4666dd7 | ||
|
|
f3f917b5fe | ||
|
|
560f7fb947 | ||
|
|
ec82c2ec02 | ||
|
|
2d66b1e5d4 | ||
|
|
d40f4f3289 | ||
|
|
cbcf3b605b | ||
|
|
c437df9367 | ||
|
|
dd6b31e902 | ||
|
|
a79bfd4051 | ||
|
|
de352657b3 | ||
|
|
9124428d67 | ||
|
|
24fa7e002b | ||
|
|
6b7d0762dc | ||
|
|
72b7ec7a3f | ||
|
|
d7e328204d | ||
|
|
ebbad56b7a | ||
|
|
262cc65127 | ||
|
|
381c0a7e4b | ||
|
|
6832a719d8 | ||
|
|
c6f3dd2e26 | ||
|
|
9eda1d5695 | ||
|
|
aa24e14cc3 | ||
|
|
84a6d290e6 | ||
|
|
56dbe06e6d | ||
|
|
32c618c35b |
@ -2,3 +2,21 @@ include:
|
||||
- project: 'gitlab/gitlab'
|
||||
ref: 'main'
|
||||
file: 'ci-templates/gradle_java.yml'
|
||||
|
||||
deploy:
|
||||
stage: deploy
|
||||
tags:
|
||||
- dind
|
||||
script:
|
||||
- echo "Building with gradle version ${BUILDVERSION}"
|
||||
- gradle -Pversion=${BUILDVERSION} publish
|
||||
- gradle bootBuildImage --publishImage -PbuildbootDockerHostNetwork=true -Pversion=${BUILDVERSION}
|
||||
- echo "BUILDVERSION=$BUILDVERSION" >> version.env
|
||||
artifacts:
|
||||
reports:
|
||||
dotenv: version.env
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
||||
- if: $CI_COMMIT_BRANCH =~ /^release/
|
||||
- if: $CI_COMMIT_BRANCH =~ /^feature/
|
||||
- if: $CI_COMMIT_TAG
|
||||
149
build.gradle.kts
149
build.gradle.kts
@ -1,149 +0,0 @@
|
||||
import org.springframework.boot.gradle.tasks.bundling.BootBuildImage
|
||||
|
||||
plugins {
|
||||
java
|
||||
id("org.springframework.boot") version "3.3.2"
|
||||
id("io.spring.dependency-management") version "1.1.6"
|
||||
id("org.sonarqube") version "4.4.1.3373"
|
||||
id("io.freefair.lombok") version "8.6"
|
||||
pmd
|
||||
checkstyle
|
||||
jacoco
|
||||
}
|
||||
|
||||
group = "com.knecon.fforesight"
|
||||
java.sourceCompatibility = JavaVersion.VERSION_17
|
||||
|
||||
configurations {
|
||||
compileOnly {
|
||||
extendsFrom(configurations.annotationProcessor.get())
|
||||
}
|
||||
}
|
||||
|
||||
pmd {
|
||||
isConsoleOutput = true
|
||||
}
|
||||
|
||||
tasks.pmdMain {
|
||||
pmd.ruleSetFiles = files("${projectDir}/config/pmd/pmd.xml")
|
||||
}
|
||||
|
||||
tasks.pmdTest {
|
||||
pmd.ruleSetFiles = files("${projectDir}/config/pmd/test_pmd.xml")
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
tasks.jacocoTestReport {
|
||||
reports {
|
||||
xml.required.set(false)
|
||||
csv.required.set(false)
|
||||
html.outputLocation.set(layout.buildDirectory.dir("jacocoHtml"))
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
maven {
|
||||
url = uri("https://nexus.knecon.com/repository/gindev/");
|
||||
credentials {
|
||||
username = providers.gradleProperty("mavenUser").getOrNull();
|
||||
password = providers.gradleProperty("mavenPassword").getOrNull();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register("publish") {
|
||||
|
||||
}
|
||||
|
||||
tasks.named<BootBuildImage>("bootBuildImage") {
|
||||
|
||||
|
||||
environment.put("BPE_DELIM_JAVA_TOOL_OPTIONS", " ")
|
||||
environment.put("BPE_APPEND_JAVA_TOOL_OPTIONS", "-Dfile.encoding=UTF-8")
|
||||
|
||||
imageName.set("nexus.knecon.com:5001/ff/${project.name}:${project.version}")
|
||||
if (project.hasProperty("buildbootDockerHostNetwork")) {
|
||||
network.set("host")
|
||||
}
|
||||
docker {
|
||||
if (project.hasProperty("buildbootDockerHostNetwork")) {
|
||||
bindHostToBuilder.set(true)
|
||||
}
|
||||
verboseLogging.set(true)
|
||||
|
||||
publishRegistry {
|
||||
username.set(providers.gradleProperty("mavenUser").getOrNull())
|
||||
password.set(providers.gradleProperty("mavenPassword").getOrNull())
|
||||
email.set(providers.gradleProperty("mavenEmail").getOrNull())
|
||||
url.set("https://nexus.knecon.com:5001/")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
configurations {
|
||||
all {
|
||||
exclude(group = "org.springframework.boot", module = "spring-boot-starter-logging")
|
||||
exclude(group = "commons-logging", module = "commons-logging")
|
||||
}
|
||||
}
|
||||
|
||||
extra["springCloudVersion"] = "2022.0.5"
|
||||
extra["testcontainersVersion"] = "1.20.0"
|
||||
|
||||
dependencies {
|
||||
implementation("org.springframework.boot:spring-boot-starter-actuator")
|
||||
implementation("org.springframework.boot:spring-boot-starter-amqp")
|
||||
implementation("org.springframework.boot:spring-boot-starter-web")
|
||||
implementation("org.springframework.cloud:spring-cloud-starter-openfeign")
|
||||
implementation("org.springframework.boot:spring-boot-starter-websocket")
|
||||
implementation("org.springframework.security:spring-security-messaging:6.1.3")
|
||||
implementation("com.iqser.red.commons:storage-commons:2.49.0")
|
||||
implementation("com.knecon.fforesight:keycloak-commons:0.29.0")
|
||||
implementation("com.knecon.fforesight:swagger-commons:0.7.0")
|
||||
implementation("com.azure:azure-ai-openai:1.0.0-beta.5")
|
||||
developmentOnly("org.springframework.boot:spring-boot-devtools")
|
||||
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
|
||||
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
||||
testImplementation("org.springframework.amqp:spring-rabbit-test")
|
||||
implementation("ch.qos.logback:logback-classic")
|
||||
}
|
||||
|
||||
|
||||
|
||||
dependencyManagement {
|
||||
imports {
|
||||
mavenBom("org.testcontainers:testcontainers-bom:${property("testcontainersVersion")}")
|
||||
mavenBom("org.springframework.cloud:spring-cloud-dependencies:${property("springCloudVersion")}")
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType<Test> {
|
||||
minHeapSize = "1024m"
|
||||
maxHeapSize = "2048m"
|
||||
useJUnitPlatform()
|
||||
reports {
|
||||
junitXml.outputLocation.set(layout.buildDirectory.dir("reports/junit"))
|
||||
}
|
||||
}
|
||||
|
||||
sonarqube {
|
||||
properties {
|
||||
providers.gradleProperty("sonarToken").getOrNull()?.let { property("sonar.login", it) }
|
||||
property("sonar.host.url", "https://sonarqube.knecon.com")
|
||||
}
|
||||
}
|
||||
|
||||
tasks.test {
|
||||
finalizedBy(tasks.jacocoTestReport) // report is always generated after tests run
|
||||
}
|
||||
tasks.jacocoTestReport {
|
||||
dependsOn(tasks.test) // tests are required to run before generating the report
|
||||
reports {
|
||||
xml.required.set(true)
|
||||
csv.required.set(false)
|
||||
}
|
||||
}
|
||||
7
buildSrc/build.gradle.kts
Normal file
7
buildSrc/build.gradle.kts
Normal file
@ -0,0 +1,7 @@
|
||||
plugins {
|
||||
`kotlin-dsl`
|
||||
}
|
||||
|
||||
repositories {
|
||||
gradlePluginPortal()
|
||||
}
|
||||
@ -0,0 +1,86 @@
|
||||
plugins {
|
||||
`java-library`
|
||||
`maven-publish`
|
||||
pmd
|
||||
checkstyle
|
||||
jacoco
|
||||
}
|
||||
|
||||
group = "com.knecon.fforesight"
|
||||
|
||||
java.sourceCompatibility = JavaVersion.VERSION_17
|
||||
java.targetCompatibility = JavaVersion.VERSION_17
|
||||
|
||||
tasks.pmdMain {
|
||||
pmd.ruleSetFiles = files("${rootDir}/config/pmd/pmd.xml")
|
||||
}
|
||||
|
||||
tasks.pmdTest {
|
||||
pmd.ruleSetFiles = files("${rootDir}/config/pmd/test_pmd.xml")
|
||||
}
|
||||
|
||||
tasks.named<Test>("test") {
|
||||
useJUnitPlatform()
|
||||
reports {
|
||||
junitXml.outputLocation.set(layout.buildDirectory.dir("reports/junit"))
|
||||
}
|
||||
minHeapSize = "512m"
|
||||
maxHeapSize = "2048m"
|
||||
}
|
||||
|
||||
tasks.test {
|
||||
finalizedBy(tasks.jacocoTestReport) // report is always generated after tests run
|
||||
}
|
||||
|
||||
tasks.jacocoTestReport {
|
||||
dependsOn(tasks.test) // tests are required to run before generating the report
|
||||
reports {
|
||||
xml.required.set(true)
|
||||
csv.required.set(false)
|
||||
html.outputLocation.set(layout.buildDirectory.dir("jacocoHtml"))
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
|
||||
tasks.withType<Javadoc> {
|
||||
options {
|
||||
this as StandardJavadocDocletOptions
|
||||
addBooleanOption("Xdoclint:none", true)
|
||||
addStringOption("Xmaxwarns", "1")
|
||||
}
|
||||
}
|
||||
|
||||
publishing {
|
||||
publications {
|
||||
create<MavenPublication>(name) {
|
||||
from(components["java"])
|
||||
}
|
||||
}
|
||||
repositories {
|
||||
maven {
|
||||
url = uri("https://nexus.knecon.com/repository/red-platform-releases/")
|
||||
credentials {
|
||||
username = providers.gradleProperty("mavenUser").getOrNull()
|
||||
password = providers.gradleProperty("mavenPassword").getOrNull()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
java {
|
||||
withJavadocJar()
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
maven {
|
||||
url = uri("https://nexus.knecon.com/repository/gindev/")
|
||||
credentials {
|
||||
username = providers.gradleProperty("mavenUser").getOrNull()
|
||||
password = providers.gradleProperty("mavenPassword").getOrNull()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -19,6 +19,7 @@
|
||||
<module name="DefaultComesLast"/>
|
||||
<module name="EmptyStatement"/>
|
||||
<module name="EqualsHashCode"/>
|
||||
<module name="ExplicitInitialization"/>
|
||||
<module name="IllegalInstantiation"/>
|
||||
<module name="ModifiedControlVariable"/>
|
||||
<module name="MultipleVariableDeclarations"/>
|
||||
|
||||
@ -1,16 +1,20 @@
|
||||
<?xml version="1.0"?>
|
||||
<ruleset name="Custom Rules"
|
||||
<ruleset name="Custom ruleset"
|
||||
xmlns="http://pmd.sourceforge.net/ruleset/2.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 https://pmd.sourceforge.io/ruleset_2_0_0.xsd">
|
||||
xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 http://pmd.sourceforge.net/ruleset_2_0_0.xsd">
|
||||
|
||||
<description>Knecon test pmd rules</description>
|
||||
<description>
|
||||
Knecon ruleset checks the code for bad stuff
|
||||
</description>
|
||||
|
||||
<rule ref="category/java/errorprone.xml">
|
||||
<exclude name="DataflowAnomalyAnalysis"/>
|
||||
<exclude name="MissingSerialVersionUID"/>
|
||||
<exclude name="AvoidLiteralsInIfCondition"/>
|
||||
<exclude name="BeanMembersShouldSerialize"/>
|
||||
<exclude name="AvoidDuplicateLiterals"/>
|
||||
<exclude name="NullAssignment"/>
|
||||
<exclude name="AssignmentInOperand"/>
|
||||
<exclude name="BeanMembersShouldSerialize"/>
|
||||
</rule>
|
||||
|
||||
</ruleset>
|
||||
|
||||
5
llm-service/llm-service-api/build.gradle.kts
Normal file
5
llm-service/llm-service-api/build.gradle.kts
Normal file
@ -0,0 +1,5 @@
|
||||
plugins {
|
||||
`maven-publish`
|
||||
id("com.knecon.fforesight.service.java-conventions")
|
||||
id("io.freefair.lombok") version "8.12.1"
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
package com.knecon.fforesight.llm.service.api.model;
|
||||
package com.knecon.fforesight.llm.service;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
@ -0,0 +1,25 @@
|
||||
package com.knecon.fforesight.llm.service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.FieldDefaults;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||
public class ChunkingResponse {
|
||||
|
||||
Map<String, String> targetFilePath;
|
||||
String responseFilePath;
|
||||
|
||||
List<ChunkingResponseData> data;
|
||||
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
package com.knecon.fforesight.llm.service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.FieldDefaults;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||
public class ChunkingResponseData {
|
||||
|
||||
Integer chunkId;
|
||||
String text;
|
||||
List<String> types;
|
||||
List<List<Integer>> treeIds;
|
||||
float[] embedding;
|
||||
Integer tokenCount;
|
||||
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
package com.knecon.fforesight.llm.service;
|
||||
|
||||
public record EntityAiDescription(String name, String aiDescription) {
|
||||
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
package com.knecon.fforesight.llm.service.api;
|
||||
package com.knecon.fforesight.llm.service;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
@ -0,0 +1,21 @@
|
||||
package com.knecon.fforesight.llm.service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.FieldDefaults;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||
public class LlmNerEntities {
|
||||
|
||||
List<LlmNerEntity> entities;
|
||||
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
package com.knecon.fforesight.llm.service;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.FieldDefaults;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||
public class LlmNerEntity {
|
||||
|
||||
String value;
|
||||
String type;
|
||||
int startOffset;
|
||||
int endOffset;
|
||||
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
package com.knecon.fforesight.llm.service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.experimental.FieldDefaults;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||
public class LlmNerMessage {
|
||||
|
||||
Map<String, String> identifier;
|
||||
List<EntityAiDescription> entityAiDescriptions;
|
||||
String chunksStorageId;
|
||||
String documentStructureStorageId;
|
||||
String documentTextStorageId;
|
||||
String documentPositionStorageId;
|
||||
String documentPagesStorageId;
|
||||
String resultStorageId;
|
||||
long aiCreationVersion;
|
||||
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
package com.knecon.fforesight.llm.service;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.FieldDefaults;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||
public class LlmNerResponseMessage {
|
||||
|
||||
Map<String, String> identifier;
|
||||
int promptTokens;
|
||||
int completionTokens;
|
||||
int duration;
|
||||
long aiCreationVersion;
|
||||
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
package com.knecon.fforesight.llm.service.api.model;
|
||||
package com.knecon.fforesight.llm.service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@ -0,0 +1,11 @@
|
||||
package com.knecon.fforesight.llm.service;
|
||||
|
||||
public class QueueNames {
|
||||
|
||||
public static final String LLM_NER_REQUEST_QUEUE_PREFIX = "llm_entity_request";
|
||||
public static final String LLM_NER_REQUEST_EXCHANGE = "llm_entity_request_exchange";
|
||||
public static final String LLM_NER_RESPONSE_QUEUE_PREFIX = "llm_entity_response";
|
||||
public static final String LLM_NER_RESPONSE_EXCHANGE = "llm_entity_response_exchange";
|
||||
public static final String LLM_NER_DLQ = "llm_entity_error";
|
||||
|
||||
}
|
||||
25
llm-service/llm-service-processor/build.gradle.kts
Normal file
25
llm-service/llm-service-processor/build.gradle.kts
Normal file
@ -0,0 +1,25 @@
|
||||
plugins {
|
||||
id("com.knecon.fforesight.service.java-conventions")
|
||||
id("io.freefair.lombok") version "8.12.1"
|
||||
}
|
||||
|
||||
configurations {
|
||||
all {
|
||||
exclude(group = "org.springframework.boot", module = "spring-boot-starter-logging")
|
||||
}
|
||||
}
|
||||
extra["springCloudVersion"] = "2022.0.5"
|
||||
extra["testcontainersVersion"] = "1.20.0"
|
||||
|
||||
dependencies {
|
||||
implementation(project(":llm-service-api"))
|
||||
implementation("com.knecon.fforesight:document:4.445.0")
|
||||
implementation("com.iqser.red.commons:storage-commons:2.51.0")
|
||||
implementation("org.springframework.boot:spring-boot-starter:3.1.1")
|
||||
implementation("com.knecon.fforesight:tenant-commons:0.31.0") {
|
||||
exclude(group = "com.iqser.red.commons", module = "storage-commons")
|
||||
}
|
||||
implementation("com.azure:azure-ai-openai:1.0.0-beta.12")
|
||||
implementation("ch.qos.logback:logback-classic:1.5.16")
|
||||
implementation("com.google.protobuf:protobuf-java:4.29.3")
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
package com.knecon.fforesight.llm.service;
|
||||
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
@ComponentScan
|
||||
@EnableConfigurationProperties(LlmServiceSettings.class)
|
||||
public class LlmServiceConfiguration {
|
||||
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
package com.knecon.fforesight.llm.service.settings;
|
||||
package com.knecon.fforesight.llm.service;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
@ -7,19 +7,12 @@ import org.springframework.context.annotation.Primary;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Primary
|
||||
@Configuration
|
||||
@ConfigurationProperties("llm-service")
|
||||
public class LlmServiceSettings {
|
||||
|
||||
private String requestQueueName = "llm_request_queue";
|
||||
private String responseQueueName = "llm_response_queue";
|
||||
private String deadLetterQueueName = "llm_dead_letter_queue";
|
||||
|
||||
|
||||
private String azureOpenAiKey;
|
||||
private String azureOpenAiEndpoint;
|
||||
private String model = "gpt-4-cqs-dev";
|
||||
|
||||
private String model = "gpt-4o-mini";
|
||||
private int concurrency = 8;
|
||||
|
||||
}
|
||||
@ -1,6 +1,26 @@
|
||||
package com.knecon.fforesight.llm.service.model;
|
||||
package com.knecon.fforesight.llm.service;
|
||||
|
||||
public class SystemMessages {
|
||||
import java.util.List;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
|
||||
@UtilityClass
|
||||
public class SystemMessageProvider {
|
||||
|
||||
public static final String PROMPT_CORRECTION = """
|
||||
You are an AI assistant specialized in identifying and correcting JSON syntax errors.
|
||||
The JSON provided below contains syntax errors and cannot be parsed correctly. Your objective is to transform it into a valid JSON format.
|
||||
Please perform the following steps:
|
||||
|
||||
1. **Error Detection:** Identify all syntax errors within the JSON structure.
|
||||
2. **Error Resolution:** Correct the identified syntax errors to rectify the JSON format.
|
||||
3. **Data Sanitization:** Remove any elements or data that cannot be automatically fixed to maintain JSON validity.
|
||||
4. **Validation:** Verify that the final JSON adheres to proper formatting and is fully valid.
|
||||
|
||||
**Output Requirements:**
|
||||
- Return only the corrected and validated JSON.
|
||||
- Do not include any additional text, explanations, or comments.
|
||||
""";
|
||||
|
||||
public static String RULES_CO_PILOT = """
|
||||
From now on, you are a Drools rule generator. This means you will start your answer with a step-by-step explanation how to write a rule, which will fulfill the prompt, followed by the rule.
|
||||
@ -322,4 +342,72 @@ public class SystemMessages {
|
||||
intersects(TextRange textRange) -> boolean
|
||||
""";
|
||||
|
||||
|
||||
public String createNerPrompt(List<EntityAiDescription> entityAiDescriptions) {
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.append("You are an AI assistant specialized in extracting named entities from text. ");
|
||||
sb.append("Your task is to identify and categorize all named entities in the provided document into the following classes:\n\n");
|
||||
|
||||
for (EntityAiDescription entity : entityAiDescriptions) {
|
||||
sb.append("- **").append(entity.name()).append("**: ").append(entity.aiDescription()).append("\n");
|
||||
}
|
||||
|
||||
sb.append("\n**Instructions:**\n\n");
|
||||
|
||||
sb.append("1. **Entity Handling**:\n");
|
||||
sb.append(" - Use the classes described above and only those for classification.\n");
|
||||
sb.append(" - Include all relevant entities. Prefer inclusion over omission.\n");
|
||||
sb.append(" - Avoid duplicates within each category.\n");
|
||||
sb.append(" - Assign each entity to only one category, prioritizing specificity.");
|
||||
sb.append("For instance, if a company's name is part of an address, classify it under ADDRESS only, not under COMPANY.\n");
|
||||
|
||||
sb.append("2. **Output Format**: Provide the extracted entities in strict JSON format as shown below.\n");
|
||||
|
||||
sb.append(" ```json\n");
|
||||
sb.append(" {\n");
|
||||
for (int i = 0; i < entityAiDescriptions.size(); i++) {
|
||||
EntityAiDescription entity = entityAiDescriptions.get(i);
|
||||
sb.append(" \"").append(entity.name()).append("\": [\"entity1\", \"entity2\"");
|
||||
if (i < entityAiDescriptions.size() - 1) {
|
||||
sb.append("],\n");
|
||||
} else {
|
||||
sb.append("]\n");
|
||||
}
|
||||
}
|
||||
sb.append(" }\n");
|
||||
sb.append(" ```\n\n");
|
||||
|
||||
sb.append(" - Ensure there is no additional text or explanation outside the JSON structure.\n");
|
||||
sb.append(" - Always replace linebreaks with whitespaces.");
|
||||
sb.append("but except that, ensure that the entities in the JSON exactly match the text from the document, preserving the original formatting and casing.\n");
|
||||
sb.append(" - Ensure there is no additional text or explanation outside the JSON structure.\n\n");
|
||||
|
||||
// examples would possibly be beneficial but cause hallucinations
|
||||
|
||||
// sb.append("**Example 1:**\n\n");
|
||||
// sb.append("_Entities Searched: PERSON, PII, ADDRESS, COMPANY_\n\n");
|
||||
// sb.append("**Input:**\n```\nContact Bob at bob@techcorp.com or visit TechCorp HQ at 456 Tech Avenue, New York, NY 10001 USA.\n```\n\n");
|
||||
// sb.append("**Output:**\n```json\n{\n");
|
||||
// sb.append(" \"PERSON\": [\"Bob\"],\n");
|
||||
// sb.append(" \"PII\": [\"bob@techcorp.com\"],\n");
|
||||
// sb.append(" \"ADDRESS\": [\"456 Tech Avenue, New York, NY 10001 USA\"],\n");
|
||||
// sb.append(" \"COMPANY\": [\"TechCorp\"],\n");
|
||||
// sb.append("}\n```\n\n");
|
||||
//
|
||||
// sb.append("**Example 2:**\n\n");
|
||||
// sb.append("_Entities Searched: EVENT, PRODUCT, DATE, LOCATION_\n\n");
|
||||
// sb.append("**Input:**\n```\nThe launch event for the new XYZ Smartphone is scheduled on September 30, 2024, at the Grand Convention Center in Berlin.");
|
||||
// sb.append("You can pre-order the device starting from August 15, 2024.\n```\n\n");
|
||||
// sb.append("**Output:**\n```json\n{\n");
|
||||
// sb.append(" \"EVENT\": [\"launch event\"],\n");
|
||||
// sb.append(" \"PRODUCT\": [\"XYZ Smartphone\"],\n");
|
||||
// sb.append(" \"DATE\": [\"September 30, 2024\", \"August 15, 2024\"],\n");
|
||||
// sb.append(" \"LOCATION\": [\"Grand Convention Center\", \"Berlin\"]\n");
|
||||
// sb.append("}\n```\n\n");
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
package com.knecon.fforesight.llm.service.models;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.DocumentTree;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.nodes.Document;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.textblock.ConsecutiveTextBlockCollector;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.textblock.TextBlock;
|
||||
import com.knecon.fforesight.llm.service.ChunkingResponseData;
|
||||
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
public record Chunk(String markdown, List<TextBlock> parts) {
|
||||
|
||||
public static Chunk create(ChunkingResponseData chunkingResponseData, Document document) {
|
||||
|
||||
return new Chunk(chunkingResponseData.getText(), getChunkParts(document, chunkingResponseData.getTreeIds()));
|
||||
}
|
||||
|
||||
|
||||
private static List<TextBlock> getChunkParts(Document document, List<List<Integer>> treeIds) {
|
||||
|
||||
return treeIds.stream()
|
||||
.map(treeId -> {
|
||||
Optional<DocumentTree.Entry> entry = document.getDocumentTree().findEntryById(treeId);
|
||||
if (entry.isEmpty()) {
|
||||
throw new RuntimeException("Could not find node with id " + treeId);
|
||||
}
|
||||
return entry.get().getNode().getTextBlock();
|
||||
})
|
||||
.collect(new ConsecutiveTextBlockCollector());
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,49 @@
|
||||
package com.knecon.fforesight.llm.service.services;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.iqser.red.service.redaction.v1.server.data.DocumentData;
|
||||
import com.iqser.red.service.redaction.v1.server.data.DocumentPageProto;
|
||||
import com.iqser.red.service.redaction.v1.server.data.DocumentPositionDataProto;
|
||||
import com.iqser.red.service.redaction.v1.server.data.DocumentStructureProto;
|
||||
import com.iqser.red.service.redaction.v1.server.data.DocumentStructureWrapper;
|
||||
import com.iqser.red.service.redaction.v1.server.data.DocumentTextDataProto;
|
||||
import com.iqser.red.service.redaction.v1.server.mapper.DocumentGraphMapper;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.nodes.Document;
|
||||
import com.iqser.red.storage.commons.service.StorageService;
|
||||
import com.knecon.fforesight.llm.service.LlmNerMessage;
|
||||
import com.knecon.fforesight.tenantcommons.TenantContext;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.experimental.FieldDefaults;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
|
||||
public class DocumentBuilderService {
|
||||
|
||||
StorageService storageService;
|
||||
|
||||
|
||||
public Document build(LlmNerMessage llmNerMessage) {
|
||||
|
||||
DocumentData documentData = new DocumentData();
|
||||
documentData.setDocumentStructureWrapper(new DocumentStructureWrapper(storageService.readProtoObject(TenantContext.getTenantId(),
|
||||
llmNerMessage.getDocumentStructureStorageId(),
|
||||
DocumentStructureProto.DocumentStructure.parser())));
|
||||
documentData.setDocumentTextData(storageService.readProtoObject(TenantContext.getTenantId(),
|
||||
llmNerMessage.getDocumentTextStorageId(),
|
||||
DocumentTextDataProto.AllDocumentTextData.parser()));
|
||||
documentData.setDocumentPositionData(storageService.readProtoObject(TenantContext.getTenantId(),
|
||||
llmNerMessage.getDocumentPositionStorageId(),
|
||||
DocumentPositionDataProto.AllDocumentPositionData.parser()));
|
||||
documentData.setDocumentPages(storageService.readProtoObject(TenantContext.getTenantId(),
|
||||
llmNerMessage.getDocumentPagesStorageId(),
|
||||
DocumentPageProto.AllDocumentPages.parser()));
|
||||
return DocumentGraphMapper.toDocumentGraph(documentData);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,245 @@
|
||||
package com.knecon.fforesight.llm.service.services;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.azure.ai.openai.models.ChatChoice;
|
||||
import com.azure.ai.openai.models.ChatCompletions;
|
||||
import com.azure.ai.openai.models.ChatCompletionsJsonResponseFormat;
|
||||
import com.azure.ai.openai.models.ChatCompletionsOptions;
|
||||
import com.azure.ai.openai.models.ChatRequestMessage;
|
||||
import com.azure.ai.openai.models.ChatRequestSystemMessage;
|
||||
import com.azure.ai.openai.models.ChatRequestUserMessage;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.nodes.Document;
|
||||
import com.iqser.red.service.redaction.v1.server.model.document.textblock.TextBlock;
|
||||
import com.iqser.red.storage.commons.service.StorageService;
|
||||
import com.knecon.fforesight.llm.service.ChunkingResponse;
|
||||
import com.knecon.fforesight.llm.service.EntityAiDescription;
|
||||
import com.knecon.fforesight.llm.service.LlmNerEntities;
|
||||
import com.knecon.fforesight.llm.service.LlmNerEntity;
|
||||
import com.knecon.fforesight.llm.service.LlmNerMessage;
|
||||
import com.knecon.fforesight.llm.service.SystemMessageProvider;
|
||||
import com.knecon.fforesight.llm.service.models.Chunk;
|
||||
import com.knecon.fforesight.llm.service.utils.FormattingUtils;
|
||||
import com.knecon.fforesight.tenantcommons.TenantContext;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.experimental.FieldDefaults;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
|
||||
public class LlmNerService {
|
||||
|
||||
LlmRessource llmRessource;
|
||||
DocumentBuilderService documentBuilderService;
|
||||
StorageService storageService;
|
||||
ObjectMapper mapper;
|
||||
|
||||
|
||||
@SneakyThrows
|
||||
public synchronized Usage runNer(LlmNerMessage llmNerMessage) {
|
||||
|
||||
int completionTokenCount = 0;
|
||||
int promptTokenCount = 0;
|
||||
llmRessource.resetConcurrencyLimiter();
|
||||
|
||||
long start = System.currentTimeMillis();
|
||||
|
||||
List<LlmNerEntity> allEntities = new ArrayList<>();
|
||||
|
||||
if (!llmNerMessage.getEntityAiDescriptions().isEmpty()) {
|
||||
Document document = documentBuilderService.build(llmNerMessage);
|
||||
ChunkingResponse chunks = readChunks(llmNerMessage.getChunksStorageId());
|
||||
|
||||
allEntities = new LinkedList<>();
|
||||
|
||||
log.info("Finished data prep in {} for {}", FormattingUtils.humanizeDuration(System.currentTimeMillis() - start), llmNerMessage.getIdentifier());
|
||||
|
||||
List<CompletableFuture<EntitiesWithUsage>> entityFutures = chunks.getData()
|
||||
.stream()
|
||||
.map(chunk -> Chunk.create(chunk, document))
|
||||
.map(chunk -> getLlmNerEntitiesFuture(chunk, llmNerMessage.getEntityAiDescriptions()))
|
||||
.toList();
|
||||
|
||||
log.info("Awaiting {} api calls for {}", entityFutures.size(), llmNerMessage.getIdentifier());
|
||||
for (CompletableFuture<EntitiesWithUsage> entityFuture : entityFutures) {
|
||||
try {
|
||||
EntitiesWithUsage entitiesWithUsage = entityFuture.get();
|
||||
allEntities.addAll(entitiesWithUsage.entities());
|
||||
completionTokenCount += entitiesWithUsage.completionTokens();
|
||||
promptTokenCount += entitiesWithUsage.promptTokens();
|
||||
} catch (Exception e) {
|
||||
log.error(e.getMessage(), e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.debug("Storing files for {}", llmNerMessage.getIdentifier());
|
||||
storageService.storeJSONObject(TenantContext.getTenantId(), llmNerMessage.getResultStorageId(), new LlmNerEntities(allEntities));
|
||||
long duration = System.currentTimeMillis() - start;
|
||||
log.info("Found {} named entities for {} in {} with {} prompt tokens and {} completion tokens.",
|
||||
allEntities.size(),
|
||||
llmNerMessage.getIdentifier(),
|
||||
FormattingUtils.humanizeDuration(duration),
|
||||
promptTokenCount,
|
||||
completionTokenCount);
|
||||
return new Usage(completionTokenCount, promptTokenCount, duration);
|
||||
}
|
||||
|
||||
|
||||
private CompletableFuture<EntitiesWithUsage> getLlmNerEntitiesFuture(Chunk chunk, List<EntityAiDescription> entityAiDescriptions) {
|
||||
|
||||
return CompletableFuture.supplyAsync(() -> getLlmNerEntities(chunk, entityAiDescriptions));
|
||||
}
|
||||
|
||||
|
||||
@SneakyThrows
|
||||
private EntitiesWithUsage getLlmNerEntities(Chunk chunk, List<EntityAiDescription> entityAiDescriptions) {
|
||||
|
||||
log.debug("Sending request with text of length {}", chunk.markdown().length());
|
||||
long start = System.currentTimeMillis();
|
||||
|
||||
String nerPrompt = SystemMessageProvider.createNerPrompt(entityAiDescriptions);
|
||||
|
||||
ChatCompletions chatCompletions = runLLM(nerPrompt, chunk.markdown());
|
||||
log.debug("Got response back, used {} prompt tokens, {} completion tokens, took {}",
|
||||
chatCompletions.getUsage().getPromptTokens(),
|
||||
chatCompletions.getUsage().getCompletionTokens(),
|
||||
FormattingUtils.humanizeDuration(System.currentTimeMillis() - start));
|
||||
|
||||
EntitiesWithUsage entitiesWithUsage;
|
||||
|
||||
try {
|
||||
entitiesWithUsage = mapEntitiesToDocument(chatCompletions, chunk.parts());
|
||||
} catch (JsonProcessingException e) {
|
||||
String faultyResponse = chatCompletions.getChoices().get(0).getMessage().getContent();
|
||||
|
||||
ChatCompletions correctionCompletions = runLLM(SystemMessageProvider.PROMPT_CORRECTION, faultyResponse);
|
||||
try {
|
||||
entitiesWithUsage = mapEntitiesToDocument(correctionCompletions, chunk.parts());
|
||||
|
||||
entitiesWithUsage = new EntitiesWithUsage(entitiesWithUsage.entities(),
|
||||
entitiesWithUsage.promptTokens() + chatCompletions.getUsage().getPromptTokens(),
|
||||
entitiesWithUsage.completionTokens() + chatCompletions.getUsage().getCompletionTokens());
|
||||
|
||||
} catch (JsonProcessingException ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
return entitiesWithUsage;
|
||||
}
|
||||
|
||||
|
||||
public ChatCompletions runLLM(String prompt, String input) throws InterruptedException {
|
||||
|
||||
List<ChatRequestMessage> chatMessages = new ArrayList<>();
|
||||
chatMessages.add(new ChatRequestSystemMessage(prompt));
|
||||
chatMessages.add(new ChatRequestUserMessage(input));
|
||||
ChatCompletionsOptions options = new ChatCompletionsOptions(chatMessages);
|
||||
options.setResponseFormat(new ChatCompletionsJsonResponseFormat());
|
||||
options.setTemperature(0.0);
|
||||
options.setN(1); // only return one choice
|
||||
return llmRessource.getChatCompletions(options);
|
||||
}
|
||||
|
||||
|
||||
private EntitiesWithUsage mapEntitiesToDocument(ChatCompletions chatCompletions, List<TextBlock> chunkParts) throws JsonProcessingException {
|
||||
|
||||
EntitiesWithUsage allEntities = new EntitiesWithUsage(new LinkedList<>(), chatCompletions.getUsage().getPromptTokens(), chatCompletions.getUsage().getCompletionTokens());
|
||||
|
||||
if (!chatCompletions.getChoices().isEmpty()) {
|
||||
ChatChoice choice = chatCompletions.getChoices().get(0);
|
||||
Map<String, List<String>> entitiesPerType = parseResponse(choice);
|
||||
|
||||
List<LlmNerEntity> entitiesFromResponse = entitiesPerType.entrySet()
|
||||
.stream()
|
||||
.flatMap(entitiesWithType -> entitiesWithType.getValue()
|
||||
.stream()
|
||||
.distinct()
|
||||
.flatMap(entity -> findInChunks(entity, chunkParts, entitiesWithType.getKey())))
|
||||
.toList();
|
||||
|
||||
allEntities.entities().addAll(entitiesFromResponse);
|
||||
}
|
||||
return allEntities;
|
||||
}
|
||||
|
||||
|
||||
private Map<String, List<String>> parseResponse(ChatChoice choice) throws JsonProcessingException {
|
||||
|
||||
String response = choice.getMessage().getContent();
|
||||
return mapper.readValue(response, new TypeReference<>() {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private Stream<LlmNerEntity> findInChunks(String entity, List<TextBlock> chunkParts, String type) {
|
||||
|
||||
Pattern entityPattern = Pattern.compile(String.format("(?:\\b|\\s)(%s)(?:\\b|\\s)", Pattern.quote(entity)));
|
||||
for (TextBlock chunkPart : chunkParts) {
|
||||
String searchText = chunkPart.getSearchText();
|
||||
Matcher matcher = entityPattern.matcher(searchText);
|
||||
List<LlmNerEntity> entitiesInCurrentChunk = matcher.results()
|
||||
.map(matchResult -> new LlmNerEntity(entity,
|
||||
type,
|
||||
matchResult.start(1) + chunkPart.getTextRange().start(),
|
||||
matchResult.end(1) + chunkPart.getTextRange().start()))
|
||||
.toList();
|
||||
|
||||
if (!entitiesInCurrentChunk.stream()
|
||||
.allMatch(nerEntity -> chunkPart.subSequence(nerEntity.getStartOffset(), nerEntity.getEndOffset()).equals(nerEntity.getValue()))) {
|
||||
log.error("Entities have wrong value, expected {}, actual {}",
|
||||
entity,
|
||||
entitiesInCurrentChunk.stream()
|
||||
.map(LlmNerEntity::getValue)
|
||||
.collect(Collectors.joining(", ")));
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
if (!entitiesInCurrentChunk.isEmpty()) {
|
||||
if (entitiesInCurrentChunk.size() > 1) {
|
||||
log.debug("Multiple entities found for {}, returning all occurrences", entity);
|
||||
}
|
||||
return entitiesInCurrentChunk.stream();
|
||||
}
|
||||
}
|
||||
|
||||
log.debug("Could not find entity {} in any of the chunks", entity);
|
||||
return Stream.empty();
|
||||
}
|
||||
|
||||
|
||||
private ChunkingResponse readChunks(String chunksStorageId) {
|
||||
|
||||
return storageService.readJSONObject(TenantContext.getTenantId(), chunksStorageId, ChunkingResponse.class);
|
||||
}
|
||||
|
||||
|
||||
private record EntitiesWithUsage(List<LlmNerEntity> entities, int promptTokens, int completionTokens) {
|
||||
|
||||
}
|
||||
|
||||
public record Usage(int completionTokenCount, int promptTokenCount, long durationMillis) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,69 @@
|
||||
package com.knecon.fforesight.llm.service.services;
|
||||
|
||||
import java.util.concurrent.Semaphore;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.azure.ai.openai.OpenAIAsyncClient;
|
||||
import com.azure.ai.openai.OpenAIClient;
|
||||
import com.azure.ai.openai.OpenAIClientBuilder;
|
||||
import com.azure.ai.openai.models.ChatCompletions;
|
||||
import com.azure.ai.openai.models.ChatCompletionsOptions;
|
||||
import com.azure.core.credential.KeyCredential;
|
||||
import com.knecon.fforesight.llm.service.LlmServiceSettings;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.experimental.FieldDefaults;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
|
||||
public class LlmRessource {
|
||||
|
||||
OpenAIAsyncClient asyncClient;
|
||||
OpenAIClient client;
|
||||
LlmServiceSettings settings;
|
||||
Semaphore concurrencyLimiter;
|
||||
|
||||
|
||||
public LlmRessource(LlmServiceSettings settings) {
|
||||
|
||||
this.settings = settings;
|
||||
this.concurrencyLimiter = new Semaphore(settings.getConcurrency());
|
||||
this.asyncClient = new OpenAIClientBuilder().credential(new KeyCredential(settings.getAzureOpenAiKey())).endpoint(settings.getAzureOpenAiEndpoint()).buildAsyncClient();
|
||||
this.client = new OpenAIClientBuilder().credential(new KeyCredential(settings.getAzureOpenAiKey())).endpoint(settings.getAzureOpenAiEndpoint()).buildClient();
|
||||
log.info("Initialized client for endpoint {} and key {}", settings.getAzureOpenAiEndpoint(), settings.getAzureOpenAiKey());
|
||||
}
|
||||
|
||||
|
||||
public Flux<ChatCompletions> getChatCompletionsFlux(ChatCompletionsOptions options) {
|
||||
|
||||
options.setStream(true);
|
||||
return asyncClient.getChatCompletionsStream(settings.getModel(), options);
|
||||
}
|
||||
|
||||
|
||||
public ChatCompletions getChatCompletions(ChatCompletionsOptions options) throws InterruptedException {
|
||||
|
||||
try {
|
||||
concurrencyLimiter.acquire();
|
||||
return client.getChatCompletions(settings.getModel(), options);
|
||||
} finally {
|
||||
concurrencyLimiter.release();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void resetConcurrencyLimiter() {
|
||||
|
||||
int currentPermits = concurrencyLimiter.availablePermits();
|
||||
if (currentPermits > settings.getConcurrency()) {
|
||||
concurrencyLimiter.acquireUninterruptibly(currentPermits - settings.getConcurrency());
|
||||
} else if (currentPermits < settings.getConcurrency()) {
|
||||
concurrencyLimiter.release(settings.getConcurrency() - currentPermits);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,53 @@
|
||||
package com.knecon.fforesight.llm.service.services;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.azure.ai.openai.models.ChatCompletions;
|
||||
import com.azure.ai.openai.models.ChatCompletionsOptions;
|
||||
import com.azure.ai.openai.models.ChatRequestMessage;
|
||||
import com.azure.ai.openai.models.ChatRequestSystemMessage;
|
||||
import com.azure.ai.openai.models.ChatRequestUserMessage;
|
||||
import com.knecon.fforesight.llm.service.ChatEvent;
|
||||
import com.knecon.fforesight.llm.service.SystemMessageProvider;
|
||||
import com.knecon.fforesight.tenantcommons.TenantContext;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.experimental.FieldDefaults;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
|
||||
public class LlmService {
|
||||
|
||||
WebSocketMessagingTemplate websocketTemplate;
|
||||
LlmRessource llmRessource;
|
||||
|
||||
|
||||
@SneakyThrows
|
||||
public void rulesCopilot(List<String> prompt, String userId) {
|
||||
|
||||
List<ChatRequestMessage> chatMessages = new ArrayList<>();
|
||||
chatMessages.add(new ChatRequestSystemMessage(SystemMessageProvider.RULES_CO_PILOT));
|
||||
chatMessages.addAll(prompt.stream()
|
||||
.map(ChatRequestUserMessage::new)
|
||||
.toList());
|
||||
ChatCompletionsOptions options = new ChatCompletionsOptions(chatMessages);
|
||||
Flux<ChatCompletions> chatCompletions = llmRessource.getChatCompletionsFlux(options);
|
||||
chatCompletions.subscribe(chatCompletion -> sendRulesCopilotEvent(userId, chatCompletion.getChoices().get(0).getDelta().getContent()));
|
||||
}
|
||||
|
||||
|
||||
private void sendRulesCopilotEvent(String userId, String token) {
|
||||
|
||||
websocketTemplate.sendEvent(userId, "/queue/" + TenantContext.getTenantId() + "/rules-copilot", new ChatEvent(token));
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
package com.knecon.fforesight.llm.service.services;
|
||||
|
||||
public interface WebSocketMessagingTemplate {
|
||||
|
||||
void sendEvent(String userId, String token, Object payload);
|
||||
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
package com.knecon.fforesight.llm.service.utils;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
|
||||
@UtilityClass
|
||||
public class FormattingUtils {
|
||||
|
||||
public String humanizeDuration(long duration) {
|
||||
|
||||
if (duration < 1000) {
|
||||
return duration + " ms";
|
||||
} else if (duration < 60 * 1000) {
|
||||
double seconds = duration / 1000.0;
|
||||
return String.format("%.1f s", seconds);
|
||||
} else if (duration < 60 * 60 * 1000) {
|
||||
long minutes = duration / (60 * 1000);
|
||||
long remainingMillis = duration % (60 * 1000);
|
||||
long seconds = remainingMillis / 1000;
|
||||
return String.format("%d:%d m", minutes, seconds);
|
||||
} else {
|
||||
long hours = duration / (60 * 60 * 1000);
|
||||
long remainingMillis = duration % (60 * 60 * 1000);
|
||||
long minutes = remainingMillis / (60 * 1000);
|
||||
remainingMillis = remainingMillis % (60 * 1000);
|
||||
long seconds = remainingMillis / 1000;
|
||||
return String.format("%d:%d:%d h", hours, minutes, seconds);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,48 @@
|
||||
package com.knecon.fforesight.llm.service.utils;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
|
||||
@UtilityClass
|
||||
public class StorageIdUtils {
|
||||
|
||||
public static final String INVALID_STORAGE_ID_FORMAT = "Invalid storageId format";
|
||||
|
||||
|
||||
public String getStorageId(String dossierId, String fileId, String fileName, String fileExtension) {
|
||||
|
||||
return dossierId + "/" + fileId + "." + fileName + fileExtension;
|
||||
}
|
||||
|
||||
|
||||
public static StorageInfo parseStorageId(String storageId) {
|
||||
|
||||
String[] parts = storageId.split("/", 2);
|
||||
|
||||
if (parts.length < 2) {
|
||||
throw new IllegalArgumentException(INVALID_STORAGE_ID_FORMAT);
|
||||
}
|
||||
|
||||
String dossierId = parts[0];
|
||||
String fileAndType = parts[1];
|
||||
|
||||
String[] fileParts = fileAndType.split("\\.");
|
||||
|
||||
if (fileParts.length < 3) {
|
||||
throw new IllegalArgumentException(INVALID_STORAGE_ID_FORMAT);
|
||||
}
|
||||
|
||||
String fileId = fileParts[0];
|
||||
String fileTypeExtension = fileParts[fileParts.length - 1];
|
||||
String fileTypeName = String.join(".", Arrays.copyOfRange(fileParts, 1, fileParts.length - 1));
|
||||
|
||||
return new StorageInfo(dossierId, fileId, fileTypeName, fileTypeExtension);
|
||||
}
|
||||
|
||||
|
||||
public record StorageInfo(String dossierId, String fileId, String fileTypeName, String fileTypeExtension) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
76
llm-service/llm-service-server/build.gradle.kts
Normal file
76
llm-service/llm-service-server/build.gradle.kts
Normal file
@ -0,0 +1,76 @@
|
||||
import org.springframework.boot.gradle.tasks.bundling.BootBuildImage
|
||||
|
||||
plugins {
|
||||
application
|
||||
id("com.knecon.fforesight.service.java-conventions")
|
||||
id("org.springframework.boot") version "3.2.3"
|
||||
id("io.spring.dependency-management") version "1.1.7"
|
||||
id("org.sonarqube") version "4.4.1.3373"
|
||||
id("io.freefair.lombok") version "8.12.1"
|
||||
}
|
||||
|
||||
configurations {
|
||||
all {
|
||||
exclude(group = "org.springframework.boot", module = "spring-boot-starter-logging")
|
||||
exclude(group = "commons-logging", module = "commons-logging")
|
||||
}
|
||||
}
|
||||
|
||||
val springBootVersion = "3.2.2"
|
||||
val springCloudVersion = "2022.0.5"
|
||||
val springSecurityVersion = "6.4.2"
|
||||
val testcontainersVersion = "1.20.0"
|
||||
|
||||
dependencies {
|
||||
implementation(project(":llm-service-api"))
|
||||
implementation(project(":llm-service-processor"))
|
||||
|
||||
implementation("org.springframework.boot:spring-boot-starter-actuator:$springBootVersion")
|
||||
implementation("org.springframework.boot:spring-boot-starter-amqp:$springBootVersion")
|
||||
implementation("org.springframework.boot:spring-boot-starter-web:$springBootVersion")
|
||||
implementation("io.github.openfeign:feign-core:12.5")
|
||||
implementation("org.springframework.cloud:spring-cloud-starter-openfeign:4.2.0")
|
||||
implementation("org.springframework.boot:spring-boot-starter-websocket:$springBootVersion")
|
||||
implementation("org.springframework.security:spring-security-messaging:$springSecurityVersion")
|
||||
implementation("com.iqser.red.commons:storage-commons:2.51.0")
|
||||
implementation("com.knecon.fforesight:keycloak-commons:0.30.0") {
|
||||
exclude(group = "com.knecon.fforesight", module = "tenant-commons")
|
||||
}
|
||||
implementation("com.knecon.fforesight:tracing-commons:0.5.0")
|
||||
implementation("com.knecon.fforesight:lifecycle-commons:0.7.0")
|
||||
|
||||
implementation("com.knecon.fforesight:tenant-commons:0.31.0")
|
||||
implementation("com.knecon.fforesight:swagger-commons:0.7.0")
|
||||
implementation("ch.qos.logback:logback-classic")
|
||||
|
||||
developmentOnly("org.springframework.boot:spring-boot-devtools:$springBootVersion")
|
||||
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor:$springBootVersion")
|
||||
testImplementation("org.springframework.boot:spring-boot-starter-test:$springBootVersion")
|
||||
testImplementation("org.springframework.amqp:spring-rabbit-test:$springBootVersion")
|
||||
}
|
||||
|
||||
|
||||
tasks.named<BootBuildImage>("bootBuildImage") {
|
||||
|
||||
|
||||
environment.put("BPE_DELIM_JAVA_TOOL_OPTIONS", " ")
|
||||
environment.put("BPE_APPEND_JAVA_TOOL_OPTIONS", "-Dfile.encoding=UTF-8")
|
||||
|
||||
imageName.set("nexus.knecon.com:5001/ff/${project.name}:${project.version}")
|
||||
if (project.hasProperty("buildbootDockerHostNetwork")) {
|
||||
network.set("host")
|
||||
}
|
||||
docker {
|
||||
if (project.hasProperty("buildbootDockerHostNetwork")) {
|
||||
bindHostToBuilder.set(true)
|
||||
}
|
||||
verboseLogging.set(true)
|
||||
|
||||
publishRegistry {
|
||||
username.set(providers.gradleProperty("mavenUser").getOrNull())
|
||||
password.set(providers.gradleProperty("mavenPassword").getOrNull())
|
||||
email.set(providers.gradleProperty("mavenEmail").getOrNull())
|
||||
url.set("https://nexus.knecon.com:5001/")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -17,7 +17,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
@Slf4j
|
||||
@EnableWebMvc
|
||||
@EnableAsync
|
||||
@Import({StorageAutoConfiguration.class})
|
||||
@Import({StorageAutoConfiguration.class, LlmServiceConfiguration.class})
|
||||
@ImportAutoConfiguration({StorageAutoConfiguration.class, MultiTenancyAutoConfiguration.class, SpringDocAutoConfiguration.class, DefaultKeyCloakCommonsAutoConfiguration.class})
|
||||
@SpringBootApplication
|
||||
public class Application {
|
||||
@ -5,7 +5,7 @@ import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import com.knecon.fforesight.llm.service.api.ErrorMessage;
|
||||
import com.knecon.fforesight.llm.service.ErrorMessage;
|
||||
|
||||
@RestControllerAdvice
|
||||
public class ControllerAdvice {
|
||||
@ -6,7 +6,7 @@ import org.springframework.messaging.handler.annotation.MessageMapping;
|
||||
import org.springframework.messaging.handler.annotation.Payload;
|
||||
import org.springframework.stereotype.Controller;
|
||||
|
||||
import com.knecon.fforesight.llm.service.api.model.PromptList;
|
||||
import com.knecon.fforesight.llm.service.PromptList;
|
||||
import com.knecon.fforesight.llm.service.services.LlmService;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@ -0,0 +1,82 @@
|
||||
package com.knecon.fforesight.llm.service.queue;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.amqp.core.Message;
|
||||
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
|
||||
import org.springframework.amqp.rabbit.annotation.RabbitListener;
|
||||
import org.springframework.amqp.rabbit.core.RabbitTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.knecon.fforesight.llm.service.LlmNerMessage;
|
||||
import com.knecon.fforesight.llm.service.LlmNerResponseMessage;
|
||||
import com.knecon.fforesight.llm.service.QueueNames;
|
||||
import com.knecon.fforesight.llm.service.services.LlmNerService;
|
||||
import com.knecon.fforesight.tenantcommons.TenantContext;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.experimental.FieldDefaults;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
|
||||
public class MessageHandler {
|
||||
|
||||
public static final String LLM_NER_REQUEST_LISTENER_ID = "llm-ner-request-listener";
|
||||
private final static String X_PIPELINE_PREFIX = "X-PIPE-";
|
||||
|
||||
LlmNerService llmNerService;
|
||||
ObjectMapper mapper;
|
||||
RabbitTemplate rabbitTemplate;
|
||||
|
||||
|
||||
@RabbitHandler
|
||||
@RabbitListener(id = LLM_NER_REQUEST_LISTENER_ID, concurrency = "1")
|
||||
public void receiveNerRequest(Message message) {
|
||||
|
||||
LlmNerMessage llmNerMessage = parseLlmNerMessage(message);
|
||||
|
||||
log.info("Starting NER with LLM for {}", llmNerMessage.getIdentifier());
|
||||
|
||||
LlmNerService.Usage usage = llmNerService.runNer(llmNerMessage);
|
||||
|
||||
LlmNerResponseMessage llmNerResponseMessage = new LlmNerResponseMessage(llmNerMessage.getIdentifier(),
|
||||
usage.promptTokenCount(),
|
||||
usage.completionTokenCount(),
|
||||
Math.toIntExact(usage.durationMillis()),
|
||||
llmNerMessage.getAiCreationVersion());
|
||||
log.info("LLM NER finished for {}", llmNerMessage.getIdentifier());
|
||||
sendFinishedMessage(llmNerResponseMessage, message);
|
||||
}
|
||||
|
||||
|
||||
public void sendFinishedMessage(LlmNerResponseMessage llmNerResponseMessage, Message message) {
|
||||
|
||||
rabbitTemplate.convertAndSend(QueueNames.LLM_NER_RESPONSE_EXCHANGE, TenantContext.getTenantId(), llmNerResponseMessage, m -> {
|
||||
var forwardHeaders = message.getMessageProperties().getHeaders().entrySet()
|
||||
.stream()
|
||||
.filter(e -> e.getKey().toUpperCase(Locale.ROOT).startsWith(X_PIPELINE_PREFIX))
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||
m.getMessageProperties().getHeaders().putAll(forwardHeaders);
|
||||
return m;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private LlmNerMessage parseLlmNerMessage(Message message) {
|
||||
|
||||
try {
|
||||
return mapper.readValue(message.getBody(), LlmNerMessage.class);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to parse LLM NER message:\n {}", new String(message.getBody()));
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
package com.knecon.fforesight.llm.service.queue;
|
||||
|
||||
import static com.knecon.fforesight.llm.service.QueueNames.LLM_NER_DLQ;
|
||||
import static com.knecon.fforesight.llm.service.QueueNames.LLM_NER_REQUEST_EXCHANGE;
|
||||
|
||||
import org.springframework.amqp.core.DirectExchange;
|
||||
import org.springframework.amqp.core.Queue;
|
||||
import org.springframework.amqp.core.QueueBuilder;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@Configuration
|
||||
@RequiredArgsConstructor
|
||||
public class MessagingConfiguration {
|
||||
|
||||
|
||||
@Bean
|
||||
public DirectExchange llmNerRequestExchange() {
|
||||
|
||||
return new DirectExchange(LLM_NER_REQUEST_EXCHANGE);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Queue llmNerDLQ() {
|
||||
|
||||
return QueueBuilder.durable(LLM_NER_DLQ).build();
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
package com.knecon.fforesight.llm.service.queue;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import com.knecon.fforesight.llm.service.QueueNames;
|
||||
import com.knecon.fforesight.tenantcommons.model.TenantQueueProvider;
|
||||
|
||||
@Configuration
|
||||
public class TenantQueueProviderConfig {
|
||||
|
||||
@Bean
|
||||
protected TenantQueueProvider getTenantQueueConfigs() {
|
||||
|
||||
return new TenantQueueProvider(Set.of(com.knecon.fforesight.tenantcommons.model.TenantQueueConfiguration.builder()
|
||||
.listenerId(MessageHandler.LLM_NER_REQUEST_LISTENER_ID)
|
||||
.exchangeName(QueueNames.LLM_NER_REQUEST_EXCHANGE)
|
||||
.queuePrefix(QueueNames.LLM_NER_REQUEST_QUEUE_PREFIX)
|
||||
.dlqName(QueueNames.LLM_NER_DLQ)
|
||||
.arguments(Map.of("x-max-priority", 2))
|
||||
.build()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -50,7 +50,6 @@ public class WebSocketConfiguration implements WebSocketMessageBrokerConfigurer
|
||||
@Override
|
||||
public void registerStompEndpoints(StompEndpointRegistry registry) {
|
||||
|
||||
|
||||
registry.addEndpoint("/api/llm/llm-websocket").setAllowedOrigins("*");
|
||||
}
|
||||
|
||||
@ -65,14 +64,13 @@ public class WebSocketConfiguration implements WebSocketMessageBrokerConfigurer
|
||||
|
||||
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
|
||||
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
|
||||
Optional.ofNullable(accessor.getNativeHeader("Authorization"))
|
||||
.ifPresent(ah -> {
|
||||
String bearerToken = ah.get(0).replace("Bearer ", "");
|
||||
log.debug("Received bearer token {}", bearerToken);
|
||||
AuthenticationManager authenticationManager = tenantAuthenticationManagerResolver.resolve(bearerToken);
|
||||
JwtAuthenticationToken token = (JwtAuthenticationToken) authenticationManager.authenticate(new BearerTokenAuthenticationToken(bearerToken));
|
||||
accessor.setUser(token);
|
||||
});
|
||||
Optional.ofNullable(accessor.getNativeHeader("Authorization")).ifPresent(ah -> {
|
||||
String bearerToken = ah.get(0).replace("Bearer ", "");
|
||||
log.debug("Received bearer token {}", bearerToken);
|
||||
AuthenticationManager authenticationManager = tenantAuthenticationManagerResolver.resolve(bearerToken);
|
||||
JwtAuthenticationToken token = (JwtAuthenticationToken) authenticationManager.authenticate(new BearerTokenAuthenticationToken(bearerToken));
|
||||
accessor.setUser(token);
|
||||
});
|
||||
}
|
||||
return message;
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
package com.knecon.fforesight.llm.service.websocket;
|
||||
|
||||
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.knecon.fforesight.llm.service.services.WebSocketMessagingTemplate;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.experimental.FieldDefaults;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
|
||||
public class WebSocketMessagingService implements WebSocketMessagingTemplate {
|
||||
|
||||
SimpMessagingTemplate messagingTemplate;
|
||||
|
||||
|
||||
@Override
|
||||
public void sendEvent(String userId, String token, Object payload) {
|
||||
|
||||
messagingTemplate.convertAndSendToUser(userId, token, payload);
|
||||
}
|
||||
|
||||
}
|
||||
@ -63,9 +63,10 @@ public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBro
|
||||
}
|
||||
|
||||
|
||||
public boolean checkAuthAndSetTenant(JwtAuthenticationToken authentication, Message<?> message){
|
||||
public boolean checkAuthAndSetTenant(JwtAuthenticationToken authentication, Message<?> message) {
|
||||
|
||||
Optional<String> tenantId = getCurrentTenant(authentication);
|
||||
if (tenantId.isPresent()){
|
||||
if (tenantId.isPresent()) {
|
||||
TenantContext.setTenantId(tenantId.get());
|
||||
return true;
|
||||
}
|
||||
@ -84,6 +85,7 @@ public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBro
|
||||
|
||||
}
|
||||
|
||||
|
||||
private Optional<String> extractTenantId(Message<?> message) {
|
||||
|
||||
StompHeaderAccessor sha = StompHeaderAccessor.wrap(message);
|
||||
@ -21,8 +21,8 @@ spring:
|
||||
prefetch: 1
|
||||
|
||||
llm-service:
|
||||
azureOpenAiKey: "Your Azure open Api Key"
|
||||
azureOpenAiEndpoint: "Your Azure open Api Endpoint"
|
||||
azureOpenAiKey: "Your key here"
|
||||
azureOpenAiEndpoint: "https://knecon-ca-demo.openai.azure.com/"
|
||||
|
||||
fforesight:
|
||||
llm-service:
|
||||
@ -63,3 +63,13 @@ keyword-service:
|
||||
url: "http://keyword-extraction-service:8080"
|
||||
|
||||
cors.enabled: true
|
||||
|
||||
management:
|
||||
tracing:
|
||||
enabled: ${TRACING_ENABLED:false}
|
||||
sampling:
|
||||
probability: ${TRACING_PROBABILITY:1.0}
|
||||
otlp:
|
||||
tracing:
|
||||
endpoint: ${OTLP_ENDPOINT:http://otel-collector-opentelemetry-collector.otel-collector:4318/v1/traces}
|
||||
|
||||
@ -6,7 +6,9 @@ import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.springframework.amqp.rabbit.core.RabbitAdmin;
|
||||
import org.springframework.amqp.rabbit.core.RabbitTemplate;
|
||||
import org.springframework.amqp.rabbit.listener.RabbitListenerEndpointRegistry;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration;
|
||||
@ -29,6 +31,7 @@ import com.iqser.red.storage.commons.utils.FileSystemBackedStorageService;
|
||||
import com.knecon.fforesight.tenantcommons.TenantContext;
|
||||
import com.knecon.fforesight.tenantcommons.TenantsClient;
|
||||
import com.knecon.fforesight.tenantcommons.model.TenantResponse;
|
||||
import com.knecon.fforesight.tenantcommons.queue.TenantMessagingConfiguration;
|
||||
|
||||
@ComponentScan
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@ -43,16 +46,22 @@ public abstract class AbstractLlmServiceIntegrationTest {
|
||||
protected StorageService storageService;
|
||||
@MockBean
|
||||
TenantsClient tenantsClient;
|
||||
@Autowired
|
||||
ObjectMapper objectMapper;
|
||||
@MockBean
|
||||
RabbitTemplate rabbitTemplate;
|
||||
@MockBean
|
||||
RabbitAdmin rabbitAdmin;
|
||||
@MockBean
|
||||
RabbitListenerEndpointRegistry rabbitListenerEndpointRegistry;
|
||||
@MockBean
|
||||
TenantMessagingConfiguration tenantMessagingConfiguration;
|
||||
|
||||
|
||||
@BeforeEach
|
||||
public void setupOptimize() {
|
||||
|
||||
var tenant = TenantResponse.builder()
|
||||
.tenantId(TEST_TENANT)
|
||||
.build();
|
||||
var tenant = TenantResponse.builder().tenantId(TEST_TENANT).build();
|
||||
|
||||
TenantContext.setTenantId(TEST_TENANT);
|
||||
|
||||
@ -0,0 +1,108 @@
|
||||
package com.knecon.fforesight.llm.service;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import com.knecon.fforesight.llm.service.services.LlmNerService;
|
||||
import com.knecon.fforesight.tenantcommons.TenantContext;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
|
||||
@Disabled
|
||||
public class LlmNerServiceTest extends AbstractLlmServiceIntegrationTest {
|
||||
|
||||
public static final String DOCUMENT_TEXT = "DOCUMENT_TEXT.proto";
|
||||
public static final String DOCUMENT_POSITIONS = "DOCUMENT_POSITION.proto";
|
||||
public static final String DOCUMENT_STRUCTURE = "DOCUMENT_STRUCTURE.proto";
|
||||
public static final String DOCUMENT_PAGES = "DOCUMENT_PAGES.proto";
|
||||
public static final String DOCUMENT_CHUNKS = "DOCUMENT_CHUNKS.json";
|
||||
public static final String STORAGE_ID = "08904e84-4a5a-4c15-bc13-200237af6434/4d81e891fd3e94dfe0b6c51073ef55b6.";
|
||||
@Autowired
|
||||
LlmNerService llmNerService;
|
||||
|
||||
Set<String> relevantFiles = Set.of(DOCUMENT_TEXT, DOCUMENT_POSITIONS, DOCUMENT_STRUCTURE, DOCUMENT_PAGES, DOCUMENT_CHUNKS);
|
||||
|
||||
|
||||
@Test
|
||||
@SneakyThrows
|
||||
public void testLlmNer() {
|
||||
|
||||
Path folder = Path.of("/home/kschuettler/Downloads/New Folder (5)/18299ec0-7659-496a-a44a-194bbffb1700/1fb7d49ae389469f60db516cf81a3510");
|
||||
LlmNerMessage message = prepStorage(folder);
|
||||
llmNerService.runNer(message);
|
||||
Path tmpFile = Path.of("/private/tmp", "LLM_ENTITIES", "entities.json");
|
||||
Files.createDirectories(tmpFile.getParent());
|
||||
storageService.downloadTo(TEST_TENANT, message.getResultStorageId(), tmpFile.toFile());
|
||||
}
|
||||
|
||||
|
||||
private LlmNerMessage prepStorage(Path folder) throws IOException {
|
||||
|
||||
LlmNerMessage message = buildMessage(folder);
|
||||
Files.walk(folder)
|
||||
.filter(path -> path.toFile().isFile())
|
||||
.filter(path -> relevantFiles.stream()
|
||||
.anyMatch(filePath -> path.getFileName().toString().contains(filePath)))
|
||||
.forEach(relevantFile -> storeFile(relevantFile, folder));
|
||||
return message;
|
||||
}
|
||||
|
||||
|
||||
@SneakyThrows
|
||||
private void storeFile(Path relevantFile, Path folder) {
|
||||
|
||||
try (var in = new FileInputStream(relevantFile.toFile())) {
|
||||
storageService.storeObject(TenantContext.getTenantId(),
|
||||
STORAGE_ID + relevantFiles.stream()
|
||||
.filter(filePath -> relevantFile.getFileName().toString().contains(filePath))
|
||||
.findFirst()
|
||||
.orElseThrow(),
|
||||
in);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static LlmNerMessage buildMessage(Path folder) {
|
||||
|
||||
List<EntityAiDescription> entityAiDescriptions = new ArrayList<>();
|
||||
|
||||
// Add descriptions for each entity type with examples
|
||||
entityAiDescriptions.add(new EntityAiDescription("PERSON",
|
||||
"A PERSON is any name referring to a human, excluding named methods (e.g., 'Klingbeil Test' is not a name). Each name should be its own entity, but first name, last name, and possibly middle name should be merged. Numbers are never part of a name. "
|
||||
+ "For example: 'Jennifer Durando, BS', 'Charlène Hernandez', 'Shaw A.', 'G J J Lubbe'."));
|
||||
entityAiDescriptions.add(new EntityAiDescription("PII",
|
||||
"PII refers to personally identifiable information such as email addresses, telephone numbers, fax numbers, or any other information that could uniquely identify an individual. "
|
||||
+ "For example: '01223 45678', 'mimi.lang@smithcorp.com', '+44 (0)1252 392460'."));
|
||||
entityAiDescriptions.add(new EntityAiDescription("ADDRESS",
|
||||
"An ADDRESS describes a real-life location. It should be as complete as possible and may include elements such as street address, city, state, postal code, and country. "
|
||||
+ "For example: 'Product Safety Labs 2394 US Highway 130 Dayton, NJ 08810 USA', 'Syngenta Crop Protection, LLC 410 Swing Road Post Office Box 18300 Greensboro, NC 27419-8300 USA'."));
|
||||
entityAiDescriptions.add(new EntityAiDescription("COMPANY",
|
||||
"A COMPANY is any corporate entity or approving body mentioned in the text, excluding companies mentioned as part of an address. "
|
||||
+ "For example: 'Syngenta', 'EFSA'."));
|
||||
entityAiDescriptions.add(new EntityAiDescription("COUNTRY",
|
||||
"A COUNTRY is any recognized nation mentioned in the text. Countries mentioned as part of an address should not be listed separately. "
|
||||
+ "For example: 'USA'."));
|
||||
|
||||
return LlmNerMessage.builder()
|
||||
.identifier(Map.of("file", folder.getFileName().toString()))
|
||||
.entityAiDescriptions(entityAiDescriptions)
|
||||
.chunksStorageId(STORAGE_ID + DOCUMENT_CHUNKS)
|
||||
.documentPagesStorageId(STORAGE_ID + DOCUMENT_PAGES)
|
||||
.documentTextStorageId(STORAGE_ID + DOCUMENT_TEXT)
|
||||
.documentPositionStorageId(STORAGE_ID + DOCUMENT_POSITIONS)
|
||||
.documentStructureStorageId(STORAGE_ID + DOCUMENT_STRUCTURE)
|
||||
.resultStorageId(STORAGE_ID + "result")
|
||||
.build();
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
package com.knecon.fforesight.llm.service;
|
||||
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import com.knecon.fforesight.llm.service.utils.StorageIdUtils;
|
||||
|
||||
public class StorageIdUtilsTest {
|
||||
|
||||
@Test
|
||||
void testParseStorageId_ValidInput() {
|
||||
StorageIdUtils.StorageInfo storageInfo = StorageIdUtils.parseStorageId("dossierId/fileId.DOCUMENT_STRUCTURE.json");
|
||||
assertEquals("dossierId", storageInfo.dossierId(), "Incorrect dossierId");
|
||||
assertEquals("fileId", storageInfo.fileId(), "Incorrect fileId");
|
||||
assertEquals("DOCUMENT_STRUCTURE", storageInfo.fileTypeName(), "Incorrect fileTypeName");
|
||||
assertEquals("json", storageInfo.fileTypeExtension(), "Incorrect fileTypeExtension");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testParseStorageId_MissingFileTypeExtension() {
|
||||
Exception exception = assertThrows(IllegalArgumentException.class, () ->
|
||||
StorageIdUtils.parseStorageId("dossierId/fileId.DOCUMENT_STRUCTURE")
|
||||
);
|
||||
assertEquals("Invalid storageId format", exception.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testParseStorageId_InvalidFormat() {
|
||||
Exception exception = assertThrows(IllegalArgumentException.class, () ->
|
||||
StorageIdUtils.parseStorageId("invalidFormat")
|
||||
);
|
||||
assertEquals("Invalid storageId format", exception.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testParseStorageId_NoDotsInFilePart() {
|
||||
Exception exception = assertThrows(IllegalArgumentException.class, () ->
|
||||
StorageIdUtils.parseStorageId("dossierId/fileId")
|
||||
);
|
||||
assertEquals("Invalid storageId format", exception.getMessage());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
server:
|
||||
port: 28080
|
||||
fforesight:
|
||||
keycloak:
|
||||
enabled: true
|
||||
springdoc:
|
||||
enabled: false
|
||||
|
||||
tenant-user-management-service.url: "http://mock.url"
|
||||
text-analysis-service.url: "http://mock.url"
|
||||
epam-poc-service.url: "http://mock.url"
|
||||
keyword-service.url: "http://mock.url"
|
||||
|
||||
|
||||
llm-service:
|
||||
azureOpenAiKey: "679b023858314dfe807e50a2e7d86d63"
|
||||
azureOpenAiEndpoint: "https://knecon-ca-demo.openai.azure.com/"
|
||||
@ -1,8 +1,45 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
dir=${PWD##*/}
|
||||
|
||||
gradle assemble
|
||||
|
||||
buildNumber=${1:-1}
|
||||
# Get the current Git branch
|
||||
branch=$(git rev-parse --abbrev-ref HEAD)
|
||||
|
||||
gradle bootBuildImage --cleanCache --publishImage -Pversion=$USER-$buildNumber
|
||||
echo "nexus.knecon.com:5001/red/${dir}-server-v1:$USER-$buildNumber"
|
||||
# Get the short commit hash (first 5 characters)
|
||||
commit_hash=$(git rev-parse --short=5 HEAD)
|
||||
|
||||
# Combine branch and commit hash
|
||||
buildName="${USER}-${branch}-${commit_hash}"
|
||||
|
||||
gradle bootBuildImage --publishImage -PbuildbootDockerHostNetwork=true -Pversion=${buildName}
|
||||
|
||||
newImageName="nexus.knecon.com:5001/ff/llm-service-server:${buildName}"
|
||||
|
||||
echo "full image name:"
|
||||
echo ${newImageName}
|
||||
echo ""
|
||||
|
||||
if [ -z "$1" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
namespace=${1}
|
||||
deployment_name="llm-service"
|
||||
|
||||
echo "deploying to ${namespace}"
|
||||
|
||||
oldImageName=$(rancher kubectl -n ${namespace} get deployment ${deployment_name} -o=jsonpath='{.spec.template.spec.containers[*].image}')
|
||||
|
||||
if [ "${newImageName}" = "${oldImageName}" ]; then
|
||||
echo "Image tag did not change, redeploying..."
|
||||
rancher kubectl rollout restart deployment ${deployment_name} -n ${namespace}
|
||||
else
|
||||
echo "upgrading the image tag..."
|
||||
rancher kubectl set image deployment/${deployment_name} ${deployment_name}=${newImageName} -n ${namespace}
|
||||
fi
|
||||
rancher kubectl rollout status deployment ${deployment_name} -n ${namespace}
|
||||
echo "Built ${deployment_name}:${buildName} and deployed to ${namespace}"
|
||||
|
||||
@ -1 +1,7 @@
|
||||
rootProject.name = "llm-service"
|
||||
rootProject.name = "llm-service"
|
||||
include(":llm-service-api")
|
||||
include(":llm-service-server")
|
||||
include(":llm-service-processor")
|
||||
project(":llm-service-api").projectDir = file("llm-service/llm-service-api")
|
||||
project(":llm-service-server").projectDir = file("llm-service/llm-service-server")
|
||||
project(":llm-service-processor").projectDir = file("llm-service/llm-service-processor")
|
||||
@ -1,21 +0,0 @@
|
||||
package com.knecon.fforesight.llm.service.queue;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class MessageHandler {
|
||||
|
||||
// @SneakyThrows
|
||||
// @RabbitHandler
|
||||
// @RabbitListener(queues = "#{llmServiceSettings.getRequestQueueName()}")
|
||||
// public void receiveIndexingRequest(Message message) {
|
||||
//
|
||||
// // TODO: Do something.
|
||||
// }
|
||||
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
package com.knecon.fforesight.llm.service.queue;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@Configuration
|
||||
@RequiredArgsConstructor
|
||||
public class MessagingConfiguration {
|
||||
|
||||
}
|
||||
@ -1,69 +0,0 @@
|
||||
package com.knecon.fforesight.llm.service.services;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.azure.ai.openai.OpenAIAsyncClient;
|
||||
import com.azure.ai.openai.OpenAIClientBuilder;
|
||||
import com.azure.ai.openai.models.ChatCompletions;
|
||||
import com.azure.ai.openai.models.ChatCompletionsOptions;
|
||||
import com.azure.ai.openai.models.ChatMessage;
|
||||
import com.azure.ai.openai.models.ChatRole;
|
||||
import com.azure.core.credential.AzureKeyCredential;
|
||||
import com.knecon.fforesight.llm.service.api.model.ChatEvent;
|
||||
import com.knecon.fforesight.llm.service.model.SystemMessages;
|
||||
import com.knecon.fforesight.llm.service.settings.LlmServiceSettings;
|
||||
import com.knecon.fforesight.tenantcommons.TenantContext;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class LlmService {
|
||||
|
||||
private final SimpMessagingTemplate websocketTemplate;
|
||||
private final LlmServiceSettings settings;
|
||||
private OpenAIAsyncClient client;
|
||||
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
|
||||
client = new OpenAIClientBuilder().credential(new AzureKeyCredential(settings.getAzureOpenAiKey())).endpoint(settings.getAzureOpenAiEndpoint()).buildAsyncClient();
|
||||
}
|
||||
|
||||
|
||||
@SneakyThrows
|
||||
public void rulesCopilot(List<String> prompt, String userId) {
|
||||
|
||||
List<ChatMessage> chatMessages = new ArrayList<>();
|
||||
chatMessages.add(new ChatMessage(ChatRole.SYSTEM, SystemMessages.RULES_CO_PILOT));
|
||||
chatMessages.addAll(prompt.stream()
|
||||
.map(p -> new ChatMessage(ChatRole.USER, p))
|
||||
.toList());
|
||||
ChatCompletionsOptions options = new ChatCompletionsOptions(chatMessages);
|
||||
options.setStream(true);
|
||||
Flux<ChatCompletions> chatCompletions = client.getChatCompletionsStream(settings.getModel(), options);
|
||||
chatCompletions.subscribe(chatCompletion -> {
|
||||
sendWebsocketEvent(userId,
|
||||
chatCompletion.getChoices()
|
||||
.get(0).getDelta().getContent());
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private void sendWebsocketEvent(String userId, String token) {
|
||||
|
||||
websocketTemplate.convertAndSendToUser(userId, "/queue/" + TenantContext.getTenantId() + "/rules-copilot", new ChatEvent(token));
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user