Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions src/main/java/org/example/projectbifrost/BifrostController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package org.example.projectbifrost;

import org.example.projectbifrost.domain.ChatSession;
import org.example.projectbifrost.dto.ChatRequestDTO;
import org.example.projectbifrost.service.ChatService;
import org.slf4j.Logger;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api")
public class BifrostController {

private static final Logger logger = org.slf4j.LoggerFactory.getLogger(BifrostController.class);

private final ChatService chatService;

public BifrostController(ChatService chatService) {
this.chatService = chatService;
}

@GetMapping("/bifrost")
public String bifrost() {
return "Welcome to Bifrost, the gateway to the realms!"; }

@PostMapping("/v1/chat")
public String sendChatRequest(@RequestBody ChatRequestDTO dto) {
logger.info("Received chat request: Personality={}, Message={}, SessionId={}", dto.personality(), dto.message(), dto.sessionId());
Comment thread
codebyNorthsteep marked this conversation as resolved.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Logging user message content at INFO level is a PII/compliance risk.

dto.message() is arbitrary user-supplied chat text and must not appear in application logs — it can contain PII, credentials, or other sensitive content. dto.sessionId() can also serve as a user identifier. At minimum, omit the message body from the log line; if session tracking in logs is required, ensure it is covered by your data-handling policy.

🛡️ Proposed fix
-        logger.info("Received chat request: Personality={}, Message={}, SessionId={}", dto.personality(), dto.message(), dto.sessionId());
+        logger.info("Received chat request: Personality={}, SessionId={}", dto.personality(), dto.sessionId());
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
logger.info("Received chat request: Personality={}, Message={}, SessionId={}", dto.personality(), dto.message(), dto.sessionId());
logger.info("Received chat request: Personality={}, SessionId={}", dto.personality(), dto.sessionId());
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/org/example/projectbifrost/BifrostController.java` at line 27,
The current logging statement in BifrostController (logger.info(...)) prints
dto.message() and dto.sessionId(), which may expose PII; remove the message body
from logs and avoid including identifiable session IDs unless allowed by policy.
Update the logger.info call in BifrostController to log only non-sensitive
metadata (e.g., dto.personality()) and, if session tracking is required, replace
dto.sessionId() with a redacted/hashed value or a boolean/opaque identifier that
complies with data-handling rules; ensure dto.message() is never logged at INFO
level (or at all) and adjust any related log messages or helper methods
accordingly.

return chatService.sendRequestToLLM(dto);
}

@GetMapping("/v1/chat/{sessionId}")
public ChatSession getChatHistory(@PathVariable String sessionId) {
return chatService.getSessionHistory(sessionId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package org.example.projectbifrost.configuration;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestClient;

import java.net.URI;

@Configuration
public class RestClientConfiguration {
@Value("${openrouter.api.key}")
private String apiKey;

@Bean
public RestClient openAIWebClient() {
Comment thread
codebyNorthsteep marked this conversation as resolved.
return RestClient.builder()
.baseUrl(URI.create("https://openrouter.ai/api/v1"))
.defaultHeader("Authorization", "Bearer " + apiKey)
.defaultHeader("Content-Type", "application/json")
.build();
Comment thread
codebyNorthsteep marked this conversation as resolved.

}
Comment thread
codebyNorthsteep marked this conversation as resolved.
}
29 changes: 29 additions & 0 deletions src/main/java/org/example/projectbifrost/domain/ChatMessage.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package org.example.projectbifrost.domain;

import lombok.Getter;
import lombok.Setter;

import java.time.LocalDateTime;

/**
* Represents a chat message in a conversation.
* This class encapsulates the details of a single message, including its role, content,
* and the timestamp at which it was created.
*
* The role typically signifies the sender's identity (e.g., user, system, assistant),
* while the message contains the text content of the chat.
* The timestamp indicates when the message was generated.
*/
@Getter
@Setter
public class ChatMessage {
private String role;
private String content;
private LocalDateTime timeStamp;

public ChatMessage(String role, String message) {
this.role = role;
this.content = message;
this.timeStamp = LocalDateTime.now();//Adds the current time to each message
}
}
28 changes: 28 additions & 0 deletions src/main/java/org/example/projectbifrost/domain/ChatSession.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package org.example.projectbifrost.domain;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;

import java.util.List;

/**
* Represents a chat session that contains a unique session identifier
* and a chronological history of chat messages.
*
* The chat session is a core entity for managing and maintaining
* conversational histories within a chat application. Each session
* is uniquely identified by its session ID and maintains a list of
* chat messages, which are instances of the ChatMessage class.
*/
@Getter
@Setter
@AllArgsConstructor
public class ChatSession {
private String sessionId;
private List<ChatMessage> chatHistory;

public void addMessage(ChatMessage message) {
chatHistory.add(message);
}
}
15 changes: 15 additions & 0 deletions src/main/java/org/example/projectbifrost/dto/ChatRequestDTO.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.example.projectbifrost.dto;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;

/**
* This record encapsulates the necessary information required to process
* a chat interaction, including the personality of the chatbot, the message
* sent by the user, and a session identifier for tracking the conversation.
*/
public record ChatRequestDTO(@NotNull Personality personality,
@NotBlank String message,
@NotBlank String sessionId) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package org.example.projectbifrost.dto;

import java.util.List;

public record OpenRouterRequestDTO(String model,
List<Message> messages) {
public record Message(String role, String content) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.example.projectbifrost.dto;

import java.util.List;

public record OpenRouterResponseDTO(List<Choice> choices) {
public record Choice(Message message) {}
public record Message(String content){}
}
17 changes: 17 additions & 0 deletions src/main/java/org/example/projectbifrost/dto/Personality.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package org.example.projectbifrost.dto;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public enum Personality {
ODIN("You are Odin, the All father of Norse mythology. You are wise, powerful, and often speak in a grandiose manner. Your responses should reflect your vast knowledge and authority."),
LOKI("You are Loki, the trickster god of Norse mythology. You are cunning, mischievous, and often speak in a playful and sarcastic tone. Your responses should reflect your love for chaos and unpredictability."),
FREYJA("You are Freyja, the goddess of love, beauty, and fertility in Norse mythology. You are compassionate, alluring, and often speak in a warm and inviting manner. Your responses should reflect your nurturing nature and your connection to the natural world."),
THOR("You are Thor, the god of thunder in Norse mythology. You are strong, brave, and often speak in a straightforward and assertive manner. Your responses should reflect your warrior spirit and your dedication to protecting Asgard."),
HEIMDALL("You are Heimdall, the guardian of the Bifrost bridge in Norse mythology. You are vigilant, noble, and often speak in a calm and measured tone. Your responses should reflect your duty to protect the realms and your ability to see all that happens.");

private final String systemPrompt;

}
73 changes: 73 additions & 0 deletions src/main/java/org/example/projectbifrost/service/ChatService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package org.example.projectbifrost.service;

import org.example.projectbifrost.domain.ChatMessage;
import org.example.projectbifrost.domain.ChatSession;
import org.example.projectbifrost.dto.ChatRequestDTO;
import org.example.projectbifrost.dto.OpenRouterRequestDTO;
import org.example.projectbifrost.dto.OpenRouterResponseDTO;
import org.example.projectbifrost.storage.ChatSessionStorage;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;

import java.util.ArrayList;
import java.util.List;

@Service
public class ChatService {

private final ChatSessionStorage chatSessionStorage;
private final RestClient restClient;

@Value("${openrouter.model}")
private String model;

public ChatService(ChatSessionStorage chatSessionStorage, RestClient restClient) {
this.chatSessionStorage = chatSessionStorage;
this.restClient = restClient;
}

/**
* Sends a request to the large language model (LLM) with the user's message and personality context
* and retrieves the response.
* <p>
* This method constructs a chat session using the provided session ID or creates a new one if it
* does not exist. It adds the user's message to the chat session and builds a context-aware request
* to be sent to the LLM. The LLM's response is then returned as a string.
*/
public String sendRequestToLLM(ChatRequestDTO dto) {
ChatSession chatSession = chatSessionStorage.getOrCreateChatSession(dto.sessionId());

List<OpenRouterRequestDTO.Message> apiMessages = new ArrayList<>();
apiMessages.add(new OpenRouterRequestDTO.Message("system", dto.personality().getSystemPrompt()));

chatSession.getChatHistory().forEach(m ->
apiMessages.add(new OpenRouterRequestDTO.Message(m.getRole(), m.getContent()))
);

apiMessages.add(new OpenRouterRequestDTO.Message("user", dto.message()));

var openRouterRequest = new OpenRouterRequestDTO(model, apiMessages);

OpenRouterResponseDTO result = restClient.post()
.uri("/chat/completions")//Start of URI configured in RestClientConfiguration.java
.body(openRouterRequest) //Send JSON-body of messages and model
.retrieve()
.body(OpenRouterResponseDTO.class);

if (result == null || result.choices() == null || result.choices().isEmpty()) {
throw new RuntimeException("Received empty or invalid response from LLM");
}
String content = result.choices().getFirst().message().content();

chatSession.addMessage(new ChatMessage("user", dto.message()));
chatSession.addMessage(new ChatMessage("assistant", content));
return content;
}


public ChatSession getSessionHistory(String sessionId) {
return chatSessionStorage.getOrCreateChatSession(sessionId);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package org.example.projectbifrost.storage;

import org.example.projectbifrost.domain.ChatSession;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import static java.util.Collections.synchronizedList;

/**
* Manages storage and retrieval of chat sessions.
* In-memory database for all sessions.
* Each chat session contains a unique session identifier and a history of chat messages.
* The class ensures that a session is either retrieved if it exists, or created if it's not
* yet present in the storage, enabling seamless session management for chat applications.
*/
@Component
public class ChatSessionStorage {
private final Map<String, ChatSession> sessionStorage = new ConcurrentHashMap<>(); //Thread-safe
Comment thread
codebyNorthsteep marked this conversation as resolved.

public ChatSession getOrCreateChatSession(String sessionId) {
return sessionStorage.computeIfAbsent(
sessionId,
id -> new ChatSession(id, synchronizedList(new ArrayList<>()))
);
}

public void deleteSession(String sessionId) {
sessionStorage.remove(sessionId);
//Copilot påpekar att det kan vara bra med en MAX-size på listan, och sköta delete om den blir för stor
}
Comment thread
codebyNorthsteep marked this conversation as resolved.
}
2 changes: 2 additions & 0 deletions src/main/resources/application.properties
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
spring.application.name=ProjectBifrost
openrouter.api.key=${OPENROUTER_API_KEY}
openrouter.model=poolside/laguna-xs.2:free