236 lines
5.2 KiB
Go
236 lines
5.2 KiB
Go
package llmapi
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"golias/config"
|
|
"golias/models"
|
|
"golias/pkg/cache"
|
|
"io"
|
|
"log/slog"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
)
|
|
|
|
// TODO: config for url and token
|
|
// completion prompt
|
|
// MIME: llm needs to know all the cards, colors and previous actions
|
|
// GUESSER: llm needs to know all the cards and previous actions
|
|
|
|
// channels are not serializable
|
|
|
|
var (
|
|
// botname -> channel
|
|
SignalChanMap = make(map[string]chan bool)
|
|
DoneChanMap = make(map[string]chan bool)
|
|
)
|
|
|
|
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
|
|
// channels for communicaton
|
|
// channels are not serializable
|
|
// SignalsCh chan bool
|
|
// DoneCh chan bool
|
|
}
|
|
|
|
// StartBot
|
|
func (b *Bot) StartBot() {
|
|
for {
|
|
select {
|
|
case <-SignalChanMap[b.BotName]:
|
|
b.log.Debug("got signal", "bot-team", b.Team, "bot-role", b.Role)
|
|
// get room cards and actions
|
|
room, err := getRoomByID(b.RoomID)
|
|
if err != nil {
|
|
b.log.Error("bot loop", "error", err)
|
|
return
|
|
}
|
|
// form prompt
|
|
prompt := b.BuildPrompt(room)
|
|
b.log.Debug("got prompt", "prompt", prompt)
|
|
// call llm
|
|
if err := b.CallLLM(prompt); err != nil {
|
|
b.log.Error("bot loop", "error", err)
|
|
return
|
|
}
|
|
// parse response
|
|
// if mime -> give clue
|
|
// if guesser -> open card (does opening one card prompting new loop?)
|
|
// send notification to sse broker
|
|
case <-DoneChanMap[b.BotName]:
|
|
b.log.Debug("got done signal", "bot-name", b.BotName)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// EndBot
|
|
|
|
func NewBot(role, team, name, roomID string, cfg *config.Config) (*Bot, error) {
|
|
bot := &Bot{
|
|
Role: role,
|
|
RoomID: roomID,
|
|
BotName: name,
|
|
Team: team,
|
|
cfg: cfg,
|
|
log: slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{
|
|
Level: slog.LevelDebug,
|
|
AddSource: true,
|
|
})),
|
|
}
|
|
// add to room
|
|
room, err := getRoomByID(bot.RoomID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// check if not running
|
|
if role == "mime" && room.IsRunning {
|
|
return nil, errors.New("cannot join after game started")
|
|
}
|
|
room.PlayerList = append(room.PlayerList, name)
|
|
bp := models.BotPlayer{
|
|
Role: models.StrToUserRole(role),
|
|
Team: models.StrToUserTeam(team),
|
|
}
|
|
room.BotMap[name] = bp
|
|
switch team {
|
|
case "red":
|
|
if role == "mime" {
|
|
room.RedTeam.Mime = name
|
|
} else if role == "guesser" {
|
|
room.RedTeam.Guessers = append(room.RedTeam.Guessers, name)
|
|
} else {
|
|
return nil, fmt.Errorf("uknown role: %s", role)
|
|
}
|
|
case "blue":
|
|
if role == "mime" {
|
|
room.BlueTeam.Mime = name
|
|
} else if role == "guesser" {
|
|
room.BlueTeam.Guessers = append(room.BlueTeam.Guessers, name)
|
|
} else {
|
|
return nil, fmt.Errorf("uknown role: %s", role)
|
|
}
|
|
default:
|
|
return nil, fmt.Errorf("uknown team: %s", team)
|
|
}
|
|
if err := saveRoom(room); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := saveBot(bot); err != nil {
|
|
return nil, err
|
|
}
|
|
SignalChanMap[bot.BotName] = make(chan bool)
|
|
DoneChanMap[bot.BotName] = make(chan bool)
|
|
go bot.StartBot() // run bot routine
|
|
return bot, nil
|
|
}
|
|
|
|
func saveBot(bot *Bot) error {
|
|
key := "botkey_" + bot.RoomID + bot.BotName
|
|
data, err := json.Marshal(bot)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cache.MemCache.Set(key, data)
|
|
return nil
|
|
}
|
|
|
|
func getRoomByID(roomID string) (*models.Room, error) {
|
|
roomBytes, err := cache.MemCache.Get(models.CacheRoomPrefix + roomID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resp := &models.Room{}
|
|
if err := json.Unmarshal(roomBytes, &resp); err != nil {
|
|
return nil, err
|
|
}
|
|
return resp, nil
|
|
}
|
|
|
|
func saveRoom(room *models.Room) error {
|
|
key := models.CacheRoomPrefix + room.ID
|
|
data, err := json.Marshal(room)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cache.MemCache.Set(key, data)
|
|
return nil
|
|
}
|
|
|
|
func (b *Bot) BuildPrompt(room *models.Room) string {
|
|
if b.Role == "" {
|
|
return ""
|
|
}
|
|
toText := make(map[string]any)
|
|
toText["backlog"] = room.ActionHistory
|
|
// mime sees all colors;
|
|
// guesser sees only revealed ones
|
|
if b.Role == models.UserRoleMime {
|
|
toText["cards"] = room.Cards
|
|
}
|
|
if b.Role == models.UserRoleGuesser {
|
|
copiedCards := []models.WordCard{}
|
|
copy(copiedCards, room.Cards)
|
|
for i, card := range copiedCards {
|
|
if !card.Revealed {
|
|
copiedCards[i].Color = models.WordColorUknown
|
|
}
|
|
}
|
|
toText["cards"] = copiedCards
|
|
}
|
|
data, err := json.MarshalIndent(toText, "", " ")
|
|
if err != nil {
|
|
// log
|
|
return ""
|
|
}
|
|
return string(data)
|
|
}
|
|
|
|
func (b *Bot) CallLLM(prompt string) error {
|
|
method := "POST"
|
|
payload := strings.NewReader(fmt.Sprintf(`{
|
|
"model": "deepseek-chat",
|
|
"prompt": "%s",
|
|
"echo": false,
|
|
"frequency_penalty": 0,
|
|
"logprobs": 0,
|
|
"max_tokens": 1024,
|
|
"presence_penalty": 0,
|
|
"stop": null,
|
|
"stream": false,
|
|
"stream_options": null,
|
|
"suffix": null,
|
|
"temperature": 1,
|
|
"top_p": 1
|
|
}`, prompt))
|
|
client := &http.Client{}
|
|
req, err := http.NewRequest(method, b.cfg.LLMConfig.URL, payload)
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
return err
|
|
}
|
|
req.Header.Add("Content-Type", "application/json")
|
|
req.Header.Add("Accept", "application/json")
|
|
req.Header.Add("Authorization", "Bearer "+b.cfg.LLMConfig.TOKEN)
|
|
res, err := client.Do(req)
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
return err
|
|
}
|
|
defer res.Body.Close()
|
|
body, err := io.ReadAll(res.Body)
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
return err
|
|
}
|
|
fmt.Println(string(body))
|
|
return nil
|
|
}
|