feat: consolidate idkgng branch features, integrate Go chatbot service, and fix local configurations
This commit is contained in:
267
chatbot-service/main.go
Normal file
267
chatbot-service/main.go
Normal file
@@ -0,0 +1,267 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"math"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type FAQ struct {
|
||||
Keywords []string `json:"keywords"`
|
||||
Question string `json:"question"`
|
||||
Answer string `json:"answer"`
|
||||
}
|
||||
|
||||
type ChatRequest struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type ChatResponse struct {
|
||||
Answer string `json:"answer"`
|
||||
MatchedQuestion string `json:"matched_question,omitempty"`
|
||||
Confidence float64 `json:"confidence"`
|
||||
}
|
||||
|
||||
var (
|
||||
faqs []FAQ
|
||||
idf map[string]float64
|
||||
docVectors []map[string]float64
|
||||
docNorms []float64
|
||||
)
|
||||
|
||||
// Stopwords set to filter out noise
|
||||
var stopwords = map[string]bool{
|
||||
"a": true, "about": true, "above": true, "after": true, "again": true, "against": true, "all": true, "am": true,
|
||||
"an": true, "and": true, "any": true, "are": true, "as": true, "at": true, "be": true, "because": true,
|
||||
"been": true, "before": true, "being": true, "below": true, "between": true, "both": true, "but": true, "by": true,
|
||||
"can": true, "could": true, "did": true, "do": true, "does": true, "doing": true, "down": true, "during": true,
|
||||
"each": true, "few": true, "for": true, "from": true, "further": true, "had": true, "has": true, "have": true,
|
||||
"having": true, "he": true, "her": true, "here": true, "hers": true, "herself": true, "him": true, "himself": true,
|
||||
"his": true, "how": true, "i": true, "if": true, "in": true, "into": true, "is": true, "it": true, "its": true,
|
||||
"itself": true, "me": true, "more": true, "most": true, "my": true, "myself": true, "no": true, "nor": true,
|
||||
"not": true, "of": true, "off": true, "on": true, "once": true, "only": true, "or": true, "other": true,
|
||||
"our": true, "ours": true, "ourselves": true, "out": true, "over": true, "own": true, "same": true, "she": true,
|
||||
"should": true, "so": true, "some": true, "such": true, "than": true, "that": true, "the": true, "their": true,
|
||||
"theirs": true, "them": true, "themselves": true, "then": true, "there": true, "these": true, "they": true,
|
||||
"this": true, "those": true, "through": true, "to": true, "too": true, "under": true, "until": true, "up": true,
|
||||
"very": true, "was": true, "we": true, "were": true, "what": true, "when": true, "where": true, "which": true,
|
||||
"while": true, "who": true, "whom": true, "why": true, "with": true, "you": true, "your": true, "yours": true,
|
||||
"yourself": true, "yourselves": true,
|
||||
}
|
||||
|
||||
var cleanRegex = regexp.MustCompile(`[^a-z0-9\s]`)
|
||||
|
||||
func tokenize(text string) []string {
|
||||
text = strings.ToLower(text)
|
||||
text = cleanRegex.ReplaceAllString(text, " ")
|
||||
words := strings.Fields(text)
|
||||
var tokens []string
|
||||
for _, word := range words {
|
||||
if !stopwords[word] && len(word) > 1 {
|
||||
tokens = append(tokens, word)
|
||||
}
|
||||
}
|
||||
return tokens
|
||||
}
|
||||
|
||||
func initTFIDF() {
|
||||
docFrequencies := make(map[string]int)
|
||||
allTokens := make([][]string, len(faqs))
|
||||
|
||||
for i, faq := range faqs {
|
||||
// Combine question and keywords to form the index document
|
||||
docText := faq.Question + " " + strings.Join(faq.Keywords, " ")
|
||||
tokens := tokenize(docText)
|
||||
allTokens[i] = tokens
|
||||
|
||||
uniqueTokens := make(map[string]bool)
|
||||
for _, token := range tokens {
|
||||
uniqueTokens[token] = true
|
||||
}
|
||||
|
||||
for token := range uniqueTokens {
|
||||
docFrequencies[token]++
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate IDF for each term
|
||||
idf = make(map[string]float64)
|
||||
numDocs := float64(len(faqs))
|
||||
for token, df := range docFrequencies {
|
||||
idf[token] = math.Log(1.0 + (numDocs / float64(df)))
|
||||
}
|
||||
|
||||
// Compute TF-IDF vectors for FAQs
|
||||
docVectors = make([]map[string]float64, len(faqs))
|
||||
docNorms = make([]float64, len(faqs))
|
||||
|
||||
for i, tokens := range allTokens {
|
||||
tfMap := make(map[string]float64)
|
||||
for _, token := range tokens {
|
||||
tfMap[token]++
|
||||
}
|
||||
|
||||
vector := make(map[string]float64)
|
||||
var sqSum float64
|
||||
|
||||
for token, tf := range tfMap {
|
||||
tfidfVal := tf * idf[token]
|
||||
vector[token] = tfidfVal
|
||||
sqSum += tfidfVal * tfidfVal
|
||||
}
|
||||
|
||||
docVectors[i] = vector
|
||||
docNorms[i] = math.Sqrt(sqSum)
|
||||
}
|
||||
|
||||
log.Printf("TF-IDF Chatbot engine initialized successfully with %d Q&As.", len(faqs))
|
||||
}
|
||||
|
||||
func getBestMatch(query string) (int, float64) {
|
||||
queryTokens := tokenize(query)
|
||||
if len(queryTokens) == 0 {
|
||||
return -1, 0.0
|
||||
}
|
||||
|
||||
// Compute TF for query
|
||||
queryTF := make(map[string]float64)
|
||||
for _, token := range queryTokens {
|
||||
queryTF[token]++
|
||||
}
|
||||
|
||||
// Compute TF-IDF vector for query
|
||||
queryVector := make(map[string]float64)
|
||||
var querySqSum float64
|
||||
for token, tf := range queryTF {
|
||||
if idfVal, exists := idf[token]; exists {
|
||||
tfidfVal := tf * idfVal
|
||||
queryVector[token] = tfidfVal
|
||||
querySqSum += tfidfVal * tfidfVal
|
||||
}
|
||||
}
|
||||
|
||||
queryNorm := math.Sqrt(querySqSum)
|
||||
if queryNorm == 0 {
|
||||
return -1, 0.0
|
||||
}
|
||||
|
||||
bestIdx := -1
|
||||
bestScore := -1.0
|
||||
|
||||
for i, docVector := range docVectors {
|
||||
var dotProduct float64
|
||||
for token, qVal := range queryVector {
|
||||
if dVal, exists := docVector[token]; exists {
|
||||
dotProduct += qVal * dVal
|
||||
}
|
||||
}
|
||||
|
||||
var cosineSim float64
|
||||
if docNorms[i] > 0 {
|
||||
cosineSim = dotProduct / (queryNorm * docNorms[i])
|
||||
}
|
||||
|
||||
// Apply exact keyword boosting
|
||||
keywordMatches := 0
|
||||
for _, kw := range faqs[i].Keywords {
|
||||
for _, qTok := range queryTokens {
|
||||
if strings.ToLower(kw) == qTok {
|
||||
keywordMatches++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A boost of 0.15 for each exact keyword match
|
||||
boost := float64(keywordMatches) * 0.15
|
||||
score := cosineSim + boost
|
||||
|
||||
if score > bestScore {
|
||||
bestScore = score
|
||||
bestIdx = i
|
||||
}
|
||||
}
|
||||
|
||||
return bestIdx, bestScore
|
||||
}
|
||||
|
||||
func handleChat(w http.ResponseWriter, r *http.Request) {
|
||||
// Add CORS headers
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
||||
|
||||
if r.Method == http.MethodOptions {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
var req ChatRequest
|
||||
err := json.NewDecoder(r.Body).Decode(&req)
|
||||
if err != nil {
|
||||
http.Error(w, "Bad Request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
bestIdx, score := getBestMatch(req.Message)
|
||||
threshold := 0.18
|
||||
|
||||
var resp ChatResponse
|
||||
if bestIdx != -1 && score >= threshold {
|
||||
resp = ChatResponse{
|
||||
Answer: faqs[bestIdx].Answer,
|
||||
MatchedQuestion: faqs[bestIdx].Question,
|
||||
Confidence: score,
|
||||
}
|
||||
} else {
|
||||
resp = ChatResponse{
|
||||
Answer: "I'm sorry, I couldn't find an answer to your question about RIT Chennai. Please try rephrasing your question or contact our administrative office at +91 8925977445 or mail@ritchennai.edu.in.",
|
||||
Confidence: score,
|
||||
}
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}
|
||||
|
||||
func handleHealth(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(`{"status":"UP"}`))
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Load FAQs
|
||||
file, err := os.ReadFile("qna.json")
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to read qna.json: %v", err)
|
||||
}
|
||||
|
||||
err = json.Unmarshal(file, &faqs)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to parse qna.json: %v", err)
|
||||
}
|
||||
|
||||
initTFIDF()
|
||||
|
||||
port := os.Getenv("PORT")
|
||||
if port == "" {
|
||||
port = "8081"
|
||||
}
|
||||
|
||||
http.HandleFunc("/api/chat", handleChat)
|
||||
http.HandleFunc("/api/health", handleHealth)
|
||||
|
||||
log.Printf("Chatbot service listening on port %s...", port)
|
||||
if err := http.ListenAndServe(fmt.Sprintf(":%s", port), nil); err != nil {
|
||||
log.Fatalf("Server failed to start: %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user