-
Notifications
You must be signed in to change notification settings - Fork 0
Feature/bifrost bridge setup #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
df818c7
65a3caa
d4e14d4
aa760ac
99fa8e0
7d5a7fc
d56b2f4
6cd0132
9e0e314
a748d8b
f40e24d
8bcd193
cbba9eb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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()); | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Logging user message content at INFO level is a PII/compliance risk.
🛡️ 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
Suggested change
🤖 Prompt for AI Agents |
||||||
| 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() { | ||
|
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(); | ||
|
codebyNorthsteep marked this conversation as resolved.
|
||
|
|
||
| } | ||
|
codebyNorthsteep marked this conversation as resolved.
|
||
| } | ||
| 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 | ||
| } | ||
| } |
| 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); | ||
| } | ||
| } |
| 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){} | ||
| } |
| 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; | ||
|
|
||
| } |
| 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 | ||
|
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 | ||
| } | ||
|
codebyNorthsteep marked this conversation as resolved.
|
||
| } | ||
| 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 |
Uh oh!
There was an error while loading. Please reload this page.