From 60d62773b85b49e84aabc3613ff63442f6d71dd0 Mon Sep 17 00:00:00 2001 From: Grail Finder Date: Thu, 12 Jun 2025 11:21:39 +0300 Subject: [PATCH] Fix: cookie; add llm parser interface --- handlers/auth.go | 11 +++--- handlers/game.go | 1 + llmapi/main.go | 75 +++++++++++++++++++---------------------- llmapi/parser.go | 88 ++++++++++++++++++++++++++++++++++++++++++++++++ models/main.go | 15 +++++++-- todos.md | 3 ++ 6 files changed, 144 insertions(+), 49 deletions(-) create mode 100644 llmapi/parser.go diff --git a/handlers/auth.go b/handlers/auth.go index 3ad4eb9..00fb6dd 100644 --- a/handlers/auth.go +++ b/handlers/auth.go @@ -142,19 +142,18 @@ func makeCookie(username string, remote string) (*http.Cookie, error) { cookieValue := base64.URLEncoding.EncodeToString([]byte( string(signature) + sessionToken)) cookie := &http.Cookie{ - Name: cookieName, - Value: cookieValue, - // Secure: true, + Name: cookieName, + Value: cookieValue, + Secure: true, HttpOnly: true, SameSite: http.SameSiteNoneMode, - // Domain: cfg.ServerConfig.Host, } log.Info("check remote addr for cookie set", "remote", remote, "session", session) if strings.Contains(remote, "192.168.0") { - // cookie.Domain = "192.168.0.101" - cookie.Domain = "" + cookie.Domain = "192.168.0.106" cookie.SameSite = http.SameSiteLaxMode + cookie.Secure = false log.Info("changing cookie domain", "domain", cookie.Domain) } // set ctx? diff --git a/handlers/game.go b/handlers/game.go index 5aa4f18..ae8262e 100644 --- a/handlers/game.go +++ b/handlers/game.go @@ -254,6 +254,7 @@ func HandleGiveClue(w http.ResponseWriter, r *http.Request) { fi.Room.ActionHistory = append(fi.Room.ActionHistory, action) fi.Room.MimeDone = true notify(models.NotifyBacklogPrefix+fi.Room.ID, clue+num) + notifyBotIfNeeded(fi) if err := saveFullInfo(fi); err != nil { abortWithError(w, err.Error()) return diff --git a/llmapi/main.go b/llmapi/main.go index b777f9a..508927b 100644 --- a/llmapi/main.go +++ b/llmapi/main.go @@ -21,7 +21,7 @@ var ( DoneChanMap = make(map[string]chan bool) // got prompt: control character (\\u0000-\\u001F) found while parsing a string at line 4 column 0 MimePrompt = `we are playing alias;\nyou are a mime (player who gives a clue of one noun word and number of cards you expect them to open) of the %s team (people who would guess by your clue want open the %s cards);\nplease return your clue, number of cards to open and what words you mean them to find using that clue in json like:\n{\n\"clue\": \"one-word-noun\",\n\"number\": \"number-from-0-to-9\",\n\"words_I_mean_my_team_to_open\": [\"this\", \"that\", ...]\n}\nthe team who openes all their cards first wins.\nplease return json only.\nunopen Blue cards left: %d;\nunopen Red cards left: %d;\nhere is the game info in json:\n%s` - GuesserPrompt = `we are playing alias;\nyou are to guess words of the %s team (people who would guess by your clue want open the %s cards) by given clue and a number of meant guesses;\nplease return your guesses and words that you did not wish to open, but think that they could be also meant by the clue in json like:\n{\n\"guesses\": [\"word1\", \"word2\", ...],\n\"could_be\": [\"this\", \"that\", ...]\n}\nthe team who openes all their cards first wins.\nplease return json only.\nunopen Blue cards left: %d;\nunopen Red cards left: %d;\nhere is the game info in json:\n%s` + GuesserPrompt = `we are playing alias;\nyou are to guess words of the %s team (you want open %s cards) by given clue and a number of meant guesses;\nplease return your guesses and words that could be meant by the clue, but you do not wish to open yet, in json like:\n{\n\"guesses\": [\"word1\", \"word2\", ...],\n\"could_be\": [\"this\", \"that\", ...]\n}\nthe team who openes all their cards first wins.\nplease return json only.\nunopen Blue cards left: %d;\nunopen Red cards left: %d;\nhere is the game info in json:\n%s` ) type DSResp struct { @@ -37,6 +37,20 @@ type DSResp struct { Object string `json:"object"` } +type LLMResp struct { + Choices []struct { + FinishReason string `json:"finish_reason"` + Index int `json:"index"` + Message struct { + Content string `json:"content"` + Role string `json:"role"` + } `json:"message"` + } `json:"choices"` + Created int `json:"created"` + Model string `json:"model"` + Object string `json:"object"` +} + type MimeResp struct { Clue string `json:"clue"` Number string `json:"number"` @@ -49,12 +63,13 @@ type GusserResp struct { } type Bot struct { - Role string // gueeser | mime - Team string - cfg *config.Config - RoomID string // can we get a room from here? - BotName string - log *slog.Logger + Role string // gueeser | mime + Team string + cfg *config.Config + RoomID string // can we get a room from here? + BotName string + log *slog.Logger + LLMParser RespParser // channels for communicaton // channels are not serializable // SignalsCh chan bool @@ -82,43 +97,16 @@ func (b *Bot) StartBot() { b.log.Error("bot loop", "error", err) return } - dsResp := DSResp{} - if err := json.Unmarshal(llmResp, &dsResp); err != nil { - b.log.Error("failed to unmarshall", "error", err) - return + tempMap, err := b.LLMParser.ParseBytes(llmResp) + if err != nil { + b.log.Error("bot loop", "error", err) + continue } - if len(dsResp.Choices) == 0 { - b.log.Error("empty choices", "dsResp", dsResp) - return - } - text := dsResp.Choices[0].Text - li := strings.Index(text, "{") - ri := strings.LastIndex(text, "}") - if li < 0 || ri < 1 { - b.log.Error("not a json", "msg", text) - return - } - sj := text[li : ri+1] - // jb, err := json.Marshal(sj) - // if err != nil { - // b.log.Error("failed to marshal", "error", err, "string-json", sj) - // return - // } - // parse response - // if mime -> give clue - // if guesser -> open card (does opening one card prompting new loop?) - // send notification to sse broker eventName := models.NotifyBacklogPrefix + room.ID eventPayload := "" - tempMap := make(map[string]any) switch b.Role { case models.UserRoleMime: - // respMap := make(map[string]any) mimeResp := MimeResp{} - if err := json.Unmarshal([]byte(sj), &tempMap); err != nil { - b.log.Error("failed to unmarshal mime resp", "error", err, "string-json", sj) - return - } b.log.Info("mime resp log", "mimeResp", tempMap) mimeResp.Clue = tempMap["clue"].(string) mimeResp.Number = tempMap["number"].(string) @@ -186,7 +174,7 @@ func (b *Bot) StartBot() { eventName = models.NotifyRoomUpdatePrefix + room.ID eventPayload = "" default: - b.log.Error("unexpected role", "role", b.Role, "llmResp", sj) + b.log.Error("unexpected role", "role", b.Role, "resp-map", tempMap) return } // save room @@ -223,6 +211,11 @@ func NewBot(role, team, name, roomID string, cfg *config.Config) (*Bot, error) { AddSource: true, })), } + // there might be a better way + bot.LLMParser = NewLCPRespParser(bot.log) + if strings.Contains(cfg.LLMConfig.URL, "api.deepseek.com") { + bot.LLMParser = NewDeepSeekParser(bot.log) + } // add to room room, err := getRoomByID(bot.RoomID) if err != nil { @@ -321,7 +314,7 @@ func (b *Bot) BuildPrompt(room *models.Room) string { toText["cards"] = room.Cards } if b.Role == models.UserRoleGuesser { - copiedCards := []models.WordCard{} + copiedCards := make([]models.WordCard, len(room.Cards)) copy(copiedCards, room.Cards) for i, card := range copiedCards { if !card.Revealed { @@ -383,6 +376,6 @@ func (b *Bot) CallLLM(prompt string) ([]byte, error) { b.log.Error("failed to read resp body", "error", err, "url", b.cfg.LLMConfig.URL) return nil, err } - b.log.Debug("llm resp", "body", string(body)) + b.log.Debug("llm resp", "body", string(body), "url", b.cfg.LLMConfig.URL) return body, nil } diff --git a/llmapi/parser.go b/llmapi/parser.go new file mode 100644 index 0000000..72dbe52 --- /dev/null +++ b/llmapi/parser.go @@ -0,0 +1,88 @@ +package llmapi + +import ( + "encoding/json" + "fmt" + "log/slog" + "strings" +) + +type RespParser interface { + ParseBytes(body []byte) (map[string]any, error) +} + +// DeepSeekParser: deepseek implementation of RespParser +type deepSeekParser struct { + log *slog.Logger +} + +func NewDeepSeekParser(log *slog.Logger) *deepSeekParser { + return &deepSeekParser{log: log} +} + +func (p *deepSeekParser) ParseBytes(body []byte) (map[string]any, error) { + // parsing logic here + dsResp := DSResp{} + if err := json.Unmarshal(body, &dsResp); err != nil { + p.log.Error("failed to unmarshall", "error", err) + return nil, err + } + if len(dsResp.Choices) == 0 { + p.log.Error("empty choices", "dsResp", dsResp) + err := fmt.Errorf("empty choices in dsResp") + return nil, err + } + text := dsResp.Choices[0].Text + li := strings.Index(text, "{") + ri := strings.LastIndex(text, "}") + if li < 0 || ri < 1 { + p.log.Error("not a json", "msg", text) + err := fmt.Errorf("fn: ParseBytes, not a json") + return nil, err + } + sj := text[li : ri+1] + respMap := make(map[string]any) + if err := json.Unmarshal([]byte(sj), &respMap); err != nil { + p.log.Error("failed to unmarshal response", "error", err, "string-json", sj) + return nil, err + } + return respMap, nil +} + +// llama.cpp implementation of RespParser +type lcpRespParser struct { + log *slog.Logger +} + +func NewLCPRespParser(log *slog.Logger) *lcpRespParser { + return &lcpRespParser{log: log} +} + +func (p *lcpRespParser) ParseBytes(body []byte) (map[string]any, error) { + // parsing logic here + resp := LLMResp{} + if err := json.Unmarshal(body, &resp); err != nil { + p.log.Error("failed to unmarshal", "error", err) + return nil, err + } + if len(resp.Choices) == 0 { + p.log.Error("empty choices", "resp", resp) + err := fmt.Errorf("empty choices in resp") + return nil, err + } + text := resp.Choices[0].Message.Content + li := strings.Index(text, "{") + ri := strings.LastIndex(text, "}") + if li < 0 || ri < 1 { + p.log.Error("not a json", "msg", text) + err := fmt.Errorf("fn: ParseBytes, not a json") + return nil, err + } + sj := text[li : ri+1] + respMap := make(map[string]any) + if err := json.Unmarshal([]byte(sj), &respMap); err != nil { + p.log.Error("failed to unmarshal response", "error", err, "string-json", sj) + return nil, err + } + return respMap, nil +} diff --git a/models/main.go b/models/main.go index 759e3d2..ca0767a 100644 --- a/models/main.go +++ b/models/main.go @@ -103,6 +103,15 @@ func (r *Room) CanStart() error { return nil } +func getGuesser(m map[string]BotPlayer, team UserTeam) string { + for k, v := range m { + if v.Team == team && v.Role == UserRoleGuesser { + return k + } + } + return "" +} + // WhichBotToMove returns bot name that have to move or empty string func (r *Room) WhichBotToMove() string { fmt.Println("looking for bot to move", "team-turn", r.TeamTurn, "mime-done", r.MimeDone, "bot-map", r.BotMap, "is_running", r.IsRunning) @@ -116,16 +125,18 @@ func (r *Room) WhichBotToMove() string { if ok { return r.BlueTeam.Mime } + } else { + return getGuesser(r.BotMap, UserTeamBlue) } - // check gussers case UserTeamRed: if !r.MimeDone { _, ok := r.BotMap[r.RedTeam.Mime] if ok { return r.RedTeam.Mime } + } else { + return getGuesser(r.BotMap, UserTeamRed) } - // check gussers default: // how did we got here? return "" diff --git a/todos.md b/todos.md index 5d7bef1..333a014 100644 --- a/todos.md +++ b/todos.md @@ -9,6 +9,9 @@ - there two places for bot to check if its its move: start-game; end-turn; - remove bot button (if game is not running); - show in backlog (and with that in prompt to llm) how many cards are left to open, also additional comment: if guess was right; +- if bot already added; remove add bot button; +- hide clue input for mime when it's not their turn; +- needs resend to llm btn; #### sse points - clue sse update;