Compare commits

...

103 Commits

Author SHA1 Message Date
Renovate Bot
f27f0cdf6e Merge branch 'renovate/main-plugins-(non-major)' into 'main'
Update plugin io.freefair.lombok to v8.12.1 (main)

See merge request fforesight/llm-service!69
2025-02-03 20:06:09 +01:00
Renovate Bot
a02e9ed917 Update plugin io.freefair.lombok to v8.12.1 2025-02-03 18:05:34 +00:00
Renovate Bot
c607b1728c Merge branch 'renovate/main-com.knecon.fforesight-document-4.x' into 'main'
Update dependency com.knecon.fforesight:document to v4.445.0 (main)

See merge request fforesight/llm-service!68
2025-01-24 20:08:19 +01:00
Renovate Bot
f6574deb61 Update dependency com.knecon.fforesight:document to v4.445.0 2025-01-24 18:05:34 +00:00
Renovate Bot
456d9bc1f5 Merge branch 'renovate/main-plugins-(non-major)' into 'main'
Update plugin io.freefair.lombok to v8.12 (main)

See merge request fforesight/llm-service!67
2025-01-22 21:06:08 +01:00
Renovate Bot
613e818c6a Update plugin io.freefair.lombok to v8.12 2025-01-22 19:05:49 +00:00
Renovate Bot
a9125c6a16 Merge branch 'renovate/main-spring-amqp' into 'main'
Update dependency org.springframework.amqp:spring-rabbit-test to v3.2.2 (main)

See merge request fforesight/llm-service!66
2025-01-21 21:05:34 +01:00
Renovate Bot
cb4cd51dba Update dependency org.springframework.amqp:spring-rabbit-test to v3.2.2 2025-01-21 19:06:11 +00:00
Renovate Bot
646f3ff862 Merge branch 'renovate/main-com.google.protobuf-protobuf-java-4.x' into 'main'
Update dependency com.google.protobuf:protobuf-java to v4.29.3 (main)

See merge request fforesight/llm-service!65
2025-01-09 00:05:18 +01:00
Renovate Bot
1adb161c7b Update dependency com.google.protobuf:protobuf-java to v4.29.3 2025-01-08 22:05:27 +00:00
Renovate Bot
93b292bcff Merge branch 'renovate/main-com.knecon.fforesight-document-4.x' into 'main'
Update dependency com.knecon.fforesight:document to v4.444.0 (main)

See merge request fforesight/llm-service!64
2025-01-08 20:05:20 +01:00
Renovate Bot
5093c2db82 Update dependency com.knecon.fforesight:document to v4.444.0 2025-01-08 18:05:38 +00:00
Renovate Bot
694590768b Merge branch 'renovate/main-ch.qos.logback-logback-classic-1.x' into 'main'
Update dependency ch.qos.logback:logback-classic to v1.5.16 (main)

See merge request fforesight/llm-service!63
2025-01-06 01:05:35 +01:00
Renovate Bot
c4143539a7 Update dependency ch.qos.logback:logback-classic to v1.5.16 2025-01-05 23:05:29 +00:00
Renovate Bot
30ef0ec59f Merge branch 'renovate/main-ch.qos.logback-logback-classic-1.x' into 'main'
Update dependency ch.qos.logback:logback-classic to v1.5.15 (main)

See merge request fforesight/llm-service!62
2024-12-21 20:07:23 +01:00
Renovate Bot
fb4e504b47 Update dependency ch.qos.logback:logback-classic to v1.5.15 2024-12-21 18:05:38 +00:00
Renovate Bot
792808a03c Merge branch 'renovate/main-ch.qos.logback-logback-classic-1.x' into 'main'
Update dependency ch.qos.logback:logback-classic to v1.5.14 (main)

See merge request fforesight/llm-service!61
2024-12-19 20:05:45 +01:00
Renovate Bot
52bf7b834b Update dependency ch.qos.logback:logback-classic to v1.5.14 2024-12-19 18:05:57 +00:00
Renovate Bot
0a993ce053 Merge branch 'renovate/main-com.google.protobuf-protobuf-java-4.x' into 'main'
Update dependency com.google.protobuf:protobuf-java to v4.29.2 (main)

See merge request fforesight/llm-service!60
2024-12-18 21:05:48 +01:00
Renovate Bot
a05a1a12b9 Update dependency com.google.protobuf:protobuf-java to v4.29.2 2024-12-18 19:05:35 +00:00
Renovate Bot
d980dbe853 Merge branch 'renovate/main-ch.qos.logback-logback-classic-1.x' into 'main'
Update dependency ch.qos.logback:logback-classic to v1.5.13 (main)

