feat: implement terminal messenger with conversation management
feat: implement terminal messenger with conversation management - Add terminal-based messenger client connecting to Verint API endpoint - Implement colorized output (blue for user input, green for assistant responses) - Add readline integration for arrow key navigation through input history - Implement conversation persistence with unique history files per conversation ID - Add conversation resume feature listing 5 most recent conversations - Store and auto-load model names per conversation - Display last 10 message exchanges when resuming conversations - Log full conversation history (user + assistant messages) with timestamps - Simplify codebase by consolidating file operations into reusable functions - Fix Sscanf error handling for conversation selection Features: - Request/response handling with conversation context via previous_response_id - Three file types per conversation: .tmp (readline history), _log.txt (full conversation), _meta.txt (model name) - Automatic conversation ID assignment after first message - Clean exit handling and proper resource cleanup
This commit is contained in:
parent
c198babc75
commit
a5c684aa72
@ -1,14 +1,16 @@
|
|||||||
# Terminal Messenger
|
# Terminal Messenger
|
||||||
|
|
||||||
A simple, colorful terminal-based messenger application written in Go that connects to a remote messaging endpoint.
|
A simple, colorful terminal-based messenger application written in Go that connects to a remote messaging endpoint with conversation persistence and history management.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- 🎨 **Colorized output** - Blue for your input, green for assistant responses
|
- 🎨 **Colorized output** - Blue for your input, green for assistant responses
|
||||||
- ⌨️ **Input history** - Navigate previous messages with arrow keys
|
- ⌨️ **Input history** - Navigate previous messages with arrow keys
|
||||||
- 💬 **Conversation context** - Maintains conversation continuity across messages
|
- 💾 **Conversation persistence** - Resume previous conversations seamlessly
|
||||||
|
- 📝 **Full conversation logs** - Both user and assistant messages saved with timestamps
|
||||||
|
- 🔄 **Auto-resume** - Lists your 5 most recent conversations on startup
|
||||||
|
- 🏷️ **Model tracking** - Automatically saves and loads the model used per conversation
|
||||||
- 🚀 **Simple and fast** - Minimal setup, quick startup
|
- 🚀 **Simple and fast** - Minimal setup, quick startup
|
||||||
- 📝 **Line editing** - Full cursor movement and text editing support
|
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
@ -19,26 +21,20 @@ A simple, colorful terminal-based messenger application written in Go that conne
|
|||||||
|
|
||||||
1. **Clone or download the project**
|
1. **Clone or download the project**
|
||||||
|
|
||||||
2. **Initialize Go module**
|
2. **Install dependencies**
|
||||||
|
|
||||||
```bash
|
|
||||||
go mod init messenger
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Install dependencies**
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
go get github.com/chzyer/readline
|
go get github.com/chzyer/readline
|
||||||
```
|
```
|
||||||
|
|
||||||
4. **Build the application (optional)**
|
3. **Build the application (optional)**
|
||||||
```bash
|
```bash
|
||||||
go build -o messenger main.go
|
go build -o messenger main.go
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
### Running the application
|
### Starting the application
|
||||||
|
|
||||||
**Using go run:**
|
**Using go run:**
|
||||||
|
|
||||||
@ -52,13 +48,29 @@ go run main.go
|
|||||||
./messenger
|
./messenger
|
||||||
```
|
```
|
||||||
|
|
||||||
### Getting started
|
### First time usage
|
||||||
|
|
||||||
1. When prompted, enter a model name (or press Enter for "default")
|
1. Press 'n' or Enter when asked about continuing a conversation
|
||||||
2. Start typing your messages
|
2. Enter a model name (or press Enter for "default")
|
||||||
3. The assistant will respond to each message
|
3. Start chatting with the assistant
|
||||||
4. Use arrow keys to navigate through your input history
|
4. Type `quit` or `exit` to end the session
|
||||||
5. Type `quit` or `exit` to end the session
|
|
||||||
|
### Resuming a conversation
|
||||||
|
|
||||||
|
1. When prompted, you'll see your recent conversations:
|
||||||
|
|
||||||
|
```
|
||||||
|
Recent conversations:
|
||||||
|
1. resp_d2925cd3-d99c-46f6-a70a-05b1453b8d4f (model: gpt-4)
|
||||||
|
2. resp_a1234567-89ab-cdef-0123-456789abcdef (model: default)
|
||||||
|
|
||||||
|
Continue? (enter number or 'n' for new):
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Enter the number (1-5) to resume that conversation
|
||||||
|
3. The last 10 message exchanges will be displayed
|
||||||
|
4. The model name is automatically loaded
|
||||||
|
5. Continue chatting from where you left off
|
||||||
|
|
||||||
### Keyboard shortcuts
|
### Keyboard shortcuts
|
||||||
|
|
||||||
@ -67,9 +79,26 @@ go run main.go
|
|||||||
- **←/→ (Left/Right arrows)** - Move cursor within current input
|
- **←/→ (Left/Right arrows)** - Move cursor within current input
|
||||||
- **Ctrl+A** - Jump to beginning of line
|
- **Ctrl+A** - Jump to beginning of line
|
||||||
- **Ctrl+E** - Jump to end of line
|
- **Ctrl+E** - Jump to end of line
|
||||||
- **Ctrl+C** - Interrupt/exit application
|
- **Ctrl+C** or type `quit`/`exit` - Exit application
|
||||||
|
|
||||||
## Configuration
|
## How It Works
|
||||||
|
|
||||||
|
### Conversation Files
|
||||||
|
|
||||||
|
For each conversation, three files are created in `/tmp/`:
|
||||||
|
|
||||||
|
1. **`messenger_{ID}.tmp`** - Readline history for arrow key navigation (user inputs only)
|
||||||
|
2. **`messenger_{ID}_log.txt`** - Full conversation log with timestamps (both user and assistant)
|
||||||
|
3. **`messenger_{ID}_meta.txt`** - Stores the model name for the conversation
|
||||||
|
|
||||||
|
### Conversation Continuity
|
||||||
|
|
||||||
|
- Each response from the API includes a unique `id`
|
||||||
|
- This ID is sent as `previous_response_id` in subsequent requests
|
||||||
|
- This maintains conversation context across the entire session
|
||||||
|
- When resuming, the conversation ID is loaded and context is preserved
|
||||||
|
|
||||||
|
## API Configuration
|
||||||
|
|
||||||
The application connects to:
|
The application connects to:
|
||||||
|
|
||||||
@ -81,8 +110,6 @@ To change the endpoint, modify the `apiURL` constant in `main.go`.
|
|||||||
|
|
||||||
### Request format
|
### Request format
|
||||||
|
|
||||||
The application sends POST requests with the following JSON structure:
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"input": "your message here",
|
"input": "your message here",
|
||||||
@ -96,8 +123,6 @@ The application sends POST requests with the following JSON structure:
|
|||||||
|
|
||||||
### Response format
|
### Response format
|
||||||
|
|
||||||
Expected response structure:
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"id": "response-id",
|
"id": "response-id",
|
||||||
@ -126,7 +151,7 @@ Expected response structure:
|
|||||||
|
|
||||||
```
|
```
|
||||||
.
|
.
|
||||||
├── main.go # Main application file
|
├── main.go # Main application file (~160 lines)
|
||||||
├── go.mod # Go module file
|
├── go.mod # Go module file
|
||||||
└── README.md # This file
|
└── README.md # This file
|
||||||
```
|
```
|
||||||
@ -151,7 +176,11 @@ go mod tidy
|
|||||||
|
|
||||||
### History file permissions
|
### History file permissions
|
||||||
|
|
||||||
The app creates a history file at `/tmp/messenger_history.tmp`. If you encounter permission errors, ensure you have write access to `/tmp/`.
|
The app creates files in `/tmp/`. If you encounter permission errors, ensure you have write access to `/tmp/`.
|
||||||
|
|
||||||
|
### Cannot find previous conversations
|
||||||
|
|
||||||
|
Conversations are stored in `/tmp/` which may be cleared on system restart. For permanent storage, modify the file paths in `main.go` to use a different directory like `~/.messenger/`.
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
|
|||||||
@ -6,6 +6,9 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -43,14 +46,7 @@ type Messenger struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Messenger) Send(input string) (string, error) {
|
func (m *Messenger) Send(input string) (string, error) {
|
||||||
reqBody := Request{
|
data, _ := json.Marshal(Request{input, m.model, m.prevID, map[string]string{"channel": "text"}})
|
||||||
Input: input,
|
|
||||||
Model: m.model,
|
|
||||||
PreviousResponseID: m.prevID,
|
|
||||||
Metadata: map[string]string{"channel": "text"},
|
|
||||||
}
|
|
||||||
|
|
||||||
data, _ := json.Marshal(reqBody)
|
|
||||||
resp, err := m.client.Post(apiURL, "application/json", bytes.NewBuffer(data))
|
resp, err := m.client.Post(apiURL, "application/json", bytes.NewBuffer(data))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@ -58,65 +54,152 @@ func (m *Messenger) Send(input string) (string, error) {
|
|||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
body, _ := io.ReadAll(resp.Body)
|
body, _ := io.ReadAll(resp.Body)
|
||||||
var response Response
|
var r Response
|
||||||
if err := json.Unmarshal(body, &response); err != nil {
|
json.Unmarshal(body, &r)
|
||||||
return "", err
|
m.prevID = r.ID
|
||||||
|
|
||||||
|
if r.Status == "completed" && len(r.Output) > 0 && len(r.Output[0].Content) > 0 {
|
||||||
|
return r.Output[0].Content[0].Text, nil
|
||||||
}
|
}
|
||||||
|
return "", fmt.Errorf("invalid response")
|
||||||
|
}
|
||||||
|
|
||||||
m.prevID = response.ID
|
func getConversations() []string {
|
||||||
|
files, _ := filepath.Glob("/tmp/messenger_*_log.txt")
|
||||||
if response.Status != "completed" || len(response.Output) == 0 || len(response.Output[0].Content) == 0 {
|
type f struct {
|
||||||
return "", fmt.Errorf("invalid response")
|
id string
|
||||||
|
t time.Time
|
||||||
}
|
}
|
||||||
|
var list []f
|
||||||
|
for _, file := range files {
|
||||||
|
info, _ := os.Stat(file)
|
||||||
|
id := strings.TrimSuffix(strings.TrimPrefix(filepath.Base(file), "messenger_"), "_log.txt")
|
||||||
|
list = append(list, f{id, info.ModTime()})
|
||||||
|
}
|
||||||
|
sort.Slice(list, func(i, j int) bool { return list[i].t.After(list[j].t) })
|
||||||
|
|
||||||
return response.Output[0].Content[0].Text, nil
|
var ids []string
|
||||||
|
for i, item := range list {
|
||||||
|
if i >= 5 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
ids = append(ids, item.id)
|
||||||
|
}
|
||||||
|
return ids
|
||||||
|
}
|
||||||
|
|
||||||
|
func readFile(path string) string {
|
||||||
|
data, _ := os.ReadFile(path)
|
||||||
|
return strings.TrimSpace(string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeFile(path, content string) {
|
||||||
|
os.WriteFile(path, []byte(content), 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendFile(path, content string) {
|
||||||
|
f, _ := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||||
|
defer f.Close()
|
||||||
|
f.WriteString(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
func showHistory(id string) {
|
||||||
|
lines := strings.Split(readFile(fmt.Sprintf("/tmp/messenger_%s_log.txt", id)), "\n")
|
||||||
|
start := len(lines) - 20
|
||||||
|
if start < 0 {
|
||||||
|
start = 0
|
||||||
|
}
|
||||||
|
fmt.Println("\n--- Previous Messages ---")
|
||||||
|
for _, line := range lines[start:] {
|
||||||
|
if strings.Contains(line, "You: ") {
|
||||||
|
fmt.Printf(blue+"You: %s"+reset+"\n", strings.SplitN(line, "You: ", 2)[1])
|
||||||
|
} else if strings.Contains(line, "Assistant: ") {
|
||||||
|
fmt.Printf(green+"Assistant: %s"+reset+"\n", strings.SplitN(line, "Assistant: ", 2)[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Println("-------------------------\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
fmt.Print("Enter model name (default: 'default'): ")
|
var id, model string
|
||||||
var model string
|
|
||||||
fmt.Scanln(&model)
|
// Check for recent conversations
|
||||||
if model == "" {
|
convs := getConversations()
|
||||||
model = "default"
|
if len(convs) > 0 {
|
||||||
|
fmt.Println("Recent conversations:")
|
||||||
|
for i, cid := range convs {
|
||||||
|
m := readFile(fmt.Sprintf("/tmp/messenger_%s_meta.txt", cid))
|
||||||
|
fmt.Printf("%d. %s (model: %s)\n", i+1, cid, m)
|
||||||
|
}
|
||||||
|
fmt.Print("\nContinue? (enter number or 'n' for new): ")
|
||||||
|
var choice string
|
||||||
|
fmt.Scanln(&choice)
|
||||||
|
var num int
|
||||||
|
if _, err := fmt.Sscanf(choice, "%d", &num); err == nil && num >= 1 && num <= len(convs) {
|
||||||
|
id = convs[num-1]
|
||||||
|
model = readFile(fmt.Sprintf("/tmp/messenger_%s_meta.txt", id))
|
||||||
|
fmt.Printf("Continuing: %s (model: %s)\n", id, model)
|
||||||
|
showHistory(id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
messenger := &Messenger{
|
if model == "" {
|
||||||
client: &http.Client{Timeout: 10 * time.Second},
|
fmt.Print("Enter model name (default: 'default'): ")
|
||||||
model: model,
|
fmt.Scanln(&model)
|
||||||
|
if model == "" {
|
||||||
|
model = "default"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m := &Messenger{&http.Client{Timeout: 10 * time.Second}, model, id}
|
||||||
|
|
||||||
fmt.Println("\n=== Terminal Messenger ===")
|
fmt.Println("\n=== Terminal Messenger ===")
|
||||||
fmt.Println("Use ↑/↓ arrows for history, type 'quit' to exit\n")
|
fmt.Println("Use ↑/↓ for history, 'quit' to exit\n")
|
||||||
|
|
||||||
rl, err := readline.NewEx(&readline.Config{
|
histFile := "/tmp/messenger_temp.tmp"
|
||||||
Prompt: blue + "You: " + reset,
|
if id != "" {
|
||||||
HistoryFile: "/tmp/messenger_history.tmp",
|
histFile = fmt.Sprintf("/tmp/messenger_%s.tmp", id)
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("Error:", err)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rl, _ := readline.NewEx(&readline.Config{Prompt: blue + "You: " + reset, HistoryFile: histFile})
|
||||||
defer rl.Close()
|
defer rl.Close()
|
||||||
|
|
||||||
|
first := id == ""
|
||||||
|
|
||||||
for {
|
for {
|
||||||
input, err := rl.Readline()
|
input, err := rl.Readline()
|
||||||
if err != nil || strings.TrimSpace(input) == "quit" || strings.TrimSpace(input) == "exit" {
|
if err != nil || input == "quit" || input == "exit" {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
if input = strings.TrimSpace(input); input == "" {
|
||||||
input = strings.TrimSpace(input)
|
|
||||||
if input == "" {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
response, err := messenger.Send(input)
|
response, err := m.Send(input)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("Error:", err)
|
fmt.Println("Error:", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if first && m.prevID != "" {
|
||||||
|
first = false
|
||||||
|
rl.Close()
|
||||||
|
rl, _ = readline.NewEx(&readline.Config{
|
||||||
|
Prompt: blue + "You: " + reset,
|
||||||
|
HistoryFile: fmt.Sprintf("/tmp/messenger_%s.tmp", m.prevID),
|
||||||
|
})
|
||||||
|
writeFile(fmt.Sprintf("/tmp/messenger_%s_meta.txt", m.prevID), m.model)
|
||||||
|
fmt.Printf("(ID: %s)\n\n", m.prevID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.prevID != "" {
|
||||||
|
ts := time.Now().Format("2006-01-02 15:04:05")
|
||||||
|
logFile := fmt.Sprintf("/tmp/messenger_%s_log.txt", m.prevID)
|
||||||
|
appendFile(logFile, fmt.Sprintf("[%s] You: %s\n[%s] Assistant: %s\n", ts, input, ts, response))
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Printf(green+"Assistant: %s"+reset+"\n\n", response)
|
fmt.Printf(green+"Assistant: %s"+reset+"\n\n", response)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("Goodbye!")
|
fmt.Println("Goodbye!")
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user