Skip to content
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

ai-chat #42

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
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
91 changes: 91 additions & 0 deletions src/main/java/pl/ateam/disasteralerts/ai/chat/OpenAiConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package pl.ateam.disasteralerts.ai.chat;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.prompt.SystemPromptTemplate;
import org.springframework.context.annotation.Configuration;
import pl.ateam.disasteralerts.ai.chat.dto.ConversationDTO;
import pl.ateam.disasteralerts.disasteralert.DisasterType;
import pl.ateam.disasteralerts.disasteralert.dto.DisasterAddDTO;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;

@Configuration
class OpenAiConfig {

private final ChatClient chatClient;
private final List<Message> messages = new ArrayList<>();

public OpenAiConfig(ChatClient.Builder builder) {
this.chatClient = builder
.build();
}

ConversationDTO getAnswer(String question) {

return chatClient.prompt()
.system(getSystemMessage().getContent())
.user(question)
.call()
.entity(ConversationDTO.class);
}

DisasterAddDTO getDisasterAdd(UUID uuid) {
return chatClient.prompt()
.system("""
Zbuduj odpowiedź na podstawie konwersacji. Odpowiedź powinna być rozbudowana i zawierać wszystkie istotne szczegóły.
Dodatkowo na podstawie uzyskanych informacji oceń poziom ryzyka dotyczący katastrofy od '0.0' do '1.0'
(gdzie 0,0 oznacza brak zagrożenia, a 1.0 oznacza bardzo prawdopodobne).
Podaj sam wynik - liczbę z zakresu od 0.0 do 1.0. Odenę dodaj do odpowiedzi.
""")
.messages(messages)
.user("userId: " + uuid)
.call()
.entity(DisasterAddDTO.class);
}

void addMessage(Message message) {
messages.add(message);
}

List<Message> getMessages() {
return messages;
}

private Message getSystemMessage() {
String stringPrompt = """
Jesteś wirtualnym asystentem do przyjmowania zgłoszeń o awariach i zagrożeniach naturalnych od użytkowników. Twoim zadaniem jest przeprowadzenie użytkownika przez proces zgłoszenia, zadawanie precyzyjnych pytań w celu uzyskania wszystkich kluczowych informacji o zgłoszeniu, a następnie wygenerowanie odpowiedzi w formacie JSON na podstawie uzyskanych danych.

Podczas rozmowy:
- Zidentyfikuj typ zgłoszenia: Na początku rozmowy ustal, czy użytkownik zgłasza zagrożenie naturalne na podstawie {disasterType}.
- Pytania o szczegóły: Zadawaj pytania, aby uzyskać szczegółowe lub ogólne informacje o:
- Skali problemu
- Widocznych skutkach
- Ewentualnej obecności służb ratunkowych na miejscu
- Dodatkowych istotnych szczegółach

Zadawaj pytania pojedynczo, w formie `question` oraz `answers`.
Użytkownik będzie odpowiadał zaproponowanymi możliwościami.
Pytanie i odpowiedź podaj w formie JSON: {conversation} (String question, List<String> answers)
Jeśli nie ma żadnych odpowiednich odpowiedzi wygeneruj podsumowanie w question i zadnych pozycji w answers.
Nie powtarzaj już raz zadanego pytania. Jeśli sugerujesz odpowiedź 'tak' i użytkownik na nią odpowiada to dopytuj o szczegóły.

Pamiętaj, by rozmowa była zwięzła i jasna, aby użytkownik wiedział, jakie informacje są potrzebne. JSON ma być przejrzysty i zawierać tylko najistotniejsze informacje o zgłoszeniu.
Do kontekstu dołączam wiadomości, które wcześniej wymieniłeś z użytkownikiem: {messages}.
""";

return new SystemPromptTemplate(stringPrompt)
.createMessage(Map.of(
"disasterType", DisasterType.values(),
"conversation", ConversationDTO.class,
"messages", messages
));
}

public void cleanMessage() {
messages.clear();
}
}
41 changes: 41 additions & 0 deletions src/main/java/pl/ateam/disasteralerts/ai/chat/OpenAiService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package pl.ateam.disasteralerts.ai.chat;

