Files
goaichat/internal/chat/service.go

112 lines
2.8 KiB
Go

package chat
import (
"context"
"errors"
"log/slog"
"strings"
"github.com/stig/goaichat/internal/config"
"github.com/stig/goaichat/internal/openai"
)
// CompletionClient defines the subset of the OpenAI client used by the chat service.
type CompletionClient interface {
CreateChatCompletion(ctx context.Context, req openai.ChatCompletionRequest) (*openai.ChatCompletionResponse, error)
}
// Service coordinates chat requests with the OpenAI client and maintains session history.
type Service struct {
logger *slog.Logger
client CompletionClient
model string
temperature float64
stream bool
history []openai.ChatMessage
}
// NewService constructs a Service from configuration and an OpenAI-compatible client.
func NewService(logger *slog.Logger, modelCfg config.ModelConfig, client CompletionClient) (*Service, error) {
if logger == nil {
return nil, errors.New("logger cannot be nil")
}
if client == nil {
return nil, errors.New("completion client cannot be nil")
}
if strings.TrimSpace(modelCfg.Name) == "" {
return nil, errors.New("model name cannot be empty")
}
return &Service{
logger: logger,
client: client,
model: modelCfg.Name,
temperature: modelCfg.Temperature,
stream: modelCfg.Stream,
history: make([]openai.ChatMessage, 0, 16),
}, nil
}
// Send submits a user message and returns the assistant reply.
func (s *Service) Send(ctx context.Context, input string) (string, error) {
if s == nil {
return "", errors.New("service is nil")
}
if ctx == nil {
return "", errors.New("context is nil")
}
content := strings.TrimSpace(input)
if content == "" {
return "", errors.New("input cannot be empty")
}
userMsg := openai.ChatMessage{Role: "user", Content: content}
s.history = append(s.history, userMsg)
messages := append([]openai.ChatMessage(nil), s.history...)
temperature := s.temperature
req := openai.ChatCompletionRequest{
Model: s.model,
Messages: messages,
Stream: s.stream,
Temperature: &temperature,
}
s.logger.DebugContext(ctx, "sending chat completion", "model", s.model, "message_count", len(messages))
resp, err := s.client.CreateChatCompletion(ctx, req)
if err != nil {
return "", err
}
if len(resp.Choices) == 0 {
return "", errors.New("no choices returned from completion")
}
reply := resp.Choices[0].Message
s.history = append(s.history, reply)
return reply.Content, nil
}
// History returns a copy of the current conversation history.
func (s *Service) History() []openai.ChatMessage {
if s == nil {
return nil
}
historyCopy := make([]openai.ChatMessage, len(s.history))
copy(historyCopy, s.history)
return historyCopy
}
// Reset clears the in-memory conversation history.
func (s *Service) Reset() {
if s == nil {
return
}
s.history = s.history[:0]
}