See merge request fforesight/llm-service!59
2024-12-18 20:05:17 +01:00
Renovate Bot
d0b3f07aaa Update dependency ch.qos.logback:logback-classic to v1.5.13 2024-12-18 18:07:53 +00:00
Renovate Bot
3b00f85e72 Merge branch 'renovate/main-plugins-(non-major)' into 'main'
Update plugin io.spring.dependency-management to v1.1.7 (main)

See merge request fforesight/llm-service!58
2024-12-17 20:06:04 +01:00
Renovate Bot
f14a4c9cb1 Update plugin io.spring.dependency-management to v1.1.7 2024-12-17 18:05:49 +00:00
Renovate Bot
9380ddd7c7 Merge branch 'renovate/main-spring-amqp' into 'main'
Update dependency org.springframework.amqp:spring-rabbit-test to v3.2.1 (main)

See merge request fforesight/llm-service!57
2024-12-16 22:05:32 +01:00
Renovate Bot
d7b2d41653 Update dependency org.springframework.amqp:spring-rabbit-test to v3.2.1 2024-12-16 20:05:30 +00:00
Renovate Bot
ba0f8e681b Merge branch 'renovate/main-spring-security' into 'main'
Update dependency org.springframework.security:spring-security-messaging to v6.4.2 (main)

See merge request fforesight/llm-service!56
2024-12-16 20:05:26 +01:00
Renovate Bot
51833879fa Update dependency org.springframework.security:spring-security-messaging to v6.4.2 2024-12-16 18:05:37 +00:00
Renovate Bot
af9c6727b4 Merge branch 'renovate/main-com.knecon.fforesight-document-4.x' into 'main'
Update dependency com.knecon.fforesight:document to v4.443.0 (main)

See merge request fforesight/llm-service!55
2024-12-13 20:06:01 +01:00
Renovate Bot
9fa2a90f8d Update dependency com.knecon.fforesight:document to v4.443.0 2024-12-13 18:05:50 +00:00
Renovate Bot
45b5568e29 Merge branch 'renovate/main-com.knecon.fforesight-document-4.x' into 'main'
Update dependency com.knecon.fforesight:document to v4.442.0 (main)

See merge request fforesight/llm-service!54
2024-12-12 20:05:57 +01:00
Renovate Bot
31b3717a2a Update dependency com.knecon.fforesight:document to v4.442.0 2024-12-12 18:05:42 +00:00
Renovate Bot
51d8280ba8 Merge branch 'renovate/main-com.knecon.fforesight-document-4.x' into 'main'
Update dependency com.knecon.fforesight:document to v4.441.0 (main)

See merge request fforesight/llm-service!53
2024-12-10 20:06:16 +01:00
Renovate Bot
316ebfdac0 Update dependency com.knecon.fforesight:document to v4.441.0 2024-12-10 18:06:05 +00:00
Renovate Bot
a7e7d589b2 Merge branch 'renovate/main-com.google.protobuf-protobuf-java-4.x' into 'main'
Update dependency com.google.protobuf:protobuf-java to v4.29.1 (main)

See merge request fforesight/llm-service!51
2024-12-04 23:05:24 +01:00
Renovate Bot
543b00713e Update dependency com.google.protobuf:protobuf-java to v4.29.1 2024-12-04 21:05:25 +00:00
Renovate Bot
f85c2b6df2 Merge branch 'renovate/main-com.knecon.fforesight-document-4.x' into 'main'
Update dependency com.knecon.fforesight:document to v4.440.0 (main)

See merge request fforesight/llm-service!50
2024-12-04 20:23:10 +01:00
Renovate Bot
e5e2d9c33b Update dependency com.knecon.fforesight:document to v4.440.0 2024-12-04 18:05:52 +00:00
Renovate Bot
c3b8a7bb1f Merge branch 'renovate/main-spring-cloud' into 'main'
Update dependency org.springframework.cloud:spring-cloud-starter-openfeign to v4.2.0 (main)

See merge request fforesight/llm-service!49
2024-12-03 20:05:46 +01:00
Renovate Bot
cccc10c15e Update dependency org.springframework.cloud:spring-cloud-starter-openfeign to v4.2.0 2024-12-03 18:08:19 +00:00
Renovate Bot
27652bfefa Merge branch 'renovate/main-com.knecon.fforesight-document-4.x' into 'main'
Update dependency com.knecon.fforesight:document to v4.439.0 (main)

