Refactor: building binary with no extra

This commit is contained in:
Grail Finder
2026-01-03 09:59:33 +03:00
parent 6b875a2782
commit aeb2700d14
14 changed files with 142 additions and 213 deletions

View File

@@ -1,4 +1,4 @@
.PHONY: setconfig run lint setup-whisper build-whisper download-whisper-model docker-up docker-down docker-logs .PHONY: setconfig run lint setup-whisper build-whisper download-whisper-model docker-up docker-down docker-logs noextra-run noextra-server
run: setconfig run: setconfig
go build -o gf-lt && ./gf-lt go build -o gf-lt && ./gf-lt
@@ -6,6 +6,12 @@ run: setconfig
server: setconfig server: setconfig
go build -o gf-lt && ./gf-lt -port 3333 go build -o gf-lt && ./gf-lt -port 3333
noextra-run: setconfig
go build -tags '!extra' -o gf-lt && ./gf-lt
noextra-server: setconfig
go build -tags '!extra' -o gf-lt && ./gf-lt -port 3333
setconfig: setconfig:
find config.toml &>/dev/null || cp config.example.toml config.toml find config.toml &>/dev/null || cp config.example.toml config.toml

62
bot.go
View File

@@ -7,7 +7,6 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"gf-lt/config" "gf-lt/config"
"gf-lt/extra"
"gf-lt/models" "gf-lt/models"
"gf-lt/rag" "gf-lt/rag"
"gf-lt/storage" "gf-lt/storage"
@@ -29,12 +28,10 @@ import (
) )
var ( var (
httpClient = &http.Client{} httpClient = &http.Client{}
cluedoState *extra.CluedoRoundInfo // Current game state cfg *config.Config
playerOrder []string // Turn order tracking logger *slog.Logger
cfg *config.Config logLevel = new(slog.LevelVar)
logger *slog.Logger
logLevel = new(slog.LevelVar)
) )
var ( var (
activeChatName string activeChatName string
@@ -51,8 +48,8 @@ var (
chunkParser ChunkParser chunkParser ChunkParser
lastToolCall *models.FuncCall lastToolCall *models.FuncCall
//nolint:unused // TTS_ENABLED conditionally uses this //nolint:unused // TTS_ENABLED conditionally uses this
orator extra.Orator orator Orator
asr extra.STT asr STT
localModelsMu sync.RWMutex localModelsMu sync.RWMutex
defaultLCPProps = map[string]float32{ defaultLCPProps = map[string]float32{
"temperature": 0.8, "temperature": 0.8,
@@ -600,32 +597,6 @@ func roleToIcon(role string) string {
return "<" + role + ">: " return "<" + role + ">: "
} }
// FIXME: it should not be here; move to extra
func checkGame(role string, tv *tview.TextView) {
// Handle Cluedo game flow
// should go before form msg, since formmsg takes chatBody and makes ioreader out of it
// role is almost always user, unless it's regen or resume
// cannot get in this block, since cluedoState is nil;
if cfg.EnableCluedo {
// Initialize Cluedo game if needed
if cluedoState == nil {
playerOrder = []string{cfg.UserRole, cfg.AssistantRole, cfg.CluedoRole2}
cluedoState = extra.CluedoPrepCards(playerOrder)
}
// notifyUser("got in cluedo", "yay")
currentPlayer := playerOrder[0]
playerOrder = append(playerOrder[1:], currentPlayer) // Rotate turns
if role == cfg.UserRole {
fmt.Fprintf(tv, "Your (%s) cards: %s\n", currentPlayer, cluedoState.GetPlayerCards(currentPlayer))
} else {
chatBody.Messages = append(chatBody.Messages, models.RoleMsg{
Role: cfg.ToolRole,
Content: cluedoState.GetPlayerCards(currentPlayer),
})
}
}
}
func chatRound(userMsg, role string, tv *tview.TextView, regen, resume bool) { func chatRound(userMsg, role string, tv *tview.TextView, regen, resume bool) {
botRespMode = true botRespMode = true
botPersona := cfg.AssistantRole botPersona := cfg.AssistantRole
@@ -643,9 +614,6 @@ func chatRound(userMsg, role string, tv *tview.TextView, regen, resume bool) {
return return
} }
} }
if !resume {
checkGame(role, tv)
}
choseChunkParser() choseChunkParser()
reader, err := chunkParser.FormMsg(userMsg, role, resume) reader, err := chunkParser.FormMsg(userMsg, role, resume)
if reader == nil || err != nil { if reader == nil || err != nil {
@@ -679,7 +647,7 @@ out:
} }
// Send chunk to audio stream handler // Send chunk to audio stream handler
if cfg.TTS_ENABLED { if cfg.TTS_ENABLED {
extra.TTSTextChan <- chunk TTSTextChan <- chunk
} }
case toolChunk := <-openAIToolChan: case toolChunk := <-openAIToolChan:
fmt.Fprint(tv, toolChunk) fmt.Fprint(tv, toolChunk)
@@ -698,7 +666,7 @@ out:
} }
// Send chunk to audio stream handler // Send chunk to audio stream handler
if cfg.TTS_ENABLED { if cfg.TTS_ENABLED {
extra.TTSTextChan <- chunk TTSTextChan <- chunk
} }
} }
break out break out
@@ -982,11 +950,6 @@ func addNewChat(chatName string) {
func applyCharCard(cc *models.CharCard) { func applyCharCard(cc *models.CharCard) {
cfg.AssistantRole = cc.Role cfg.AssistantRole = cc.Role
// FIXME: remove // FIXME: remove
// Initialize Cluedo if enabled and matching role
if cfg.EnableCluedo && cc.Role == "CluedoPlayer" {
playerOrder = []string{cfg.UserRole, cfg.AssistantRole, cfg.CluedoRole2}
cluedoState = extra.CluedoPrepCards(playerOrder)
}
history, err := loadAgentsLastChat(cfg.AssistantRole) history, err := loadAgentsLastChat(cfg.AssistantRole)
if err != nil { if err != nil {
// too much action for err != nil; loadAgentsLastChat needs to be split up // too much action for err != nil; loadAgentsLastChat needs to be split up
@@ -1123,18 +1086,13 @@ func init() {
Stream: true, Stream: true,
Messages: lastChat, Messages: lastChat,
} }
// Initialize Cluedo if enabled and matching role
if cfg.EnableCluedo && cfg.AssistantRole == "CluedoPlayer" {
playerOrder = []string{cfg.UserRole, cfg.AssistantRole, cfg.CluedoRole2}
cluedoState = extra.CluedoPrepCards(playerOrder)
}
choseChunkParser() choseChunkParser()
httpClient = createClient(time.Second * 90) httpClient = createClient(time.Second * 90)
if cfg.TTS_ENABLED { if cfg.TTS_ENABLED {
orator = extra.NewOrator(logger, cfg) orator = NewOrator(logger, cfg)
} }
if cfg.STT_ENABLED { if cfg.STT_ENABLED {
asr = extra.NewSTT(logger, cfg) asr = NewSTT(logger, cfg)
} }
// Initialize scrollToEndEnabled based on config // Initialize scrollToEndEnabled based on config
scrollToEndEnabled = cfg.AutoScrollEnabled scrollToEndEnabled = cfg.AutoScrollEnabled

28
extra.go Normal file
View File

@@ -0,0 +1,28 @@
//go:build extra
// +build extra
package main
import (
"gf-lt/config"
"gf-lt/extra"
"log/slog"
)
// Interfaces and implementations when extra modules are included
type Orator = extra.Orator
type STT = extra.STT
func NewOrator(logger *slog.Logger, cfg *config.Config) Orator {
return extra.NewOrator(logger, cfg)
}
func NewSTT(logger *slog.Logger, cfg *config.Config) STT {
return extra.NewSTT(logger, cfg)
}
// TTS channels from extra package
var TTSTextChan = extra.TTSTextChan
var TTSFlushChan = extra.TTSFlushChan
var TTSDoneChan = extra.TTSDoneChan

View File

@@ -1,73 +0,0 @@
package extra
import (
"math/rand"
"strings"
)
var (
rooms = []string{"HALL", "LOUNGE", "DINING ROOM", "KITCHEN", "BALLROOM", "CONSERVATORY", "BILLIARD ROOM", "LIBRARY", "STUDY"}
weapons = []string{"CANDLESTICK", "DAGGER", "LEAD PIPE", "REVOLVER", "ROPE", "SPANNER"}
people = []string{"Miss Scarlett", "Colonel Mustard", "Mrs. White", "Reverend Green", "Mrs. Peacock", "Professor Plum"}
)
type MurderTrifecta struct {
Murderer string
Weapon string
Room string
}
type CluedoRoundInfo struct {
Answer MurderTrifecta
PlayersCards map[string][]string
}
func (c *CluedoRoundInfo) GetPlayerCards(player string) string {
// maybe format it a little
return "cards of " + player + "are " + strings.Join(c.PlayersCards[player], ",")
}
func CluedoPrepCards(playerOrder []string) *CluedoRoundInfo {
res := &CluedoRoundInfo{}
// Select murder components
trifecta := MurderTrifecta{
Murderer: people[rand.Intn(len(people))],
Weapon: weapons[rand.Intn(len(weapons))],
Room: rooms[rand.Intn(len(rooms))],
}
// Collect non-murder cards
var notInvolved []string
for _, room := range rooms {
if room != trifecta.Room {
notInvolved = append(notInvolved, room)
}
}
for _, weapon := range weapons {
if weapon != trifecta.Weapon {
notInvolved = append(notInvolved, weapon)
}
}
for _, person := range people {
if person != trifecta.Murderer {
notInvolved = append(notInvolved, person)
}
}
// Shuffle and distribute cards
rand.Shuffle(len(notInvolved), func(i, j int) {
notInvolved[i], notInvolved[j] = notInvolved[j], notInvolved[i]
})
players := map[string][]string{}
cardsPerPlayer := len(notInvolved) / len(playerOrder)
// playerOrder := []string{"{{user}}", "{{char}}", "{{char2}}"}
for i, player := range playerOrder {
start := i * cardsPerPlayer
end := (i + 1) * cardsPerPlayer
if end > len(notInvolved) {
end = len(notInvolved)
}
players[player] = notInvolved[start:end]
}
res.Answer = trifecta
res.PlayersCards = players
return res
}

View File

@@ -1,50 +0,0 @@
package extra
import (
"testing"
)
func TestPrepCards(t *testing.T) {
// Run the function to get the murder combination and player cards
roundInfo := CluedoPrepCards([]string{"{{user}}", "{{char}}", "{{char2}}"})
// Create a map to track all distributed cards
distributedCards := make(map[string]bool)
// Check that the murder combination cards are not distributed to players
murderCards := []string{roundInfo.Answer.Murderer, roundInfo.Answer.Weapon, roundInfo.Answer.Room}
for _, card := range murderCards {
if distributedCards[card] {
t.Errorf("Murder card %s was distributed to a player", card)
}
}
// Check each player's cards
for player, cards := range roundInfo.PlayersCards {
for _, card := range cards {
// Ensure the card is not part of the murder combination
for _, murderCard := range murderCards {
if card == murderCard {
t.Errorf("Player %s has a murder card: %s", player, card)
}
}
// Ensure the card is unique and not already distributed
if distributedCards[card] {
t.Errorf("Card %s is duplicated in player %s's hand", card, player)
}
distributedCards[card] = true
}
}
// Verify that all non-murder cards are distributed
allCards := append(append([]string{}, rooms...), weapons...)
allCards = append(allCards, people...)
for _, card := range allCards {
isMurderCard := false
for _, murderCard := range murderCards {
if card == murderCard {
isMurderCard = true
break
}
}
if !isMurderCard && !distributedCards[card] {
t.Errorf("Card %s was not distributed to any player", card)
}
}
}

View File

@@ -1,3 +1,6 @@
//go:build extra
// +build extra
package extra package extra
import ( import (

View File

@@ -1,3 +1,6 @@
//go:build extra
// +build extra
package extra package extra
import ( import (

View File

@@ -1,11 +0,0 @@
package extra
import "math/rand"
var (
chars = []string{"Shrek", "Garfield", "Jack the Ripper"}
)
func GetRandomChar() string {
return chars[rand.Intn(len(chars))]
}

View File

@@ -1 +0,0 @@
package extra

View File

@@ -1,13 +0,0 @@
package extra
import "github.com/GrailFinder/searchagent/searcher"
var WebSearcher searcher.WebSurfer
func init() {
sa, err := searcher.NewWebSurfer(searcher.SearcherTypeScraper, "")
if err != nil {
panic("failed to init seachagent; error: " + err.Error())
}
WebSearcher = sa
}

View File

@@ -1,3 +1,6 @@
//go:build extra
// +build extra
package extra package extra
import ( import (

73
noextra.go Normal file
View File

@@ -0,0 +1,73 @@
//go:build !extra
// +build !extra
package main
import (
"gf-lt/config"
"log/slog"
)
// Interfaces and implementations when extra modules are not included
type Orator interface {
Speak(text string) error
Stop()
GetLogger() *slog.Logger
}
type STT interface {
StartRecording() error
StopRecording() (string, error)
IsRecording() bool
}
// DefaultOrator is a no-op implementation when TTS is not available
type DefaultOrator struct {
logger *slog.Logger
}
func NewOrator(logger *slog.Logger, cfg *config.Config) Orator {
return &DefaultOrator{logger: logger}
}
func (d *DefaultOrator) Speak(text string) error {
d.logger.Debug("TTS not available - extra modules disabled")
return nil
}
func (d *DefaultOrator) Stop() {
// No-op
}
func (d *DefaultOrator) GetLogger() *slog.Logger {
return d.logger
}
// DefaultSTT is a no-op implementation when STT is not available
type DefaultSTT struct {
logger *slog.Logger
}
func NewSTT(logger *slog.Logger, cfg *config.Config) STT {
return &DefaultSTT{logger: logger}
}
func (d *DefaultSTT) StartRecording() error {
d.logger.Debug("STT not available - extra modules disabled")
return nil
}
func (d *DefaultSTT) StopRecording() (string, error) {
d.logger.Debug("STT not available - extra modules disabled")
return "", nil
}
func (d *DefaultSTT) IsRecording() bool {
return false
}
// TTS channels - no-op when extra is not available
var TTSTextChan = make(chan string, 10000)
var TTSFlushChan = make(chan bool, 1)
var TTSDoneChan = make(chan bool, 1)

View File

@@ -5,7 +5,6 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"gf-lt/agent" "gf-lt/agent"
"gf-lt/extra"
"gf-lt/models" "gf-lt/models"
"io" "io"
"os" "os"
@@ -15,6 +14,8 @@ import (
"strings" "strings"
"sync" "sync"
"time" "time"
"github.com/GrailFinder/searchagent/searcher"
) )
var ( var (
@@ -141,13 +142,6 @@ After that you are free to respond to the user.
Role: "", Role: "",
FilePath: "", FilePath: "",
} }
// toolCard = &models.CharCard{
// SysPrompt: toolSysMsg,
// FirstMsg: defaultFirstMsg,
// Role: "",
// FilePath: "",
// }
// sysMap = map[string]string{"basic_sys": basicSysMsg, "tool_sys": toolSysMsg}
sysMap = map[string]*models.CharCard{"basic_sys": basicCard} sysMap = map[string]*models.CharCard{"basic_sys": basicCard}
sysLabels = []string{"basic_sys"} sysLabels = []string{"basic_sys"}
@@ -156,6 +150,16 @@ After that you are free to respond to the user.
webAgentsOnce sync.Once webAgentsOnce sync.Once
) )
var WebSearcher searcher.WebSurfer
func init() {
sa, err := searcher.NewWebSurfer(searcher.SearcherTypeScraper, "")
if err != nil {
panic("failed to init seachagent; error: " + err.Error())
}
WebSearcher = sa
}
// getWebAgentClient returns a singleton AgentClient for web agents. // getWebAgentClient returns a singleton AgentClient for web agents.
func getWebAgentClient() *agent.AgentClient { func getWebAgentClient() *agent.AgentClient {
webAgentClientOnce.Do(func() { webAgentClientOnce.Do(func() {
@@ -208,7 +212,7 @@ func websearch(args map[string]string) []byte {
"limit_arg", limitS, "error", err) "limit_arg", limitS, "error", err)
limit = 3 limit = 3
} }
resp, err := extra.WebSearcher.Search(context.Background(), query, limit) resp, err := WebSearcher.Search(context.Background(), query, limit)
if err != nil { if err != nil {
msg := "search tool failed; error: " + err.Error() msg := "search tool failed; error: " + err.Error()
logger.Error(msg) logger.Error(msg)
@@ -232,7 +236,7 @@ func readURL(args map[string]string) []byte {
logger.Error(msg) logger.Error(msg)
return []byte(msg) return []byte(msg)
} }
resp, err := extra.WebSearcher.RetrieveFromLink(context.Background(), link) resp, err := WebSearcher.RetrieveFromLink(context.Background(), link)
if err != nil { if err != nil {
msg := "search tool failed; error: " + err.Error() msg := "search tool failed; error: " + err.Error()
logger.Error(msg) logger.Error(msg)

3
tui.go
View File

@@ -2,7 +2,6 @@ package main
import ( import (
"fmt" "fmt"
"gf-lt/extra"
"gf-lt/models" "gf-lt/models"
"image" "image"
_ "image/jpeg" _ "image/jpeg"
@@ -1147,7 +1146,7 @@ func init() {
// textArea.SetText("pressed ctrl+A", true) // textArea.SetText("pressed ctrl+A", true)
if cfg.TTS_ENABLED { if cfg.TTS_ENABLED {
// audioStream.TextChan <- chunk // audioStream.TextChan <- chunk
extra.TTSDoneChan <- true TTSDoneChan <- true
} }
} }
if event.Key() == tcell.KeyCtrlW { if event.Key() == tcell.KeyCtrlW {