Skip to content

Commit a2454dc

Browse files
committed
fix: add suggestion and improve compliance issue detection
- translate existing compliance suggestions during snippet mapping - preserve suggestion meaning across languages - require exact target-text matches for mapped snippets
1 parent 18d38e9 commit a2454dc

8 files changed

Lines changed: 38 additions & 31 deletions

File tree

openapi/openapi.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3905,9 +3905,10 @@ components:
39053905
type: array
39063906
items: {$ref: '#/components/schemas/ComplianceIssue'}
39073907
jobId: {type: string, format: uuid}
3908+
text: {type: string, minLength: 1}
39083909
toLang: {type: string}
39093910
translatedText: {type: string, minLength: 1}
3910-
required: [complianceIssues, jobId, translatedText]
3911+
required: [complianceIssues, jobId, text, translatedText]
39113912
MultipartUploadRequest:
39123913
type: object
39133914
properties:

src/main/java/de/tum/cit/aet/ai/dto/MapComplianceIssuesRequestDTO.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
public record MapComplianceIssuesRequestDTO(
1212
String toLang,
1313
@NotNull UUID jobId,
14+
@NotBlank String text,
1415
@NotBlank String translatedText,
1516
@NotNull List<ComplianceIssue> complianceIssues
1617
) {}

src/main/java/de/tum/cit/aet/ai/service/AiService.java

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -520,9 +520,13 @@ public List<ComplianceIssue> mapComplianceIssues(MapComplianceIssuesRequestDTO r
520520
return List.of();
521521
}
522522

523-
String snippets = java.util.stream.IntStream.range(0, request.complianceIssues().size())
524-
.mapToObj(index -> (index + 1) + "\t" + request.complianceIssues().get(index).getText().trim())
525-
.collect(Collectors.joining("\n"));
523+
String issues = request
524+
.complianceIssues()
525+
.stream()
526+
.map(
527+
issue -> "Text: " + issue.getText().trim() + "\nSuggestion: " + (issue.getSuggestion() == null ? "" : issue.getSuggestion())
528+
)
529+
.collect(Collectors.joining("\n---\n"));
526530

