diff --git a/chat_service.go b/chat_service.go index a444618..513c574 100644 --- a/chat_service.go +++ b/chat_service.go @@ -18,14 +18,14 @@ type ChatServiceAPI interface { // ChatService handles chat interactions and orchestrates LLM and DB calls. type ChatService struct { - LLM LLMClientAPI - reasonsDB ReasonDBAPI + LLM LLMClientAPI + visitsDB VisitDBAPI } var _ ChatServiceAPI = (*ChatService)(nil) -func NewChatService(llm LLMClientAPI, db ReasonDBAPI) ChatServiceAPI { - return &ChatService{LLM: llm, reasonsDB: db} +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. @@ -40,7 +40,7 @@ func (cs *ChatService) HandleChat(c *gin.Context) { cs.respondWithError(c, req, keywords, err) return } - best, err := cs.findBestReason(ctx, req, keywords) + best, err := cs.findBestVisit(ctx, req, keywords) resp := cs.buildResponse(best) c.JSON(http.StatusOK, resp) } @@ -65,10 +65,10 @@ func (cs *ChatService) extractKeywords(ctx context.Context, message string) ([]s return cs.keywordsToStrings(kwResp["keyword"]), nil } -// findBestReason finds candidate reasons and disambiguates the best match. -func (cs *ChatService) findBestReason(ctx context.Context, req ChatRequest, keywords []string) (*Reason, error) { +// 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.reasonsDB.FindCandidates(keywords) + candidates, err := cs.visitsDB.FindCandidates(keywords) cs.logCandidates(candidates, err) if err != nil { return nil, err @@ -78,15 +78,15 @@ func (cs *ChatService) findBestReason(ctx context.Context, req ChatRequest, keyw bestID, err = cs.LLM.DisambiguateBestMatch(ctx, req.Message, candidates) cs.logBestID(bestID, err) } - reason, err := cs.reasonsDB.FindById(bestID) + visit, err := cs.visitsDB.FindById(bestID) if err != nil { return nil, fmt.Errorf("FindById: %w", err) } - return &reason, nil + return &visit, nil } -// buildResponse constructs the ChatResponse from the best Reason. -func (cs *ChatService) buildResponse(best *Reason) ChatResponse { +// 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") @@ -132,15 +132,15 @@ func (cs *ChatService) logKeywords(keywords []string, message string) { logrus.WithFields(logrus.Fields{ "keywords": keywords, "message": message, - }).Info("Finding visit reason candidates") + }).Info("Finding visit candidates") } -// logCandidates logs candidate reasons. -func (cs *ChatService) logCandidates(candidates []Reason, err error) { +// logCandidates logs candidate visits. +func (cs *ChatService) logCandidates(candidates []Visit, err error) { logrus.WithFields(logrus.Fields{ "candidates": candidates, "error": err, - }).Info("Candidate reasons found") + }).Info("Candidate visits found") } // logBestID logs the best candidate ID. @@ -152,7 +152,7 @@ func (cs *ChatService) logBestID(bestID string, err error) { } // logChat logs the chat request and result details. -func (cs *ChatService) logChat(req ChatRequest, keywords interface{}, candidates []Reason, bestID string, err error) { +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: @@ -164,7 +164,7 @@ func (cs *ChatService) logChat(req ChatRequest, keywords interface{}, candidates } logRequest(req, kwMap, candidates, bestID, err) if candidates != nil && bestID != "" { - var best *Reason + var best *Visit for i := range candidates { if candidates[i].ID == bestID { best = &candidates[i] diff --git a/chat_service_integration_test.go b/chat_service_integration_test.go index 5f94655..837da5d 100644 --- a/chat_service_integration_test.go +++ b/chat_service_integration_test.go @@ -24,26 +24,26 @@ var _ LLMClientAPI = (*mockLLM)(nil) func (m *mockLLM) ExtractKeywords(ctx context.Context, msg string) (map[string]interface{}, error) { return m.keywordsResp, m.keywordsErr } -func (m *mockLLM) DisambiguateBestMatch(ctx context.Context, msg string, candidates []Reason) (string, error) { +func (m *mockLLM) DisambiguateBestMatch(ctx context.Context, msg string, candidates []Visit) (string, error) { return m.disambigID, m.disambigErr } -// --- Test ReasonDB --- -type testReasonDB struct { - candidates []Reason +// --- Test VisitDB --- +type testVisitDB struct { + candidates []Visit findErr error - byID map[string]Reason + byID map[string]Visit } -var _ ReasonDBAPI = (*testReasonDB)(nil) +var _ VisitDBAPI = (*testVisitDB)(nil) -func (db *testReasonDB) FindCandidates(keywords []string) ([]Reason, error) { +func (db *testVisitDB) FindCandidates(keywords []string) ([]Visit, error) { return db.candidates, db.findErr } -func (db *testReasonDB) FindById(id string) (Reason, error) { +func (db *testVisitDB) FindById(id string) (Visit, error) { r, ok := db.byID[id] if !ok { - return Reason{}, context.DeadlineExceeded + return Visit{}, context.DeadlineExceeded } return r, nil } @@ -55,14 +55,14 @@ func TestChatService_MatchFound(t *testing.T) { keywordsResp: map[string]interface{}{"keyword": []string{"worms", "deworming"}}, disambigID: "deworming", } - reason := Reason{ + visit := Visit{ ID: "deworming", Procedures: []Procedure{{Name: "Deworming tablet", Price: 30, DurationMin: 10}}, Notes: "Bloodwork ensures organs are safe for treatment.", } - var db ReasonDBAPI = &testReasonDB{ - candidates: []Reason{reason}, - byID: map[string]Reason{"deworming": reason}, + var db VisitDBAPI = &testVisitDB{ + candidates: []Visit{visit}, + byID: map[string]Visit{"deworming": visit}, } var cs ChatServiceAPI = NewChatService(llm, db) r := gin.New() @@ -88,8 +88,8 @@ func TestChatService_MatchFound(t *testing.T) { if len(resp.Procedures) != 1 || resp.Procedures[0].Name != "Deworming tablet" { t.Errorf("Expected procedure 'Deworming tablet', got %+v", resp.Procedures) } - if resp.Notes != reason.Notes { - t.Errorf("Expected notes '%s', got '%s'", reason.Notes, resp.Notes) + if resp.Notes != visit.Notes { + t.Errorf("Expected notes '%s', got '%s'", visit.Notes, resp.Notes) } } @@ -99,9 +99,9 @@ func TestChatService_NoMatch(t *testing.T) { keywordsResp: map[string]interface{}{"keyword": []string{"unknown"}}, disambigID: "", } - db := &testReasonDB{ - candidates: []Reason{}, - byID: map[string]Reason{}, + db := &testVisitDB{ + candidates: []Visit{}, + byID: map[string]Visit{}, } cs := NewChatService(llm, db) r := gin.New() @@ -131,7 +131,7 @@ func TestChatService_LLMError(t *testing.T) { llm := &mockLLM{ keywordsErr: context.DeadlineExceeded, } - db := &testReasonDB{} + db := &testVisitDB{} cs := NewChatService(llm, db) r := gin.New() r.POST("/chat", cs.HandleChat) diff --git a/db.go b/db.go index deab67e..8de8842 100644 --- a/db.go +++ b/db.go @@ -12,79 +12,79 @@ import ( "github.com/blevesearch/bleve/v2" ) -type ReasonDB struct { - reasonsDB []Reason - reasonsIdx bleve.Index +type VisitDB struct { + visitsDB []Visit + visitsIdx bleve.Index } -func NewReasonDB() ReasonDB { - db := ReasonDB{} +func NewVisitDB() VisitDB { + db := VisitDB{} db.init() return db } -func (rdb *ReasonDB) FindById(reasonId string) (Reason, error) { - for _, reason := range rdb.reasonsDB { - if reason.ID == reasonId { - return reason, nil +func (vdb *VisitDB) FindById(visitId string) (Visit, error) { + for _, visit := range vdb.visitsDB { + if visit.ID == visitId { + return visit, nil } } - return Reason{}, errors.New("reason not found") + return Visit{}, errors.New("visit not found") } -func (rdb *ReasonDB) init() { - idxPath := "reasons.bleve" +func (vdb *VisitDB) init() { + idxPath := "visits.bleve" if _, err := os.Stat(idxPath); err == nil { - rdb.reasonsIdx, err = bleve.Open(idxPath) + vdb.visitsIdx, err = bleve.Open(idxPath) if err != nil { panic(err) } } else if os.IsNotExist(err) { - rdb.reasonsIdx, err = bleve.New(idxPath, bleve.NewIndexMapping()) + vdb.visitsIdx, err = bleve.New(idxPath, bleve.NewIndexMapping()) if err != nil { panic(err) } } else { panic(err) } - err := rdb.loadYAMLDB("db.yaml") + err := vdb.loadYAMLDB("db.yaml") if err != nil { logrus.Fatalf("Failed to load db.yaml: %v", err) panic(err) } } -func (rdb *ReasonDB) loadYAMLDB(path string) error { +func (vdb *VisitDB) loadYAMLDB(path string) error { data, err := ioutil.ReadFile(path) if err != nil { return err } - if err := yaml.Unmarshal(data, &rdb.reasonsDB); err != nil { + if err := yaml.Unmarshal(data, &vdb.visitsDB); err != nil { return err } - return rdb.indexReasons(rdb.reasonsDB) + return vdb.indexVisits(vdb.visitsDB) } -func (rdb *ReasonDB) indexReasons(reasons []Reason) error { - batch := rdb.reasonsIdx.NewBatch() - for _, reason := range reasons { - if err := batch.Index(reason.ID, reason); err != nil { +func (vdb *VisitDB) indexVisits(visits []Visit) error { + batch := vdb.visitsIdx.NewBatch() + for _, visit := range visits { + if err := batch.Index(visit.ID, visit); err != nil { return err } } - return rdb.reasonsIdx.Batch(batch) + return vdb.visitsIdx.Batch(batch) } -// FindCandidates returns reasons with overlapping keywords -func (rdb *ReasonDB) FindCandidates(keywords []string) ([]Reason, error) { +// FindCandidates returns visits with overlapping keywords +func (vdb *VisitDB) FindCandidates(keywords []string) ([]Visit, error) { query := bleve.NewMatchQuery(strings.Join(keywords, " ")) search := bleve.NewSearchRequest(query) - searchResults, err := rdb.reasonsIdx.Search(search) + searchResults, err := vdb.visitsIdx.Search(search) if err != nil { return nil, err } - var candidates []Reason + var candidates []Visit for _, hit := range searchResults.Hits { - for _, r := range rdb.reasonsDB { + for _, r := range vdb.visitsDB { if r.ID == hit.ID { candidates = append(candidates, r) break @@ -105,12 +105,12 @@ func sumProcedures(procs []Procedure) (int, int) { return totalPrice, totalDuration } -// ReasonDBAPI allows mocking ReasonDB in other places +// VisitDBAPI allows mocking VisitDB in other places // Only public methods should be included -type ReasonDBAPI interface { - FindById(reasonId string) (Reason, error) - FindCandidates(keywords []string) ([]Reason, error) +type VisitDBAPI interface { + FindById(visitId string) (Visit, error) + FindCandidates(keywords []string) ([]Visit, error) } -var _ ReasonDBAPI = (*ReasonDB)(nil) +var _ VisitDBAPI = (*VisitDB)(nil) diff --git a/db.yaml b/db.yaml index 77bb4d3..4f2a6dd 100644 --- a/db.yaml +++ b/db.yaml @@ -1,5 +1,5 @@ - id: deworming - reason: "Féregtelenítés kutyának" + visit: "Féregtelenítés kutyának" keywords: ["worm", "deworming", "parasite", "intestinal worm", "dog"] procedures: - name: "Alap vérvizsgálat" @@ -11,7 +11,7 @@ notes: "A kezelés előtt vérvizsgálat szükséges a biztonságos gyógyszeradás miatt." - id: vaccination - reason: "Oltás kutyának" + visit: "Oltás kutyának" keywords: ["vaccination", "vaccine", "to vaccinate", "dog disease", "rabies"] procedures: - name: "Általános állapotfelmérés" @@ -23,7 +23,7 @@ notes: "A kutyák oltási programja eltérhet az életkortól és korábbi oltásoktól függően." - id: neutering - reason: "Ivartalanítás macskának" + visit: "Ivartalanítás macskának" keywords: ["neutering", "surgery", "cat", "tomcat", "female cat"] procedures: - name: "Műtéti előzetes vizsgálat" @@ -35,7 +35,7 @@ notes: "A műtét után 2-3 nap pihenő szükséges." - id: dental_cleaning - reason: "Fogkő eltávolítás kutyának" + visit: "Fogkő eltávolítás kutyának" keywords: ["tooth", "tartar", "dentition", "tooth cleaning", "dog teeth","plaque"] procedures: - name: "Altatás előtti vizsgálat" @@ -47,7 +47,7 @@ notes: "Az altatás kockázata miatt minden esetben szükséges előzetes vizsgálat." - id: checkup - reason: "Általános állapotfelmérés" + visit: "Általános állapotfelmérés" keywords: ["examination", "checkup", "check-up", "general assessment"] procedures: - name: "Teljes fizikai vizsgálat" @@ -56,7 +56,7 @@ notes: "Évente legalább egyszer javasolt a rutin állapotfelmérés." - id: allergy - reason: "Allergiás tünetek vizsgálata" + visit: "Allergiás tünetek vizsgálata" keywords: ["allergy", "itching", "rash", "redness", "allergic"] procedures: - name: "Bőr- és vérvizsgálat" @@ -65,7 +65,7 @@ notes: "Az allergia gyakran étel vagy környezeti tényező miatt alakul ki." - id: ultrasound - reason: "Ultrahangos vizsgálat" + visit: "Ultrahangos vizsgálat" keywords: ["ultrasound", "abdomen", "examination", "US"] procedures: - name: "Ultrahang vizsgálat" @@ -74,7 +74,7 @@ notes: "Terhesség vagy belső szervi problémák vizsgálatára gyakran használt módszer." - id: bloodwork - reason: "Laborvizsgálat vérből" + visit: "Laborvizsgálat vérből" keywords: ["blood", "blood test", "lab", "test"] procedures: - name: "Teljes vérkép" @@ -83,7 +83,7 @@ notes: "Sok más vizsgálat alapja a laboreredmény." - id: xray - reason: "Röntgenfelvétel" + visit: "Röntgenfelvétel" keywords: ["x-ray", "bone", "scan", "fracture"] procedures: - name: "Röntgen vizsgálat" @@ -92,7 +92,7 @@ notes: "Törések, csontelváltozások vizsgálatára javasolt." - id: diarrhea - reason: "Hasmenés vizsgálata" + visit: "Hasmenés vizsgálata" keywords: ["diarrhea", "vomiting", "stomach", "intestine"] procedures: - name: "Állatorvosi konzultáció" diff --git a/llm.go b/llm.go index 319c6e9..417449a 100644 --- a/llm.go +++ b/llm.go @@ -71,7 +71,7 @@ func (llm *LLMClient) ExtractKeywords(ctx context.Context, message string) (map[ } // DisambiguateBestMatch calls LLM to pick best match from candidates -func (llm *LLMClient) DisambiguateBestMatch(ctx context.Context, message string, candidates []Reason) (string, error) { +func (llm *LLMClient) DisambiguateBestMatch(ctx context.Context, message string, candidates []Visit) (string, error) { format := map[string]interface{}{ "type": "object", "properties": map[string]interface{}{ @@ -138,7 +138,7 @@ func (llm *LLMClient) openAICompletion(ctx context.Context, prompt string, forma return "", err } if result.Message.Content == "" { - logrus.Warn("[LLM] openAICompletion: no content returned") + logrus.Warn("[LLM] openAICompletion: no content returned %v body:[%v]", resp.Status, resp.Body) return "", nil } logrus.WithField("content", result.Message.Content).Info("[LLM] openAICompletion: got content") @@ -150,7 +150,7 @@ func (llm *LLMClient) openAICompletion(ctx context.Context, prompt string, forma type LLMClientAPI interface { ExtractKeywords(ctx context.Context, message string) (map[string]interface{}, error) - DisambiguateBestMatch(ctx context.Context, message string, candidates []Reason) (string, error) + DisambiguateBestMatch(ctx context.Context, message string, candidates []Visit) (string, error) } var _ LLMClientAPI = (*LLMClient)(nil) diff --git a/log.go b/log.go index 8a35418..cd90f4e 100644 --- a/log.go +++ b/log.go @@ -5,7 +5,7 @@ import ( ) // logRequest logs incoming chat requests and extracted info -func logRequest(req ChatRequest, keywords map[string]interface{}, candidates []Reason, bestID string, err error) { +func logRequest(req ChatRequest, keywords map[string]interface{}, candidates []Visit, bestID string, err error) { logrus.WithFields(logrus.Fields{ "message": req.Message, "keywords": keywords, @@ -15,7 +15,7 @@ func logRequest(req ChatRequest, keywords map[string]interface{}, candidates []R }).Info("Chat request trace") } -func getCandidateIDs(candidates []Reason) []string { +func getCandidateIDs(candidates []Visit) []string { ids := make([]string, len(candidates)) for i, c := range candidates { ids[i] = c.ID @@ -23,17 +23,17 @@ func getCandidateIDs(candidates []Reason) []string { return ids } -// Procedure represents a single procedure for a visit reason +// Procedure represents a single procedure for a visit type Procedure struct { Name string `yaml:"name" json:"name"` Price int `yaml:"price" json:"price"` DurationMin int `yaml:"duration_minutes" json:"duration_minutes"` } -// Reason represents a visit reason entry -type Reason struct { +// Visit represents a visit entry +type Visit struct { ID string `yaml:"id" json:"id"` - Reason string `yaml:"reason" json:"reason"` + Visit string `yaml:"visit" json:"visit"` Keywords []string `yaml:"keywords" json:"keywords"` Procedures []Procedure `yaml:"procedures" json:"procedures"` Notes string `yaml:"notes" json:"notes,omitempty"` diff --git a/main.go b/main.go index b4a0132..3ed9248 100644 --- a/main.go +++ b/main.go @@ -15,7 +15,7 @@ func main() { logrus.Fatalf("Failed to load config.yaml: %v", err) } logrus.Infof("Loaded config: %+v", appConfig) - reasonDB := NewReasonDB() + visitDB := NewVisitDB() if err := loadUITemplate("ui.html"); err != nil { logrus.Fatalf("Failed to load ui.html: %v", err) @@ -25,7 +25,7 @@ func main() { os.Getenv("OPENAI_BASE_URL"), os.Getenv("OPENAI_MODEL"), ) - chatService := NewChatService(llm, &reasonDB) + chatService := NewChatService(llm, &visitDB) r := gin.Default() r.GET("/", func(c *gin.Context) { c.Status(200) diff --git a/maindb.yaml b/maindb.yaml index 63e3f9a..b867a92 100644 --- a/maindb.yaml +++ b/maindb.yaml @@ -1,5 +1,5 @@ - id: deworming - reason: "Féregtelenítés kutyának" + visit: "Féregtelenítés kutyának" keywords: ["féreg", "féregtelenítés", "parazita", "bélféreg", "kutya"] procedures: - name: "Alap vérvizsgálat" @@ -11,7 +11,7 @@ notes: "A kezelés előtt vérvizsgálat szükséges a biztonságos gyógyszeradás miatt." - id: vaccination - reason: "Oltás kutyának" + visit: "Oltás kutyának" keywords: ["oltás", "vakcina", "oltani", "kutyabetegség", "veszettség"] procedures: - name: "Általános állapotfelmérés" @@ -23,7 +23,7 @@ notes: "A kutyák oltási programja eltérhet az életkortól és korábbi oltásoktól függően." - id: neutering - reason: "Ivartalanítás macskának" + visit: "Ivartalanítás macskának" keywords: ["ivartalanítás", "műtét", "macska", "kandúr", "nőstény"] procedures: - name: "Műtéti előzetes vizsgálat" @@ -35,7 +35,7 @@ notes: "A műtét után 2-3 nap pihenő szükséges." - id: dental_cleaning - reason: "Fogkő eltávolítás kutyának" + visit: "Fogkő eltávolítás kutyának" keywords: ["fog", "fogkő", "fogsor", "fogtisztítás", "kutyafog"] procedures: - name: "Altatás előtti vizsgálat" @@ -47,7 +47,7 @@ notes: "Az altatás kockázata miatt minden esetben szükséges előzetes vizsgálat." - id: checkup - reason: "Általános állapotfelmérés" + visit: "Általános állapotfelmérés" keywords: ["vizsgálat", "ellenőrzés", "checkup", "állapotfelmérés"] procedures: - name: "Teljes fizikai vizsgálat" @@ -56,7 +56,7 @@ notes: "Évente legalább egyszer javasolt a rutin állapotfelmérés." - id: allergy - reason: "Allergiás tünetek vizsgálata" + visit: "Allergiás tünetek vizsgálata" keywords: ["allergia", "viszketés", "kiütés", "bőrpír", "allergiás"] procedures: - name: "Bőr- és vérvizsgálat" @@ -65,7 +65,7 @@ notes: "Az allergia gyakran étel vagy környezeti tényező miatt alakul ki." - id: ultrasound - reason: "Ultrahangos vizsgálat" + visit: "Ultrahangos vizsgálat" keywords: ["ultrahang", "has", "vizsgálat", "UH"] procedures: - name: "Ultrahang vizsgálat" @@ -74,7 +74,7 @@ notes: "Terhesség vagy belső szervi problémák vizsgálatára gyakran használt módszer." - id: bloodwork - reason: "Laborvizsgálat vérből" + visit: "Laborvizsgálat vérből" keywords: ["vér", "vérvizsgálat", "labor", "teszt"] procedures: - name: "Teljes vérkép" @@ -83,7 +83,7 @@ notes: "Sok más vizsgálat alapja a laboreredmény." - id: xray - reason: "Röntgenfelvétel" + visit: "Röntgenfelvétel" keywords: ["röntgen", "csont", "felvétel", "törés"] procedures: - name: "Röntgen vizsgálat" @@ -92,7 +92,7 @@ notes: "Törések, csontelváltozások vizsgálatára javasolt." - id: diarrhea - reason: "Hasmenés vizsgálata" + visit: "Hasmenés vizsgálata" keywords: ["hasmenés", "hányás", "gyomor", "bél"] procedures: - name: "Állatorvosi konzultáció"