This commit is contained in:
lehel 2025-09-27 09:22:32 +02:00
parent 86fe25dbee
commit e60f0c0ed6
No known key found for this signature in database
GPG Key ID: 9C4F9D6111EE5CFA
9 changed files with 227 additions and 60 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
reasons.bleve

View File

@ -2,6 +2,7 @@ package main
import ( import (
"context" "context"
"fmt"
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@ -10,10 +11,11 @@ import (
type ChatService struct { type ChatService struct {
LLM *LLMClient LLM *LLMClient
reasonsDB ReasonDB
} }
func NewChatService(llm *LLMClient) *ChatService { func NewChatService(llm *LLMClient, db ReasonDB) *ChatService {
return &ChatService{LLM: llm} return &ChatService{LLM: llm, reasonsDB: db}
} }
func (cs *ChatService) HandleChat(c *gin.Context) { func (cs *ChatService) HandleChat(c *gin.Context) {
@ -22,15 +24,15 @@ func (cs *ChatService) HandleChat(c *gin.Context) {
if err != nil { if err != nil {
return return
} }
keywords, err := cs.extractKeywords(ctx, req.Message) keywordsResponse, err := cs.getKeywordsResponse(ctx, req.Message)
if err != nil { if err != nil {
cs.logChat(req, keywords, nil, "", err) cs.logChat(req, keywordsResponse, nil, "", err)
c.JSON(http.StatusOK, ChatResponse{Match: nil}) c.JSON(http.StatusOK, ChatResponse{Match: nil})
return return
} }
kwArr := cs.keywordsToStrings(keywords["keyword"]) keywordList := cs.keywordsToStrings(keywordsResponse["keyword"])
best, bestID, candidates, err := cs.findBestCandidate(ctx, req, kwArr) best, err := cs.findVisitReason(ctx, req, keywordList)
cs.logChat(req, keywords, candidates, bestID, err)
resp := cs.buildResponse(best) resp := cs.buildResponse(best)
c.JSON(http.StatusOK, resp) c.JSON(http.StatusOK, resp)
} }
@ -45,7 +47,7 @@ func (cs *ChatService) parseRequest(c *gin.Context) (ChatRequest, error) {
return req, nil return req, nil
} }
func (cs *ChatService) extractKeywords(ctx context.Context, message string) (map[string]interface{}, error) { func (cs *ChatService) getKeywordsResponse(ctx context.Context, message string) (map[string]interface{}, error) {
keywords, err := cs.LLM.ExtractKeywords(ctx, message) keywords, err := cs.LLM.ExtractKeywords(ctx, message)
return keywords, err return keywords, err
} }
@ -65,38 +67,52 @@ func (cs *ChatService) keywordsToStrings(kwIface interface{}) []string {
return kwArr return kwArr
} }
func (cs *ChatService) findBestCandidate(ctx context.Context, req ChatRequest, kwArr []string) (*Reason, string, []Reason, error) { func (cs *ChatService) findVisitReason(ctx context.Context, req ChatRequest, keywordList []string) (*Reason, error) {
candidates := findCandidates(kwArr) logrus.WithFields(logrus.Fields{
"keywords": keywordList,
"message": req.Message,
}).Info("Finding visit reason candidates")
candidateReasonse, err := cs.reasonsDB.findCandidates(keywordList)
logrus.WithFields(logrus.Fields{
"candidates": candidateReasonse,
"error": err,
}).Info("Candidate reasons found")
if err != nil {
return nil, err
}
bestID := "" bestID := ""
var err error if len(candidateReasonse) > 0 {
if len(candidates) > 0 { bestID, err = cs.LLM.DisambiguateBestMatch(ctx, req.Message, candidateReasonse)
bestID, err = cs.LLM.DisambiguateBestMatch(ctx, req.Message, candidates) logrus.WithFields(logrus.Fields{
"bestID": bestID,
"error": err,
}).Info("Disambiguated best match")
} }
var best *Reason reason, err := cs.reasonsDB.findById(bestID)
for i := range candidates { if err != nil {
if candidates[i].ID == bestID { return nil, fmt.Errorf("findById: %w", err)
best = &candidates[i]
break
} }
} return &reason, nil
if err != nil || len(kwArr) == 0 || len(candidates) == 0 || bestID == "" || best == nil {
return nil, bestID, candidates, err
}
return best, bestID, candidates, nil
} }
func (cs *ChatService) buildResponse(best *Reason) ChatResponse { func (cs *ChatService) buildResponse(best *Reason) ChatResponse {
if best == nil { if best == nil {
return ChatResponse{Match: nil} resp := ChatResponse{Match: nil}
logrus.WithFields(logrus.Fields{"response": resp}).Info("Build response: no match")
return resp
} }
totalPrice, totalDuration := sumProcedures(best.Procedures) totalPrice, totalDuration := sumProcedures(best.Procedures)
return ChatResponse{ resp := ChatResponse{
Match: &best.ID, Match: &best.ID,
Procedures: best.Procedures, Procedures: best.Procedures,
TotalPrice: totalPrice, TotalPrice: totalPrice,
TotalDuration: totalDuration, TotalDuration: totalDuration,
Notes: best.Notes, Notes: best.Notes,
} }
logrus.WithFields(logrus.Fields{"response": resp}).Info("Build response: match found")
return resp
} }
func (cs *ChatService) logChat(req ChatRequest, keywords map[string]interface{}, candidates []Reason, bestID string, err error) { func (cs *ChatService) logChat(req ChatRequest, keywords map[string]interface{}, candidates []Reason, bestID string, err error) {

View File

@ -1,4 +1,4 @@
llm: llm:
extract_keywords_prompt: "Translate [{{.Message}}] to English, then output only 35 comma-separated veterinary-related keywords derived strictly from [{{.Message}}]. example output [\"keyword1\",\"keyword2\"] No other text, no extra punctuation, no explanations, no quotes, no formatting." extract_keywords_prompt: "Translate [{{.Message}}] to English, then output only 35 comma-separated veterinary-related keywords IN ENGLISH derived strictly from [{{.Message}}]. example output [\"keyword1\",\"keyword2\"] No other text, no extra punctuation, no explanations, no quotes, no formatting."
disambiguate_prompt: "Given these possible vet visit reasons: {{.Entries}}, choose the single best match for this user message: {{.Message}}. Reply with the id or none." disambiguate_prompt: "Given these possible vet visit reasons: [{{.Entries}}], choose the single best match for this user message: {{.Message}}. Reply with id ex {\"visitReason\":\"bloodwork\"} No other text, no extra punctuation, no explanations, no quotes, no formatting."

81
db.go
View File

@ -1,38 +1,97 @@
package main package main
import ( import (
"errors"
"io/ioutil" "io/ioutil"
"os"
"strings" "strings"
"github.com/sirupsen/logrus"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"github.com/blevesearch/bleve/v2"
) )
var reasonsDB []Reason type ReasonDB struct {
reasonsDB []Reason
reasonsIdx bleve.Index
}
func loadYAMLDB(path string) error { func NewReasonDB() ReasonDB {
db := ReasonDB{}
db.init()
return db
}
func (rdb *ReasonDB) findById(reasonId string) (Reason, error) {
for _, reason := range rdb.reasonsDB {
if reason.ID == reasonId {
return reason, nil
}
}
return Reason{}, errors.New("reason not found")
}
func (rdb *ReasonDB) init() {
idxPath := "reasons.bleve"
if _, err := os.Stat(idxPath); err == nil {
rdb.reasonsIdx, err = bleve.Open(idxPath)
if err != nil {
panic(err)
}
} else if os.IsNotExist(err) {
rdb.reasonsIdx, err = bleve.New(idxPath, bleve.NewIndexMapping())
if err != nil {
panic(err)
}
} else {
panic(err)
}
err := rdb.loadYAMLDB("db.yaml")
if err != nil {
logrus.Fatalf("Failed to load db.yaml: %v", err)
panic(err)
}
}
func (rdb *ReasonDB) loadYAMLDB(path string) error {
data, err := ioutil.ReadFile(path) data, err := ioutil.ReadFile(path)
if err != nil { if err != nil {
return err return err
} }
return yaml.Unmarshal(data, &reasonsDB) if err := yaml.Unmarshal(data, &rdb.reasonsDB); err != nil {
return err
}
return rdb.indexReasons(rdb.reasonsDB)
}
func (rdb *ReasonDB) indexReasons(reasons []Reason) error {
batch := rdb.reasonsIdx.NewBatch()
for _, reason := range reasons {
if err := batch.Index(reason.ID, reason); err != nil {
return err
}
}
return rdb.reasonsIdx.Batch(batch)
} }
// findCandidates returns reasons with overlapping keywords // findCandidates returns reasons with overlapping keywords
func findCandidates(keywords []string) []Reason { func (rdb *ReasonDB) findCandidates(keywords []string) ([]Reason, error) {
kwSet := make(map[string]struct{}) query := bleve.NewMatchQuery(strings.Join(keywords, " "))
for _, k := range keywords { search := bleve.NewSearchRequest(query)
kwSet[k] = struct{}{} searchResults, err := rdb.reasonsIdx.Search(search)
if err != nil {
return nil, err
} }
var candidates []Reason var candidates []Reason
for _, r := range reasonsDB { for _, hit := range searchResults.Hits {
for _, k := range r.Keywords { for _, r := range rdb.reasonsDB {
if _, ok := kwSet[strings.ToLower(k)]; ok { if r.ID == hit.ID {
candidates = append(candidates, r) candidates = append(candidates, r)
break break
} }
} }
} }
return candidates return candidates, nil
} }
// sumProcedures calculates total price and duration // sumProcedures calculates total price and duration

20
db.yaml
View File

@ -1,6 +1,6 @@
- id: deworming - id: deworming
reason: "Féregtelenítés kutyának" reason: "Féregtelenítés kutyának"
keywords: ["féreg", "féregtelenítés", "parazita", "bélféreg", "kutya"] keywords: ["worm", "deworming", "parasite", "intestinal worm", "dog"]
procedures: procedures:
- name: "Alap vérvizsgálat" - name: "Alap vérvizsgálat"
price: 12000 price: 12000
@ -12,7 +12,7 @@
- id: vaccination - id: vaccination
reason: "Oltás kutyának" reason: "Oltás kutyának"
keywords: ["oltás", "vakcina", "oltani", "kutyabetegség", "veszettség"] keywords: ["vaccination", "vaccine", "to vaccinate", "dog disease", "rabies"]
procedures: procedures:
- name: "Általános állapotfelmérés" - name: "Általános állapotfelmérés"
price: 6000 price: 6000
@ -24,7 +24,7 @@
- id: neutering - id: neutering
reason: "Ivartalanítás macskának" reason: "Ivartalanítás macskának"
keywords: ["ivartalanítás", "műtét", "macska", "kandúr", "nőstény"] 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"
price: 15000 price: 15000
@ -36,7 +36,7 @@
- id: dental_cleaning - id: dental_cleaning
reason: "Fogkő eltávolítás kutyának" reason: "Fogkő eltávolítás kutyának"
keywords: ["fog", "fogkő", "fogsor", "fogtisztítás", "kutyafog"] 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"
price: 12000 price: 12000
@ -48,7 +48,7 @@
- id: checkup - id: checkup
reason: "Általános állapotfelmérés" reason: "Általános állapotfelmérés"
keywords: ["vizsgálat", "ellenőrzés", "checkup", "állapotfelmérés"] keywords: ["examination", "checkup", "check-up", "general assessment"]
procedures: procedures:
- name: "Teljes fizikai vizsgálat" - name: "Teljes fizikai vizsgálat"
price: 10000 price: 10000
@ -57,7 +57,7 @@
- id: allergy - id: allergy
reason: "Allergiás tünetek vizsgálata" reason: "Allergiás tünetek vizsgálata"
keywords: ["allergia", "viszketés", "kiütés", "bőrpír", "allergiás"] keywords: ["allergy", "itching", "rash", "redness", "allergic"]
procedures: procedures:
- name: "Bőr- és vérvizsgálat" - name: "Bőr- és vérvizsgálat"
price: 20000 price: 20000
@ -66,7 +66,7 @@
- id: ultrasound - id: ultrasound
reason: "Ultrahangos vizsgálat" reason: "Ultrahangos vizsgálat"
keywords: ["ultrahang", "has", "vizsgálat", "UH"] keywords: ["ultrasound", "abdomen", "examination", "US"]
procedures: procedures:
- name: "Ultrahang vizsgálat" - name: "Ultrahang vizsgálat"
price: 18000 price: 18000
@ -75,7 +75,7 @@
- id: bloodwork - id: bloodwork
reason: "Laborvizsgálat vérből" reason: "Laborvizsgálat vérből"
keywords: ["vér", "vérvizsgálat", "labor", "teszt"] keywords: ["blood", "blood test", "lab", "test"]
procedures: procedures:
- name: "Teljes vérkép" - name: "Teljes vérkép"
price: 15000 price: 15000
@ -84,7 +84,7 @@
- id: xray - id: xray
reason: "Röntgenfelvétel" reason: "Röntgenfelvétel"
keywords: ["röntgen", "csont", "felvétel", "törés"] keywords: ["x-ray", "bone", "scan", "fracture"]
procedures: procedures:
- name: "Röntgen vizsgálat" - name: "Röntgen vizsgálat"
price: 16000 price: 16000
@ -93,7 +93,7 @@
- id: diarrhea - id: diarrhea
reason: "Hasmenés vizsgálata" reason: "Hasmenés vizsgálata"
keywords: ["hasmenés", "hányás", "gyomor", "bél"] keywords: ["diarrhea", "vomiting", "stomach", "intestine"]
procedures: procedures:
- name: "Állatorvosi konzultáció" - name: "Állatorvosi konzultáció"
price: 8000 price: 8000

24
go.mod
View File

@ -3,12 +3,32 @@ module vetrag
go 1.25 go 1.25
require ( require (
github.com/blevesearch/bleve/v2 v2.5.3
github.com/gin-gonic/gin v1.11.0 github.com/gin-gonic/gin v1.11.0
github.com/sirupsen/logrus v1.9.3 github.com/sirupsen/logrus v1.9.3
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
) )
require ( require (
github.com/RoaringBitmap/roaring/v2 v2.4.5 // indirect
github.com/bits-and-blooms/bitset v1.22.0 // indirect
github.com/blevesearch/bleve_index_api v1.2.8 // indirect
github.com/blevesearch/geo v0.2.4 // indirect
github.com/blevesearch/go-faiss v1.0.25 // indirect
github.com/blevesearch/go-porterstemmer v1.0.3 // indirect
github.com/blevesearch/gtreap v0.1.1 // indirect
github.com/blevesearch/mmap-go v1.0.4 // indirect
github.com/blevesearch/scorch_segment_api/v2 v2.3.10 // indirect
github.com/blevesearch/segment v0.9.1 // indirect
github.com/blevesearch/snowballstem v0.9.0 // indirect
github.com/blevesearch/upsidedown_store_api v1.0.2 // indirect
github.com/blevesearch/vellum v1.1.0 // indirect
github.com/blevesearch/zapx/v11 v11.4.2 // indirect
github.com/blevesearch/zapx/v12 v12.4.2 // indirect
github.com/blevesearch/zapx/v13 v13.4.2 // indirect
github.com/blevesearch/zapx/v14 v14.4.2 // indirect
github.com/blevesearch/zapx/v15 v15.4.2 // indirect
github.com/blevesearch/zapx/v16 v16.2.4 // indirect
github.com/bytedance/sonic v1.14.0 // indirect github.com/bytedance/sonic v1.14.0 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect github.com/cloudwego/base64x v0.1.6 // indirect
@ -19,17 +39,21 @@ require (
github.com/go-playground/validator/v10 v10.27.0 // indirect github.com/go-playground/validator/v10 v10.27.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect github.com/goccy/go-json v0.10.2 // indirect
github.com/goccy/go-yaml v1.18.0 // indirect github.com/goccy/go-yaml v1.18.0 // indirect
github.com/golang/protobuf v1.5.0 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mschoch/smat v0.2.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/quic-go/qpack v0.5.1 // indirect github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.54.0 // indirect github.com/quic-go/quic-go v0.54.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect github.com/ugorji/go/codec v1.3.0 // indirect
go.etcd.io/bbolt v1.4.0 // indirect
go.uber.org/mock v0.5.0 // indirect go.uber.org/mock v0.5.0 // indirect
golang.org/x/arch v0.20.0 // indirect golang.org/x/arch v0.20.0 // indirect
golang.org/x/crypto v0.40.0 // indirect golang.org/x/crypto v0.40.0 // indirect

56
go.sum
View File

@ -1,3 +1,44 @@
github.com/RoaringBitmap/roaring/v2 v2.4.5 h1:uGrrMreGjvAtTBobc0g5IrW1D5ldxDQYe2JW2gggRdg=
github.com/RoaringBitmap/roaring/v2 v2.4.5/go.mod h1:FiJcsfkGje/nZBZgCu0ZxCPOKD/hVXDS2dXi7/eUFE0=
github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/bits-and-blooms/bitset v1.22.0 h1:Tquv9S8+SGaS3EhyA+up3FXzmkhxPGjQQCkcs2uw7w4=
github.com/bits-and-blooms/bitset v1.22.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/blevesearch/bleve/v2 v2.5.3 h1:9l1xtKaETv64SZc1jc4Sy0N804laSa/LeMbYddq1YEM=
github.com/blevesearch/bleve/v2 v2.5.3/go.mod h1:Z/e8aWjiq8HeX+nW8qROSxiE0830yQA071dwR3yoMzw=
github.com/blevesearch/bleve_index_api v1.2.8 h1:Y98Pu5/MdlkRyLM0qDHostYo7i+Vv1cDNhqTeR4Sy6Y=
github.com/blevesearch/bleve_index_api v1.2.8/go.mod h1:rKQDl4u51uwafZxFrPD1R7xFOwKnzZW7s/LSeK4lgo0=
github.com/blevesearch/geo v0.2.4 h1:ECIGQhw+QALCZaDcogRTNSJYQXRtC8/m8IKiA706cqk=
github.com/blevesearch/geo v0.2.4/go.mod h1:K56Q33AzXt2YExVHGObtmRSFYZKYGv0JEN5mdacJJR8=
github.com/blevesearch/go-faiss v1.0.25 h1:lel1rkOUGbT1CJ0YgzKwC7k+XH0XVBHnCVWahdCXk4U=
github.com/blevesearch/go-faiss v1.0.25/go.mod h1:OMGQwOaRRYxrmeNdMrXJPvVx8gBnvE5RYrr0BahNnkk=
github.com/blevesearch/go-porterstemmer v1.0.3 h1:GtmsqID0aZdCSNiY8SkuPJ12pD4jI+DdXTAn4YRcHCo=
github.com/blevesearch/go-porterstemmer v1.0.3/go.mod h1:angGc5Ht+k2xhJdZi511LtmxuEf0OVpvUUNrwmM1P7M=
github.com/blevesearch/gtreap v0.1.1 h1:2JWigFrzDMR+42WGIN/V2p0cUvn4UP3C4Q5nmaZGW8Y=
github.com/blevesearch/gtreap v0.1.1/go.mod h1:QaQyDRAT51sotthUWAH4Sj08awFSSWzgYICSZ3w0tYk=
github.com/blevesearch/mmap-go v1.0.4 h1:OVhDhT5B/M1HNPpYPBKIEJaD0F3Si+CrEKULGCDPWmc=
github.com/blevesearch/mmap-go v1.0.4/go.mod h1:EWmEAOmdAS9z/pi/+Toxu99DnsbhG1TIxUoRmJw/pSs=
github.com/blevesearch/scorch_segment_api/v2 v2.3.10 h1:Yqk0XD1mE0fDZAJXTjawJ8If/85JxnLd8v5vG/jWE/s=
github.com/blevesearch/scorch_segment_api/v2 v2.3.10/go.mod h1:Z3e6ChN3qyN35yaQpl00MfI5s8AxUJbpTR/DL8QOQ+8=
github.com/blevesearch/segment v0.9.1 h1:+dThDy+Lvgj5JMxhmOVlgFfkUtZV2kw49xax4+jTfSU=
github.com/blevesearch/segment v0.9.1/go.mod h1:zN21iLm7+GnBHWTao9I+Au/7MBiL8pPFtJBJTsk6kQw=
github.com/blevesearch/snowballstem v0.9.0 h1:lMQ189YspGP6sXvZQ4WZ+MLawfV8wOmPoD/iWeNXm8s=
github.com/blevesearch/snowballstem v0.9.0/go.mod h1:PivSj3JMc8WuaFkTSRDW2SlrulNWPl4ABg1tC/hlgLs=
github.com/blevesearch/upsidedown_store_api v1.0.2 h1:U53Q6YoWEARVLd1OYNc9kvhBMGZzVrdmaozG2MfoB+A=
github.com/blevesearch/upsidedown_store_api v1.0.2/go.mod h1:M01mh3Gpfy56Ps/UXHjEO/knbqyQ1Oamg8If49gRwrQ=
github.com/blevesearch/vellum v1.1.0 h1:CinkGyIsgVlYf8Y2LUQHvdelgXr6PYuvoDIajq6yR9w=
github.com/blevesearch/vellum v1.1.0/go.mod h1:QgwWryE8ThtNPxtgWJof5ndPfx0/YMBh+W2weHKPw8Y=
github.com/blevesearch/zapx/v11 v11.4.2 h1:l46SV+b0gFN+Rw3wUI1YdMWdSAVhskYuvxlcgpQFljs=
github.com/blevesearch/zapx/v11 v11.4.2/go.mod h1:4gdeyy9oGa/lLa6D34R9daXNUvfMPZqUYjPwiLmekwc=
github.com/blevesearch/zapx/v12 v12.4.2 h1:fzRbhllQmEMUuAQ7zBuMvKRlcPA5ESTgWlDEoB9uQNE=
github.com/blevesearch/zapx/v12 v12.4.2/go.mod h1:TdFmr7afSz1hFh/SIBCCZvcLfzYvievIH6aEISCte58=
github.com/blevesearch/zapx/v13 v13.4.2 h1:46PIZCO/ZuKZYgxI8Y7lOJqX3Irkc3N8W82QTK3MVks=
github.com/blevesearch/zapx/v13 v13.4.2/go.mod h1:knK8z2NdQHlb5ot/uj8wuvOq5PhDGjNYQQy0QDnopZk=
github.com/blevesearch/zapx/v14 v14.4.2 h1:2SGHakVKd+TrtEqpfeq8X+So5PShQ5nW6GNxT7fWYz0=
github.com/blevesearch/zapx/v14 v14.4.2/go.mod h1:rz0XNb/OZSMjNorufDGSpFpjoFKhXmppH9Hi7a877D8=
github.com/blevesearch/zapx/v15 v15.4.2 h1:sWxpDE0QQOTjyxYbAVjt3+0ieu8NCE0fDRaFxEsp31k=
github.com/blevesearch/zapx/v15 v15.4.2/go.mod h1:1pssev/59FsuWcgSnTa0OeEpOzmhtmr/0/11H0Z8+Nw=
github.com/blevesearch/zapx/v16 v16.2.4 h1:tGgfvleXTAkwsD5mEzgM3zCS/7pgocTCnO1oyAUjlww=
github.com/blevesearch/zapx/v16 v16.2.4/go.mod h1:Rti/REtuuMmzwsI8/C/qIzRaEoSK/wiFYw5e5ctUKKs=
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ= github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA= github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
@ -25,9 +66,16 @@ github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
@ -40,6 +88,8 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OH
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM=
github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@ -64,6 +114,8 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
go.etcd.io/bbolt v1.4.0 h1:TU77id3TnN/zKr7CO/uk+fBCwF2jGcMuw2B/FMAzYIk=
go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk=
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
@ -76,6 +128,7 @@ golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
@ -84,10 +137,13 @@ golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

24
llm.go
View File

@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"context" "context"
"encoding/json" "encoding/json"
"fmt"
"net/http" "net/http"
"strings" "strings"
"text/template" "text/template"
@ -61,6 +62,13 @@ 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 []Reason) (string, error) {
format := map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"visitReason": map[string]interface{}{"type": "string"},
},
"required": []string{"visitReason"},
}
entries, _ := json.Marshal(candidates) entries, _ := json.Marshal(candidates)
prompt, err := renderPrompt(appConfig.LLM.DisambiguatePrompt, map[string]string{"Entries": string(entries), "Message": message}) prompt, err := renderPrompt(appConfig.LLM.DisambiguatePrompt, map[string]string{"Entries": string(entries), "Message": message})
if err != nil { if err != nil {
@ -68,16 +76,22 @@ func (llm *LLMClient) DisambiguateBestMatch(ctx context.Context, message string,
return "", err return "", err
} }
logrus.WithField("prompt", prompt).Info("[LLM] DisambiguateBestMatch prompt") logrus.WithField("prompt", prompt).Info("[LLM] DisambiguateBestMatch prompt")
resp, err := llm.openAICompletion(ctx, prompt, nil) resp, err := llm.openAICompletion(ctx, prompt, format)
logrus.WithFields(logrus.Fields{"response": resp, "err": err}).Info("[LLM] DisambiguateBestMatch response") logrus.WithFields(logrus.Fields{"response": resp, "err": err}).Info("[LLM] DisambiguateBestMatch response")
if err != nil { if err != nil {
return "", err return "", err
} }
id := strings.TrimSpace(resp) var parsed map[string]string
if id == "none" || id == "null" { if err := json.Unmarshal([]byte(resp), &parsed); err != nil {
return "", nil return "", fmt.Errorf("failed to unmarshal disambiguation response: %w", err)
} }
return id, nil
visitReason := strings.TrimSpace(parsed["visitReason"])
if visitReason == "" {
return "", fmt.Errorf("visitReason not found in response")
}
return visitReason, nil
} }
// openAICompletion calls Ollama API with prompt and structure, returns structured result // openAICompletion calls Ollama API with prompt and structure, returns structured result

View File

@ -1,7 +1,6 @@
package main package main
import ( import (
"fmt"
"os" "os"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@ -16,10 +15,8 @@ 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)
if err := loadYAMLDB("db.yaml"); err != nil { reasonDB := NewReasonDB()
logrus.Fatalf("Failed to load db.yaml: %v", err)
}
fmt.Printf("Loaded %d reasons from db.yaml\n", len(reasonsDB))
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)
} }
@ -27,7 +24,7 @@ func main() {
APIKey: os.Getenv("OPENAI_API_KEY"), APIKey: os.Getenv("OPENAI_API_KEY"),
BaseURL: os.Getenv("OPENAI_BASE_URL"), BaseURL: os.Getenv("OPENAI_BASE_URL"),
} }
chatService := NewChatService(llm) chatService := NewChatService(llm, reasonDB)
r := gin.Default() r := gin.Default()
r.GET("/", func(c *gin.Context) { r.GET("/", func(c *gin.Context) {
c.Status(200) c.Status(200)