Compare commits
13 Commits
168e0cf5d3
...
a8b7bd7662
Author | SHA1 | Date | |
---|---|---|---|
a8b7bd7662 | |||
90d85e676d | |||
ec013236b8 | |||
6fde3f8932 | |||
6af9377cf5 | |||
cf0e98f656 | |||
e66016aedd | |||
9a1aae83da | |||
b0e4739f4f | |||
4e3976fc73 | |||
b87c3ffc53 | |||
b0a1299e0b | |||
ae424530f9 |
2
go.mod
2
go.mod
@ -1,6 +1,6 @@
|
|||||||
module git.mlow.ca/mlow/lmcli
|
module git.mlow.ca/mlow/lmcli
|
||||||
|
|
||||||
go 1.19
|
go 1.21
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/go-yaml/yaml v2.1.0+incompatible
|
github.com/go-yaml/yaml v2.1.0+incompatible
|
||||||
|
244
pkg/cli/cmd.go
244
pkg/cli/cmd.go
@ -2,7 +2,9 @@ package cli
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
@ -24,17 +26,21 @@ var lsCmd = &cobra.Command{
|
|||||||
Short: "List existing conversations",
|
Short: "List existing conversations",
|
||||||
Long: `List all existing conversations in descending order of recent activity.`,
|
Long: `List all existing conversations in descending order of recent activity.`,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
fmt.Println("Listing conversations...")
|
conversations, err := store.Conversations()
|
||||||
// Example output, asterisk to indicate current converation
|
if err != nil {
|
||||||
|
fmt.Println("Could not fetch conversations.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// $ lm ls
|
// Example output
|
||||||
|
// $ lmcli ls
|
||||||
// last hour:
|
// last hour:
|
||||||
// 98sg - 12 minutes ago - Project discussion
|
// 98sg - 12 minutes ago - Project discussion
|
||||||
// last day:
|
// last day:
|
||||||
// tj3l - 10 hours ago - Deep learning concepts
|
// tj3l - 10 hours ago - Deep learning concepts
|
||||||
// last week:
|
// last week:
|
||||||
// bwfm - 2 days ago - Machine learning study
|
// bwfm - 2 days ago - Machine learning study
|
||||||
// * 8n3h - 3 days ago - Weekend plans
|
// 8n3h - 3 days ago - Weekend plans
|
||||||
// f3n7 - 6 days ago - CLI development
|
// f3n7 - 6 days ago - CLI development
|
||||||
// last month:
|
// last month:
|
||||||
// 5hn2 - 8 days ago - Book club discussion
|
// 5hn2 - 8 days ago - Book club discussion
|
||||||
@ -45,15 +51,149 @@ var lsCmd = &cobra.Command{
|
|||||||
// g8d9 - 3 months ago - History book club
|
// g8d9 - 3 months ago - History book club
|
||||||
// 4lk3 - 4 months ago - Local events and meetups
|
// 4lk3 - 4 months ago - Local events and meetups
|
||||||
// 43jn - 6 months ago - Mobile photography techniques
|
// 43jn - 6 months ago - Mobile photography techniques
|
||||||
|
|
||||||
|
type ConversationLine struct {
|
||||||
|
timeSinceReply time.Duration
|
||||||
|
formatted string
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
categories := []string{
|
||||||
|
"recent",
|
||||||
|
"last hour",
|
||||||
|
"last 6 hours",
|
||||||
|
"last day",
|
||||||
|
"last week",
|
||||||
|
"last month",
|
||||||
|
"last 6 months",
|
||||||
|
"older",
|
||||||
|
}
|
||||||
|
categorized := map[string][]ConversationLine{}
|
||||||
|
|
||||||
|
for _, conversation := range conversations {
|
||||||
|
lastMessage, err := store.LastMessage(&conversation)
|
||||||
|
if lastMessage == nil || err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
messageAge := now.Sub(lastMessage.CreatedAt)
|
||||||
|
|
||||||
|
var category string
|
||||||
|
switch {
|
||||||
|
case messageAge <= 10*time.Minute:
|
||||||
|
category = "recent"
|
||||||
|
case messageAge <= time.Hour:
|
||||||
|
category = "last hour"
|
||||||
|
case messageAge <= 6*time.Hour:
|
||||||
|
category = "last 6 hours"
|
||||||
|
case messageAge <= 24*time.Hour:
|
||||||
|
category = "last day"
|
||||||
|
case messageAge <= 7*24*time.Hour:
|
||||||
|
category = "last week"
|
||||||
|
case messageAge <= 30*24*time.Hour:
|
||||||
|
category = "last month"
|
||||||
|
case messageAge <= 6*30*24*time.Hour: // Approximate as 6 months
|
||||||
|
category = "last 6 months"
|
||||||
|
default:
|
||||||
|
category = "older"
|
||||||
|
}
|
||||||
|
|
||||||
|
formatted := fmt.Sprintf(
|
||||||
|
"%s - %s - %s",
|
||||||
|
conversation.ShortName.String,
|
||||||
|
humanTimeElapsedSince(messageAge),
|
||||||
|
conversation.Title,
|
||||||
|
)
|
||||||
|
categorized[category] = append(
|
||||||
|
categorized[category],
|
||||||
|
ConversationLine{messageAge, formatted},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, category := range categories {
|
||||||
|
conversations, ok := categorized[category]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
slices.SortFunc(conversations, func(a, b ConversationLine) int {
|
||||||
|
return int(a.timeSinceReply - b.timeSinceReply)
|
||||||
|
})
|
||||||
|
fmt.Printf("%s:\n", category)
|
||||||
|
for _, conv := range conversations {
|
||||||
|
fmt.Printf(" %s\n", conv.formatted)
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
var viewCmd = &cobra.Command{
|
var rmCmd = &cobra.Command{
|
||||||
Use: "view",
|
Use: "rm [conversation]",
|
||||||
Short: "View messages in a conversation",
|
Short: "Remove a conversation",
|
||||||
Long: `Displays all the messages in a coversation.`,
|
Long: `Removes a conversation by its short name.`,
|
||||||
|
Args: func(cmd *cobra.Command, args []string) error {
|
||||||
|
argCount := 1
|
||||||
|
if err := cobra.MinimumNArgs(argCount)(cmd, args); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
fmt.Println("Displaying conversation messages...")
|
shortName := args[0]
|
||||||
|
conversation, err := store.ConversationByShortName(shortName)
|
||||||
|
if err != nil {
|
||||||
|
Fatal("Could not search for conversation: %v\n", err)
|
||||||
|
}
|
||||||
|
if conversation.ID == 0 {
|
||||||
|
Fatal("Conversation not found with short name: %s\n", shortName)
|
||||||
|
}
|
||||||
|
err = store.DeleteConversation(conversation)
|
||||||
|
if err != nil {
|
||||||
|
Fatal("Could not delete conversation: %v\n", err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
compMode := cobra.ShellCompDirectiveNoFileComp
|
||||||
|
if len(args) != 0 {
|
||||||
|
return nil, compMode
|
||||||
|
}
|
||||||
|
return store.ConversationShortNameCompletions(toComplete), compMode
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var showCmd = &cobra.Command{
|
||||||
|
Use: "show [conversation]",
|
||||||
|
Short: "Show messages in a conversation",
|
||||||
|
Long: `Finds a conversation by its short name and displays its contents.`,
|
||||||
|
Args: func(cmd *cobra.Command, args []string) error {
|
||||||
|
argCount := 1
|
||||||
|
if err := cobra.MinimumNArgs(argCount)(cmd, args); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
shortName := args[0]
|
||||||
|
conversation, err := store.ConversationByShortName(shortName)
|
||||||
|
if conversation.ID == 0 {
|
||||||
|
Fatal("Conversation not found with short name: %s\n", shortName)
|
||||||
|
}
|
||||||
|
|
||||||
|
messages, err := store.Messages(conversation)
|
||||||
|
if err != nil {
|
||||||
|
Fatal("Could not retrieve messages for conversation: %s\n", conversation.Title)
|
||||||
|
}
|
||||||
|
|
||||||
|
l := len(messages)
|
||||||
|
for i, message := range messages {
|
||||||
|
message.RenderTTY(i < l-1)
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
},
|
||||||
|
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
compMode := cobra.ShellCompDirectiveNoFileComp
|
||||||
|
if len(args) != 0 {
|
||||||
|
return nil, compMode
|
||||||
|
}
|
||||||
|
return store.ConversationShortNameCompletions(toComplete), compMode
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,8 +201,75 @@ var replyCmd = &cobra.Command{
|
|||||||
Use: "reply",
|
Use: "reply",
|
||||||
Short: "Send a reply to a conversation",
|
Short: "Send a reply to a conversation",
|
||||||
Long: `Sends a reply to conversation and writes the response to stdout.`,
|
Long: `Sends a reply to conversation and writes the response to stdout.`,
|
||||||
|
Args: func(cmd *cobra.Command, args []string) error {
|
||||||
|
argCount := 1
|
||||||
|
if err := cobra.MinimumNArgs(argCount)(cmd, args); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
fmt.Println("Replying to a conversation...")
|
shortName := args[0]
|
||||||
|
conversation, err := store.ConversationByShortName(shortName)
|
||||||
|
if conversation.ID == 0 {
|
||||||
|
Fatal("Conversation not found with short name: %s\n", shortName)
|
||||||
|
}
|
||||||
|
|
||||||
|
messages, err := store.Messages(conversation)
|
||||||
|
if err != nil {
|
||||||
|
Fatal("Could not retrieve messages for conversation: %s\n", conversation.Title)
|
||||||
|
}
|
||||||
|
|
||||||
|
messageContents, err := InputFromEditor("# How would you like to reply?\n", "reply.*.md")
|
||||||
|
|
||||||
|
userReply := Message{
|
||||||
|
ConversationID: conversation.ID,
|
||||||
|
Role: "user",
|
||||||
|
OriginalContent: messageContents,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = store.SaveMessage(&userReply)
|
||||||
|
if err != nil {
|
||||||
|
Warn("Could not save your reply: %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
messages = append(messages, userReply)
|
||||||
|
for _, message := range messages {
|
||||||
|
message.RenderTTY(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
assistantReply := Message{
|
||||||
|
ConversationID: conversation.ID,
|
||||||
|
Role: "assistant",
|
||||||
|
}
|
||||||
|
assistantReply.RenderTTY(false)
|
||||||
|
|
||||||
|
receiver := make(chan string)
|
||||||
|
response := make(chan string)
|
||||||
|
go func() {
|
||||||
|
response <- HandleDelayedResponse(receiver)
|
||||||
|
}()
|
||||||
|
|
||||||
|
err = CreateChatCompletionStream(config.OpenAI.DefaultModel, messages, MAX_TOKENS, receiver)
|
||||||
|
if err != nil {
|
||||||
|
Fatal("%v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assistantReply.OriginalContent = <-response
|
||||||
|
|
||||||
|
err = store.SaveMessage(&assistantReply)
|
||||||
|
if err != nil {
|
||||||
|
Fatal("Could not save assistant reply: %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
|
},
|
||||||
|
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
compMode := cobra.ShellCompDirectiveNoFileComp
|
||||||
|
if len(args) != 0 {
|
||||||
|
return nil, compMode
|
||||||
|
}
|
||||||
|
return store.ConversationShortNameCompletions(toComplete), compMode
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,7 +330,7 @@ var newCmd = &cobra.Command{
|
|||||||
response <- HandleDelayedResponse(receiver)
|
response <- HandleDelayedResponse(receiver)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
err = CreateChatCompletionStream(messages, MAX_TOKENS, receiver)
|
err = CreateChatCompletionStream(config.OpenAI.DefaultModel, messages, MAX_TOKENS, receiver)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Fatal("%v\n", err)
|
Fatal("%v\n", err)
|
||||||
}
|
}
|
||||||
@ -136,6 +343,15 @@ var newCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|
||||||
|
err = conversation.GenerateTitle()
|
||||||
|
if err != nil {
|
||||||
|
Warn("Could not generate title for conversation: %v\n", err)
|
||||||
|
}
|
||||||
|
err = store.SaveConversation(&conversation)
|
||||||
|
if err != nil {
|
||||||
|
Warn("Could not save conversation after generating title: %v\n", err)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -163,7 +379,7 @@ var promptCmd = &cobra.Command{
|
|||||||
|
|
||||||
receiver := make(chan string)
|
receiver := make(chan string)
|
||||||
go HandleDelayedResponse(receiver)
|
go HandleDelayedResponse(receiver)
|
||||||
err := CreateChatCompletionStream(messages, MAX_TOKENS, receiver)
|
err := CreateChatCompletionStream(config.OpenAI.DefaultModel, messages, MAX_TOKENS, receiver)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Fatal("%v\n", err)
|
Fatal("%v\n", err)
|
||||||
}
|
}
|
||||||
@ -174,8 +390,12 @@ var promptCmd = &cobra.Command{
|
|||||||
|
|
||||||
func NewRootCmd() *cobra.Command {
|
func NewRootCmd() *cobra.Command {
|
||||||
rootCmd.AddCommand(
|
rootCmd.AddCommand(
|
||||||
|
lsCmd,
|
||||||
newCmd,
|
newCmd,
|
||||||
promptCmd,
|
promptCmd,
|
||||||
|
replyCmd,
|
||||||
|
rmCmd,
|
||||||
|
showCmd,
|
||||||
)
|
)
|
||||||
return rootCmd
|
return rootCmd
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ import (
|
|||||||
type Config struct {
|
type Config struct {
|
||||||
OpenAI struct {
|
OpenAI struct {
|
||||||
APIKey string `yaml:"apiKey"`
|
APIKey string `yaml:"apiKey"`
|
||||||
|
DefaultModel string `yaml:"defaultModel"`
|
||||||
} `yaml:"openai"`
|
} `yaml:"openai"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
56
pkg/cli/conversation.go
Normal file
56
pkg/cli/conversation.go
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FriendlyRole returns a human friendly signifier for the message's role.
|
||||||
|
func (m *Message) FriendlyRole() string {
|
||||||
|
var friendlyRole string
|
||||||
|
switch m.Role {
|
||||||
|
case "user":
|
||||||
|
friendlyRole = "You"
|
||||||
|
case "system":
|
||||||
|
friendlyRole = "System"
|
||||||
|
case "assistant":
|
||||||
|
friendlyRole = "Assistant"
|
||||||
|
default:
|
||||||
|
friendlyRole = m.Role
|
||||||
|
}
|
||||||
|
return friendlyRole
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conversation) GenerateTitle() error {
|
||||||
|
const header = "Generate a short title for the conversation below."
|
||||||
|
prompt := fmt.Sprintf("%s\n\n---\n\n%s", header, c.FormatForExternalPrompting())
|
||||||
|
|
||||||
|
messages := []Message{
|
||||||
|
{
|
||||||
|
Role: "user",
|
||||||
|
OriginalContent: prompt,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
model := "gpt-3.5-turbo" // use cheap model to generate title
|
||||||
|
response, err := CreateChatCompletion(model, messages, 10)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Title = response
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conversation) FormatForExternalPrompting() string {
|
||||||
|
sb := strings.Builder{}
|
||||||
|
messages, err := store.Messages(c)
|
||||||
|
if err != nil {
|
||||||
|
Fatal("Could not retrieve messages for conversation %v", c)
|
||||||
|
}
|
||||||
|
for _, message := range messages {
|
||||||
|
sb.WriteString(fmt.Sprintf("<%s>\n", message.FriendlyRole()))
|
||||||
|
sb.WriteString(fmt.Sprintf("\"\"\"\n%s\n\"\"\"\n\n", message.OriginalContent))
|
||||||
|
}
|
||||||
|
return sb.String()
|
||||||
|
}
|
@ -8,7 +8,7 @@ import (
|
|||||||
openai "github.com/sashabaranov/go-openai"
|
openai "github.com/sashabaranov/go-openai"
|
||||||
)
|
)
|
||||||
|
|
||||||
func CreateChatCompletionRequest(messages []Message, maxTokens int) openai.ChatCompletionRequest {
|
func CreateChatCompletionRequest(model string, messages []Message, maxTokens int) openai.ChatCompletionRequest {
|
||||||
chatCompletionMessages := []openai.ChatCompletionMessage{}
|
chatCompletionMessages := []openai.ChatCompletionMessage{}
|
||||||
for _, m := range messages {
|
for _, m := range messages {
|
||||||
chatCompletionMessages = append(chatCompletionMessages, openai.ChatCompletionMessage{
|
chatCompletionMessages = append(chatCompletionMessages, openai.ChatCompletionMessage{
|
||||||
@ -18,7 +18,7 @@ func CreateChatCompletionRequest(messages []Message, maxTokens int) openai.ChatC
|
|||||||
}
|
}
|
||||||
|
|
||||||
return openai.ChatCompletionRequest{
|
return openai.ChatCompletionRequest{
|
||||||
Model: openai.GPT3Dot5Turbo,
|
Model: model,
|
||||||
Messages: chatCompletionMessages,
|
Messages: chatCompletionMessages,
|
||||||
MaxTokens: maxTokens,
|
MaxTokens: maxTokens,
|
||||||
}
|
}
|
||||||
@ -26,9 +26,9 @@ func CreateChatCompletionRequest(messages []Message, maxTokens int) openai.ChatC
|
|||||||
|
|
||||||
// CreateChatCompletion submits a Chat Completion API request and returns the
|
// CreateChatCompletion submits a Chat Completion API request and returns the
|
||||||
// response.
|
// response.
|
||||||
func CreateChatCompletion(messages []Message, maxTokens int) (string, error) {
|
func CreateChatCompletion(model string, messages []Message, maxTokens int) (string, error) {
|
||||||
client := openai.NewClient(config.OpenAI.APIKey)
|
client := openai.NewClient(config.OpenAI.APIKey)
|
||||||
req := CreateChatCompletionRequest(messages, maxTokens)
|
req := CreateChatCompletionRequest(model, messages, maxTokens)
|
||||||
resp, err := client.CreateChatCompletion(context.Background(), req)
|
resp, err := client.CreateChatCompletion(context.Background(), req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@ -39,9 +39,9 @@ func CreateChatCompletion(messages []Message, maxTokens int) (string, error) {
|
|||||||
|
|
||||||
// CreateChatCompletionStream submits a streaming Chat Completion API request
|
// CreateChatCompletionStream submits a streaming Chat Completion API request
|
||||||
// and streams the response to the provided output channel.
|
// and streams the response to the provided output channel.
|
||||||
func CreateChatCompletionStream(messages []Message, maxTokens int, output chan string) error {
|
func CreateChatCompletionStream(model string, messages []Message, maxTokens int, output chan string) error {
|
||||||
client := openai.NewClient(config.OpenAI.APIKey)
|
client := openai.NewClient(config.OpenAI.APIKey)
|
||||||
req := CreateChatCompletionRequest(messages, maxTokens)
|
req := CreateChatCompletionRequest(model, messages, maxTokens)
|
||||||
|
|
||||||
defer close(output)
|
defer close(output)
|
||||||
|
|
||||||
|
@ -5,6 +5,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
sqids "github.com/sqids/sqids-go"
|
sqids "github.com/sqids/sqids-go"
|
||||||
"gorm.io/driver/sqlite"
|
"gorm.io/driver/sqlite"
|
||||||
@ -22,6 +24,7 @@ type Message struct {
|
|||||||
Conversation Conversation
|
Conversation Conversation
|
||||||
OriginalContent string
|
OriginalContent string
|
||||||
Role string // 'user' or 'assistant'
|
Role string // 'user' or 'assistant'
|
||||||
|
CreatedAt time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
type Conversation struct {
|
type Conversation struct {
|
||||||
@ -83,18 +86,46 @@ func (s *Store) SaveConversation(conversation *Conversation) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Store) DeleteConversation(conversation *Conversation) error {
|
||||||
|
s.db.Where("conversation_id = ?", conversation.ID).Delete(&Message{})
|
||||||
|
return s.db.Delete(&conversation).Error
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Store) SaveMessage(message *Message) error {
|
func (s *Store) SaveMessage(message *Message) error {
|
||||||
return s.db.Create(message).Error
|
return s.db.Create(message).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) GetConversations() ([]Conversation, error) {
|
func (s *Store) Conversations() ([]Conversation, error) {
|
||||||
var conversations []Conversation
|
var conversations []Conversation
|
||||||
err := s.db.Find(&conversations).Error
|
err := s.db.Find(&conversations).Error
|
||||||
return conversations, err
|
return conversations, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) GetMessages(conversation *Conversation) ([]Message, error) {
|
func (s *Store) ConversationShortNameCompletions(shortName string) []string {
|
||||||
|
var completions []string
|
||||||
|
conversations, _ := s.Conversations() // ignore error for completions
|
||||||
|
for _, conversation := range conversations {
|
||||||
|
if shortName == "" || strings.HasPrefix(conversation.ShortName.String, shortName) {
|
||||||
|
completions = append(completions, fmt.Sprintf("%s\t%s", conversation.ShortName.String, conversation.Title))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return completions
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) ConversationByShortName(shortName string) (*Conversation, error) {
|
||||||
|
var conversation Conversation
|
||||||
|
err := s.db.Where("short_name = ?", shortName).Find(&conversation).Error
|
||||||
|
return &conversation, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) Messages(conversation *Conversation) ([]Message, error) {
|
||||||
var messages []Message
|
var messages []Message
|
||||||
err := s.db.Where("conversation_id = ?", conversation.ID).Find(&messages).Error
|
err := s.db.Where("conversation_id = ?", conversation.ID).Find(&messages).Error
|
||||||
return messages, err
|
return messages, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Store) LastMessage(conversation *Conversation) (*Message, error) {
|
||||||
|
var message Message
|
||||||
|
err := s.db.Where("conversation_id = ?", conversation.ID).Last(&message).Error
|
||||||
|
return &message, err
|
||||||
|
}
|
||||||
|
@ -59,16 +59,7 @@ func HandleDelayedResponse(response chan string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Message) RenderTTY(paddingDown bool) {
|
func (m *Message) RenderTTY(paddingDown bool) {
|
||||||
var friendlyRole string
|
fmt.Printf("<%s>\n\n", m.FriendlyRole())
|
||||||
switch m.Role {
|
|
||||||
case "user":
|
|
||||||
friendlyRole = "You"
|
|
||||||
case "system":
|
|
||||||
friendlyRole = "System"
|
|
||||||
case "assistant":
|
|
||||||
friendlyRole = "Assistant"
|
|
||||||
}
|
|
||||||
fmt.Printf("<%s>\n\n", friendlyRole)
|
|
||||||
if m.OriginalContent != "" {
|
if m.OriginalContent != "" {
|
||||||
fmt.Print(m.OriginalContent)
|
fmt.Print(m.OriginalContent)
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
package cli
|
package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// InputFromEditor retrieves user input by opening an editor (one specified by
|
// InputFromEditor retrieves user input by opening an editor (one specified by
|
||||||
@ -48,3 +50,44 @@ func InputFromEditor(placeholder string, pattern string) (string, error) {
|
|||||||
|
|
||||||
return strings.Trim(content, "\n \t"), nil
|
return strings.Trim(content, "\n \t"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// humanTimeElapsedSince returns a human-friendly representation of the given time
|
||||||
|
// duration.
|
||||||
|
func humanTimeElapsedSince(d time.Duration) string {
|
||||||
|
seconds := d.Seconds()
|
||||||
|
minutes := seconds / 60
|
||||||
|
hours := minutes / 60
|
||||||
|
days := hours / 24
|
||||||
|
weeks := days / 7
|
||||||
|
months := days / 30
|
||||||
|
years := days / 365
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case seconds < 60:
|
||||||
|
return "seconds ago"
|
||||||
|
case minutes < 2:
|
||||||
|
return "1 minute ago"
|
||||||
|
case minutes < 60:
|
||||||
|
return fmt.Sprintf("%d minutes ago", int64(minutes))
|
||||||
|
case hours < 2:
|
||||||
|
return "1 hour ago"
|
||||||
|
case hours < 24:
|
||||||
|
return fmt.Sprintf("%d hours ago", int64(hours))
|
||||||
|
case days < 2:
|
||||||
|
return "1 day ago"
|
||||||
|
case days < 7:
|
||||||
|
return fmt.Sprintf("%d days ago", int64(days))
|
||||||
|
case weeks < 2:
|
||||||
|
return "1 week ago"
|
||||||
|
case weeks <= 4:
|
||||||
|
return fmt.Sprintf("%d weeks ago", int64(weeks))
|
||||||
|
case months < 2:
|
||||||
|
return "1 month ago"
|
||||||
|
case months < 12:
|
||||||
|
return fmt.Sprintf("%d months ago", int64(months))
|
||||||
|
case years < 2:
|
||||||
|
return "1 year ago"
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("%d years ago", int64(years))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user