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'
|
- project: 'gitlab/gitlab'
|
||||||
ref: 'main'
|
ref: 'main'
|
||||||
file: 'ci-templates/gradle_java.yml'
|
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="DefaultComesLast"/>
|
||||||
<module name="EmptyStatement"/>
|
<module name="EmptyStatement"/>
|
||||||
<module name="EqualsHashCode"/>
|
<module name="EqualsHashCode"/>
|
||||||
|
<module name="ExplicitInitialization"/>
|
||||||
<module name="IllegalInstantiation"/>
|
<module name="IllegalInstantiation"/>
|
||||||
<module name="ModifiedControlVariable"/>
|
<module name="ModifiedControlVariable"/>
|
||||||
<module name="MultipleVariableDeclarations"/>
|
<module name="MultipleVariableDeclarations"/>
|
||||||
|
|||||||
@ -1,16 +1,20 @@
|
|||||||
<?xml version="1.0"?>
|
<?xml version="1.0"?>
|
||||||
<ruleset name="Custom Rules"
|
<ruleset name="Custom ruleset"
|
||||||
xmlns="http://pmd.sourceforge.net/ruleset/2.0.0"
|
xmlns="http://pmd.sourceforge.net/ruleset/2.0.0"
|
||||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
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">
|
<rule ref="category/java/errorprone.xml">
|
||||||
<exclude name="DataflowAnomalyAnalysis"/>
|
|
||||||
<exclude name="MissingSerialVersionUID"/>
|
<exclude name="MissingSerialVersionUID"/>
|
||||||
<exclude name="AvoidLiteralsInIfCondition"/>
|
<exclude name="AvoidLiteralsInIfCondition"/>
|
||||||
<exclude name="BeanMembersShouldSerialize"/>
|
|
||||||
<exclude name="AvoidDuplicateLiterals"/>
|
<exclude name="AvoidDuplicateLiterals"/>
|
||||||
|
<exclude name="NullAssignment"/>
|
||||||
|
<exclude name="AssignmentInOperand"/>
|
||||||
|
<exclude name="BeanMembersShouldSerialize"/>
|
||||||
</rule>
|
</rule>
|
||||||
|
|
||||||
</ruleset>
|
</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.AllArgsConstructor;
|
||||||
import lombok.Builder;
|
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;
|
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.ArrayList;
|
||||||
import java.util.List;
|
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.boot.context.properties.ConfigurationProperties;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
@ -7,19 +7,12 @@ import org.springframework.context.annotation.Primary;
|
|||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@Primary
|
|
||||||
@Configuration
|
|
||||||
@ConfigurationProperties("llm-service")
|
@ConfigurationProperties("llm-service")
|
||||||
public class LlmServiceSettings {
|
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 azureOpenAiKey;
|
||||||
private String azureOpenAiEndpoint;
|
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 = """
|
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.
|
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
|
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
|
@Slf4j
|
||||||
@EnableWebMvc
|
@EnableWebMvc
|
||||||
@EnableAsync
|
@EnableAsync
|
||||||
@Import({StorageAutoConfiguration.class})
|
@Import({StorageAutoConfiguration.class, LlmServiceConfiguration.class})
|
||||||
@ImportAutoConfiguration({StorageAutoConfiguration.class, MultiTenancyAutoConfiguration.class, SpringDocAutoConfiguration.class, DefaultKeyCloakCommonsAutoConfiguration.class})
|
@ImportAutoConfiguration({StorageAutoConfiguration.class, MultiTenancyAutoConfiguration.class, SpringDocAutoConfiguration.class, DefaultKeyCloakCommonsAutoConfiguration.class})
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
public class Application {
|
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.bind.annotation.RestControllerAdvice;
|
||||||
import org.springframework.web.server.ResponseStatusException;
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
import com.knecon.fforesight.llm.service.api.ErrorMessage;
|
import com.knecon.fforesight.llm.service.ErrorMessage;
|
||||||
|
|
||||||
@RestControllerAdvice
|
@RestControllerAdvice
|
||||||
public class ControllerAdvice {
|
public class ControllerAdvice {
|
||||||
@ -6,7 +6,7 @@ import org.springframework.messaging.handler.annotation.MessageMapping;
|
|||||||
import org.springframework.messaging.handler.annotation.Payload;
|
import org.springframework.messaging.handler.annotation.Payload;
|
||||||
import org.springframework.stereotype.Controller;
|
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 com.knecon.fforesight.llm.service.services.LlmService;
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
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
|
@Override
|
||||||
public void registerStompEndpoints(StompEndpointRegistry registry) {
|
public void registerStompEndpoints(StompEndpointRegistry registry) {
|
||||||
|
|
||||||
|
|
||||||
registry.addEndpoint("/api/llm/llm-websocket").setAllowedOrigins("*");
|
registry.addEndpoint("/api/llm/llm-websocket").setAllowedOrigins("*");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,14 +64,13 @@ public class WebSocketConfiguration implements WebSocketMessageBrokerConfigurer
|
|||||||
|
|
||||||
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
|
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
|
||||||
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
|
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
|
||||||
Optional.ofNullable(accessor.getNativeHeader("Authorization"))
|
Optional.ofNullable(accessor.getNativeHeader("Authorization")).ifPresent(ah -> {
|
||||||
.ifPresent(ah -> {
|
String bearerToken = ah.get(0).replace("Bearer ", "");
|
||||||
String bearerToken = ah.get(0).replace("Bearer ", "");
|
log.debug("Received bearer token {}", bearerToken);
|
||||||
log.debug("Received bearer token {}", bearerToken);
|
AuthenticationManager authenticationManager = tenantAuthenticationManagerResolver.resolve(bearerToken);
|
||||||
AuthenticationManager authenticationManager = tenantAuthenticationManagerResolver.resolve(bearerToken);
|
JwtAuthenticationToken token = (JwtAuthenticationToken) authenticationManager.authenticate(new BearerTokenAuthenticationToken(bearerToken));
|
||||||
JwtAuthenticationToken token = (JwtAuthenticationToken) authenticationManager.authenticate(new BearerTokenAuthenticationToken(bearerToken));
|
accessor.setUser(token);
|
||||||
accessor.setUser(token);
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
return message;
|
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);
|
Optional<String> tenantId = getCurrentTenant(authentication);
|
||||||
if (tenantId.isPresent()){
|
if (tenantId.isPresent()) {
|
||||||
TenantContext.setTenantId(tenantId.get());
|
TenantContext.setTenantId(tenantId.get());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -84,6 +85,7 @@ public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBro
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private Optional<String> extractTenantId(Message<?> message) {
|
private Optional<String> extractTenantId(Message<?> message) {
|
||||||
|
|
||||||
StompHeaderAccessor sha = StompHeaderAccessor.wrap(message);
|
StompHeaderAccessor sha = StompHeaderAccessor.wrap(message);
|
||||||
@ -21,8 +21,8 @@ spring:
|
|||||||
prefetch: 1
|
prefetch: 1
|
||||||
|
|
||||||
llm-service:
|
llm-service:
|
||||||
azureOpenAiKey: "Your Azure open Api Key"
|
azureOpenAiKey: "Your key here"
|
||||||
azureOpenAiEndpoint: "Your Azure open Api Endpoint"
|
azureOpenAiEndpoint: "https://knecon-ca-demo.openai.azure.com/"
|
||||||
|
|
||||||
fforesight:
|
fforesight:
|
||||||
llm-service:
|
llm-service:
|
||||||
@ -63,3 +63,13 @@ keyword-service:
|
|||||||
url: "http://keyword-extraction-service:8080"
|
url: "http://keyword-extraction-service:8080"
|
||||||
|
|
||||||
cors.enabled: true
|
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.BeforeEach;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
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.core.RabbitTemplate;
|
||||||
|
import org.springframework.amqp.rabbit.listener.RabbitListenerEndpointRegistry;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||||
import org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration;
|
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.TenantContext;
|
||||||
import com.knecon.fforesight.tenantcommons.TenantsClient;
|
import com.knecon.fforesight.tenantcommons.TenantsClient;
|
||||||
import com.knecon.fforesight.tenantcommons.model.TenantResponse;
|
import com.knecon.fforesight.tenantcommons.model.TenantResponse;
|
||||||
|
import com.knecon.fforesight.tenantcommons.queue.TenantMessagingConfiguration;
|
||||||
|
|
||||||
@ComponentScan
|
@ComponentScan
|
||||||
@ExtendWith(SpringExtension.class)
|
@ExtendWith(SpringExtension.class)
|
||||||
@ -43,16 +46,22 @@ public abstract class AbstractLlmServiceIntegrationTest {
|
|||||||
protected StorageService storageService;
|
protected StorageService storageService;
|
||||||
@MockBean
|
@MockBean
|
||||||
TenantsClient tenantsClient;
|
TenantsClient tenantsClient;
|
||||||
|
@Autowired
|
||||||
|
ObjectMapper objectMapper;
|
||||||
@MockBean
|
@MockBean
|
||||||
RabbitTemplate rabbitTemplate;
|
RabbitTemplate rabbitTemplate;
|
||||||
|
@MockBean
|
||||||
|
RabbitAdmin rabbitAdmin;
|
||||||
|
@MockBean
|
||||||
|
RabbitListenerEndpointRegistry rabbitListenerEndpointRegistry;
|
||||||
|
@MockBean
|
||||||
|
TenantMessagingConfiguration tenantMessagingConfiguration;
|
||||||
|
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
public void setupOptimize() {
|
public void setupOptimize() {
|
||||||
|
|
||||||
var tenant = TenantResponse.builder()
|
var tenant = TenantResponse.builder().tenantId(TEST_TENANT).build();
|
||||||
.tenantId(TEST_TENANT)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
TenantContext.setTenantId(TEST_TENANT);
|
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
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
dir=${PWD##*/}
|
dir=${PWD##*/}
|
||||||
|
|
||||||
gradle assemble
|
gradle assemble
|
||||||
|
|
||||||
buildNumber=${1:-1}
|
# Get the current Git branch
|
||||||
|
branch=$(git rev-parse --abbrev-ref HEAD)
|
||||||
|
|
||||||
gradle bootBuildImage --cleanCache --publishImage -Pversion=$USER-$buildNumber
|
# Get the short commit hash (first 5 characters)
|
||||||
echo "nexus.knecon.com:5001/red/${dir}-server-v1:$USER-$buildNumber"
|
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