From 72f783dc08a3e25fa56d7160be3210f1d9883869 Mon Sep 17 00:00:00 2001 From: Laura Randl Date: Fri, 30 Jan 2026 12:44:54 +0100 Subject: [PATCH 1/8] :sparkles: feat(microblog-service): Request spam label from rag-service for new post --- chart/templates/microblog-service.yaml | 2 + chart/values.yaml | 1 + .../microblog/MicroblogController.java | 32 +++++++- .../org/dynatrace/microblog/dto/Post.java | 7 +- .../microblog/dto/SerializedPost.java | 5 +- .../ragservice/RAGServiceClient.java | 76 +++++++++++++++++++ .../microblog/redis/RedisClient.java | 11 ++- .../microblog/utils/PostSerializerTest.java | 14 ++-- 8 files changed, 136 insertions(+), 12 deletions(-) create mode 100644 src/microblog-service/src/main/java/org/dynatrace/microblog/ragservice/RAGServiceClient.java diff --git a/chart/templates/microblog-service.yaml b/chart/templates/microblog-service.yaml index 908f2043..9fdbcba8 100644 --- a/chart/templates/microblog-service.yaml +++ b/chart/templates/microblog-service.yaml @@ -66,6 +66,8 @@ spec: value: {{ quote .Values.microblogService.deployment.container.env.REDIS_SERVICE_ADDRESS }} - name: USER_AUTH_SERVICE_ADDRESS value: {{ quote .Values.microblogService.deployment.container.env.USER_AUTH_SERVICE_ADDRESS }} + - name: RAG_SERVICE_ADDRESS + value: {{ quote .Values.microblogService.deployment.container.env.RAG_SERVICE_ADDRESS }} - name: OPENTRACING_JAEGER_ENABLED value: {{ quote .Values.microblogService.deployment.container.env.OPENTRACING_JAEGER_ENABLED }} - name: JAEGER_SERVICE_NAME diff --git a/chart/values.yaml b/chart/values.yaml index 567b7ff5..c6dbfcf4 100644 --- a/chart/values.yaml +++ b/chart/values.yaml @@ -279,6 +279,7 @@ microblogService: OPENTRACING_JAEGER_ENABLED: false REDIS_SERVICE_ADDRESS: unguard-redis USER_AUTH_SERVICE_ADDRESS: unguard-user-auth-service + RAG_SERVICE_ADDRESS: unguard-rag-service:8000 # Status Service statusService: diff --git a/src/microblog-service/src/main/java/org/dynatrace/microblog/MicroblogController.java b/src/microblog-service/src/main/java/org/dynatrace/microblog/MicroblogController.java index 08aff25c..b7b6c9ab 100644 --- a/src/microblog-service/src/main/java/org/dynatrace/microblog/MicroblogController.java +++ b/src/microblog-service/src/main/java/org/dynatrace/microblog/MicroblogController.java @@ -29,6 +29,7 @@ import org.dynatrace.microblog.exceptions.NotLoggedInException; import org.dynatrace.microblog.exceptions.UserNotFoundException; import org.dynatrace.microblog.form.PostForm; +import org.dynatrace.microblog.ragservice.RAGServiceClient; import org.dynatrace.microblog.redis.RedisClient; import org.dynatrace.microblog.utils.JwtTokensUtils; import org.dynatrace.microblog.utils.PostSerializer; @@ -49,6 +50,9 @@ import java.util.Collection; import java.util.List; import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import static org.springframework.http.HttpStatus.NOT_FOUND; @@ -58,12 +62,17 @@ public class MicroblogController { private final RedisClient redisClient; Logger logger = LoggerFactory.getLogger(MicroblogController.class); private final UserAuthServiceClient userAuthServiceClient; + private final RAGServiceClient ragServiceClient; private final PostSerializer postSerializer; + private final ExecutorService ragExecutor = Executors.newFixedThreadPool( + Math.max(20, Runtime.getRuntime().availableProcessors() / 2) + ); @Autowired public MicroblogController(Tracer tracer, PostSerializer postSerializer) { String redisServiceAddress; String userAuthServiceAddress; + String ragServiceAddress; if (System.getenv("REDIS_SERVICE_ADDRESS") != null) { redisServiceAddress = System.getenv("REDIS_SERVICE_ADDRESS"); logger.info("REDIS_SERVICE_ADDRESS set to {}", redisServiceAddress); @@ -80,8 +89,17 @@ public MicroblogController(Tracer tracer, PostSerializer postSerializer) { logger.warn("No USER_AUTH_SERVICE_ADDRESS environment variable defined, falling back to localhost:9091."); } + if (System.getenv("RAG_SERVICE_ADDRESS") != null) { + ragServiceAddress = System.getenv("RAG_SERVICE_ADDRESS"); + logger.info("RAG_SERVICE_ADDRESS set to {}", ragServiceAddress); + } else { + ragServiceAddress = "localhost:8000"; + logger.warn("No RAG_SERVICE_ADDRESS environment variable defined, falling back to localhost:8000."); + } + this.userAuthServiceClient = new UserAuthServiceClient(userAuthServiceAddress); this.redisClient = new RedisClient(redisServiceAddress, this.userAuthServiceClient, tracer); + this.ragServiceClient = new RAGServiceClient(ragServiceAddress); this.postSerializer = postSerializer; } @@ -171,6 +189,18 @@ public PostId post(@RequestBody PostForm postForm, @CookieValue(value = "jwt", r // decode JWT Claims claims = JwtTokensUtils.decodeTokenClaims(jwt); String postId = redisClient.newPost(claims.get("userid").toString(), postForm.getContent(), postForm.getImageUrl()); + + CompletableFuture.runAsync(() -> { + try { + String spamClassificationResult = ragServiceClient.getSpamClassification(postForm.getContent()); + logger.info("RAG spam classification result for post with ID {}: {}", postId, spamClassificationResult); + boolean isSpam = spamClassificationResult.trim().equalsIgnoreCase("spam"); + redisClient.setSpamPredictedLabel(postId, isSpam); + } catch (Exception e) { + logger.warn("RAG spam classification failed for post with ID {}", postId, e); + } + }, ragExecutor); + return new PostId(postId); } @@ -181,7 +211,7 @@ public Post getPost(@PathVariable("postid") String postId, @CookieValue(value = if (post == null) { throw new ResponseStatusException(NOT_FOUND, "Post not found."); } - postSerializer.serializePost(new SerializedPost(postId, post.getUsername(), post.getBody(), post.getImageUrl(), post.getTimestamp(), UUID.randomUUID())); + postSerializer.serializePost(new SerializedPost(postId, post.getUsername(), post.getBody(), post.getImageUrl(), post.getTimestamp(), UUID.randomUUID(), post.getIsSpamPredictedLabel())); return post; } diff --git a/src/microblog-service/src/main/java/org/dynatrace/microblog/dto/Post.java b/src/microblog-service/src/main/java/org/dynatrace/microblog/dto/Post.java index a8eebbeb..750b7c8f 100644 --- a/src/microblog-service/src/main/java/org/dynatrace/microblog/dto/Post.java +++ b/src/microblog-service/src/main/java/org/dynatrace/microblog/dto/Post.java @@ -26,18 +26,21 @@ public class Post { private final String body; private final Date timestamp; private final String imageUrl; + private final Boolean isSpamPredictedLabel; public Post( @JsonProperty("postId") String postId, @JsonProperty("username") String username, @JsonProperty("body") String body, @JsonProperty("imageUrl") String imageUrl, - @JsonProperty("timestamp") Date timestamp) { + @JsonProperty("timestamp") Date timestamp, + @JsonProperty("isSpamPredictedLabel") Boolean isSpamPredictedLabel) { this.postId = postId; this.username = username; this.body = body; this.imageUrl = imageUrl; this.timestamp = timestamp; + this.isSpamPredictedLabel = isSpamPredictedLabel; } public String getPostId() { @@ -59,4 +62,6 @@ public String getImageUrl() { public Date getTimestamp() { return timestamp; } + + public Boolean getIsSpamPredictedLabel() { return isSpamPredictedLabel; } } diff --git a/src/microblog-service/src/main/java/org/dynatrace/microblog/dto/SerializedPost.java b/src/microblog-service/src/main/java/org/dynatrace/microblog/dto/SerializedPost.java index 070e4a31..571d054d 100644 --- a/src/microblog-service/src/main/java/org/dynatrace/microblog/dto/SerializedPost.java +++ b/src/microblog-service/src/main/java/org/dynatrace/microblog/dto/SerializedPost.java @@ -31,8 +31,9 @@ public SerializedPost( @JsonProperty("body") String body, @JsonProperty("imageUrl") String imageUrl, @JsonProperty("timestamp") Date timestamp, - @JsonProperty("serialId") UUID serialId) { - super(postId, username, body, imageUrl, timestamp); + @JsonProperty("serialId") UUID serialId, + @JsonProperty("isSpamPredictedLabel") Boolean isSpamPredictedLabel) { + super(postId, username, body, imageUrl, timestamp, isSpamPredictedLabel); this.serialId = serialId; } diff --git a/src/microblog-service/src/main/java/org/dynatrace/microblog/ragservice/RAGServiceClient.java b/src/microblog-service/src/main/java/org/dynatrace/microblog/ragservice/RAGServiceClient.java new file mode 100644 index 00000000..63716742 --- /dev/null +++ b/src/microblog-service/src/main/java/org/dynatrace/microblog/ragservice/RAGServiceClient.java @@ -0,0 +1,76 @@ +package org.dynatrace.microblog.ragservice; + +import com.google.gson.JsonObject; +import io.opentracing.contrib.okhttp3.OkHttpClientSpanDecorator; +import io.opentracing.contrib.okhttp3.TracingInterceptor; +import io.opentracing.util.GlobalTracer; +import okhttp3.*; +import org.dynatrace.microblog.exceptions.InvalidJwtException; +import org.dynatrace.microblog.exceptions.UserNotFoundException; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.time.Duration; +import java.util.Collections; + +public class RAGServiceClient { + + private final OkHttpClient client; + private final Logger logger = LoggerFactory.getLogger(org.dynatrace.microblog.ragservice.RAGServiceClient.class); + + private final String ragServiceHost; + + public RAGServiceClient(String ragServiceHost) { + this.ragServiceHost = ragServiceHost; + + TracingInterceptor tracingInterceptor = new TracingInterceptor( + GlobalTracer.get(), + Collections.singletonList(OkHttpClientSpanDecorator.STANDARD_TAGS)); + client = new OkHttpClient.Builder() + .connectTimeout(Duration.ofSeconds(240)) + .writeTimeout(Duration.ofSeconds(240)) + .readTimeout(Duration.ofSeconds(240)) + .callTimeout(Duration.ofSeconds(240)) + .addInterceptor(tracingInterceptor) + .addNetworkInterceptor(tracingInterceptor) + .build(); + + } + + public String getSpamClassification(String postText) throws InvalidJwtException, UserNotFoundException, IOException { + JsonObject obj = new JsonObject(); + obj.addProperty("text", postText); + String jsonRequest = obj.toString(); + + RequestBody body = RequestBody.create( + MediaType.parse("application/json"), jsonRequest); + + String requestUrl = "http://" + this.ragServiceHost + "/classifyPost"; + + Request request = new Request.Builder() + .url(requestUrl) + .post(body) + .build(); + + + Call call = client.newCall(request); + try (Response response = call.execute()) { + + if (response.code() == 200) { + JSONObject responseObject = new JSONObject(response.body().string()); + return responseObject.getString("classification"); + } else if (response.code() == 401) { + throw new InvalidJwtException(); + } else { + throw new RuntimeException("Error retrieving spam classification from RAG service: " + response.code() + " - " + response.message()); + } + } catch (IOException e) { + logger.error("Request response error during spam classification", e); + throw e; + } + } +} + + diff --git a/src/microblog-service/src/main/java/org/dynatrace/microblog/redis/RedisClient.java b/src/microblog-service/src/main/java/org/dynatrace/microblog/redis/RedisClient.java index d198e37c..3479b1c1 100644 --- a/src/microblog-service/src/main/java/org/dynatrace/microblog/redis/RedisClient.java +++ b/src/microblog-service/src/main/java/org/dynatrace/microblog/redis/RedisClient.java @@ -119,6 +119,13 @@ public String newPost(@NotNull String userId, @NotNull String body, @Nullable St } } + public void setSpamPredictedLabel(@NotNull String postId, boolean isSpamPredictedLabel) { + try (Jedis jedis = jedisPool.getResource()) { + String postKey = getCombinedKey(POST_KEY_PREFIX, postId); + jedis.hset(postKey, "isSpamPredictedLabel", String.valueOf(isSpamPredictedLabel)); + } + } + @NotNull public List getUserTimeline(String userId) { List postIds; @@ -195,8 +202,10 @@ private Post readAndTransformPost(String postId, Jedis jedis) throws UserNotFoun String body = postMap.get("body"); String imageUrl = postMap.get("imageUrl"); Date timestamp = new Date(Long.parseLong(postMap.get("time"))); + String spamClassification = postMap.get("isSpamPredictedLabel"); + Boolean isSpamPredictedLabel = spamClassification == null? null : Boolean.valueOf(spamClassification); - return new Post(postId, userName, body, imageUrl, timestamp); + return new Post(postId, userName, body, imageUrl, timestamp, isSpamPredictedLabel); } public List getTimeline() { diff --git a/src/microblog-service/src/test/java/org/dynatrace/microblog/utils/PostSerializerTest.java b/src/microblog-service/src/test/java/org/dynatrace/microblog/utils/PostSerializerTest.java index 34928f32..03953e14 100644 --- a/src/microblog-service/src/test/java/org/dynatrace/microblog/utils/PostSerializerTest.java +++ b/src/microblog-service/src/test/java/org/dynatrace/microblog/utils/PostSerializerTest.java @@ -10,12 +10,12 @@ class PostSerializerTest { - private final PostSerializer postSerializer = new PostSerializer(); + private final PostSerializer postSerializer = new PostSerializer(); - @Test - void serializePost_ReturnsTrue_WhenObjectCouldBeDeserialized() { - assertThat(postSerializer.serializePost( - new SerializedPost("1", "username", "body", "imageURL", new Date(), UUID.randomUUID()))) - .isTrue(); - } + @Test + void serializePost_ReturnsTrue_WhenObjectCouldBeDeserialized() { + assertThat(postSerializer.serializePost( + new SerializedPost("1", "username", "body", "imageURL", new Date(), UUID.randomUUID(), null))) + .isTrue(); + } } From f2bd2c51f4cb8d33d140090d5736b8877a1ab017 Mon Sep 17 00:00:00 2001 From: Laura Randl Date: Tue, 3 Feb 2026 09:52:11 +0100 Subject: [PATCH 2/8] :sparkles: feat(frontend): Display predicted spam label in post --- chart/templates/microblog-service.yaml | 2 ++ chart/values.yaml | 3 +- src/frontend-nextjs/app/post/page.tsx | 1 + .../components/Timeline/Post.tsx | 8 ++++- .../Timeline/PostSpamPrediction.tsx | 34 +++++++++++++++++++ .../components/Timeline/Timeline.tsx | 4 +-- .../microblog/MicroblogController.java | 32 +++++++++++------ .../InvalidSpamPredictionException.java | 11 ++++++ .../ragservice/RAGServiceClient.java | 13 +++++-- 9 files changed, 90 insertions(+), 18 deletions(-) create mode 100644 src/frontend-nextjs/components/Timeline/PostSpamPrediction.tsx create mode 100644 src/microblog-service/src/main/java/org/dynatrace/microblog/exceptions/InvalidSpamPredictionException.java diff --git a/chart/templates/microblog-service.yaml b/chart/templates/microblog-service.yaml index 9fdbcba8..2f32ac61 100644 --- a/chart/templates/microblog-service.yaml +++ b/chart/templates/microblog-service.yaml @@ -68,6 +68,8 @@ spec: value: {{ quote .Values.microblogService.deployment.container.env.USER_AUTH_SERVICE_ADDRESS }} - name: RAG_SERVICE_ADDRESS value: {{ quote .Values.microblogService.deployment.container.env.RAG_SERVICE_ADDRESS }} + - name: RAG_SERVICE_PORT + value: {{ quote .Values.microblogService.deployment.container.env.RAG_SERVICE_PORT }} - name: OPENTRACING_JAEGER_ENABLED value: {{ quote .Values.microblogService.deployment.container.env.OPENTRACING_JAEGER_ENABLED }} - name: JAEGER_SERVICE_NAME diff --git a/chart/values.yaml b/chart/values.yaml index c6dbfcf4..b4fba005 100644 --- a/chart/values.yaml +++ b/chart/values.yaml @@ -279,7 +279,8 @@ microblogService: OPENTRACING_JAEGER_ENABLED: false REDIS_SERVICE_ADDRESS: unguard-redis USER_AUTH_SERVICE_ADDRESS: unguard-user-auth-service - RAG_SERVICE_ADDRESS: unguard-rag-service:8000 + RAG_SERVICE_ADDRESS: unguard-rag-service + RAG_SERVICE_PORT: 8000 # Status Service statusService: diff --git a/src/frontend-nextjs/app/post/page.tsx b/src/frontend-nextjs/app/post/page.tsx index d1794f8a..3e5600c0 100644 --- a/src/frontend-nextjs/app/post/page.tsx +++ b/src/frontend-nextjs/app/post/page.tsx @@ -33,6 +33,7 @@ function SinglePost() { postId={postData.postId} timestamp={postData.timestamp} username={postData.username} + isSpamPredictedLabel={postData.isSpamPredictedLabel} /> )} diff --git a/src/frontend-nextjs/components/Timeline/Post.tsx b/src/frontend-nextjs/components/Timeline/Post.tsx index 6ceb6ea2..6566a469 100644 --- a/src/frontend-nextjs/components/Timeline/Post.tsx +++ b/src/frontend-nextjs/components/Timeline/Post.tsx @@ -10,6 +10,7 @@ import { BASE_PATH } from '@/constants'; import { useCheckLogin } from '@/hooks/queries/useCheckLogin'; import { LikeButton } from '@/components/Timeline/LikeButton'; import { ErrorCard } from '@/components/ErrorCard'; +import { PostSpamPrediction } from '@/components/Timeline/PostSpamPrediction'; export interface PostProps { username: string; @@ -17,6 +18,7 @@ export interface PostProps { body: string; imageUrl?: string; postId: string; + isSpamPredictedLabel?: boolean; } export function Post(props: PostProps) { @@ -69,7 +71,11 @@ export function Post(props: PostProps) { )}

