Added llm client and ui
This commit is contained in:
@@ -25,3 +25,4 @@ go.work.sum
|
||||
# env file
|
||||
.env
|
||||
|
||||
.config.json
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"log/slog"
|
||||
"os"
|
||||
)
|
||||
|
||||
var ENV_PREFIX = "SOUV__"
|
||||
|
||||
type Config struct {
|
||||
Api ApiConfig
|
||||
Llm LLMConfig
|
||||
Logging []LogConfig
|
||||
}
|
||||
|
||||
type ApiConfig struct {
|
||||
Url string
|
||||
Key string
|
||||
}
|
||||
|
||||
type LLMConfig struct {
|
||||
Model string
|
||||
}
|
||||
|
||||
type LogConfig struct {
|
||||
Level slog.Level
|
||||
Format string
|
||||
Target string
|
||||
}
|
||||
|
||||
func Load(path string) (*Config, error) {
|
||||
cfg := defaultConfig()
|
||||
if path != "" {
|
||||
if data, err := os.ReadFile(path); err == nil {
|
||||
if err := json.Unmarshal(data, cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
overrideFromEnv(cfg)
|
||||
configLogger(*cfg)
|
||||
return cfg, cfg.validate()
|
||||
}
|
||||
|
||||
func configLogger(cfg Config) {
|
||||
if len(cfg.Logging) < 1 {
|
||||
opts := &slog.HandlerOptions{Level: slog.LevelInfo.Level()}
|
||||
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stdout, opts)))
|
||||
return
|
||||
}
|
||||
var handlers []slog.Handler
|
||||
for _, c := range cfg.Logging {
|
||||
opts := &slog.HandlerOptions{Level: c.Level}
|
||||
target, err := getLogTarget(c.Target)
|
||||
if err != nil {
|
||||
slog.Error("Config error. Could not open target", "target", c.Target)
|
||||
}
|
||||
switch c.Format {
|
||||
case "text":
|
||||
handlers = append(handlers, slog.NewTextHandler(target, opts))
|
||||
case "json":
|
||||
handlers = append(handlers, slog.NewJSONHandler(target, opts))
|
||||
}
|
||||
}
|
||||
slog.SetDefault(slog.New(slog.NewMultiHandler(handlers...)))
|
||||
}
|
||||
|
||||
func getLogTarget(target string) (io.Writer, error) {
|
||||
switch target {
|
||||
case "", "stdout":
|
||||
return os.Stdout, nil
|
||||
case "stderr":
|
||||
return os.Stderr, nil
|
||||
case "discrd":
|
||||
return io.Discard, nil
|
||||
default:
|
||||
return os.OpenFile(target, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
|
||||
}
|
||||
}
|
||||
|
||||
func defaultConfig() *Config {
|
||||
return &Config{
|
||||
ApiConfig{
|
||||
Url: "",
|
||||
Key: ""},
|
||||
LLMConfig{Model: ""},
|
||||
[]LogConfig{{Level: slog.LevelInfo.Level(), Format: "text"}},
|
||||
}
|
||||
}
|
||||
|
||||
func overrideFromEnv(cfg *Config) {
|
||||
val := os.Getenv(ENV_PREFIX + "api__url")
|
||||
if val != "" {
|
||||
cfg.Api.Url = val
|
||||
}
|
||||
val = os.Getenv(ENV_PREFIX + "api__key")
|
||||
if val != "" {
|
||||
cfg.Api.Key = val
|
||||
}
|
||||
val = os.Getenv(ENV_PREFIX + "llm__model")
|
||||
if val != "" {
|
||||
cfg.Llm.Model = val
|
||||
}
|
||||
}
|
||||
|
||||
func (cfg Config) validate() error {
|
||||
if cfg.Api.Key == "" {
|
||||
return errors.New("No API key configured")
|
||||
}
|
||||
if cfg.Api.Url == "" {
|
||||
return errors.New("No API url configured")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package config
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
type Tool struct {
|
||||
Type string `json:"type"`
|
||||
Function FunctionDefinition `json:"function"`
|
||||
}
|
||||
|
||||
type FunctionDefinition struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Parameters json.RawMessage `json:"parameters,omitempty"`
|
||||
}
|
||||
|
||||
func AvailableTools() []Tool {
|
||||
return []Tool{}
|
||||
}
|
||||
@@ -1,3 +1,28 @@
|
||||
module git.estatecloud.org/radumaco/souvenir
|
||||
|
||||
go 1.26.3
|
||||
|
||||
require (
|
||||
charm.land/bubbles/v2 v2.1.0
|
||||
charm.land/bubbletea/v2 v2.0.7
|
||||
charm.land/lipgloss/v2 v2.0.3
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/atotto/clipboard v0.1.4 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.4.3 // indirect
|
||||
github.com/charmbracelet/ultraviolet v0.0.0-20260525132238-948f4557a654 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.11.7 // indirect
|
||||
github.com/charmbracelet/x/term v0.2.2 // indirect
|
||||
github.com/charmbracelet/x/termios v0.1.1 // indirect
|
||||
github.com/charmbracelet/x/windows v0.2.2 // indirect
|
||||
github.com/clipperhouse/displaywidth v0.11.0 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.4.0 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.23 // indirect
|
||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.45.0 // indirect
|
||||
)
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
charm.land/bubbles/v2 v2.1.0 h1:YSnNh5cPYlYjPxRrzs5VEn3vwhtEn3jVGRBT3M7/I0g=
|
||||
charm.land/bubbles/v2 v2.1.0/go.mod h1:l97h4hym2hvWBVfmJDtrEHHCtkIKeTEb3TTJ4ZOB3wY=
|
||||
charm.land/bubbletea/v2 v2.0.7 h1:7qw2tTAVar7m7klOPBYfTB0mniv/RuexsYwMRNxSeL0=
|
||||
charm.land/bubbletea/v2 v2.0.7/go.mod h1:DGW2q8gvzHnOpMpZTORs0aySVHCox5C+2Svk0fci1qs=
|
||||
charm.land/lipgloss/v2 v2.0.3 h1:yM2zJ4Cf5Y51b7RHIwioil4ApI/aypFXXVHSwlM6RzU=
|
||||
charm.land/lipgloss/v2 v2.0.3/go.mod h1:7myLU9iG/3xluAWzpY/fSxYYHCgoKTie7laxk6ATwXA=
|
||||
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
|
||||
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
|
||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||
github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o=
|
||||
github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w=
|
||||
github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q=
|
||||
github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q=
|
||||
github.com/charmbracelet/ultraviolet v0.0.0-20260525132238-948f4557a654 h1:FpSYhY28ucg9ZRr+2wj67FAQ0Ey5yiK0072PmRDJNek=
|
||||
github.com/charmbracelet/ultraviolet v0.0.0-20260525132238-948f4557a654/go.mod h1:hFpumms29Smx3LStRfku8vcCTBe1Kq8aCXtHUJa3mjY=
|
||||
github.com/charmbracelet/x/ansi v0.11.7 h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dAYC84jI=
|
||||
github.com/charmbracelet/x/ansi v0.11.7/go.mod h1:9qGpnAVYz+8ACONkZBUWPtL7lulP9No6p1epAihUZwQ=
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA=
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I=
|
||||
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
||||
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
||||
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
|
||||
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
|
||||
github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM=
|
||||
github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k=
|
||||
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
|
||||
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
||||
github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4=
|
||||
github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw=
|
||||
github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
|
||||
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
@@ -0,0 +1,26 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"os"
|
||||
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"git.estatecloud.org/radumaco/souvenir/config"
|
||||
ui "git.estatecloud.org/radumaco/souvenir/ui"
|
||||
)
|
||||
|
||||
|
||||
func main() {
|
||||
config, err := config.Load(".config.json")
|
||||
if err != nil {
|
||||
slog.Error("Config error:", "message", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
var logger = slog.Default().With("Compoent", "Main")
|
||||
logger.Debug("Config initialized")
|
||||
p := tea.NewProgram(ui.InitialModel(*config))
|
||||
if _, err := p.Run(); err != nil {
|
||||
logger.Error("Alas, there's been an error:", "message", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
package llm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
config "git.estatecloud.org/radumaco/souvenir/config"
|
||||
)
|
||||
|
||||
type LLMClient struct {
|
||||
Cfg config.Config
|
||||
}
|
||||
|
||||
// Request Types
|
||||
|
||||
type ChatRequest struct {
|
||||
Model string `json:"model"`
|
||||
Messages []Message `json:"messages"`
|
||||
Tools []config.Tool `json:"tools,omitempty"`
|
||||
}
|
||||
|
||||
type Message struct {
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content"`
|
||||
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
|
||||
}
|
||||
|
||||
// Response types //
|
||||
|
||||
type ChatResponse struct {
|
||||
ID string `json:"id"`
|
||||
Object string `json:"object"`
|
||||
Created int64 `json:"created"`
|
||||
Model string `json:"model"`
|
||||
Choices []Choice `json:"choices"`
|
||||
Usage Usage `json:"usage"`
|
||||
}
|
||||
|
||||
type Choice struct {
|
||||
Index int `json:"index"`
|
||||
Message Message `json:"message"`
|
||||
FinishReason string `json:"finish_reason"`
|
||||
}
|
||||
|
||||
type ToolCall struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Function ToolCallFunc `json:"function"`
|
||||
}
|
||||
|
||||
type ToolCallFunc struct {
|
||||
Name string `json:"name"`
|
||||
Arguments string `json:"arguments"`
|
||||
}
|
||||
|
||||
type Usage struct {
|
||||
PromptTokens int `json:"prompt_tokens"`
|
||||
CompletionTokens int `json:"completion_tokens"`
|
||||
TotalTokens int `json:"total_tokens"`
|
||||
}
|
||||
|
||||
func (client LLMClient) Call(query string, messages []Message) ([]Message, error) {
|
||||
var logger = slog.Default().With("Component", "LLM Client")
|
||||
requestBody := ChatRequest{
|
||||
Model: client.Cfg.Llm.Model,
|
||||
Messages: messages,
|
||||
Tools: config.AvailableTools(),
|
||||
}
|
||||
serialized, err := json.Marshal(requestBody)
|
||||
if err != nil {
|
||||
return []Message{}, err
|
||||
}
|
||||
|
||||
body := bytes.NewBuffer(serialized)
|
||||
logger.Debug("Executing llm call", "query", query, "messages", messages, "body", serialized)
|
||||
req, err := http.NewRequest("POST", client.Cfg.Api.Url, body)
|
||||
if err != nil {
|
||||
logger.Error("There was an error creating the request", "error", err.Error())
|
||||
return []Message{}, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+client.Cfg.Api.Key)
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
logger.Error("There was an error during http call", "error", err.Error())
|
||||
return []Message{}, err
|
||||
}
|
||||
logger.Debug("LLM Call returned", "response", resp)
|
||||
defer resp.Body.Close()
|
||||
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
logger.Error("There was an error reading the response body", "error", err.Error())
|
||||
return []Message{}, err
|
||||
}
|
||||
if resp.StatusCode != 200 && resp.StatusCode != 202 {
|
||||
logger.Error("Call returned non 200 status",
|
||||
"statusCode", resp.StatusCode,
|
||||
"status", resp.Status)
|
||||
return []Message{},
|
||||
errors.New("Call returned invalid status " +
|
||||
strconv.Itoa(resp.StatusCode) +
|
||||
resp.Status)
|
||||
}
|
||||
|
||||
logger.Debug("Received data", "data", data)
|
||||
var response ChatResponse
|
||||
if err := json.Unmarshal(data, &response); err != nil {
|
||||
logger.Error("There was an error parsing the response", "error", err.Error())
|
||||
return []Message{}, err
|
||||
}
|
||||
|
||||
var result []Message
|
||||
if len(response.Choices) == 0 {
|
||||
logger.Error("The call returned an empty response")
|
||||
return []Message{}, errors.New("The call returned an empty response")
|
||||
}
|
||||
for _, choice := range response.Choices {
|
||||
logger.Error("Appending response message", "message", choice.Message)
|
||||
result = append(result, choice.Message)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
+175
@@ -0,0 +1,175 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strings"
|
||||
|
||||
"charm.land/bubbles/v2/cursor"
|
||||
"charm.land/bubbles/v2/textarea"
|
||||
"charm.land/bubbles/v2/viewport"
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"charm.land/lipgloss/v2"
|
||||
"git.estatecloud.org/radumaco/souvenir/config"
|
||||
llm "git.estatecloud.org/radumaco/souvenir/llm"
|
||||
)
|
||||
|
||||
var logger *slog.Logger
|
||||
|
||||
type model struct {
|
||||
viewport viewport.Model
|
||||
messages []llm.Message
|
||||
textarea textarea.Model
|
||||
senderStyle lipgloss.Style
|
||||
receiverStyle lipgloss.Style
|
||||
client llm.LLMClient
|
||||
waiting bool
|
||||
err error
|
||||
}
|
||||
|
||||
type agentResponseMessage struct {
|
||||
response []llm.Message
|
||||
err error
|
||||
}
|
||||
|
||||
func InitialModel(config config.Config) model {
|
||||
logger = slog.Default().With("Component", "UI")
|
||||
ta := textarea.New()
|
||||
ta.Placeholder = "Send a message..."
|
||||
ta.SetVirtualCursor(false)
|
||||
ta.Focus()
|
||||
|
||||
ta.Prompt = "⦀ "
|
||||
ta.CharLimit = 280
|
||||
|
||||
ta.SetWidth(30)
|
||||
ta.SetHeight(3)
|
||||
|
||||
s := ta.Styles()
|
||||
s.Focused.CursorLine = lipgloss.NewStyle()
|
||||
ta.SetStyles(s)
|
||||
|
||||
ta.ShowLineNumbers = false
|
||||
|
||||
vp := viewport.New(viewport.WithWidth(30), viewport.WithHeight(5))
|
||||
vp.SetContent(`What are we doing on today?`)
|
||||
vp.KeyMap.Left.SetEnabled(false)
|
||||
vp.KeyMap.Right.SetEnabled(false)
|
||||
|
||||
ta.KeyMap.InsertNewline.SetEnabled(false)
|
||||
|
||||
return model{
|
||||
textarea: ta,
|
||||
messages: []llm.Message{},
|
||||
viewport: vp,
|
||||
senderStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("5")),
|
||||
client: llm.LLMClient{Cfg: config},
|
||||
err: nil,
|
||||
}
|
||||
}
|
||||
|
||||
func (m model) Init() tea.Cmd {
|
||||
return textarea.Blink
|
||||
}
|
||||
|
||||
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.WindowSizeMsg:
|
||||
logger.Debug("Received WindowSizeMsg", "width", msg.Width, "height", msg.Height)
|
||||
m.viewport.SetWidth(msg.Width)
|
||||
m.textarea.SetWidth(msg.Width)
|
||||
m.viewport.SetHeight(msg.Height - m.textarea.Height())
|
||||
|
||||
var messages = getMessages(m.messages)
|
||||
|
||||
if len(m.messages) > 0 {
|
||||
m.viewport.SetContent(lipgloss.NewStyle().Width(m.viewport.Width()).Render(messages))
|
||||
}
|
||||
m.viewport.GotoBottom()
|
||||
|
||||
case tea.KeyPressMsg:
|
||||
logger.Debug("Received KeyPressMsg")
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "esc":
|
||||
logger.Debug("Received exit sequence. Quiting")
|
||||
fmt.Println(m.textarea.Value())
|
||||
return m, tea.Quit
|
||||
case "enter":
|
||||
input := m.textarea.Value()
|
||||
m.messages = append(m.messages, llm.Message{
|
||||
Content: m.senderStyle.Render(input),
|
||||
Role: "You",
|
||||
})
|
||||
logger.Debug("Received enter. Creating new message", "message", input)
|
||||
var messages = getMessages(m.messages)
|
||||
m.viewport.SetContent(lipgloss.NewStyle().Width(m.viewport.Width()).Render(messages))
|
||||
m.textarea.Reset()
|
||||
m.viewport.GotoBottom()
|
||||
m.waiting = true
|
||||
return m, m.callAgent(input, m.messages)
|
||||
default:
|
||||
var cmd tea.Cmd
|
||||
m.textarea, cmd = m.textarea.Update(msg)
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
case cursor.BlinkMsg:
|
||||
logger.Debug("Received BlinkMsg")
|
||||
var cmd tea.Cmd
|
||||
m.textarea, cmd = m.textarea.Update(msg)
|
||||
return m, cmd
|
||||
|
||||
case agentResponseMessage:
|
||||
logger.Debug("Received agentResponseMessage")
|
||||
m.waiting = false
|
||||
resp := msg.response
|
||||
if msg.err != nil {
|
||||
logger.Error("Message is error", "error", msg.err)
|
||||
resp = []llm.Message{{
|
||||
Role: "Error",
|
||||
Content: msg.err.Error(),
|
||||
}}
|
||||
}
|
||||
|
||||
for _, r := range resp {
|
||||
logger.Debug("Appending response message", "message", r)
|
||||
m.messages = append(m.messages, r)
|
||||
}
|
||||
var messages = getMessages(m.messages)
|
||||
m.viewport.SetContent(lipgloss.NewStyle().Width(m.viewport.Width()).Render(messages))
|
||||
return m, nil
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m model) View() tea.View {
|
||||
viewportView := m.viewport.View()
|
||||
v := tea.NewView(viewportView + "\n" + m.textarea.View())
|
||||
c := m.textarea.Cursor()
|
||||
if c != nil {
|
||||
c.Y += lipgloss.Height(viewportView)
|
||||
}
|
||||
v.Cursor = c
|
||||
v.AltScreen = true
|
||||
return v
|
||||
}
|
||||
|
||||
func (m model) callAgent(input string, messages []llm.Message) tea.Cmd {
|
||||
logger.Debug("Calling agent", "query", input, "messages", messages)
|
||||
return func() tea.Msg {
|
||||
resp, err := m.client.Call(input, messages)
|
||||
return agentResponseMessage{response: resp, err: err}
|
||||
}
|
||||
}
|
||||
|
||||
func getMessages(m []llm.Message) string {
|
||||
var messages strings.Builder
|
||||
for _, m := range m {
|
||||
messages.WriteString(m.Role)
|
||||
messages.WriteString(": ")
|
||||
messages.WriteString(m.Content)
|
||||
messages.WriteString("\n")
|
||||
}
|
||||
return messages.String()
|
||||
}
|
||||
Reference in New Issue
Block a user