See merge request fforesight/llm-service!48
2024-12-02 20:06:09 +01:00
Renovate Bot
32995327ff Update dependency com.knecon.fforesight:document to v4.439.0 2024-12-02 18:05:33 +00:00
Renovate Bot
f199b42490 Merge branch 'renovate/main-com.google.protobuf-protobuf-java-4.x' into 'main'
Update dependency com.google.protobuf:protobuf-java to v4.29.0 (main)

See merge request fforesight/llm-service!47
2024-11-28 00:07:17 +01:00
Renovate Bot
415a2aca72 Update dependency com.google.protobuf:protobuf-java to v4.29.0 2024-11-27 22:05:27 +00:00
Renovate Bot
66fcf88535 Merge branch 'renovate/main-com.knecon.fforesight-document-4.x' into 'main'
Update dependency com.knecon.fforesight:document to v4.438.0 (main)

See merge request fforesight/llm-service!46
2024-11-27 20:05:29 +01:00
Renovate Bot
223f87400a Update dependency com.knecon.fforesight:document to v4.438.0 2024-11-27 18:05:37 +00:00
Renovate Bot
187ed3ec4f Merge branch 'renovate/main-spring-cloud' into 'main'
Update dependency org.springframework.cloud:spring-cloud-starter-openfeign to v4.1.4 (main)

See merge request fforesight/llm-service!45
2024-11-27 04:05:33 +01:00
Renovate Bot
1d02bddb63 Update dependency org.springframework.cloud:spring-cloud-starter-openfeign to v4.1.4 2024-11-27 02:05:36 +00:00
Renovate Bot
449515ed53 Merge branch 'renovate/main-com.knecon.fforesight-document-4.x' into 'main'
Update dependency com.knecon.fforesight:document to v4.437.0 (main)

See merge request fforesight/llm-service!44
2024-11-26 20:06:39 +01:00
Renovate Bot
bbe2ee71ca Update dependency com.knecon.fforesight:document to v4.437.0 2024-11-26 18:05:46 +00:00
Maverick Studer
c454b86776 Merge branch 'RED-10508' into 'main'
RED-10508: activate tracing

See merge request fforesight/llm-service!41
2024-11-22 12:30:24 +01:00
maverickstuder
2d5ab7045f RED-10508: activate tracing 2024-11-22 12:18:15 +01:00
Renovate Bot
7070c8847d Merge branch 'renovate/main-com.knecon.fforesight-document-4.x' into 'main'
Update dependency com.knecon.fforesight:document to v4.436.0 (main)

See merge request fforesight/llm-service!40
2024-11-21 20:07:49 +01:00
Renovate Bot
cad4f9ab09 Update dependency com.knecon.fforesight:document to v4.436.0 2024-11-21 18:05:33 +00:00
Renovate Bot
c70637eae7 Merge branch 'renovate/main-spring-security' into 'main'
Update dependency org.springframework.security:spring-security-messaging to v6.4.1 (main)

See merge request fforesight/llm-service!39
2024-11-21 00:05:28 +01:00
Renovate Bot
f8b50f8eb0 Update dependency org.springframework.security:spring-security-messaging to v6.4.1 2024-11-20 22:05:22 +00:00
Renovate Bot
e7e91547a2 Merge branch 'renovate/main-com.knecon.fforesight-document-4.x' into 'main'
Update dependency com.knecon.fforesight:document to v4.435.0 (main)

See merge request fforesight/llm-service!38
2024-11-20 20:07:42 +01:00
Renovate Bot
5011707319 Update dependency com.knecon.fforesight:document to v4.435.0 2024-11-20 18:08:42 +00:00
Renovate Bot
4456b7144d Merge branch 'renovate/main-spring-security' into 'main'
Update dependency org.springframework.security:spring-security-messaging to v6.4.0 (main)

See merge request fforesight/llm-service!33
2024-11-20 03:06:03 +01:00
Renovate Bot
b1603b450d Update dependency org.springframework.security:spring-security-messaging to v6.4.0 2024-11-20 01:06:04 +00:00
Renovate Bot
45e959d112 Merge branch 'renovate/main-spring-amqp' into 'main'
Update dependency org.springframework.amqp:spring-rabbit-test to v3.2.0 (main)

See merge request fforesight/llm-service!32
2024-11-20 02:05:41 +01:00
Renovate Bot
9b62571172 Update dependency org.springframework.amqp:spring-rabbit-test to v3.2.0 2024-11-20 00:06:10 +00:00
Renovate Bot
0de27df114 Merge branch 'renovate/main-feign-monorepo' into 'main'
Update dependency io.github.openfeign:feign-core to v12.5 (main)