{props.body}

- + +
+ +
+ {isLoggedIn && (
}> diff --git a/src/frontend-nextjs/components/Timeline/PostSpamPrediction.tsx b/src/frontend-nextjs/components/Timeline/PostSpamPrediction.tsx new file mode 100644 index 00000000..f2cbef1d --- /dev/null +++ b/src/frontend-nextjs/components/Timeline/PostSpamPrediction.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import {Alert} from "@heroui/react"; + +export interface PostSpamPredictionProps { + isSpamPredictedLabel?: boolean | null; +} + + + +export function PostSpamPrediction(props: Readonly) { + console.log("PostSpamPrediction props:", props); + if (props.isSpamPredictedLabel == null) return null; + + + if (props.isSpamPredictedLabel) { + const color='danger' + return ( +
+
+ +
+
+ ); + } + + const color = 'primary' + return ( +
+
+ +
+
+ ); +} diff --git a/src/frontend-nextjs/components/Timeline/Timeline.tsx b/src/frontend-nextjs/components/Timeline/Timeline.tsx index fa9f7400..0b510903 100644 --- a/src/frontend-nextjs/components/Timeline/Timeline.tsx +++ b/src/frontend-nextjs/components/Timeline/Timeline.tsx @@ -1,8 +1,7 @@ 'use client'; import { Card, Spacer, Spinner } from '@heroui/react'; -import { Post } from '@/components/Timeline/Post'; -import { PostProps } from '@/components/Timeline/Post'; +import { Post, PostProps } from '@/components/Timeline/Post'; interface TimelineProps { posts: PostProps[] | undefined; @@ -31,6 +30,7 @@ export function Timeline({ posts, isLoading }: TimelineProps) { postId={post.postId} timestamp={post.timestamp} username={post.username} + isSpamPredictedLabel={post.isSpamPredictedLabel} />
diff --git a/src/microblog-service/src/main/java/org/dynatrace/microblog/MicroblogController.java b/src/microblog-service/src/main/java/org/dynatrace/microblog/MicroblogController.java index b7b6c9ab..d10cc117 100644 --- a/src/microblog-service/src/main/java/org/dynatrace/microblog/MicroblogController.java +++ b/src/microblog-service/src/main/java/org/dynatrace/microblog/MicroblogController.java @@ -23,11 +23,7 @@ import org.dynatrace.microblog.dto.PostId; import org.dynatrace.microblog.dto.SerializedPost; import org.dynatrace.microblog.dto.User; -import org.dynatrace.microblog.exceptions.FollowYourselfException; -import org.dynatrace.microblog.exceptions.InvalidJwtException; -import org.dynatrace.microblog.exceptions.InvalidUserException; -import org.dynatrace.microblog.exceptions.NotLoggedInException; -import org.dynatrace.microblog.exceptions.UserNotFoundException; +import org.dynatrace.microblog.exceptions.*; import org.dynatrace.microblog.form.PostForm; import org.dynatrace.microblog.ragservice.RAGServiceClient; import org.dynatrace.microblog.redis.RedisClient; @@ -73,6 +69,7 @@ public MicroblogController(Tracer tracer, PostSerializer postSerializer) { String redisServiceAddress; String userAuthServiceAddress; String ragServiceAddress; + String ragServicePort; if (System.getenv("REDIS_SERVICE_ADDRESS") != null) { redisServiceAddress = System.getenv("REDIS_SERVICE_ADDRESS"); logger.info("REDIS_SERVICE_ADDRESS set to {}", redisServiceAddress); @@ -93,13 +90,20 @@ public MicroblogController(Tracer tracer, PostSerializer postSerializer) { ragServiceAddress = System.getenv("RAG_SERVICE_ADDRESS"); logger.info("RAG_SERVICE_ADDRESS set to {}", ragServiceAddress); } else { - ragServiceAddress = "localhost:8000"; - logger.warn("No RAG_SERVICE_ADDRESS environment variable defined, falling back to localhost:8000."); + ragServiceAddress = "localhost"; + logger.warn("No RAG_SERVICE_ADDRESS environment variable defined, falling back to localhost."); + } + if (System.getenv("RAG_SERVICE_PORT") != null) { + ragServicePort = System.getenv("RAG_SERVICE_PORT"); + logger.info("RAG_SERVICE_PORT set to {}", ragServicePort); + } else { + ragServicePort = "8000"; + logger.warn("No RAG_SERVICE_PORT environment variable defined, falling back to 8000."); } this.userAuthServiceClient = new UserAuthServiceClient(userAuthServiceAddress); this.redisClient = new RedisClient(redisServiceAddress, this.userAuthServiceClient, tracer); - this.ragServiceClient = new RAGServiceClient(ragServiceAddress); + this.ragServiceClient = new RAGServiceClient(ragServiceAddress, ragServicePort); this.postSerializer = postSerializer; } @@ -193,9 +197,15 @@ public PostId post(@RequestBody PostForm postForm, @CookieValue(value = "jwt", r CompletableFuture.runAsync(() -> { try { String spamClassificationResult = ragServiceClient.getSpamClassification(postForm.getContent()); - logger.info("RAG spam classification result for post with ID {}: {}", postId, spamClassificationResult); - boolean isSpam = spamClassificationResult.trim().equalsIgnoreCase("spam"); - redisClient.setSpamPredictedLabel(postId, isSpam); + if(spamClassificationResult == null || spamClassificationResult.trim().isEmpty()) { + throw new InvalidSpamPredictionException("RAG spam classification result is null or empty for post with ID " + postId); + } else if (spamClassificationResult.trim().equalsIgnoreCase("not_spam")) { + redisClient.setSpamPredictedLabel(postId, false); + } else if (spamClassificationResult.trim().equalsIgnoreCase("spam")) { + redisClient.setSpamPredictedLabel(postId, true); + } else { + throw new InvalidSpamPredictionException("RAG spam classification result is invalid for post with ID " + postId + ": " + spamClassificationResult); + } } catch (Exception e) { logger.warn("RAG spam classification failed for post with ID {}", postId, e); } diff --git a/src/microblog-service/src/main/java/org/dynatrace/microblog/exceptions/InvalidSpamPredictionException.java b/src/microblog-service/src/main/java/org/dynatrace/microblog/exceptions/InvalidSpamPredictionException.java new file mode 100644 index 00000000..8b1f64d0 --- /dev/null +++ b/src/microblog-service/src/main/java/org/dynatrace/microblog/exceptions/InvalidSpamPredictionException.java @@ -0,0 +1,11 @@ +package org.dynatrace.microblog.exceptions; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "Invalid spam prediction") +public class InvalidSpamPredictionException extends Exception { + public InvalidSpamPredictionException(String message) { + super(message); + } +} diff --git a/src/microblog-service/src/main/java/org/dynatrace/microblog/ragservice/RAGServiceClient.java b/src/microblog-service/src/main/java/org/dynatrace/microblog/ragservice/RAGServiceClient.java index 63716742..b59cfe83 100644 --- a/src/microblog-service/src/main/java/org/dynatrace/microblog/ragservice/RAGServiceClient.java +++ b/src/microblog-service/src/main/java/org/dynatrace/microblog/ragservice/RAGServiceClient.java @@ -21,9 +21,11 @@ public class RAGServiceClient { private final Logger logger = LoggerFactory.getLogger(org.dynatrace.microblog.ragservice.RAGServiceClient.class); private final String ragServiceHost; + private final int ragServicePort; - public RAGServiceClient(String ragServiceHost) { + public RAGServiceClient(String ragServiceHost, String ragsServicePort) { this.ragServiceHost = ragServiceHost; + this.ragServicePort = Integer.parseInt(ragsServicePort); TracingInterceptor tracingInterceptor = new TracingInterceptor( GlobalTracer.get(), @@ -47,10 +49,15 @@ public String getSpamClassification(String postText) throws InvalidJwtException, RequestBody body = RequestBody.create( MediaType.parse("application/json"), jsonRequest); - String requestUrl = "http://" + this.ragServiceHost + "/classifyPost"; + HttpUrl url = new HttpUrl.Builder() + .scheme("http") + .host(this.ragServiceHost) + .port(this.ragServicePort) + .addPathSegment("classifyPost") + .build(); Request request = new Request.Builder() - .url(requestUrl) + .url(url) .post(body) .build(); From 448e64f914de2d71c7d09c2745fd1d0733fc3733 Mon Sep 17 00:00:00 2001 From: Laura Randl Date: Thu, 5 Feb 2026 13:31:55 +0100 Subject: [PATCH 3/8] :sparkles: feat: Add user ratings for spam predictions --- .../downvote/route.ts | 21 ++++++ .../spam-prediction-user-rating/route.ts | 18 +++++ .../upvote/route.ts | 21 ++++++ .../components/Timeline/Post.tsx | 2 +- .../Timeline/PostSpamPrediction.tsx | 38 +++++----- .../Timeline/SpamPredictionUserRating.tsx | 36 ++++++++++ src/frontend-nextjs/enums/queryKeys.ts | 1 + .../hooks/mutations/useRateSpamPrediction.ts | 27 +++++++ .../queries/useSpamPredictionUserRating.ts | 31 ++++++++ .../services/SpamPredictionVotingService.ts | 17 +++++ .../api/SpamPredictionVotesService.ts | 46 ++++++++++++ .../microblog/MicroblogController.java | 26 +++++++ .../microblog/dto/SpamPredictionRatings.java | 29 ++++++++ .../microblog/redis/RedisClient.java | 70 +++++++++++++++++++ .../microblog/utils/JwtTokensUtils.java | 5 ++ 15 files changed, 370 insertions(+), 18 deletions(-) create mode 100644 src/frontend-nextjs/app/api/post/[postId]/spam-prediction-user-rating/downvote/route.ts create mode 100644 src/frontend-nextjs/app/api/post/[postId]/spam-prediction-user-rating/route.ts create mode 100644 src/frontend-nextjs/app/api/post/[postId]/spam-prediction-user-rating/upvote/route.ts create mode 100644 src/frontend-nextjs/components/Timeline/SpamPredictionUserRating.tsx create mode 100644 src/frontend-nextjs/hooks/mutations/useRateSpamPrediction.ts create mode 100644 src/frontend-nextjs/hooks/queries/useSpamPredictionUserRating.ts create mode 100644 src/frontend-nextjs/services/SpamPredictionVotingService.ts create mode 100644 src/frontend-nextjs/services/api/SpamPredictionVotesService.ts create mode 100644 src/microblog-service/src/main/java/org/dynatrace/microblog/dto/SpamPredictionRatings.java diff --git a/src/frontend-nextjs/app/api/post/[postId]/spam-prediction-user-rating/downvote/route.ts b/src/frontend-nextjs/app/api/post/[postId]/spam-prediction-user-rating/downvote/route.ts new file mode 100644 index 00000000..ce97862b --- /dev/null +++ b/src/frontend-nextjs/app/api/post/[postId]/spam-prediction-user-rating/downvote/route.ts @@ -0,0 +1,21 @@ +import { NextResponse } from 'next/server'; + +import { addSpamPredictionUserRatingDownvote } from '@/services/api/SpamPredictionVotesService'; + +/** + * @swagger + * /ui/api/post/{postId}/spam-prediction-user-rating/downvote: + * post: + * description: Downvote the Spam Prediction. + */ + +export type PostParams = { + postId: string; +}; + +export async function POST(req: Request, { params }: { params: Promise }): Promise { + const { postId } = await params; + const res = await addSpamPredictionUserRatingDownvote(postId); + + return NextResponse.json(res.data, { status: res.status }); +} diff --git a/src/frontend-nextjs/app/api/post/[postId]/spam-prediction-user-rating/route.ts b/src/frontend-nextjs/app/api/post/[postId]/spam-prediction-user-rating/route.ts new file mode 100644 index 00000000..61ff5fe5 --- /dev/null +++ b/src/frontend-nextjs/app/api/post/[postId]/spam-prediction-user-rating/route.ts @@ -0,0 +1,18 @@ +import { NextResponse } from 'next/server'; + +import { fetchSpamPredictionUserRating } from '@/services/api/SpamPredictionVotesService'; +import { PostParams } from '@/app/api/like/[postId]/route'; + +/** + * @swagger + * /ui/api/post/{postId}/spam-prediction-user-rating: + * get: + * description: Get the spam prediction user ratings for a post by its ID. + */ + +export async function GET(req: Request, { params }: { params: Promise }): Promise { + const { postId } = await params; + const res = await fetchSpamPredictionUserRating(postId); + + return NextResponse.json(res.data); +} diff --git a/src/frontend-nextjs/app/api/post/[postId]/spam-prediction-user-rating/upvote/route.ts b/src/frontend-nextjs/app/api/post/[postId]/spam-prediction-user-rating/upvote/route.ts new file mode 100644 index 00000000..9f6ea1f4 --- /dev/null +++ b/src/frontend-nextjs/app/api/post/[postId]/spam-prediction-user-rating/upvote/route.ts @@ -0,0 +1,21 @@ +import { NextResponse } from 'next/server'; + +import { addSpamPredictionUserRatingUpvote } from '@/services/api/SpamPredictionVotesService'; + +/** + * @swagger + * /ui/api/post/{postId}/spam-prediction-user-rating/upvote: + * post: + * description: Handle spam prediction upvote action + */ + +export type PostParams = { + postId: string; +}; + +export async function POST(req: Request, { params }: { params: Promise }): Promise { + const { postId } = await params; + const res = await addSpamPredictionUserRatingUpvote(postId); + + return NextResponse.json(res.data, { status: res.status }); +} diff --git a/src/frontend-nextjs/components/Timeline/Post.tsx b/src/frontend-nextjs/components/Timeline/Post.tsx index 6566a469..c320714e 100644 --- a/src/frontend-nextjs/components/Timeline/Post.tsx +++ b/src/frontend-nextjs/components/Timeline/Post.tsx @@ -73,7 +73,7 @@ export function Post(props: PostProps) {
- +
{isLoggedIn && ( diff --git a/src/frontend-nextjs/components/Timeline/PostSpamPrediction.tsx b/src/frontend-nextjs/components/Timeline/PostSpamPrediction.tsx index f2cbef1d..5268f939 100644 --- a/src/frontend-nextjs/components/Timeline/PostSpamPrediction.tsx +++ b/src/frontend-nextjs/components/Timeline/PostSpamPrediction.tsx @@ -1,33 +1,37 @@ +'use client'; + import React from 'react'; import {Alert} from "@heroui/react"; +import {SpamPredictionUserRating} from "@/components/Timeline/SpamPredictionUserRating"; +import {useCheckLogin} from "@/hooks/queries/useCheckLogin"; +import {ErrorBoundary} from "react-error-boundary"; +import {ErrorCard} from "@/components/ErrorCard"; + export interface PostSpamPredictionProps { isSpamPredictedLabel?: boolean | null; + postId: string; } - - export function PostSpamPrediction(props: Readonly) { - console.log("PostSpamPrediction props:", props); + const { isLoggedIn } = useCheckLogin(); if (props.isSpamPredictedLabel == null) return null; + const color = props.isSpamPredictedLabel ? 'danger' : 'primary'; - if (props.isSpamPredictedLabel) { - const color='danger' - return ( -
-
- -
-
- ); - } - - const color = 'primary' return (
-
- +
+ +
+
{props.isSpamPredictedLabel? "Potential Spam Detected" : "No Spam Detected"}
+ {isLoggedIn && ( + }> + + + )} +
+
); diff --git a/src/frontend-nextjs/components/Timeline/SpamPredictionUserRating.tsx b/src/frontend-nextjs/components/Timeline/SpamPredictionUserRating.tsx new file mode 100644 index 00000000..98dd77b4 --- /dev/null +++ b/src/frontend-nextjs/components/Timeline/SpamPredictionUserRating.tsx @@ -0,0 +1,36 @@ +'use client'; + +import React from 'react'; +import {BsHandThumbsDown, BsHandThumbsDownFill, BsHandThumbsUp, BsHandThumbsUpFill} from 'react-icons/bs'; +import { Button, Spinner } from '@heroui/react'; +import {useSpamPredictionUserRating} from "@/hooks/queries/useSpamPredictionUserRating"; +import {useRateSpamPrediction} from "@/hooks/mutations/useRateSpamPrediction"; + +export interface SpamPredictionUserRatingProps { + isSpamPredictedLabel?: boolean | null; + postId: string; +} + +export function SpamPredictionUserRating(props: Readonly) { + + const { data: spamPredictionUserRatingData, isLoading} = useSpamPredictionUserRating(props.postId); + const { handleSpamPredictionUpvote, handleSpamPredictionDownvote } = useRateSpamPrediction(props.postId); + + if (isLoading) { + return ; + } + + return ( +
+ + +
+ ) + +} diff --git a/src/frontend-nextjs/enums/queryKeys.ts b/src/frontend-nextjs/enums/queryKeys.ts index c3424a6f..622ce91b 100644 --- a/src/frontend-nextjs/enums/queryKeys.ts +++ b/src/frontend-nextjs/enums/queryKeys.ts @@ -17,4 +17,5 @@ export enum QUERY_KEYS { ad_manager = 'ad-manager', ad_list = 'ad-list', deployment_health = 'deployment-health', + spam_prediction_user_rating = 'spam-prediction-user-rating', } diff --git a/src/frontend-nextjs/hooks/mutations/useRateSpamPrediction.ts b/src/frontend-nextjs/hooks/mutations/useRateSpamPrediction.ts new file mode 100644 index 00000000..a0dbf703 --- /dev/null +++ b/src/frontend-nextjs/hooks/mutations/useRateSpamPrediction.ts @@ -0,0 +1,27 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import {handleSpamPredictionDownvote, handleSpamPredictionUpvote} from '@/services/SpamPredictionVotingService'; +import { QUERY_KEYS } from '@/enums/queryKeys'; + +export function useRateSpamPrediction(postId: string) { + const queryClient = useQueryClient(); + + const handleUpvoteMutation = useMutation({ + mutationFn: () => handleSpamPredictionUpvote(postId), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.spam_prediction_user_rating, postId] }); + }, + }); + + const handleDownvoteMutation = useMutation({ + mutationFn: () => handleSpamPredictionDownvote(postId), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.spam_prediction_user_rating, postId] }); + }, + }); + + return { + handleSpamPredictionUpvote: handleUpvoteMutation.mutate, + handleSpamPredictionDownvote: handleDownvoteMutation.mutate + }; +} diff --git a/src/frontend-nextjs/hooks/queries/useSpamPredictionUserRating.ts b/src/frontend-nextjs/hooks/queries/useSpamPredictionUserRating.ts new file mode 100644 index 00000000..8e71c2cf --- /dev/null +++ b/src/frontend-nextjs/hooks/queries/useSpamPredictionUserRating.ts @@ -0,0 +1,31 @@ +import path from 'path'; + +import {useQuery} from '@tanstack/react-query'; + +import {QUERY_KEYS} from '@/enums/queryKeys'; +import {BASE_PATH} from '@/constants'; + +type SpamPredictionUserRating = { + spamPredictionUserUpvotes: number; + spamPredictionUserDownvotes: boolean; + isUpvotedByUser?: boolean; + isDownvotedByUser?: boolean; +}; + +async function fetchSpamPredictionUserRatings(postId: string): Promise { + const res = await fetch(path.join(BASE_PATH, `/api/post/${postId}/spam-prediction-user-rating/`)); + + if (!res.ok) { + throw new Error('Failed to fetch spam prediction user ratings'); + } + + return await res.json(); +} + +export function useSpamPredictionUserRating(postId: string) { + return useQuery({ + queryKey: [QUERY_KEYS.spam_prediction_user_rating, postId], + queryFn: () => fetchSpamPredictionUserRatings(postId), + throwOnError: true, + }); +} diff --git a/src/frontend-nextjs/services/SpamPredictionVotingService.ts b/src/frontend-nextjs/services/SpamPredictionVotingService.ts new file mode 100644 index 00000000..ee1f671c --- /dev/null +++ b/src/frontend-nextjs/services/SpamPredictionVotingService.ts @@ -0,0 +1,17 @@ +import path from 'path'; + +import { BASE_PATH } from '@/constants'; + +export async function handleSpamPredictionDownvote(postId: string): Promise { + return await fetch(path.join(BASE_PATH, `/api/post/${postId}/spam-prediction-user-rating/downvote/`), { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + }); +} + +export async function handleSpamPredictionUpvote(postId: string): Promise { + return await fetch(path.join(BASE_PATH, `/api/post/${postId}/spam-prediction-user-rating/upvote/`), { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + }); +} diff --git a/src/frontend-nextjs/services/api/SpamPredictionVotesService.ts b/src/frontend-nextjs/services/api/SpamPredictionVotesService.ts new file mode 100644 index 00000000..1d6d4628 --- /dev/null +++ b/src/frontend-nextjs/services/api/SpamPredictionVotesService.ts @@ -0,0 +1,46 @@ +import {getMicroblogApi} from '@/axios'; +import {getJwtFromCookie} from '@/services/api/AuthService'; +import type {AxiosRequestConfig} from 'axios'; +import {AxiosHeaders} from 'axios'; + +async function withJwtCookie(): Promise { + const jwt = await getJwtFromCookie(); + + const headers = new AxiosHeaders(); + headers.set('Cookie', `jwt=${jwt}`); + + return {headers}; +} + +export async function fetchSpamPredictionUserRating(postId: string): Promise { + return await getMicroblogApi() + .get(`/spam-prediction-user-rating/${postId}`, await withJwtCookie()) + .then((response) => { + return response; + }) + .catch((error) => { + return error.response; + }); +} + +export async function addSpamPredictionUserRatingUpvote(postId: string): Promise { + return await getMicroblogApi() + .post(`/spam-prediction-user-rating/${postId}/upvote/`, {}, await withJwtCookie()) + .then((response) => { + return response; + }) + .catch((error) => { + return error.response; + }); +} + +export async function addSpamPredictionUserRatingDownvote(postId: string): Promise { + return await getMicroblogApi() + .post(`/spam-prediction-user-rating/${postId}/downvote/`, {}, await withJwtCookie()) + .then((response) => { + return response; + }) + .catch((error) => { + return error.response; + }); +} diff --git a/src/microblog-service/src/main/java/org/dynatrace/microblog/MicroblogController.java b/src/microblog-service/src/main/java/org/dynatrace/microblog/MicroblogController.java index d10cc117..02402371 100644 --- a/src/microblog-service/src/main/java/org/dynatrace/microblog/MicroblogController.java +++ b/src/microblog-service/src/main/java/org/dynatrace/microblog/MicroblogController.java @@ -23,6 +23,7 @@ import org.dynatrace.microblog.dto.PostId; import org.dynatrace.microblog.dto.SerializedPost; import org.dynatrace.microblog.dto.User; +import org.dynatrace.microblog.dto.SpamPredictionRatings; import org.dynatrace.microblog.exceptions.*; import org.dynatrace.microblog.form.PostForm; import org.dynatrace.microblog.ragservice.RAGServiceClient; @@ -50,6 +51,7 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import static org.dynatrace.microblog.utils.JwtTokensUtils.decodeTokenUserId; import static org.springframework.http.HttpStatus.NOT_FOUND; @RestController @@ -225,6 +227,30 @@ public Post getPost(@PathVariable("postid") String postId, @CookieValue(value = return post; } + + @GetMapping("/spam-prediction-user-rating/{postid}") + public SpamPredictionRatings getSpamPredictionUserRating(@PathVariable("postid") String postId, @CookieValue(value = "jwt", required = false) String jwt) throws InvalidJwtException, NotLoggedInException { + checkJwt(jwt); + + SpamPredictionRatings spamPredictionUserRatings = redisClient.getSpamPredictionUserRatings(postId, decodeTokenUserId(jwt)); + if (spamPredictionUserRatings == null) { + throw new ResponseStatusException(NOT_FOUND, "Spam prediction user ratings for post with ID " + postId + " not found."); + } + return spamPredictionUserRatings; + } + + @PostMapping("/spam-prediction-user-rating/{postid}/upvote") + public void upvoteSpamPrediction(@PathVariable("postid") String postId, @CookieValue(value = "jwt", required = false) String jwt) throws InvalidJwtException, NotLoggedInException { + checkJwt(jwt); + redisClient.handleSpamPredictionUpvote(postId, decodeTokenUserId(jwt)); + } + + @PostMapping("/spam-prediction-user-rating/{postid}/downvote") + public void downvoteSpamPrediction(@PathVariable("postid") String postId, @CookieValue(value = "jwt", required = false) String jwt) throws InvalidJwtException, NotLoggedInException { + checkJwt(jwt); + redisClient.handleSpamPredictionDownvote(postId, decodeTokenUserId(jwt)); + } + public void checkJwt(String jwt) throws InvalidJwtException, NotLoggedInException { if (jwt == null) { throw new NotLoggedInException(); diff --git a/src/microblog-service/src/main/java/org/dynatrace/microblog/dto/SpamPredictionRatings.java b/src/microblog-service/src/main/java/org/dynatrace/microblog/dto/SpamPredictionRatings.java new file mode 100644 index 00000000..5eda5070 --- /dev/null +++ b/src/microblog-service/src/main/java/org/dynatrace/microblog/dto/SpamPredictionRatings.java @@ -0,0 +1,29 @@ +package org.dynatrace.microblog.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class SpamPredictionRatings { + private final Integer spamPredictionUserUpvotes; + private final Integer spamPredictionUserDownvotes; + private final Boolean isUpvotedByUser; + private final Boolean isDownvotedByUser; + + public SpamPredictionRatings( + @JsonProperty("spamPredictionUserUpvotes") Integer spamPredictionUserUpvotes, + @JsonProperty("spamPredictionUserDownvotes") Integer spamPredictionUserDownvotes, + @JsonProperty("isUpvotedByUser") Boolean isUpvotedByUser, + @JsonProperty("isDownvotedByUser") Boolean isDownvotedByUser) { + this.spamPredictionUserUpvotes = spamPredictionUserUpvotes; + this.spamPredictionUserDownvotes = spamPredictionUserDownvotes; + this.isUpvotedByUser = isUpvotedByUser; + this.isDownvotedByUser = isDownvotedByUser; + } + + public Integer getSpamPredictionUserUpvotes() { return spamPredictionUserUpvotes; } + + public Integer getSpamPredictionUserDownvotes() { return spamPredictionUserDownvotes; } + + public Boolean getIsUpvotedByUser() { return isUpvotedByUser; } + + public Boolean getIsDownvotedByUser() { return isDownvotedByUser; } +} diff --git a/src/microblog-service/src/main/java/org/dynatrace/microblog/redis/RedisClient.java b/src/microblog-service/src/main/java/org/dynatrace/microblog/redis/RedisClient.java index 3479b1c1..5bdcdd8f 100644 --- a/src/microblog-service/src/main/java/org/dynatrace/microblog/redis/RedisClient.java +++ b/src/microblog-service/src/main/java/org/dynatrace/microblog/redis/RedisClient.java @@ -21,6 +21,7 @@ import io.opentracing.contrib.redis.jedis3.TracingJedisPool; import org.dynatrace.microblog.authservice.UserAuthServiceClient; import org.dynatrace.microblog.dto.Post; +import org.dynatrace.microblog.dto.SpamPredictionRatings; import org.dynatrace.microblog.dto.User; import org.dynatrace.microblog.exceptions.InvalidJwtException; import org.dynatrace.microblog.exceptions.UserNotFoundException; @@ -53,6 +54,9 @@ public class RedisClient { private static final String POSTS_KEY_PREFIX = "posts"; private static final String FOLLOWERS_KEY_PREFIX = "followers"; + private static final String SPAM_PREDICTION_UPVOTERS_KEY_SUFFIX = "spamPrediction:upvoters"; + private static final String SPAM_PREDICTION_DOWNVOTERS_KEY_SUFFIX = "spamPrediction:downvoters"; + private static final int REDIS_PORT = 6379; public static final int POST_TIMEOUT_SECONDS = 24 * 60 * 60; @@ -74,6 +78,14 @@ private String getCombinedKey(String keyPrefix, String value) { return String.format("%s:%s", keyPrefix, value); } + private String getSpamPredictionUpvotersKey(@NotNull String postId) { + return getCombinedKey(getCombinedKey(POST_KEY_PREFIX, postId), SPAM_PREDICTION_UPVOTERS_KEY_SUFFIX); + } + + private String getSpamPredictionDownvotersKey(@NotNull String postId) { + return getCombinedKey(getCombinedKey(POST_KEY_PREFIX, postId), SPAM_PREDICTION_DOWNVOTERS_KEY_SUFFIX); + } + /** * Creates and persists a new post with the given details * @param userId the user id of the author @@ -115,6 +127,10 @@ public String newPost(@NotNull String userId, @NotNull String body, @Nullable St jedis.lpush(TIMELINE_KEY, postId); jedis.ltrim(TIMELINE_KEY, 0, MAX_TIMELINE_ENTRIES); + // create (initially empty) sets of upvoters and downvoters for the post's spam prediction + jedis.expire(getSpamPredictionUpvotersKey(postId), POST_TIMEOUT_SECONDS); + jedis.expire(getSpamPredictionDownvotersKey(postId), POST_TIMEOUT_SECONDS); + return postId; } } @@ -126,6 +142,41 @@ public void setSpamPredictedLabel(@NotNull String postId, boolean isSpamPredicte } } + public SpamPredictionRatings getSpamPredictionUserRatings(@NotNull String postId, @NotNull String userId) { + try (Jedis jedis = jedisPool.getResource()) { + return readAndTransformSpamPredictionUserRating(postId, userId, jedis); + } + } + + public void handleSpamPredictionUpvote(@NotNull String postId, @NotNull String userId) { + try (Jedis jedis = jedisPool.getResource()) { + if (Boolean.TRUE.equals(jedis.sismember(getSpamPredictionUpvotersKey(postId), userId))) { + // remove the user from the upvoters if already contained + jedis.srem(getSpamPredictionUpvotersKey(postId), userId); + } else { + // add user to the upvoters set + jedis.sadd(getSpamPredictionUpvotersKey(postId), userId); + // remove the user from the downvoters set if contained + jedis.srem(getSpamPredictionDownvotersKey(postId), userId); + } + } + } + + public void handleSpamPredictionDownvote(@NotNull String postId, @NotNull String userId) { + try (Jedis jedis = jedisPool.getResource()) { + if (Boolean.TRUE.equals(jedis.sismember(getSpamPredictionDownvotersKey(postId), userId))) { + // remove the user from the downvoters if already contained + jedis.srem(getSpamPredictionDownvotersKey(postId), userId); + } else { + // add user to the downvoters set + jedis.sadd(getSpamPredictionDownvotersKey(postId), userId); + // remove the user from the upvoters set if contained + jedis.srem(getSpamPredictionUpvotersKey(postId), userId); + } + } + } + + @NotNull public List getUserTimeline(String userId) { List postIds; @@ -208,6 +259,25 @@ private Post readAndTransformPost(String postId, Jedis jedis) throws UserNotFoun return new Post(postId, userName, body, imageUrl, timestamp, isSpamPredictedLabel); } + private SpamPredictionRatings readAndTransformSpamPredictionUserRating(String postId, String userId, Jedis jedis) { + Map postMap = jedis.hgetAll(getCombinedKey(POST_KEY_PREFIX, postId)); + if (postMap.isEmpty()) { + logger.warn("Could not get post with id {}", postId); + return null; + } + + Set upvoters = jedis.smembers(getSpamPredictionUpvotersKey(postId)); + Set downvoters = jedis.smembers(getSpamPredictionDownvotersKey(postId)); + + Boolean isUpvotedByUser = upvoters.contains(userId); + Boolean isDownvotedByUser = downvoters.contains(userId); + + Integer spamPredictionUserUpvotes = upvoters.size(); + Integer spamPredictionUserDownvotes = downvoters.size(); + + return new SpamPredictionRatings(spamPredictionUserUpvotes, spamPredictionUserDownvotes, isUpvotedByUser, isDownvotedByUser); + } + public List getTimeline() { List posts; try (Jedis jedis = jedisPool.getResource()) { diff --git a/src/microblog-service/src/main/java/org/dynatrace/microblog/utils/JwtTokensUtils.java b/src/microblog-service/src/main/java/org/dynatrace/microblog/utils/JwtTokensUtils.java index 5411ef3c..d4563e9b 100644 --- a/src/microblog-service/src/main/java/org/dynatrace/microblog/utils/JwtTokensUtils.java +++ b/src/microblog-service/src/main/java/org/dynatrace/microblog/utils/JwtTokensUtils.java @@ -16,4 +16,9 @@ public static Claims decodeTokenClaims(String token) { Claims claims = (Claims) untrusted.getBody(); return claims; } + + public static String decodeTokenUserId(String token) { + Claims claims = decodeTokenClaims(token); + return claims.get("userid").toString(); + } } From a269edbd1152ad4ba3cc1dc1ccf9cff265662899 Mon Sep 17 00:00:00 2001 From: Laura Randl Date: Thu, 5 Feb 2026 14:19:37 +0100 Subject: [PATCH 4/8] :art: refactor(frontend): Reformat with prettier --- .../components/Timeline/Post.tsx | 2 +- .../Timeline/PostSpamPrediction.tsx | 23 ++++++++++------- .../Timeline/SpamPredictionUserRating.tsx | 25 ++++++++++++------- .../components/Timeline/Timeline.tsx | 2 +- .../hooks/mutations/useRateSpamPrediction.ts | 4 +-- .../queries/useSpamPredictionUserRating.ts | 6 ++--- .../api/SpamPredictionVotesService.ts | 11 ++++---- src/microblog-service/Dockerfile | 2 +- 8 files changed, 44 insertions(+), 31 deletions(-) diff --git a/src/frontend-nextjs/components/Timeline/Post.tsx b/src/frontend-nextjs/components/Timeline/Post.tsx index c320714e..f7ab5fb7 100644 --- a/src/frontend-nextjs/components/Timeline/Post.tsx +++ b/src/frontend-nextjs/components/Timeline/Post.tsx @@ -73,7 +73,7 @@ export function Post(props: PostProps) {
- +
{isLoggedIn && ( diff --git a/src/frontend-nextjs/components/Timeline/PostSpamPrediction.tsx b/src/frontend-nextjs/components/Timeline/PostSpamPrediction.tsx index 5268f939..9a59ec1a 100644 --- a/src/frontend-nextjs/components/Timeline/PostSpamPrediction.tsx +++ b/src/frontend-nextjs/components/Timeline/PostSpamPrediction.tsx @@ -1,12 +1,12 @@ 'use client'; import React from 'react'; -import {Alert} from "@heroui/react"; +import { Alert } from '@heroui/react'; +import { ErrorBoundary } from 'react-error-boundary'; -import {SpamPredictionUserRating} from "@/components/Timeline/SpamPredictionUserRating"; -import {useCheckLogin} from "@/hooks/queries/useCheckLogin"; -import {ErrorBoundary} from "react-error-boundary"; -import {ErrorCard} from "@/components/ErrorCard"; +import { SpamPredictionUserRating } from '@/components/Timeline/SpamPredictionUserRating'; +import { useCheckLogin } from '@/hooks/queries/useCheckLogin'; +import { ErrorCard } from '@/components/ErrorCard'; export interface PostSpamPredictionProps { isSpamPredictedLabel?: boolean | null; @@ -21,13 +21,18 @@ export function PostSpamPrediction(props: Readonly) { return (
-
+
-
-
{props.isSpamPredictedLabel? "Potential Spam Detected" : "No Spam Detected"}
+
+
+ {props.isSpamPredictedLabel ? 'Potential Spam Detected' : 'No Spam Detected'} +
{isLoggedIn && ( }> - + )}
diff --git a/src/frontend-nextjs/components/Timeline/SpamPredictionUserRating.tsx b/src/frontend-nextjs/components/Timeline/SpamPredictionUserRating.tsx index 98dd77b4..787c04d5 100644 --- a/src/frontend-nextjs/components/Timeline/SpamPredictionUserRating.tsx +++ b/src/frontend-nextjs/components/Timeline/SpamPredictionUserRating.tsx @@ -1,10 +1,11 @@ 'use client'; import React from 'react'; -import {BsHandThumbsDown, BsHandThumbsDownFill, BsHandThumbsUp, BsHandThumbsUpFill} from 'react-icons/bs'; +import { BsHandThumbsDown, BsHandThumbsDownFill, BsHandThumbsUp, BsHandThumbsUpFill } from 'react-icons/bs'; import { Button, Spinner } from '@heroui/react'; -import {useSpamPredictionUserRating} from "@/hooks/queries/useSpamPredictionUserRating"; -import {useRateSpamPrediction} from "@/hooks/mutations/useRateSpamPrediction"; + +import { useSpamPredictionUserRating } from '@/hooks/queries/useSpamPredictionUserRating'; +import { useRateSpamPrediction } from '@/hooks/mutations/useRateSpamPrediction'; export interface SpamPredictionUserRatingProps { isSpamPredictedLabel?: boolean | null; @@ -12,8 +13,7 @@ export interface SpamPredictionUserRatingProps { } export function SpamPredictionUserRating(props: Readonly) { - - const { data: spamPredictionUserRatingData, isLoading} = useSpamPredictionUserRating(props.postId); + const { data: spamPredictionUserRatingData, isLoading } = useSpamPredictionUserRating(props.postId); const { handleSpamPredictionUpvote, handleSpamPredictionDownvote } = useRateSpamPrediction(props.postId); if (isLoading) { @@ -22,15 +22,22 @@ export function SpamPredictionUserRating(props: Readonly - -
- ) - + ); } diff --git a/src/frontend-nextjs/components/Timeline/Timeline.tsx b/src/frontend-nextjs/components/Timeline/Timeline.tsx index 0b510903..6315c2ec 100644 --- a/src/frontend-nextjs/components/Timeline/Timeline.tsx +++ b/src/frontend-nextjs/components/Timeline/Timeline.tsx @@ -27,10 +27,10 @@ export function Timeline({ posts, isLoading }: TimelineProps) {
diff --git a/src/frontend-nextjs/hooks/mutations/useRateSpamPrediction.ts b/src/frontend-nextjs/hooks/mutations/useRateSpamPrediction.ts index a0dbf703..e6237b8e 100644 --- a/src/frontend-nextjs/hooks/mutations/useRateSpamPrediction.ts +++ b/src/frontend-nextjs/hooks/mutations/useRateSpamPrediction.ts @@ -1,6 +1,6 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; -import {handleSpamPredictionDownvote, handleSpamPredictionUpvote} from '@/services/SpamPredictionVotingService'; +import { handleSpamPredictionDownvote, handleSpamPredictionUpvote } from '@/services/SpamPredictionVotingService'; import { QUERY_KEYS } from '@/enums/queryKeys'; export function useRateSpamPrediction(postId: string) { @@ -22,6 +22,6 @@ export function useRateSpamPrediction(postId: string) { return { handleSpamPredictionUpvote: handleUpvoteMutation.mutate, - handleSpamPredictionDownvote: handleDownvoteMutation.mutate + handleSpamPredictionDownvote: handleDownvoteMutation.mutate, }; } diff --git a/src/frontend-nextjs/hooks/queries/useSpamPredictionUserRating.ts b/src/frontend-nextjs/hooks/queries/useSpamPredictionUserRating.ts index 8e71c2cf..c710f450 100644 --- a/src/frontend-nextjs/hooks/queries/useSpamPredictionUserRating.ts +++ b/src/frontend-nextjs/hooks/queries/useSpamPredictionUserRating.ts @@ -1,9 +1,9 @@ import path from 'path'; -import {useQuery} from '@tanstack/react-query'; +import { useQuery } from '@tanstack/react-query'; -import {QUERY_KEYS} from '@/enums/queryKeys'; -import {BASE_PATH} from '@/constants'; +import { QUERY_KEYS } from '@/enums/queryKeys'; +import { BASE_PATH } from '@/constants'; type SpamPredictionUserRating = { spamPredictionUserUpvotes: number; diff --git a/src/frontend-nextjs/services/api/SpamPredictionVotesService.ts b/src/frontend-nextjs/services/api/SpamPredictionVotesService.ts index 1d6d4628..7bd29576 100644 --- a/src/frontend-nextjs/services/api/SpamPredictionVotesService.ts +++ b/src/frontend-nextjs/services/api/SpamPredictionVotesService.ts @@ -1,15 +1,16 @@ -import {getMicroblogApi} from '@/axios'; -import {getJwtFromCookie} from '@/services/api/AuthService'; -import type {AxiosRequestConfig} from 'axios'; -import {AxiosHeaders} from 'axios'; +import { AxiosHeaders, AxiosRequestConfig } from 'axios'; + +import { getMicroblogApi } from '@/axios'; +import { getJwtFromCookie } from '@/services/api/AuthService'; async function withJwtCookie(): Promise { const jwt = await getJwtFromCookie(); const headers = new AxiosHeaders(); + headers.set('Cookie', `jwt=${jwt}`); - return {headers}; + return { headers }; } export async function fetchSpamPredictionUserRating(postId: string): Promise { diff --git a/src/microblog-service/Dockerfile b/src/microblog-service/Dockerfile index 0038eafe..7cc9643e 100644 --- a/src/microblog-service/Dockerfile +++ b/src/microblog-service/Dockerfile @@ -1,4 +1,4 @@ -FROM gradle:6.9.1-jdk11 as builder +FROM gradle:6.9.1-jdk11 AS builder COPY --chown=gradle:gradle . /home/gradle/src WORKDIR /home/gradle/src From 993516c0171811533e464dcefbe699df8a117ad8 Mon Sep 17 00:00:00 2001 From: Laura Randl Date: Thu, 5 Feb 2026 15:14:25 +0100 Subject: [PATCH 5/8] :recycle: refactor(frontend): Fix ESLint warnings & small improvements --- chart/templates/rag-service-configmap.yaml | 3 +++ src/frontend-nextjs/app/api/post/route.ts | 1 + src/frontend-nextjs/app/post/page.tsx | 2 +- .../components/SwaggerUIReact.tsx | 1 + .../components/Timeline/PostSpamPrediction.tsx | 1 + .../Timeline/SpamPredictionUserRating.tsx | 5 +++-- src/microblog-service/README.md | 2 ++ src/rag-service/README.md | 16 ++++++++++++++++ 8 files changed, 28 insertions(+), 3 deletions(-) diff --git a/chart/templates/rag-service-configmap.yaml b/chart/templates/rag-service-configmap.yaml index 2c7e844c..1628d715 100644 --- a/chart/templates/rag-service-configmap.yaml +++ b/chart/templates/rag-service-configmap.yaml @@ -17,4 +17,7 @@ data: USE_DATA_POISONING_DETECTION: {{ quote .Values.ragService.deployment.container.env.USE_DATA_POISONING_DETECTION }} DATA_POISONING_DETECTION_STRATEGY: {{ quote .Values.ragService.deployment.container.env.DATA_POISONING_DETECTION_STRATEGY }} LABEL_CONSISTENCY_DETECTION_DECISION_VARIANT: {{ quote .Values.ragService.deployment.container.env.LABEL_CONSISTENCY_DETECTION_DECISION_VARIANT }} + {{- with .Values.ragService.deployment.container.env.LANGDOCK_API_KEY }} + LANGDOCK_API_KEY: {{ quote . }} + {{- end }} {{- end }} diff --git a/src/frontend-nextjs/app/api/post/route.ts b/src/frontend-nextjs/app/api/post/route.ts index c58003e8..9891f567 100644 --- a/src/frontend-nextjs/app/api/post/route.ts +++ b/src/frontend-nextjs/app/api/post/route.ts @@ -12,6 +12,7 @@ import { createNewPost } from '@/services/api/CreatePostService'; export async function POST(request: Request): Promise { const body = await request.json(); let header = request.headers.get('header'); + if (header) { header = Buffer.from(header, 'latin1').toString('utf-8'); } diff --git a/src/frontend-nextjs/app/post/page.tsx b/src/frontend-nextjs/app/post/page.tsx index 3e5600c0..6d597d54 100644 --- a/src/frontend-nextjs/app/post/page.tsx +++ b/src/frontend-nextjs/app/post/page.tsx @@ -30,10 +30,10 @@ function SinglePost() { )}
diff --git a/src/frontend-nextjs/components/SwaggerUIReact.tsx b/src/frontend-nextjs/components/SwaggerUIReact.tsx index df5e0c6e..edab3d65 100644 --- a/src/frontend-nextjs/components/SwaggerUIReact.tsx +++ b/src/frontend-nextjs/components/SwaggerUIReact.tsx @@ -21,6 +21,7 @@ const HideTryItOutPlugin = () => ({ function ReactSwagger({ spec }: Props) { const specWithoutServers = { ...spec }; + delete specWithoutServers.servers; return ; diff --git a/src/frontend-nextjs/components/Timeline/PostSpamPrediction.tsx b/src/frontend-nextjs/components/Timeline/PostSpamPrediction.tsx index 9a59ec1a..8ad57d72 100644 --- a/src/frontend-nextjs/components/Timeline/PostSpamPrediction.tsx +++ b/src/frontend-nextjs/components/Timeline/PostSpamPrediction.tsx @@ -15,6 +15,7 @@ export interface PostSpamPredictionProps { export function PostSpamPrediction(props: Readonly) { const { isLoggedIn } = useCheckLogin(); + if (props.isSpamPredictedLabel == null) return null; const color = props.isSpamPredictedLabel ? 'danger' : 'primary'; diff --git a/src/frontend-nextjs/components/Timeline/SpamPredictionUserRating.tsx b/src/frontend-nextjs/components/Timeline/SpamPredictionUserRating.tsx index 787c04d5..9fa7bd6f 100644 --- a/src/frontend-nextjs/components/Timeline/SpamPredictionUserRating.tsx +++ b/src/frontend-nextjs/components/Timeline/SpamPredictionUserRating.tsx @@ -23,15 +23,16 @@ export function SpamPredictionUserRating(props: Readonly +
{isLoggedIn && ( - }> + } + > { return await getMicroblogApi() - .post(`/spam-prediction-user-rating/${postId}/upvote/`, {}, await withJwtCookie()) + .post(`/spam-prediction-user-rating/${postId}/upvote`, {}, await withJwtCookie()) .then((response) => { return response; }) @@ -37,7 +37,7 @@ export async function addSpamPredictionUserRatingUpvote(postId: string): Promise export async function addSpamPredictionUserRatingDownvote(postId: string): Promise { return await getMicroblogApi() - .post(`/spam-prediction-user-rating/${postId}/downvote/`, {}, await withJwtCookie()) + .post(`/spam-prediction-user-rating/${postId}/downvote`, {}, await withJwtCookie()) .then((response) => { return response; }) diff --git a/src/microblog-service/README.md b/src/microblog-service/README.md index a994b336..15fc91ec 100644 --- a/src/microblog-service/README.md +++ b/src/microblog-service/README.md @@ -51,14 +51,15 @@ Running the application should start a webserver accessible on [localhost:8080]( To get more information about the JAEGER config options, see https://www.jaegertracing.io/docs/1.19/client-features/ -| Name | Example Value | Description | -|---------------------------|---------------------------|--------------------------------------------------------------| -| SERVER_PORT | 8080 | The port that the server will run on | -| REDIS_SERVICE_ADDRESS | localhost | Change to hostname/IP of your Redis instance | -| JAEGER_AGENT_HOST | localhost | Change to hostname/IP of your Jaeger agent | -| JAEGER_SERVICE_NAME | microblog-service | Name that will be used for the service in the Jaeger traces | -| JAEGER_SAMPLER_TYPE | const | (optional) Set to const to get all traces | -| JAEGER_SAMPLER_PARAM | 1 | (optional) Set to 1 while sampler is const to get all traces | -| USER_AUTH_SERVICE_ADDRESS | unguard-user-auth-service | Change to hostname/IP of user-auth-service instance | -| RAG_SERVICE_ADDRESS | unguard-rag-service | Change to hostname/IP of rag-service instance | -| RAG_SERVICE_PORT | 8000 | Change to port number of rag-service instance | +| Name | Example Value | Description | +|---------------------------|---------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| SERVER_PORT | 8080 | The port that the server will run on | +| REDIS_SERVICE_ADDRESS | localhost | Change to hostname/IP of your Redis instance | +| JAEGER_AGENT_HOST | localhost | Change to hostname/IP of your Jaeger agent | +| JAEGER_SERVICE_NAME | microblog-service | Name that will be used for the service in the Jaeger traces | +| JAEGER_SAMPLER_TYPE | const | (optional) Set to const to get all traces | +| JAEGER_SAMPLER_PARAM | 1 | (optional) Set to 1 while sampler is const to get all traces | +| USER_AUTH_SERVICE_ADDRESS | unguard-user-auth-service | Change to hostname/IP of user-auth-service instance | +| RAG_SERVICE_ADDRESS | unguard-rag-service | Change to hostname/IP of rag-service instance | +| RAG_SERVICE_PORT | 8000 | Change to port number of rag-service instance | +| RAG_SERVICE_ENABLED | true | Configure whether the RAG service for Spam Detection should be deployed, including other necessary services such as the Ollama service and the Feedback Ingestion Service | diff --git a/src/microblog-service/src/main/java/org/dynatrace/microblog/MicroblogController.java b/src/microblog-service/src/main/java/org/dynatrace/microblog/MicroblogController.java index 35db5b33..ebefc443 100644 --- a/src/microblog-service/src/main/java/org/dynatrace/microblog/MicroblogController.java +++ b/src/microblog-service/src/main/java/org/dynatrace/microblog/MicroblogController.java @@ -43,6 +43,8 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.server.ResponseStatusException; +import javax.annotation.PreDestroy; +import java.util.concurrent.TimeUnit; import java.io.IOException; import java.util.Collection; import java.util.List; @@ -119,6 +121,19 @@ public MicroblogController(Tracer tracer, PostSerializer postSerializer) { this.ragServiceEnabled = isRagServiceEnabled; } + @PreDestroy + public void shutdownRagExecutor() { + ragExecutor.shutdown(); + try { + if (!ragExecutor.awaitTermination(30, TimeUnit.SECONDS)) { + ragExecutor.shutdownNow(); + } + } catch (InterruptedException e) { + ragExecutor.shutdownNow(); + Thread.currentThread().interrupt(); + } + } + @RequestMapping("/timeline") public List timeline() { return redisClient.getTimeline(); diff --git a/src/microblog-service/src/main/java/org/dynatrace/microblog/exceptions/EmptySpamClassificationException.java b/src/microblog-service/src/main/java/org/dynatrace/microblog/exceptions/EmptySpamClassificationException.java new file mode 100644 index 00000000..35db5239 --- /dev/null +++ b/src/microblog-service/src/main/java/org/dynatrace/microblog/exceptions/EmptySpamClassificationException.java @@ -0,0 +1,7 @@ +package org.dynatrace.microblog.exceptions; + +public class EmptySpamClassificationException extends RuntimeException { + public EmptySpamClassificationException(String message) { + super(message); + } +} diff --git a/src/microblog-service/src/main/java/org/dynatrace/microblog/exceptions/InvalidPostException.java b/src/microblog-service/src/main/java/org/dynatrace/microblog/exceptions/InvalidPostException.java new file mode 100644 index 00000000..5e14de3d --- /dev/null +++ b/src/microblog-service/src/main/java/org/dynatrace/microblog/exceptions/InvalidPostException.java @@ -0,0 +1,7 @@ +package org.dynatrace.microblog.exceptions; + +public class InvalidPostException extends RuntimeException { + public InvalidPostException(String message) { + super(message); + } +} diff --git a/src/microblog-service/src/main/java/org/dynatrace/microblog/ragservice/RAGServiceClient.java b/src/microblog-service/src/main/java/org/dynatrace/microblog/ragservice/RAGServiceClient.java index b59cfe83..bbb157ae 100644 --- a/src/microblog-service/src/main/java/org/dynatrace/microblog/ragservice/RAGServiceClient.java +++ b/src/microblog-service/src/main/java/org/dynatrace/microblog/ragservice/RAGServiceClient.java @@ -5,8 +5,7 @@ import io.opentracing.contrib.okhttp3.TracingInterceptor; import io.opentracing.util.GlobalTracer; import okhttp3.*; -import org.dynatrace.microblog.exceptions.InvalidJwtException; -import org.dynatrace.microblog.exceptions.UserNotFoundException; +import org.dynatrace.microblog.exceptions.EmptySpamClassificationException; import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -23,9 +22,9 @@ public class RAGServiceClient { private final String ragServiceHost; private final int ragServicePort; - public RAGServiceClient(String ragServiceHost, String ragsServicePort) { + public RAGServiceClient(String ragServiceHost, String ragServicePort) { this.ragServiceHost = ragServiceHost; - this.ragServicePort = Integer.parseInt(ragsServicePort); + this.ragServicePort = Integer.parseInt(ragServicePort); TracingInterceptor tracingInterceptor = new TracingInterceptor( GlobalTracer.get(), @@ -41,7 +40,7 @@ public RAGServiceClient(String ragServiceHost, String ragsServicePort) { } - public String getSpamClassification(String postText) throws InvalidJwtException, UserNotFoundException, IOException { + public String getSpamClassification(String postText) throws IOException, EmptySpamClassificationException { JsonObject obj = new JsonObject(); obj.addProperty("text", postText); String jsonRequest = obj.toString(); @@ -66,10 +65,12 @@ public String getSpamClassification(String postText) throws InvalidJwtException, try (Response response = call.execute()) { if (response.code() == 200) { - JSONObject responseObject = new JSONObject(response.body().string()); + ResponseBody responseBody = response.body(); + if (responseBody == null) { + throw new EmptySpamClassificationException("Error retrieving spam classification from RAG service: empty response body"); + } + JSONObject responseObject = new JSONObject(responseBody.string()); return responseObject.getString("classification"); - } else if (response.code() == 401) { - throw new InvalidJwtException(); } else { throw new RuntimeException("Error retrieving spam classification from RAG service: " + response.code() + " - " + response.message()); } diff --git a/src/microblog-service/src/main/java/org/dynatrace/microblog/redis/RedisClient.java b/src/microblog-service/src/main/java/org/dynatrace/microblog/redis/RedisClient.java index 5bdcdd8f..5e2b48de 100644 --- a/src/microblog-service/src/main/java/org/dynatrace/microblog/redis/RedisClient.java +++ b/src/microblog-service/src/main/java/org/dynatrace/microblog/redis/RedisClient.java @@ -24,6 +24,7 @@ import org.dynatrace.microblog.dto.SpamPredictionRatings; import org.dynatrace.microblog.dto.User; import org.dynatrace.microblog.exceptions.InvalidJwtException; +import org.dynatrace.microblog.exceptions.InvalidPostException; import org.dynatrace.microblog.exceptions.UserNotFoundException; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -150,6 +151,11 @@ public SpamPredictionRatings getSpamPredictionUserRatings(@NotNull String postId public void handleSpamPredictionUpvote(@NotNull String postId, @NotNull String userId) { try (Jedis jedis = jedisPool.getResource()) { + if (Boolean.FALSE.equals(jedis.exists(getCombinedKey(POST_KEY_PREFIX, postId)))) { + logger.warn("Could not find post with id {} for spam prediction upvote", postId); + throw new InvalidPostException("Post with this id does not exist."); + } + if (Boolean.TRUE.equals(jedis.sismember(getSpamPredictionUpvotersKey(postId), userId))) { // remove the user from the upvoters if already contained jedis.srem(getSpamPredictionUpvotersKey(postId), userId); @@ -164,6 +170,10 @@ public void handleSpamPredictionUpvote(@NotNull String postId, @NotNull String u public void handleSpamPredictionDownvote(@NotNull String postId, @NotNull String userId) { try (Jedis jedis = jedisPool.getResource()) { + if (Boolean.FALSE.equals(jedis.exists(getCombinedKey(POST_KEY_PREFIX, postId)))) { + logger.warn("Could not find post with id {} for spam prediction upvote", postId); + throw new InvalidPostException("Post with this id does not exist."); + } if (Boolean.TRUE.equals(jedis.sismember(getSpamPredictionDownvotersKey(postId), userId))) { // remove the user from the downvoters if already contained jedis.srem(getSpamPredictionDownvotersKey(postId), userId); diff --git a/src/rag-service/README.md b/src/rag-service/README.md index 07f0af3d..208ae1a7 100644 --- a/src/rag-service/README.md +++ b/src/rag-service/README.md @@ -11,19 +11,19 @@ A microservice for spam classification using Retrieval-Augmented Generation (RAG - **[Ollama](https://ollama.com/)**: Local open-source LLM and embeddings ## Environment Variables -| Name | Example Value | Description | -|-------------------------------------------------|----------------------------------|-----------------------------------------------------------------------------------| -| LLM_MODEL | llama3.2:latest | The LLM model that will be used by the RAG service | -| EMBEDDINGS_MODEL | nomic-embed-text | The embeddings model that will be used by the RAG service | -| MODEL_PROVIDER | Ollama | Can be either "Ollama" or "LangDock" | -| MODEL_PROVIDER_BASE_URL | http://localhost:11434 | Base url to your model | -| LANGDOCK_API_KEY | | (optional) Langdock API key, only needed when using LangDock | -| EVALUATE_AFTER_ATTACK | false | Configure whether the RAG service performance should be evaluated after an attack | -| LIMIT_EVALUATION_SAMPLES | 0 | | -| LIMIT_KEYWORD_ATTACK_SUCCESS_EVALUATION_SAMPLES | 0 | Change to hostname/IP of rag-service instance | -| USE_DATA_POISONING_DETECTION | falses | Change to port number of rag-service instance | -| DATA_POISONING_DETECTION_STRATEGY | embedding_similarity_entry_level | The data poisoning detection strategy to use | -| LABEL_CONSISTENCY_DETECTION_DECISION_VARIANT | majority_voting | The variant of the label consistency detection strategy to use | +| Name | Example Value | Description | +|-------------------------------------------------|----------------------------------|--------------------------------------------------------------------------------------------------------------| +| LLM_MODEL | llama3.2:latest | The LLM model that will be used by the RAG service | +| EMBEDDINGS_MODEL | nomic-embed-text | The embeddings model that will be used by the RAG service | +| MODEL_PROVIDER | Ollama | Can be either "Ollama" or "LangDock" | +| MODEL_PROVIDER_BASE_URL | http://localhost:11434 | Base url to your model | +| LANGDOCK_API_KEY | | (optional) Langdock API key, only needed when using LangDock | +| EVALUATE_AFTER_ATTACK | false | Configure whether the RAG service performance should be evaluated after an attack | +| LIMIT_EVALUATION_SAMPLES | 0 | Configure the size of the evaluation set, set 0 for no limit. | +| LIMIT_KEYWORD_ATTACK_SUCCESS_EVALUATION_SAMPLES | 0 | Configure the size of the evaluation set for the keyword attack success rate evaluation. Set 0 for no limit. | +| USE_DATA_POISONING_DETECTION | false | Configure whether data poisoning detection should be used before ingesting new data into the Knowledge Base. | +| DATA_POISONING_DETECTION_STRATEGY | embedding_similarity_entry_level | The data poisoning detection strategy to use | +| LABEL_CONSISTENCY_DETECTION_DECISION_VARIANT | majority_voting | The variant of the label consistency detection strategy to use | ## Getting Started for running the RAG Service locally