527531
List<String> mappedTexts;
528532
try {
@@ -531,9 +535,10 @@ public List<ComplianceIssue> mapComplianceIssues(MapComplianceIssuesRequestDTO r
531535
.user(u ->
532536
u
533537
.text(snippetMappingResource)
534-
.param("count", String.valueOf(request.complianceIssues().size()))
535-
.param("snippets", snippets)
538+
.param("issues", issues)
539+
.param("jobDescription", request.text())
536540
.param("translatedText", request.translatedText())
541+
.param("targetLanguage", request.toLang())
537542
)
538543
.call()
539544
.entity(new ParameterizedTypeReference<List<String>>() {});
@@ -543,14 +548,14 @@ public List<ComplianceIssue> mapComplianceIssues(MapComplianceIssuesRequestDTO r
543548
throw new InternalServerException("Compliance issue mapping failed", e);
544549
}
545550

546-
if (mappedTexts == null || mappedTexts.size() != request.complianceIssues().size()) {
551+
if (mappedTexts == null || mappedTexts.size() != request.complianceIssues().size() * 2) {
547552
aiFeatureToggleService.recordFailure();
548553
throw new InternalServerException("Mapping returned an invalid number of snippets");
549554
}
550555

551556
List<ComplianceIssue> mappedIssues = new ArrayList<>();
552557
for (int i = 0; i < request.complianceIssues().size(); i++) {
553-
String mappedText = mappedTexts.get(i);
558+
String mappedText = mappedTexts.get(i * 2);
554559
String mapped = mappedText == null ? null : mappedText.trim();
555560
if (!SnippetMatcher.isVerbatim(request.translatedText(), mapped)) {
556561
log.warn("Snippet {} not found in translated text, dropping", i);
@@ -565,7 +570,7 @@ public List<ComplianceIssue> mapComplianceIssues(MapComplianceIssuesRequestDTO r
565570
sourceIssue.getArticle(),
566571
sourceIssue.getExplanation(),
567572
sourceIssue.getAction(),
568-
sourceIssue.getSuggestion(),
573+
mappedTexts.get(i * 2 + 1).trim(),
569574
request.toLang()
570575
)
571576
);

src/main/resources/prompts/AnalyzeComplianceText.st

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ TASK:
77
Deeply analyze the job posting for legal risks (AGG, DSGVO, WissZeitVG) based on German and EU law.
88
The input description language is: {descriptionLanguage}.
99
EXPLANATIONS must be written in: {userLang}.
10-
SUGGESTIONS must be written in: {descriptionLanguage}.
10+
SUGGESTIONS must be written in: {descriptionLanguage}
1111

1212
SSTRICT ANALYSIS RULES:
1313
1. NO SUMMARIES: Every single violation must be a separate JSON object.
@@ -35,8 +35,8 @@ CATEGORIES & ACTIONS:
3535
- Detect mentions of THIRD PARTIES/TOOLS (Workday, Headhunter, Partner Uni, consortia) without stating that data is shared with them.
3636
- If entire jobDescription does NOT contain a sentence clarifying that applicant data is shared with external recipients, flag detected mention
3737
- NOT triggered by: When explicit DATA is requested from the applicant. DO NOT confuse this with MINIMIZATION. Do not flag links here. Flag the MENTION of external partners.
38-
- TRANSPARENCY SUGGESTION: suggestion must be a consent-based sentence like: By applying, you consent to your data being shared with "third party" for the purpose of the application process (Art. 13 DSGVO)
39-
- Before reporting an ADD issue, check the entire description for an equivalent disclosure. Never suggest information that is already present, even with different wording.
38+
- TRANSPARENCY SUGGESTION: suggestion must be a consent-based sentence like: By applying, you consent to your data being shared with "third party" for the purpose of the application process (Art. 13 DSGVO).
39+
- Before reporting an ADD issue, check the entire description for an EQUIVALENT disclosure. Never suggest information that is already present, even with different wording.
4040
- Every ADD suggestion must be one grammatically independent, complete sentence. Never start it with a comma, conjunction, or subordinate clause.
4141
4. PUBLIC_SECTOR (action: ADD):
4242
- FOCUS: Qualification Purpose (WissZeitVG).
@@ -50,14 +50,6 @@ INPUT TEXT:
5050
{jobDescription}
5151
----------------------
5252

53-
COMPLETENESS REQUIREMENT:
54-
Before returning the JSON, you MUST internally conduct 4 separate passes over the ENTIRE text:
55-
Pass 1: Check every sentence for implicit/explicit AGG and vague language skills.
56-
Pass 2: Check every sentence for uncontrolled data intake or external URLs (Minimization).
57-
Pass 3: Check every sentence for mentions of external collaborators without privacy clauses (Transparency).
58-
Pass 4: Check for WissZeitVG purpose.
59-
The output MUST contain the combined findings of all passes.
60-
6153
OUTPUT:
6254
Return ONLY a valid JSON array. No markdown. No prose.
6355
Object fields must be exactly:
Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
1-
Map every numbered source issue to the exact phrase in TARGET that expresses the same issue.
2-
Return exactly {count} strings, one per issue, preserving order and duplicates.
3-
Every non-empty result MUST be copied verbatim from TARGET. Use "" only if no matching phrase exists.
4-
Do not analyze compliance. Output one JSON string array immediately, without reasoning, prose, or markdown.
1+
For each source issue, map its text to the exact phrase in TARGET that expresses the same issue.
2+
Directly after each mapped text, return that issue's existing suggestion translated into {targetLanguage} without changing its meaning.
3+
Return one flat JSON string array containing exactly two strings per issue, preserving order and duplicates.
4+
Every non-empty mapped text MUST be copied verbatim from TARGET. Use "" if no matching phrase or source suggestion exists.
5+
Do not create new suggestions or analyze compliance. Output the JSON array immediately, without reasoning, prose, or markdown.
56
67
--- EXAMPLE ---
78
TARGET: Wir suchen ein junges, dynamisches Team für unsere Arbeitsgruppe.
89
ISSUES:
9-
1 young and dynamic team
10-
2 must hold a German passport
11-
OUTPUT: ["junges, dynamisches Team", ""]
10+
Text: young and dynamic team
11+
Suggestion: experienced team
12+
OUTPUT: ["junges, dynamisches Team", "erfahrenes und dynamisches Team"]
1213
1314
--- INPUT ---
1415
TARGET:
1516
{translatedText}
1617
1718
ISSUES:
18-
{snippets}
19+
{issues}

src/main/webapp/app/generated/model/map-compliance-issues-request-dto.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
44
*
55
* API Version: v0
6-
*
6+
*
77
*
88
* NOTE: This file is auto-generated. Do not edit manually.
99
*/
@@ -13,6 +13,7 @@ import type { ComplianceIssue } from './compliance-issue';
1313
export interface MapComplianceIssuesRequestDTO {
1414
readonly complianceIssues: Array<ComplianceIssue>;
1515
readonly jobId: string;
16+
readonly text: string;
1617
readonly toLang?: string;
1718
readonly translatedText: string;
1819
}

src/main/webapp/app/job/job-creation-form/job-creation-form.component.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1822,7 +1822,7 @@ export class JobCreationFormComponent {
18221822
if (text === lastBaseline) {
18231823
const targetHtml = targetLang === 'en' ? this.jobDescriptionEN() : this.jobDescriptionDE();
18241824
try {
1825-
await this.mapIssuesToTargetLanguage(targetLang, targetHtml, sourceIssuesPromise, jobId);
1825+
await this.mapIssuesToTargetLanguage(text, targetLang, targetHtml, sourceIssuesPromise, jobId);
18261826
} catch {
18271827
// Silent mapping failure — the next analysis can retry without retranslating.
18281828
}
@@ -1865,6 +1865,8 @@ export class JobCreationFormComponent {
18651865
abortController.signal,
18661866
);
18671867

1868+
if (this.translationAbortController !== abortController) return;
1869+
18681870
let hasTranslation = false;
18691871
let finalContent: string | undefined = '';
18701872
if (accumulatedContent) {
@@ -1905,7 +1907,7 @@ export class JobCreationFormComponent {
19051907
const saved = await firstValueFrom(this.jobApi.updateJob(jobId, currentData));
19061908
this.lastSavedData.set(saved);
19071909

1908-
await this.mapIssuesToTargetLanguage(targetLang, finalContent ?? '', sourceIssuesPromise, jobId);
1910+
await this.mapIssuesToTargetLanguage(text, targetLang, finalContent ?? '', sourceIssuesPromise, jobId);
19091911
} catch {
19101912
// Silent save failure — will be caught by next autosave
19111913
}
@@ -1921,6 +1923,7 @@ export class JobCreationFormComponent {
19211923

19221924
/** Maps source issues to an existing target text and applies actions accepted while mapping was pending. */
19231925
private async mapIssuesToTargetLanguage(
1926+
sourceText: string,
19241927
targetLang: Language,
19251928
targetHtml: string,
19261929
sourceIssuesPromise: Promise<ComplianceIssue[] | undefined>,
@@ -1936,6 +1939,7 @@ export class JobCreationFormComponent {
19361939
this.aiApi.mapComplianceIssues({
19371940
toLang: targetLang,
19381941
jobId,
1942+
text: extractTextFromHtml(sourceText),
19391943
translatedText: extractTextFromHtml(targetHtml),
19401944
complianceIssues: sourceIssues,
19411945
}),

src/test/webapp/app/job/job-creation-form/job-creation-form.component.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -842,6 +842,7 @@ describe('JobCreationFormComponent', () => {
842842
expect(mapSpy).toHaveBeenCalledWith({
843843
toLang: 'de',
844844
jobId: 'job1',
845+
text: 'Hello',
845846
translatedText: 'Hallo',
846847
complianceIssues: [sourceIssue],
847848
});
@@ -876,6 +877,7 @@ describe('JobCreationFormComponent', () => {
876877
expect(mapSpy).toHaveBeenCalledWith({
877878
toLang: 'de',
878879
jobId: 'job1',
880+
text: 'Hello dynamic team.',
879881
translatedText: 'Hallo dynamisches Team.',
880882
complianceIssues: [sourceIssue],
881883
});

0 commit comments

Comments
 (0)