import lombok.RequiredArgsConstructor;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.stereotype.Service;
import pl.ateam.disasteralerts.ai.chat.dto.ConversationDTO;
import pl.ateam.disasteralerts.disasteralert.dto.DisasterAddDTO;

import java.util.UUID;

@Service
@RequiredArgsConstructor
public class OpenAiService {

private final OpenAiConfig openAiConfig;

public ConversationDTO getResponse(String answerFromUser) {

ConversationDTO answer = openAiConfig.getAnswer(answerFromUser);

addToMessages(answerFromUser, answer);

return answer;
}

public DisasterAddDTO getDisasterAdd(UUID uuid) {
return openAiConfig.getDisasterAdd(uuid);
}

private void addToMessages(String answerFromUser, ConversationDTO answer) {
openAiConfig.addMessage(new UserMessage(answerFromUser));
if (answer != null) {
openAiConfig.addMessage(new AssistantMessage(answer.toString()));
}
}

public void cleanMessage() {
openAiConfig.cleanMessage();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package pl.ateam.disasteralerts.ai.chat.dto;

import java.util.List;

public record ConversationDTO(String question,
List<String> answers) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,14 @@
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import pl.ateam.disasteralerts.ai.chat.OpenAiService;
import pl.ateam.disasteralerts.ai.chat.dto.ConversationDTO;
import pl.ateam.disasteralerts.disasteralert.DisasterAlertFacade;
import pl.ateam.disasteralerts.disasteralert.DisasterStatus;
import pl.ateam.disasteralerts.disasteralert.DisasterType;
Expand All @@ -31,14 +37,24 @@ public class DisasterViewController {

private final DisasterAlertFacade disasterAlertFacade;
private final String USER_AS_DISASTER_SOURCE = "user";
private final OpenAiService openAiService;


@GetMapping("add")
public String showAddDisasterForm(Model model, @AuthenticationPrincipal AppUser appUser) {

baseModel(model, appUser);
model.addAttribute("disasterTypSelected", null);
model.addAttribute("disasterAddDTO", new DisasterAddDTO(null, null, null, null));

if (!model.containsAttribute("conversation")) {
openAiService.cleanMessage();
ConversationDTO conversation = openAiService.getResponse("Jestem " + appUser.getUsername() + " i chcę zgłosić zagrożenie.");
model.addAttribute("conversation", conversation);
}

if (!model.containsAttribute("disasterAddDTO")) {
model.addAttribute("disasterTypSelected", null);
model.addAttribute("disasterAddDTO", new DisasterAddDTO(null, null, null, null));
}

model.addAttribute("selectedLocation", appUser.getUserDTO().location());
model.addAttribute("googleApiKey", googleApiKey);
model.addAttribute("disasterTypes", DisasterType.values());
Expand All @@ -64,6 +80,27 @@ public String createDisaster(Model model, @AuthenticationPrincipal AppUser userD
return "redirect:/disasters/add";
}

@PostMapping("chat")
String sendMessages(Model model, @RequestParam String selectedAnswer,
@AuthenticationPrincipal AppUser userDetails,
RedirectAttributes redirectAttributes) {

ConversationDTO conversation = openAiService.getResponse(selectedAnswer);

if (conversation.answers().isEmpty()) {
DisasterAddDTO disasterAdd = openAiService.getDisasterAdd(userDetails.getUserDTO().id());
redirectAttributes.addFlashAttribute("disasterAddDTO", disasterAdd);
redirectAttributes.addFlashAttribute("disasterTypSelected", disasterAdd.type());
redirectAttributes.addFlashAttribute("description", disasterAdd.description());
redirectAttributes.addFlashAttribute("conversation", null);
return "redirect:/disasters/add";
}
redirectAttributes.addFlashAttribute("conversation", conversation);

baseModel(model, userDetails);
return "redirect:/disasters/add";
}

@GetMapping("list")
public String showDisasterList(Model model, @AuthenticationPrincipal AppUser userDetails) {
baseModel(model, userDetails);
Expand Down
4 changes: 4 additions & 0 deletions src/main/resources/application-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ spring:
options:
model: gpt-4o
temperature: 0.5
my:
ai:
openai:
api-key: ${OPENAI_API_KEY}

server:
port: 8080
Expand Down
42 changes: 29 additions & 13 deletions src/main/resources/templates/addDisaster.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,39 @@ <h1 class="display-5 fw-bold text-body-emphasis">Dodaj <span class="text-primary
<div>
<div class="container col-xl-10 col-xxl-10 px-4 ">
<div class="row g-lg-5 py-5">
<div class="col-md-10 mx-auto col-lg-8">
<form action="/disasters/add" method="post" th:object="${disasterAddDTO}" class="p-4 p-md-2 rounded-3 bg-body-tertiary">
<div th:if="${disasterTypSelected == null}" class="col-md-10 mx-auto col-lg-8">
<div class="chat-container d-flex flex-column h-100">
<div class="textarea-container bg-body-tertiary p-3 rounded mt-3">
<form th:action="@{/disasters/chat}" method="post">
<div th:if="${conversation != null}">
<label class="form-label fw-bold" th:text="${conversation.question}"></label>
<div class="form-check" th:each="answer, iter : ${conversation.answers}">
<input class="form-check-input" type="radio" name="selectedAnswer"
th:value="${answer}" th:id="${'answer-' + iter.index}" required>
<label class="form-check-label" th:for="${'answer-' + iter.index}" th:text="${answer}"></label>
</div>
<div class="form-check mt-2">
<input class="form-check-input" type="radio" name="selectedAnswer" value="custom" id="answer-custom">
<input type="text" class="form-control mt-2" name="selectedAnswer" placeholder="Wpisz własną odpowiedź">
</div>

<button type="submit" class="w-100 btn btn-lg btn-primary">Wyślij</button>
<div th:replace="~{fragments :: toast}"></div>
</div>
</form>
</div>
</div>
</div>

<div th:if="${disasterTypSelected != null}" class="col-md-10 mx-auto col-lg-8">
<form th:action="@{/disasters/add}" method="post" th:object="${disasterAddDTO}" class="p-4 p-md-5 rounded-3 bg-body-tertiary">

<div class="form-floating mb-3">
<select th:value="${disasterTypSelected}" name="type" class="form-select" id="type" required>
<option th:if="${disasterTypSelected == null}" value="" disabled selected>Wybierz typ zdarzenia</option>
<option th:each="type : ${disasterType}"
th:value="${type}"
th:selected="${type == disasterTypSelected}"
th:text="${type.getPolishName()}"></option>
</select>
<label for="type">Typ Zdarzenia</label>
<input th:value="${disasterTypSelected}" name="type" type="hidden" class="form-control" id="tye" required>
</div>

<div class="form-floating mb-3">
<input th:value="*{description()}" name="description" type="text" class="form-control" id="description" placeholder="Opis" required>
<label for="description">Opis</label>
<input th:value="*{description()}" name="description" type="hidden" class="form-control" id="description" required>
</div>
<div th:replace="~{fragments :: citiesList}"></div>

Expand All @@ -39,7 +56,6 @@ <h1 class="display-5 fw-bold text-body-emphasis">Dodaj <span class="text-primary
</div>

<button class="w-100 btn btn-lg btn-primary" type="submit">Dodaj Zdarzenie</button>
<div th:replace="~{fragments :: toast}"></div>
</form>
</div>
</div>
Expand Down