See merge request fforesight/llm-service!31
2024-11-20 01:05:45 +01:00
Renovate Bot
ed7ed6539b Update dependency io.github.openfeign:feign-core to v12.5 2024-11-19 23:06:40 +00:00
Renovate Bot
65b8c67844 Merge branch 'renovate/main-com.iqser.red.commons-storage-commons-2.x' into 'main'
Update dependency com.iqser.red.commons:storage-commons to v2.51.0 (main)

See merge request fforesight/llm-service!36
2024-11-20 00:06:09 +01:00
Renovate Bot
685df45f2e Update dependency com.iqser.red.commons:storage-commons to v2.51.0 2024-11-19 22:06:20 +00:00
Renovate Bot
b96881baa5 Merge branch 'renovate/main-com.knecon.fforesight-document-4.x' into 'main'
Update dependency com.knecon.fforesight:document to v4.434.0 (main)

See merge request fforesight/llm-service!30
2024-11-19 23:06:02 +01:00
Renovate Bot
d2716f31fa Update dependency com.knecon.fforesight:document to v4.434.0 2024-11-19 21:06:22 +00:00
Renovate Bot
6b1399daae Merge branch 'renovate/main-com.google.protobuf-protobuf-java-4.x' into 'main'
Update dependency com.google.protobuf:protobuf-java to v4.28.3 (main)

See merge request fforesight/llm-service!29
2024-11-19 22:06:04 +01:00
Renovate Bot
66561cf8dd Update dependency com.google.protobuf:protobuf-java to v4.28.3 2024-11-19 20:06:58 +00:00
Renovate Bot
1bb1b02c83 Merge branch 'renovate/main-plugins-(non-major)' into 'main'
Update Plugins (non-major) (main)

See merge request fforesight/llm-service!28
2024-11-19 21:06:42 +01:00
Renovate Bot
9a63ee5351 Update Plugins (non-major) 2024-11-19 19:06:54 +00:00
Renovate Bot
58ab93cffc Merge branch 'renovate/main-com.azure-azure-ai-openai-1.x' into 'main'
Update dependency com.azure:azure-ai-openai to v1.0.0-beta.12 (main)

See merge request fforesight/llm-service!1
2024-11-19 20:06:36 +01:00
Renovate Bot
d4ec66f762 Update dependency com.azure:azure-ai-openai to v1.0.0-beta.12 2024-11-19 18:07:23 +00:00
Renovate Bot
032fa06c42 Merge branch 'renovate/main-ch.qos.logback-logback-classic-1.x' into 'main'
Update dependency ch.qos.logback:logback-classic to v1.5.12 (main)

See merge request fforesight/llm-service!27
2024-11-19 19:07:02 +01:00
Renovate Bot
1c902d0592 Update dependency ch.qos.logback:logback-classic to v1.5.12 2024-11-19 13:28:53 +00:00
Kilian Schüttler
2026316694 Merge branch 'feature/RED-9139' into 'main'
RED-9319: move document to its own module

See merge request fforesight/llm-service!26
2024-11-14 16:29:25 +01:00
Kilian Schüttler
fab4666dd7 RED-9319: move document to its own module 2024-11-14 16:29:25 +01:00
Maverick Studer
f3f917b5fe Merge branch 'feature/RED-10072' into 'main'
RED-10072: AI description field and toggle for entities

See merge request fforesight/llm-service!25
2024-11-07 14:43:58 +01:00
Maverick Studer
560f7fb947 RED-10072: AI description field and toggle for entities 2024-11-07 14:43:58 +01:00
Kevin Tumma
ec82c2ec02 Update .gitlab-ci.yml file 2024-10-10 13:17:52 +02:00
Maverick Studer
2d66b1e5d4 Merge branch 'RED-9123' into 'main'
RED-9123: Protobuf serialization of document data files

See merge request fforesight/llm-service!24
2024-10-07 13:37:34 +02:00
maverickstuder
d40f4f3289 RED-9123: Protobuf serialization of document data files 2024-10-07 13:10:27 +02:00
Kilian Schüttler
cbcf3b605b Merge branch 'refactor' into 'main'
Refactor

See merge request fforesight/llm-service!23
2024-09-04 12:40:07 +02:00
Kilian Schüttler
c437df9367 Refactor 2024-09-04 12:40:06 +02:00
Maverick Studer
dd6b31e902 Merge branch 'update-tc' into 'main'
Update tenant-commons for dlq fix

See merge request fforesight/llm-service!22
2024-09-03 13:43:19 +02:00
maverickstuder
a79bfd4051 Update tenant-commons for dlq fix 2024-09-03 13:13:38 +02:00
Maverick Studer
de352657b3 Merge branch 'feign-exception-workaround' into 'main'
Feign exception workaround

