This commit is contained in:
lehel 2025-09-24 13:20:20 +02:00
parent 0e7d284f09
commit 8b615def81
No known key found for this signature in database
GPG Key ID: 9C4F9D6111EE5CFA
3 changed files with 38 additions and 21 deletions

1
go.mod
View File

@ -4,6 +4,7 @@ go 1.25
require ( require (
github.com/gin-gonic/gin v1.11.0 github.com/gin-gonic/gin v1.11.0
github.com/sirupsen/logrus v1.9.3
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
) )

4
go.sum
View File

@ -48,10 +48,13 @@ github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg= github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
@ -73,6 +76,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-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=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=

54
main.go
View File

@ -7,13 +7,12 @@ import (
"fmt" "fmt"
"html/template" "html/template"
"io/ioutil" "io/ioutil"
"log"
"net/http" "net/http"
"os" "os"
"strings" "strings"
"time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
@ -85,12 +84,12 @@ func loadConfig(path string) error {
func (llm *LLMClient) ExtractKeywords(ctx context.Context, message string) ([]string, error) { func (llm *LLMClient) ExtractKeywords(ctx context.Context, message string) ([]string, error) {
prompt, err := renderPrompt(appConfig.LLM.ExtractKeywordsPrompt, map[string]string{"Message": message}) prompt, err := renderPrompt(appConfig.LLM.ExtractKeywordsPrompt, map[string]string{"Message": message})
if err != nil { if err != nil {
log.Printf("[CONFIG] Failed to render ExtractKeywords prompt: %v", err) logrus.WithError(err).Error("[CONFIG] Failed to render ExtractKeywords prompt")
return nil, err return nil, err
} }
log.Printf("[LLM] ExtractKeywords prompt: %q", prompt) logrus.WithField("prompt", prompt).Info("[LLM] ExtractKeywords prompt")
resp, err := llm.openAICompletion(ctx, prompt) resp, err := llm.openAICompletion(ctx, prompt)
log.Printf("[LLM] ExtractKeywords response: %q, err: %v", resp, err) logrus.WithFields(logrus.Fields{"response": resp, "err": err}).Info("[LLM] ExtractKeywords response")
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -113,12 +112,12 @@ func (llm *LLMClient) DisambiguateBestMatch(ctx context.Context, message string,
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 {
log.Printf("[CONFIG] Failed to render Disambiguate prompt: %v", err) logrus.WithError(err).Error("[CONFIG] Failed to render Disambiguate prompt")
return "", err return "", err
} }
log.Printf("[LLM] DisambiguateBestMatch prompt: %q", prompt) logrus.WithField("prompt", prompt).Info("[LLM] DisambiguateBestMatch prompt")
resp, err := llm.openAICompletion(ctx, prompt) resp, err := llm.openAICompletion(ctx, prompt)
log.Printf("[LLM] DisambiguateBestMatch response: %q, err: %v", resp, err) logrus.WithFields(logrus.Fields{"response": resp, "err": err}).Info("[LLM] DisambiguateBestMatch response")
if err != nil { if err != nil {
return "", err return "", err
} }
@ -135,7 +134,7 @@ func (llm *LLMClient) openAICompletion(ctx context.Context, prompt string) (stri
if apiURL == "" { if apiURL == "" {
apiURL = "https://api.openai.com/v1/completions" apiURL = "https://api.openai.com/v1/completions"
} }
log.Printf("[LLM] openAICompletion POST %s | prompt: %q", apiURL, prompt) logrus.WithFields(logrus.Fields{"api_url": apiURL, "prompt": prompt}).Info("[LLM] openAICompletion POST")
body := map[string]interface{}{ body := map[string]interface{}{
"model": "text-davinci-003", "model": "text-davinci-003",
"prompt": prompt, "prompt": prompt,
@ -151,7 +150,7 @@ func (llm *LLMClient) openAICompletion(ctx context.Context, prompt string) (stri
client := &http.Client{} client := &http.Client{}
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
log.Printf("[LLM] openAICompletion error: %v", err) logrus.WithError(err).Error("[LLM] openAICompletion error")
return "", err return "", err
} }
defer resp.Body.Close() defer resp.Body.Close()
@ -161,14 +160,14 @@ func (llm *LLMClient) openAICompletion(ctx context.Context, prompt string) (stri
} `json:"choices"` } `json:"choices"`
} }
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
log.Printf("[LLM] openAICompletion decode error: %v", err) logrus.WithError(err).Error("[LLM] openAICompletion decode error")
return "", err return "", err
} }
if len(result.Choices) == 0 { if len(result.Choices) == 0 {
log.Printf("[LLM] openAICompletion: no choices returned") logrus.Warn("[LLM] openAICompletion: no choices returned")
return "", nil return "", nil
} }
log.Printf("[LLM] openAICompletion: got text: %q", result.Choices[0].Text) logrus.WithField("text", result.Choices[0].Text).Info("[LLM] openAICompletion: got text")
return result.Choices[0].Text, nil return result.Choices[0].Text, nil
} }
@ -242,16 +241,19 @@ func loadUITemplate(path string) error {
} }
func main() { func main() {
logrus.SetFormatter(&logrus.TextFormatter{FullTimestamp: true})
logrus.SetLevel(logrus.InfoLevel)
if err := loadConfig("config.yaml"); err != nil { if err := loadConfig("config.yaml"); err != nil {
log.Fatalf("Failed to load config.yaml: %v", err) logrus.Fatalf("Failed to load config.yaml: %v", err)
} }
log.Printf("Loaded config: %+v", appConfig) logrus.Infof("Loaded config: %+v", appConfig)
if err := loadYAMLDB("db.yaml"); err != nil { if err := loadYAMLDB("db.yaml"); err != nil {
log.Fatalf("Failed to load db.yaml: %v", err) logrus.Fatalf("Failed to load db.yaml: %v", err)
} }
fmt.Printf("Loaded %d reasons from db.yaml\n", len(reasonsDB)) 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 {
log.Fatalf("Failed to load ui.html: %v", err) logrus.Fatalf("Failed to load ui.html: %v", err)
} }
llm := &LLMClient{ llm := &LLMClient{
APIKey: os.Getenv("OPENAI_API_KEY"), APIKey: os.Getenv("OPENAI_API_KEY"),
@ -265,7 +267,7 @@ func main() {
r.POST("/chat", func(c *gin.Context) { r.POST("/chat", func(c *gin.Context) {
var req ChatRequest var req ChatRequest
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
log.Printf("[ERROR] Invalid request: %v", err) logrus.WithError(err).Error("Invalid request")
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"}) c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
return return
} }
@ -293,7 +295,12 @@ func main() {
return return
} }
totalPrice, totalDuration := sumProcedures(best.Procedures) totalPrice, totalDuration := sumProcedures(best.Procedures)
log.Printf("[TRACE] Responding with match: %q, totalPrice: %d, totalDuration: %d, notes: %q", best.ID, totalPrice, totalDuration, best.Notes) logrus.WithFields(logrus.Fields{
"match": best.ID,
"total_price": totalPrice,
"total_duration": totalDuration,
"notes": best.Notes,
}).Info("Responding with match")
c.JSON(http.StatusOK, ChatResponse{ c.JSON(http.StatusOK, ChatResponse{
Match: &best.ID, Match: &best.ID,
Procedures: best.Procedures, Procedures: best.Procedures,
@ -308,8 +315,13 @@ func main() {
// logRequest logs incoming chat requests and extracted info // logRequest logs incoming chat requests and extracted info
func logRequest(req ChatRequest, keywords []string, candidates []Reason, bestID string, err error) { func logRequest(req ChatRequest, keywords []string, candidates []Reason, bestID string, err error) {
log.Printf("[TRACE] %s | message: %q | keywords: %v | candidates: %v | bestID: %q | err: %v", logrus.WithFields(logrus.Fields{
time.Now().Format(time.RFC3339), req.Message, keywords, getCandidateIDs(candidates), bestID, err) "message": req.Message,
"keywords": keywords,
"candidates": getCandidateIDs(candidates),
"bestID": bestID,
"err": err,
}).Info("Chat request trace")
} }
func getCandidateIDs(candidates []Reason) []string { func getCandidateIDs(candidates []Reason) []string {