From d4d395356bf39eb8b14f5d63d5ed4029c0bb4d60 Mon Sep 17 00:00:00 2001 From: SekiDesu0 Date: Sun, 1 Mar 2026 06:34:23 -0300 Subject: [PATCH] modified: bot.db modified: db/db.go modified: handlers/dashboard.go modified: main.go modified: services/openrouter.go modified: templates/dashboard.html --- .vscode/launch.json | 15 +++ bot.db | Bin 20480 -> 28672 bytes db/db.go | 14 +++ handlers/dashboard.go | 139 +++++++++++++++++++-- main.go | 5 + services/openrouter.go | 88 +++++++++---- templates/dashboard.html | 264 +++++++++++++-------------------------- 7 files changed, 316 insertions(+), 209 deletions(-) create mode 100644 .vscode/launch.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..608d3c6 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Launch Package", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${fileDirname}" + } + ] +} \ No newline at end of file diff --git a/bot.db b/bot.db index e2b21a97d887749ee593859e80f4523d3cf20bc0..baa299429c886cf2879448f9edbd2642601d9a60 100644 GIT binary patch literal 28672 zcmeI5&2Jk;6u@_F$8M~o8>y;%s3N}#N^Fv~y!Iv~=1@$sZ6o3|*c*umRo2v=B-`3M z&h8q>p$Dhp$bZ0v6Y7Bj;>LlB=7f4h+z=At%o#ZGX8n~orBbCHQ1wRI-I>|9Gw=Q8 zt)*u+ilSV?BZ0>-#V{j}VN!>g7hEqXxi9{i)&5fA zGy96Rru}sB@M3yaV1W`5AOb{y2oM1xKm>>Y5g-DuD}m#wcp^1FuRgjPuzM}u=DyGN zxj(!n)~Z(7w!kjGziGj+7Bb1{X{QMrTefw}s={`4qf)N!!Uxtaly~eq8(a8T#oDr$ zP_A*ng8EokR(Ybu!L~lKmq6EHq3d&R0fYw&%=aBXU~aG=9~5rD-JmZh%N}O|Z`N@- zw2nI28x;%It()bYO&ivBs#WZ-E-Pzxxw0+0xp}8*ZQR>Y5eNy$@BdNlp`v}QJ=A_CC`5n=5CI}U1c(3;AOb{y2oM1xKm>@uD<_bN z#uX*4r&r%GO|!Uc77O{pik@H54Fe3bl+Ta;XRsKNlEpKUW?#|{B~J^#9Yf(a^z`z| zM8ieh$lH0Nls88W&uV|*{QsGv{i!|EetG2>Y5g-CYfCvx)B0vO) zz^f%N9f_vYFmgm>CK{brhha42|NN6-d<=h1jb2kvg*xzK^80^8`$5ru!xJSUKm>>Y z5g-CYfCvx)B0vO)01+SpMBsl&ARUP*^TlC|$NuVpqMkP(zgjAor9vUC{z9`L5zg$zbZmH`9sPp*fanBf#k zhRG7x=jGm|q73hU*u{Y90&r^qx5+%{FpmLm0WPO*a=71h9f6q^u#;~^*fua|F(}Bp zcnz4>-fRgu3H{!ILwC*Y8-UIJ^3C(%FD(f;H{I#d%_htC?5#y-{lx8 z-w6a@)O_6905BlCgIQnpxX1B(EgV58$lXw+$$gG34{boHh7+(Qz}x*@+63%k(km5x zeI0J!E>$X!k$Uj^yxnPulC;d!je>40%T?sXd^R^>luQMZZ2NBVZ%zQQ@!MxFN9C&F7AGeB#6XCFri_gp{{p+ODj z1Y~uR->C5jv&$WhTo`0&)OGLugU}sG1=~uT!i(Q49IFjzd zWae}lUGXnzyD-go~(?lI(QDz3c-e0`@KNwi} zS)T8yA{!(B6$bt*K!H>IOj?YSui0yJ0`)WUzh~fo4-|dMFUZfxtjd{KP>`RQSCX5W HS5gcBQ6w8r diff --git a/db/db.go b/db/db.go index 3e285b0..4bc00a5 100644 --- a/db/db.go +++ b/db/db.go @@ -28,6 +28,20 @@ func Init() { customer_phone TEXT, appointment_date TEXT, status TEXT DEFAULT 'confirmed' + ); + CREATE TABLE IF NOT EXISTS chats ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT DEFAULT 'New Chat', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + ); + + CREATE TABLE IF NOT EXISTS messages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + chat_id INTEGER, + role TEXT, -- 'user' or 'assistant' + content TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY(chat_id) REFERENCES chats(id) );` Conn.Exec(schema) } diff --git a/handlers/dashboard.go b/handlers/dashboard.go index d6a7b3f..f9f4947 100644 --- a/handlers/dashboard.go +++ b/handlers/dashboard.go @@ -1,7 +1,9 @@ package handlers import ( + "log" "net/http" + "strconv" "whatsapp-bot/db" "whatsapp-bot/services" @@ -9,23 +11,52 @@ import ( ) func ShowDashboard(c *gin.Context) { - rows, err := db.Conn.Query("SELECT customer_phone, appointment_date, status FROM appointments") - if err != nil { - c.String(http.StatusInternalServerError, "DB Error") - return + // 1. Fetch Appointments for the Table + type Appt struct { + ID int + CustomerPhone string + Date string + Status string } - defer rows.Close() - - type Appt struct{ CustomerPhone, Date, Status string } var appts []Appt - for rows.Next() { - var a Appt - rows.Scan(&a.CustomerPhone, &a.Date, &a.Status) - appts = append(appts, a) + + approws, err := db.Conn.Query("SELECT id, customer_phone, appointment_date, status FROM appointments ORDER BY id DESC") + if err != nil { + log.Println("Appt Query Error:", err) + } else { + defer approws.Close() + for approws.Next() { + var a Appt + approws.Scan(&a.ID, &a.CustomerPhone, &a.Date, &a.Status) + appts = append(appts, a) + } } + // 2. Fetch Chats for the Sidebar + type Chat struct { + ID int + Title string + } + var chats []Chat + + chatrows, err := db.Conn.Query("SELECT id FROM chats ORDER BY id DESC") + if err != nil { + log.Println("Chat Query Error:", err) + } else { + defer chatrows.Close() + for chatrows.Next() { + var ch Chat + chatrows.Scan(&ch.ID) + // Give it a default title so the sidebar isn't empty + ch.Title = "Chat #" + strconv.Itoa(ch.ID) + chats = append(chats, ch) + } + } + + // 3. Render the Template with BOTH data sets c.HTML(http.StatusOK, "dashboard.html", gin.H{ "Appointments": appts, + "Chats": chats, }) } @@ -40,7 +71,10 @@ func TestAIHandler(c *gin.Context) { } // Calling the service we wrote earlier - response, err := services.GetAIResponse(body.Prompt) + response, err := services.GetAIResponse([]services.Message{ + {Role: "user", Content: body.Prompt}, + }) + if err != nil { c.JSON(500, gin.H{"response": "AI Error: " + err.Error()}) return @@ -79,3 +113,84 @@ func DeleteAppointmentHandler(c *gin.Context) { } c.Status(http.StatusOK) } + +// PUT /admin/appointment/:id +func UpdateAppointmentHandler(c *gin.Context) { + id := c.Param("id") + var body struct { + Phone string `json:"phone"` + Date string `json:"date"` + } + if err := c.BindJSON(&body); err != nil { + c.Status(400) + return + } + + _, err := db.Conn.Exec( + "UPDATE appointments SET customer_phone = ?, appointment_date = ? WHERE id = ?", + body.Phone, body.Date, id, + ) + if err != nil { + c.JSON(500, gin.H{"error": err.Error()}) + return + } + c.Status(200) +} + +// POST /admin/chat +func NewChatHandler(c *gin.Context) { + // Insert a new chat record. In SQLite, this is enough to generate an ID. + res, err := db.Conn.Exec("INSERT INTO chats (title) VALUES ('New Chat')") + if err != nil { + log.Println("Database Error in NewChat:", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create chat"}) + return + } + + id, _ := res.LastInsertId() + c.JSON(http.StatusOK, gin.H{"id": id}) +} + +// GET /admin/chat/:id/messages +func GetMessagesHandler(c *gin.Context) { + id := c.Param("id") + rows, _ := db.Conn.Query("SELECT role, content FROM messages WHERE chat_id = ? ORDER BY created_at ASC", id) + defer rows.Close() + + var msgs []services.Message + for rows.Next() { + var m services.Message + rows.Scan(&m.Role, &m.Content) + msgs = append(msgs, m) + } + c.JSON(200, msgs) +} + +// POST /admin/chat/:id/message +func PostMessageHandler(c *gin.Context) { + chatId := c.Param("id") + var body struct { + Content string `json:"content"` + } + c.BindJSON(&body) + + // 1. Save User Message + db.Conn.Exec("INSERT INTO messages (chat_id, role, content) VALUES (?, 'user', ?)", chatId, body.Content) + + // 2. Fetch history for AI + rows, _ := db.Conn.Query("SELECT role, content FROM messages WHERE chat_id = ? ORDER BY created_at ASC", chatId) + var history []services.Message + for rows.Next() { + var m services.Message + rows.Scan(&m.Role, &m.Content) + history = append(history, m) + } + + // 3. Get AI Response + aiResp, _ := services.GetAIResponse(history) + + // 4. Save Assistant Message + db.Conn.Exec("INSERT INTO messages (chat_id, role, content) VALUES (?, 'assistant', ?)", chatId, aiResp) + + c.JSON(200, gin.H{"response": aiResp}) +} diff --git a/main.go b/main.go index fdb5051..0819269 100644 --- a/main.go +++ b/main.go @@ -31,6 +31,11 @@ func main() { r.POST("/admin/test-ai", handlers.TestAIHandler) r.DELETE("/admin/appointment/:id", handlers.DeleteAppointmentHandler) r.POST("/admin/appointment", handlers.CreateAppointmentHandler) + r.PUT("/admin/appointment/:id", handlers.UpdateAppointmentHandler) + + r.POST("/admin/chat", handlers.NewChatHandler) // THE BUTTON HITS THIS + r.GET("/admin/chat/:id/messages", handlers.GetMessagesHandler) + r.POST("/admin/chat/:id/message", handlers.PostMessageHandler) // A little something for the root so you don't get a 404 again, seki r.GET("/", func(c *gin.Context) { diff --git a/services/openrouter.go b/services/openrouter.go index 32b1651..a6aa78f 100644 --- a/services/openrouter.go +++ b/services/openrouter.go @@ -1,30 +1,51 @@ package services import ( - "bytes" - "encoding/json" - "io" - "net/http" - "os" + bytes "bytes" + json "encoding/json" + io "io" + http "net/http" + os "os" + "whatsapp-bot/db" // Ensure this matches your module name ) -// The structs to map OpenRouter's response +type Message struct { + Role string `json:"role"` + Content string `json:"content"` +} + type OpenRouterResponse struct { Choices []struct { Message struct { - Content string `json:"content"` + Content string `json:"content"` + ToolCalls []struct { + ID string `json:"id"` + Type string `json:"type"` + Function struct { + Name string `json:"name"` + Arguments string `json:"arguments"` + } `json:"function"` + } `json:"tool_calls"` } `json:"message"` } `json:"choices"` } -func GetAIResponse(input string) (string, error) { +func GetAIResponse(chatHistory []Message) (string, error) { apiKey := os.Getenv("OPENROUTER_API_KEY") - payload := map[string]interface{}{ - "model": "google/gemini-2.0-flash-001", - "messages": []map[string]string{ - {"role": "system", "content": "You are a Chilean business assistant. Be brief."}, - {"role": "user", "content": input}, + url := "https://openrouter.ai/api/v1/chat/completions" + + // 1. Build the full message list with a System Prompt + fullMessages := append([]Message{ + { + Role: "system", + Content: "You are a helpful Chilean business assistant. You can book appointments. If a user wants to schedule something, use the create_appointment tool. Always be concise and polite.", }, + }, chatHistory...) + + // 2. Define the tools the AI can use + payload := map[string]interface{}{ + "model": "stepfun/step-3.5-flash:free", + "messages": fullMessages, "tools": []map[string]interface{}{ { "type": "function", @@ -34,8 +55,8 @@ func GetAIResponse(input string) (string, error) { "parameters": map[string]interface{}{ "type": "object", "properties": map[string]interface{}{ - "customer_phone": map[string]string{"type": "string"}, - "date": map[string]string{"type": "string", "description": "ISO format date and time"}, + "customer_phone": map[string]string{"type": "string", "description": "The user's phone number"}, + "date": map[string]string{"type": "string", "description": "The date and time in YYYY-MM-DD HH:MM format"}, }, "required": []string{"customer_phone", "date"}, }, @@ -44,8 +65,8 @@ func GetAIResponse(input string) (string, error) { }, } - data, _ := json.Marshal(payload) - req, _ := http.NewRequest("POST", "https://openrouter.ai/api/v1/chat/completions", bytes.NewBuffer(data)) + jsonData, _ := json.Marshal(payload) + req, _ := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) req.Header.Set("Authorization", "Bearer "+apiKey) req.Header.Set("Content-Type", "application/json") @@ -56,10 +77,8 @@ func GetAIResponse(input string) (string, error) { defer resp.Body.Close() bodyBytes, _ := io.ReadAll(resp.Body) - - // If it's not a 200, return the error body so you can see why it failed if resp.StatusCode != http.StatusOK { - return "API Error: " + string(bodyBytes), nil + return "Error from OpenRouter: " + string(bodyBytes), nil } var result OpenRouterResponse @@ -68,8 +87,33 @@ func GetAIResponse(input string) (string, error) { } if len(result.Choices) > 0 { - return result.Choices[0].Message.Content, nil + aiMsg := result.Choices[0].Message + + // 3. Handle Tool Calls (The "Logic" part) + if len(aiMsg.ToolCalls) > 0 { + tc := aiMsg.ToolCalls[0].Function + if tc.Name == "create_appointment" { + var args struct { + Phone string `json:"customer_phone"` + Date string `json:"date"` + } + // Unmarshal the AI-generated arguments + json.Unmarshal([]byte(tc.Arguments), &args) + + // Save to DB using your helper + err := db.SaveAppointment(args.Phone, args.Date) + if err != nil { + return "I tried to book it, but the database hates me: " + err.Error(), nil + } + return "✅ [SYSTEM] Appointment automatically booked for " + args.Phone + " at " + args.Date, nil + } + } + + // 4. Return plain text if no tool was called + if aiMsg.Content != "" { + return aiMsg.Content, nil + } } - return "No response from AI.", nil + return "The AI is giving me the silent treatment, seki.", nil } diff --git a/templates/dashboard.html b/templates/dashboard.html index 14abb2e..598c037 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -1,198 +1,99 @@ {{ define "dashboard.html" }} - + - - - SekiBot Admin | Discord Edition + SekiBot | Conversations - -
-

Welcome back, seki

- - -
-

Test OpenRouter AI

-
- - -
-
+ +
+
+

Select a chat to begin.

+
+ +
+
- -
-

Quick Create Appointment

+ +