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