185 lines
5.1 KiB
Go
185 lines
5.1 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
// ChatServiceAPI allows mocking ChatService in other places
|
|
// Only public methods should be included
|
|
|
|
type ChatServiceAPI interface {
|
|
HandleChat(c *gin.Context)
|
|
}
|
|
|
|
// ChatService handles chat interactions and orchestrates LLM and DB calls.
|
|
type ChatService struct {
|
|
LLM LLMClientAPI
|
|
visitsDB VisitDBAPI
|
|
}
|
|
|
|
var _ ChatServiceAPI = (*ChatService)(nil)
|
|
|
|
func NewChatService(llm LLMClientAPI, db VisitDBAPI) ChatServiceAPI {
|
|
return &ChatService{LLM: llm, visitsDB: db}
|
|
}
|
|
|
|
// HandleChat is the main entrypoint for chat requests. It delegates to modular helpers.
|
|
func (cs *ChatService) HandleChat(c *gin.Context) {
|
|
ctx := context.Background()
|
|
req, err := cs.parseRequest(c)
|
|
if err != nil {
|
|
return
|
|
}
|
|
keywords, err := cs.extractKeywords(ctx, req.Message)
|
|
if err != nil {
|
|
cs.respondWithError(c, req, keywords, err)
|
|
return
|
|
}
|
|
best, err := cs.findBestVisit(ctx, req, keywords)
|
|
resp := cs.buildResponse(best)
|
|
c.JSON(http.StatusOK, resp)
|
|
}
|
|
|
|
// parseRequest parses and validates the incoming chat request.
|
|
func (cs *ChatService) parseRequest(c *gin.Context) (ChatRequest, error) {
|
|
var req ChatRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
logrus.WithError(err).Error("Invalid request")
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
|
|
return req, err
|
|
}
|
|
return req, nil
|
|
}
|
|
|
|
// extractKeywords gets keywords from the LLM and normalizes them.
|
|
func (cs *ChatService) extractKeywords(ctx context.Context, message string) ([]string, error) {
|
|
kwResp, err := cs.LLM.ExtractKeywords(ctx, message)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return cs.keywordsToStrings(kwResp["keyword"]), nil
|
|
}
|
|
|
|
// findBestVisit finds candidate visits and disambiguates the best match.
|
|
func (cs *ChatService) findBestVisit(ctx context.Context, req ChatRequest, keywords []string) (*Visit, error) {
|
|
cs.logKeywords(keywords, req.Message)
|
|
candidates, err := cs.visitsDB.FindCandidates(keywords)
|
|
cs.logCandidates(candidates, err)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
bestID := ""
|
|
if len(candidates) > 0 {
|
|
bestID, err = cs.LLM.DisambiguateBestMatch(ctx, req.Message, candidates)
|
|
cs.logBestID(bestID, err)
|
|
}
|
|
visit, err := cs.visitsDB.FindById(bestID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("FindById: %w", err)
|
|
}
|
|
return &visit, nil
|
|
}
|
|
|
|
// buildResponse constructs the ChatResponse from the best Visit.
|
|
func (cs *ChatService) buildResponse(best *Visit) ChatResponse {
|
|
if best == nil {
|
|
resp := ChatResponse{Match: nil}
|
|
logrus.WithFields(logrus.Fields{"response": resp}).Info("Build response: no match")
|
|
return resp
|
|
}
|
|
totalPrice, totalDuration := sumProcedures(best.Procedures)
|
|
resp := ChatResponse{
|
|
Match: &best.ID,
|
|
Procedures: best.Procedures,
|
|
TotalPrice: totalPrice,
|
|
TotalDuration: totalDuration,
|
|
Notes: best.Notes,
|
|
}
|
|
logrus.WithFields(logrus.Fields{"response": resp}).Info("Build response: match found")
|
|
return resp
|
|
}
|
|
|
|
// respondWithError logs and responds with error details.
|
|
func (cs *ChatService) respondWithError(c *gin.Context, req ChatRequest, keywords []string, err error) {
|
|
kwMap := map[string]interface{}{"keyword": keywords}
|
|
cs.logChat(req, kwMap, nil, "", err)
|
|
c.JSON(http.StatusOK, ChatResponse{Match: nil})
|
|
}
|
|
|
|
// keywordsToStrings normalizes keyword interface to []string.
|
|
func (cs *ChatService) keywordsToStrings(kwIface interface{}) []string {
|
|
var kwArr []string
|
|
switch v := kwIface.(type) {
|
|
case []interface{}:
|
|
for _, item := range v {
|
|
if s, ok := item.(string); ok {
|
|
kwArr = append(kwArr, s)
|
|
}
|
|
}
|
|
case []string:
|
|
kwArr = v
|
|
}
|
|
return kwArr
|
|
}
|
|
|
|
// logKeywords logs extracted keywords.
|
|
func (cs *ChatService) logKeywords(keywords []string, message string) {
|
|
logrus.WithFields(logrus.Fields{
|
|
"keywords": keywords,
|
|
"message": message,
|
|
}).Info("Finding visit candidates")
|
|
}
|
|
|
|
// logCandidates logs candidate visits.
|
|
func (cs *ChatService) logCandidates(candidates []Visit, err error) {
|
|
logrus.WithFields(logrus.Fields{
|
|
"candidates": candidates,
|
|
"error": err,
|
|
}).Info("Candidate visits found")
|
|
}
|
|
|
|
// logBestID logs the best candidate ID.
|
|
func (cs *ChatService) logBestID(bestID string, err error) {
|
|
logrus.WithFields(logrus.Fields{
|
|
"bestID": bestID,
|
|
"error": err,
|
|
}).Info("Disambiguated best match")
|
|
}
|
|
|
|
// logChat logs the chat request and result details.
|
|
func (cs *ChatService) logChat(req ChatRequest, keywords interface{}, candidates []Visit, bestID string, err error) {
|
|
var kwMap map[string]interface{}
|
|
switch v := keywords.(type) {
|
|
case []string:
|
|
kwMap = map[string]interface{}{"keyword": v}
|
|
case map[string]interface{}:
|
|
kwMap = v
|
|
default:
|
|
kwMap = map[string]interface{}{"keyword": v}
|
|
}
|
|
logRequest(req, kwMap, candidates, bestID, err)
|
|
if candidates != nil && bestID != "" {
|
|
var best *Visit
|
|
for i := range candidates {
|
|
if candidates[i].ID == bestID {
|
|
best = &candidates[i]
|
|
break
|
|
}
|
|
}
|
|
if best != nil {
|
|
totalPrice, totalDuration := sumProcedures(best.Procedures)
|
|
logrus.WithFields(logrus.Fields{
|
|
"match": best.ID,
|
|
"total_price": totalPrice,
|
|
"total_duration": totalDuration,
|
|
"notes": best.Notes,
|
|
}).Info("Responding with match")
|
|
}
|
|
}
|
|
}
|