stable POC version 1 - chat and topics
This commit is contained in:
@@ -32,6 +32,21 @@ public class ChatController {
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(response);
|
||||
}
|
||||
|
||||
@GetMapping("/sessions")
|
||||
public ResponseEntity<List<Map<String, Object>>> getSessionsByTopic(@RequestParam String topicId) {
|
||||
List<ChatSession> sessions = chatService.getSessionsByTopic(topicId);
|
||||
List<Map<String, Object>> response = sessions.stream()
|
||||
.map(s -> {
|
||||
Map<String, Object> m = new LinkedHashMap<>();
|
||||
m.put("sessionId", s.getId());
|
||||
m.put("topicId", s.getTopicId());
|
||||
m.put("createdAt", s.getCreatedAt());
|
||||
return m;
|
||||
})
|
||||
.toList();
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
@GetMapping("/sessions/{sessionId}/messages")
|
||||
public ResponseEntity<List<Map<String, Object>>> getMessages(@PathVariable UUID sessionId) {
|
||||
List<Message> messages = chatService.getMessages(sessionId);
|
||||
|
||||
@@ -26,17 +26,18 @@ public class ChatService {
|
||||
private static final Logger log = LoggerFactory.getLogger(ChatService.class);
|
||||
|
||||
private static final String SYSTEM_PROMPT = """
|
||||
You are an expert neurosurgery educator assistant. Your role is to answer
|
||||
questions based ONLY on the content from uploaded medical textbooks that has been
|
||||
retrieved for you as context.
|
||||
You are an expert neurosurgery educator assistant. Answer questions using the
|
||||
medical textbook content provided to you as context.
|
||||
|
||||
Rules:
|
||||
- Answer only from the provided context chunks
|
||||
- If the context does not contain enough information, explicitly state:
|
||||
"I could not find relevant information about this topic in the uploaded books."
|
||||
- Cite sources when possible (book title and page number from the context metadata)
|
||||
- Give comprehensive, detailed answers — cover all relevant aspects found in the context: anatomy, indications, contraindications, techniques, procedural steps, complications, outcomes, and clinical considerations
|
||||
- When the context contains related information, synthesize a thorough answer from it — do not apologize for missing definitions or lead with what you couldn't find
|
||||
- Build answers from what is present: procedures, conditions, techniques, and descriptions all contribute; combine them into a rich, structured response
|
||||
- Use clear structure: headings, bullet points, or numbered steps where appropriate to maximize clarity
|
||||
- Only say you cannot answer if the context is entirely unrelated to the question
|
||||
- Cite sources for each major point (book title and page number from the context metadata)
|
||||
- Maintain continuity with the conversation history
|
||||
- Never fabricate clinical information
|
||||
- Never fabricate clinical information not present in the context
|
||||
""";
|
||||
|
||||
private final ChatClient chatClient;
|
||||
@@ -87,7 +88,7 @@ public class ChatService {
|
||||
String fullQuestion = buildQuestionWithHistory(history, userContent, session.getTopicId());
|
||||
|
||||
var qaAdvisor = QuestionAnswerAdvisor.builder(vectorStore)
|
||||
.searchRequest(SearchRequest.builder().similarityThreshold(0.8d).topK(6).build())
|
||||
.searchRequest(SearchRequest.builder().similarityThreshold(0.5d).topK(6).build())
|
||||
.build();
|
||||
|
||||
ChatResponse response = chatClient.prompt()
|
||||
@@ -106,6 +107,10 @@ public class ChatService {
|
||||
return messageRepository.save(assistantMessage);
|
||||
}
|
||||
|
||||
public List<ChatSession> getSessionsByTopic(String topicId) {
|
||||
return sessionRepository.findByTopicIdOrderByCreatedAtDesc(topicId);
|
||||
}
|
||||
|
||||
public void deleteSession(UUID sessionId) {
|
||||
if (!sessionRepository.existsById(sessionId)) {
|
||||
throw new NoSuchElementException("Session not found.");
|
||||
@@ -115,16 +120,17 @@ public class ChatService {
|
||||
|
||||
private String buildQuestionWithHistory(List<Message> history, String currentQuestion,
|
||||
String topicId) {
|
||||
boolean hasTopic = topicId != null && !topicId.equals("free-form");
|
||||
|
||||
if (history.size() <= 1) {
|
||||
// Only the current user message is in history; just ask the question
|
||||
return topicId != null
|
||||
return hasTopic
|
||||
? String.format("[Context: This is a question about the neurosurgery topic '%s']\n%s",
|
||||
topicId, currentQuestion)
|
||||
: currentQuestion;
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (topicId != null) {
|
||||
if (hasTopic) {
|
||||
sb.append(String.format("[Context: This conversation is about the neurosurgery topic '%s']\n\n",
|
||||
topicId));
|
||||
}
|
||||
@@ -154,6 +160,7 @@ public class ChatService {
|
||||
Map<String, Object> source = new HashMap<>();
|
||||
source.put("bookTitle", bookTitle);
|
||||
source.put("page", page);
|
||||
source.put("chunkText", doc.getText());
|
||||
sources.add(source);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,10 @@ package com.aiteacher.chat;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Repository
|
||||
public interface ChatSessionRepository extends JpaRepository<ChatSession, UUID> {
|
||||
List<ChatSession> findByTopicIdOrderByCreatedAtDesc(String topicId);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,37 @@
|
||||
package com.aiteacher.topic;
|
||||
|
||||
public record Topic(
|
||||
String id,
|
||||
String name,
|
||||
String description,
|
||||
String category
|
||||
) {
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
@Entity
|
||||
@Table(name = "topic")
|
||||
public class Topic {
|
||||
|
||||
@Id
|
||||
private String id;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String name;
|
||||
|
||||
@Column(nullable = false, columnDefinition = "TEXT")
|
||||
private String description;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String category;
|
||||
|
||||
protected Topic() {}
|
||||
|
||||
public Topic(String id, String name, String description, String category) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.description = description;
|
||||
this.category = category;
|
||||
}
|
||||
|
||||
public String getId() { return id; }
|
||||
public String getName() { return name; }
|
||||
public String getDescription() { return description; }
|
||||
public String getCategory() { return category; }
|
||||
}
|
||||
|
||||
@@ -6,14 +6,12 @@ import jakarta.annotation.PostConstruct;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Component
|
||||
// Replaced by TopicRepository (DB-backed). Kept for reference only.
|
||||
public class TopicConfigLoader {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(TopicConfigLoader.class);
|
||||
@@ -40,7 +38,7 @@ public class TopicConfigLoader {
|
||||
|
||||
public Optional<Topic> findById(String id) {
|
||||
return topics.stream()
|
||||
.filter(t -> t.id().equals(id))
|
||||
.filter(t -> t.getId().equals(id))
|
||||
.findFirst();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,23 +10,23 @@ import java.util.NoSuchElementException;
|
||||
@RequestMapping("/api/v1/topics")
|
||||
public class TopicController {
|
||||
|
||||
private final TopicConfigLoader topicConfigLoader;
|
||||
private final TopicRepository topicRepository;
|
||||
private final TopicSummaryService topicSummaryService;
|
||||
|
||||
public TopicController(TopicConfigLoader topicConfigLoader,
|
||||
public TopicController(TopicRepository topicRepository,
|
||||
TopicSummaryService topicSummaryService) {
|
||||
this.topicConfigLoader = topicConfigLoader;
|
||||
this.topicRepository = topicRepository;
|
||||
this.topicSummaryService = topicSummaryService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<List<Topic>> list() {
|
||||
return ResponseEntity.ok(topicConfigLoader.getAll());
|
||||
return ResponseEntity.ok(topicRepository.findAll());
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/summary")
|
||||
public ResponseEntity<TopicSummaryResponse> generateSummary(@PathVariable String id) {
|
||||
Topic topic = topicConfigLoader.findById(id)
|
||||
Topic topic = topicRepository.findById(id)
|
||||
.orElseThrow(() -> new NoSuchElementException("Topic not found."));
|
||||
|
||||
TopicSummaryResponse response = topicSummaryService.generateSummary(topic);
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.aiteacher.topic;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public interface TopicRepository extends JpaRepository<Topic, String> {
|
||||
}
|
||||
@@ -65,8 +65,8 @@ public class TopicSummaryService {
|
||||
List<TopicSummaryResponse.SourceReference> sources = extractSources(response);
|
||||
|
||||
return new TopicSummaryResponse(
|
||||
topic.id(),
|
||||
topic.name(),
|
||||
topic.getId(),
|
||||
topic.getName(),
|
||||
summary,
|
||||
sources,
|
||||
Instant.now()
|
||||
@@ -78,7 +78,7 @@ public class TopicSummaryService {
|
||||
"Please provide a comprehensive educational summary of the following neurosurgery topic: " +
|
||||
"%s. Topic description: %s. " +
|
||||
"Include key concepts, clinical considerations, and important details that a neurosurgeon should know.",
|
||||
topic.name(), topic.description()
|
||||
topic.getName(), topic.getDescription()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user