See merge request fforesight/llm-service!21
2024-08-30 10:24:53 +02:00
Maverick Studer
9124428d67 Feign exception workaround 2024-08-30 10:24:53 +02:00
Maverick Studer
24fa7e002b Merge branch 'fix-migration' into 'main'
Fix migration

See merge request fforesight/llm-service!20
2024-08-29 13:14:34 +02:00
Maverick Studer
6b7d0762dc Fix migration 2024-08-29 13:14:34 +02:00
Maverick Studer
72b7ec7a3f Merge branch 'tenants-retry' into 'main'
Tenants retry logic and queue renames

See merge request fforesight/llm-service!19
2024-08-29 10:35:36 +02:00
maverickstuder
d7e328204d Tenants retry logic and queue renames 2024-08-29 10:20:32 +02:00
Maverick Studer
ebbad56b7a Merge branch 'queue-naming-convention' into 'main'
changed queue names according to convention

See merge request fforesight/llm-service!18
2024-08-28 11:23:26 +02:00
maverickstuder
262cc65127 changed queue names according to convention 2024-08-28 11:19:54 +02:00
Kilian Schüttler
381c0a7e4b Merge branch 'tenant-queues' into 'main'
Tenant queues

See merge request fforesight/llm-service!17
2024-08-28 11:16:35 +02:00
Kilian Schüttler
6832a719d8 Tenant queues 2024-08-28 11:16:34 +02:00
Kilian Schüttler
c6f3dd2e26 Merge branch 'hotfix' into 'main'
fix tests in redaction-service

See merge request fforesight/llm-service!16
2024-08-27 15:59:20 +02:00
Kilian Schuettler
9eda1d5695 fix tests in redaction-service 2024-08-27 15:56:53 +02:00
Kevin Tumma
aa24e14cc3 Update .gitlab-ci.yml file 2024-08-27 06:54:21 +02:00
Kilian Schüttler
84a6d290e6 Merge branch 'reformat' into 'main'
reformat

See merge request fforesight/llm-service!15
2024-08-26 18:56:01 +02:00
Kilian Schuettler
56dbe06e6d reformat 2024-08-26 18:53:26 +02:00
Kilian Schuettler
32c618c35b SPIKE: LLM-NER 2024-08-26 18:52:12 +02:00
56 changed files with 1461 additions and 293 deletions

View File

@ -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

View File

@ -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)
}
}

View File

@ -0,0 +1,7 @@
plugins {
`kotlin-dsl`
}
repositories {
gradlePluginPortal()
}

View File

@ -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()
}
}
}

View File

@ -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"/>

View File

@ -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>

View File

@ -0,0 +1,5 @@
plugins {
`maven-publish`
id("com.knecon.fforesight.service.java-conventions")
id("io.freefair.lombok") version "8.12.1"
}

View File

@ -1,4 +1,4 @@
package com.knecon.fforesight.llm.service.api.model;
package com.knecon.fforesight.llm.service;
import lombok.AllArgsConstructor;
import lombok.Builder;

View File

@ -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;
}

View File

@ -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;
}

View File

@ -0,0 +1,5 @@
package com.knecon.fforesight.llm.service;
public record EntityAiDescription(String name, String aiDescription) {
}

View File

@ -1,4 +1,4 @@
package com.knecon.fforesight.llm.service.api;
package com.knecon.fforesight.llm.service;
import java.time.OffsetDateTime;

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;

View File

@ -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";
}

View 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")
}

View File

@ -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 {
}

View File

@ -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;
}

View File

@ -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();
}
}

View File

@ -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());
}
}

View File

@ -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);
}
}

View File

@ -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) {
}
}

View File

@ -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);
}
}
}

View File

@ -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));
}
}

View File

@ -0,0 +1,7 @@
package com.knecon.fforesight.llm.service.services;
public interface WebSocketMessagingTemplate {
void sendEvent(String userId, String token, Object payload);
}

View File

@ -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);
}
}
}

View File

@ -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) {
}
}

View 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/")
}
}
}

View File

@ -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 {

View File

@ -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 {

View File

@ -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;

View File

@ -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);
}
}
}

View File

@ -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();
}
}

View File

@ -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()));
}
}

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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);

View File

@ -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}

View File

@ -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);

View File

@ -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();
}
}

View File

@ -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());
}
}

View File

@ -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/"

View File

@ -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}"

View File

@ -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")

View File

@ -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.
// }
}

View File

@ -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 {
}

View File

@ -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));
}
}