194 lines
3.7 KiB
Go
194 lines
3.7 KiB
Go
package app
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/stig/goaichat/internal/chat"
|
|
"github.com/stig/goaichat/internal/config"
|
|
"github.com/stig/goaichat/internal/openai"
|
|
)
|
|
|
|
// App encapsulates the Goaichat application runtime wiring.
|
|
type App struct {
|
|
logger *slog.Logger
|
|
config *config.Config
|
|
openAI *openai.Client
|
|
chat *chat.Service
|
|
input io.Reader
|
|
output io.Writer
|
|
}
|
|
|
|
// New constructs a new App instance.
|
|
func New(logger *slog.Logger, cfg *config.Config, opts ...Option) *App {
|
|
app := &App{
|
|
logger: logger,
|
|
config: cfg,
|
|
input: os.Stdin,
|
|
output: os.Stdout,
|
|
}
|
|
|
|
for _, opt := range opts {
|
|
opt(app)
|
|
}
|
|
|
|
if app.input == nil {
|
|
app.input = os.Stdin
|
|
}
|
|
if app.output == nil {
|
|
app.output = os.Stdout
|
|
}
|
|
|
|
return app
|
|
}
|
|
|
|
// Option configures an App instance.
|
|
type Option func(*App)
|
|
|
|
// WithIO overrides the input/output streams, primarily for testing.
|
|
func WithIO(in io.Reader, out io.Writer) Option {
|
|
return func(a *App) {
|
|
a.input = in
|
|
a.output = out
|
|
}
|
|
}
|
|
|
|
// Run starts the application lifecycle.
|
|
func (a *App) Run(ctx context.Context) error {
|
|
if a == nil {
|
|
return errors.New("app is nil")
|
|
}
|
|
if ctx == nil {
|
|
return errors.New("context is nil")
|
|
}
|
|
|
|
if err := a.initOpenAIClient(); err != nil {
|
|
return err
|
|
}
|
|
if err := a.initChatService(); err != nil {
|
|
return err
|
|
}
|
|
|
|
a.logger.InfoContext(ctx, "starting goaichat", "model", a.config.Model.Name, "api_url", a.config.API.URL)
|
|
|
|
if err := a.runCLILoop(ctx); err != nil {
|
|
return err
|
|
}
|
|
|
|
a.logger.InfoContext(ctx, "goaichat shutdown complete")
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) initOpenAIClient() error {
|
|
if a.openAI != nil {
|
|
return nil
|
|
}
|
|
|
|
client, err := openai.NewClient(
|
|
a.config.API.Key,
|
|
openai.WithBaseURL(a.config.API.URL),
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
a.openAI = client
|
|
return nil
|
|
}
|
|
|
|
func (a *App) initChatService() error {
|
|
if a.chat != nil {
|
|
return nil
|
|
}
|
|
|
|
service, err := chat.NewService(a.logger.With("component", "chat"), a.config.Model, a.openAI)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
a.chat = service
|
|
return nil
|
|
}
|
|
|
|
func (a *App) runCLILoop(ctx context.Context) error {
|
|
scanner := bufio.NewScanner(a.input)
|
|
|
|
if _, err := fmt.Fprintln(a.output, "Type your message. Use /exit to quit, /reset to clear history."); err != nil {
|
|
return err
|
|
}
|
|
|
|
for {
|
|
if _, err := fmt.Fprint(a.output, "> "); err != nil {
|
|
return err
|
|
}
|
|
|
|
if !scanner.Scan() {
|
|
if err := scanner.Err(); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
input := scanner.Text()
|
|
|
|
handled, exit, err := a.handleCommand(ctx, input)
|
|
if err != nil {
|
|
if _, writeErr := fmt.Fprintf(a.output, "Command error: %v\n", err); writeErr != nil {
|
|
return writeErr
|
|
}
|
|
continue
|
|
}
|
|
if handled {
|
|
if exit {
|
|
return nil
|
|
}
|
|
continue
|
|
}
|
|
|
|
reply, err := a.chat.Send(ctx, input)
|
|
if err != nil {
|
|
if _, writeErr := fmt.Fprintf(a.output, "Error: %v\n", err); writeErr != nil {
|
|
return writeErr
|
|
}
|
|
continue
|
|
}
|
|
|
|
if _, err := fmt.Fprintf(a.output, "AI: %s\n", reply); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
func (a *App) handleCommand(ctx context.Context, input string) (handled bool, exit bool, err error) {
|
|
_ = ctx
|
|
trimmed := strings.TrimSpace(input)
|
|
if trimmed == "" {
|
|
return true, false, errors.New("no input provided")
|
|
}
|
|
if !strings.HasPrefix(trimmed, "/") {
|
|
return false, false, nil
|
|
}
|
|
|
|
switch trimmed {
|
|
case "/exit":
|
|
return true, true, nil
|
|
case "/reset":
|
|
a.chat.Reset()
|
|
_, err := fmt.Fprintln(a.output, "History cleared.")
|
|
return true, false, err
|
|
case "/help":
|
|
_, err := fmt.Fprintln(a.output, "Commands: /exit, /reset, /help (more coming soon)")
|
|
return true, false, err
|
|
default:
|
|
_, err := fmt.Fprintf(a.output, "Unknown command %q. Try /help.\n", trimmed)
|
|
return true, false, err
|
|
}
|
|
}
|