Refactor: building binary with no extra
This commit is contained in:
8
Makefile
8
Makefile
@@ -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
|
||||
go build -o gf-lt && ./gf-lt
|
||||
@@ -6,6 +6,12 @@ run: setconfig
|
||||
server: setconfig
|
||||
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:
|
||||
find config.toml &>/dev/null || cp config.example.toml config.toml
|
||||
|
||||
|
||||
54
bot.go
54
bot.go
@@ -7,7 +7,6 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"gf-lt/config"
|
||||
"gf-lt/extra"
|
||||
"gf-lt/models"
|
||||
"gf-lt/rag"
|
||||
"gf-lt/storage"
|
||||
@@ -30,8 +29,6 @@ import (
|
||||
|
||||
var (
|
||||
httpClient = &http.Client{}
|
||||
cluedoState *extra.CluedoRoundInfo // Current game state
|
||||
playerOrder []string // Turn order tracking
|
||||
cfg *config.Config
|
||||
logger *slog.Logger
|
||||
logLevel = new(slog.LevelVar)
|
||||
@@ -51,8 +48,8 @@ var (
|
||||
chunkParser ChunkParser
|
||||
lastToolCall *models.FuncCall
|
||||
//nolint:unused // TTS_ENABLED conditionally uses this
|
||||
orator extra.Orator
|
||||
asr extra.STT
|
||||
orator Orator
|
||||
asr STT
|
||||
localModelsMu sync.RWMutex
|
||||
defaultLCPProps = map[string]float32{
|
||||
"temperature": 0.8,
|
||||
@@ -600,32 +597,6 @@ func roleToIcon(role string) string {
|
||||
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) {
|
||||
botRespMode = true
|
||||
botPersona := cfg.AssistantRole
|
||||
@@ -643,9 +614,6 @@ func chatRound(userMsg, role string, tv *tview.TextView, regen, resume bool) {
|
||||
return
|
||||
}
|
||||
}
|
||||
if !resume {
|
||||
checkGame(role, tv)
|
||||
}
|
||||
choseChunkParser()
|
||||
reader, err := chunkParser.FormMsg(userMsg, role, resume)
|
||||
if reader == nil || err != nil {
|
||||
@@ -679,7 +647,7 @@ out:
|
||||
}
|
||||
// Send chunk to audio stream handler
|
||||
if cfg.TTS_ENABLED {
|
||||
extra.TTSTextChan <- chunk
|
||||
TTSTextChan <- chunk
|
||||
}
|
||||
case toolChunk := <-openAIToolChan:
|
||||
fmt.Fprint(tv, toolChunk)
|
||||
@@ -698,7 +666,7 @@ out:
|
||||
}
|
||||
// Send chunk to audio stream handler
|
||||
if cfg.TTS_ENABLED {
|
||||
extra.TTSTextChan <- chunk
|
||||
TTSTextChan <- chunk
|
||||
}
|
||||
}
|
||||
break out
|
||||
@@ -982,11 +950,6 @@ func addNewChat(chatName string) {
|
||||
func applyCharCard(cc *models.CharCard) {
|
||||
cfg.AssistantRole = cc.Role
|
||||
// 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)
|
||||
if err != nil {
|
||||
// too much action for err != nil; loadAgentsLastChat needs to be split up
|
||||
@@ -1123,18 +1086,13 @@ func init() {
|
||||
Stream: true,
|
||||
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()
|
||||
httpClient = createClient(time.Second * 90)
|
||||
if cfg.TTS_ENABLED {
|
||||
orator = extra.NewOrator(logger, cfg)
|
||||
orator = NewOrator(logger, cfg)
|
||||
}
|
||||
if cfg.STT_ENABLED {
|
||||
asr = extra.NewSTT(logger, cfg)
|
||||
asr = NewSTT(logger, cfg)
|
||||
}
|
||||
// Initialize scrollToEndEnabled based on config
|
||||
scrollToEndEnabled = cfg.AutoScrollEnabled
|
||||
|
||||
28
extra.go
Normal file
28
extra.go
Normal 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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
//go:build extra
|
||||
// +build extra
|
||||
|
||||
package extra
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
//go:build extra
|
||||
// +build extra
|
||||
|
||||
package extra
|
||||
|
||||
import (
|
||||
|
||||
@@ -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))]
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
package extra
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
//go:build extra
|
||||
// +build extra
|
||||
|
||||
package extra
|
||||
|
||||
import (
|
||||
|
||||
73
noextra.go
Normal file
73
noextra.go
Normal 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)
|
||||
24
tools.go
24
tools.go
@@ -5,7 +5,6 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"gf-lt/agent"
|
||||
"gf-lt/extra"
|
||||
"gf-lt/models"
|
||||
"io"
|
||||
"os"
|
||||
@@ -15,6 +14,8 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/GrailFinder/searchagent/searcher"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -141,13 +142,6 @@ After that you are free to respond to the user.
|
||||
Role: "",
|
||||
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}
|
||||
sysLabels = []string{"basic_sys"}
|
||||
|
||||
@@ -156,6 +150,16 @@ After that you are free to respond to the user.
|
||||
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.
|
||||
func getWebAgentClient() *agent.AgentClient {
|
||||
webAgentClientOnce.Do(func() {
|
||||
@@ -208,7 +212,7 @@ func websearch(args map[string]string) []byte {
|
||||
"limit_arg", limitS, "error", err)
|
||||
limit = 3
|
||||
}
|
||||
resp, err := extra.WebSearcher.Search(context.Background(), query, limit)
|
||||
resp, err := WebSearcher.Search(context.Background(), query, limit)
|
||||
if err != nil {
|
||||
msg := "search tool failed; error: " + err.Error()
|
||||
logger.Error(msg)
|
||||
@@ -232,7 +236,7 @@ func readURL(args map[string]string) []byte {
|
||||
logger.Error(msg)
|
||||
return []byte(msg)
|
||||
}
|
||||
resp, err := extra.WebSearcher.RetrieveFromLink(context.Background(), link)
|
||||
resp, err := WebSearcher.RetrieveFromLink(context.Background(), link)
|
||||
if err != nil {
|
||||
msg := "search tool failed; error: " + err.Error()
|
||||
logger.Error(msg)
|
||||
|
||||
3
tui.go
3
tui.go
@@ -2,7 +2,6 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"gf-lt/extra"
|
||||
"gf-lt/models"
|
||||
"image"
|
||||
_ "image/jpeg"
|
||||
@@ -1147,7 +1146,7 @@ func init() {
|
||||
// textArea.SetText("pressed ctrl+A", true)
|
||||
if cfg.TTS_ENABLED {
|
||||
// audioStream.TextChan <- chunk
|
||||
extra.TTSDoneChan <- true
|
||||
TTSDoneChan <- true
|
||||
}
|
||||
}
|
||||
if event.Key() == tcell.KeyCtrlW {
|
||||
|
||||
Reference in New Issue
Block a user