From a5c684aa72bd8f671f848fb04bb7fe75edbba98a Mon Sep 17 00:00:00 2001 From: "Peter.Morton" Date: Sat, 1 Nov 2025 23:33:57 -0500 Subject: [PATCH] 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 --- terminal-messenger/application/README.md | 81 ++++++++---- terminal-messenger/application/main.go | 159 +++++++++++++++++------ 2 files changed, 176 insertions(+), 64 deletions(-) diff --git a/terminal-messenger/application/README.md b/terminal-messenger/application/README.md index 9405909..6e1869e 100644 --- a/terminal-messenger/application/README.md +++ b/terminal-messenger/application/README.md @@ -1,14 +1,16 @@ # 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 - 🎨 **Colorized output** - Blue for your input, green for assistant responses - ⌨️ **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 -- 📝 **Line editing** - Full cursor movement and text editing support ## Prerequisites @@ -19,26 +21,20 @@ A simple, colorful terminal-based messenger application written in Go that conne 1. **Clone or download the project** -2. **Initialize Go module** - - ```bash - go mod init messenger - ``` - -3. **Install dependencies** +2. **Install dependencies** ```bash go get github.com/chzyer/readline ``` -4. **Build the application (optional)** +3. **Build the application (optional)** ```bash go build -o messenger main.go ``` ## Usage -### Running the application +### Starting the application **Using go run:** @@ -52,13 +48,29 @@ go run main.go ./messenger ``` -### Getting started +### First time usage -1. When prompted, enter a model name (or press Enter for "default") -2. Start typing your messages -3. The assistant will respond to each message -4. Use arrow keys to navigate through your input history -5. Type `quit` or `exit` to end the session +1. Press 'n' or Enter when asked about continuing a conversation +2. Enter a model name (or press Enter for "default") +3. Start chatting with the assistant +4. 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 @@ -67,9 +79,26 @@ go run main.go - **←/→ (Left/Right arrows)** - Move cursor within current input - **Ctrl+A** - Jump to beginning 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: @@ -81,8 +110,6 @@ To change the endpoint, modify the `apiURL` constant in `main.go`. ### Request format -The application sends POST requests with the following JSON structure: - ```json { "input": "your message here", @@ -96,8 +123,6 @@ The application sends POST requests with the following JSON structure: ### Response format -Expected response structure: - ```json { "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 └── README.md # This file ``` @@ -151,7 +176,11 @@ go mod tidy ### 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 diff --git a/terminal-messenger/application/main.go b/terminal-messenger/application/main.go index 01f7156..49f3da9 100644 --- a/terminal-messenger/application/main.go +++ b/terminal-messenger/application/main.go @@ -6,6 +6,9 @@ import ( "fmt" "io" "net/http" + "os" + "path/filepath" + "sort" "strings" "time" @@ -43,14 +46,7 @@ type Messenger struct { } func (m *Messenger) Send(input string) (string, error) { - reqBody := Request{ - Input: input, - Model: m.model, - PreviousResponseID: m.prevID, - Metadata: map[string]string{"channel": "text"}, - } - - data, _ := json.Marshal(reqBody) + data, _ := json.Marshal(Request{input, m.model, m.prevID, map[string]string{"channel": "text"}}) resp, err := m.client.Post(apiURL, "application/json", bytes.NewBuffer(data)) if err != nil { return "", err @@ -58,65 +54,152 @@ func (m *Messenger) Send(input string) (string, error) { defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) - var response Response - if err := json.Unmarshal(body, &response); err != nil { - return "", err + var r Response + json.Unmarshal(body, &r) + 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 - - if response.Status != "completed" || len(response.Output) == 0 || len(response.Output[0].Content) == 0 { - return "", fmt.Errorf("invalid response") +func getConversations() []string { + files, _ := filepath.Glob("/tmp/messenger_*_log.txt") + type f struct { + 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() { - fmt.Print("Enter model name (default: 'default'): ") - var model string - fmt.Scanln(&model) - if model == "" { - model = "default" + var id, model string + + // Check for recent conversations + convs := getConversations() + 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{ - client: &http.Client{Timeout: 10 * time.Second}, - model: model, + if model == "" { + fmt.Print("Enter model name (default: 'default'): ") + fmt.Scanln(&model) + if model == "" { + model = "default" + } } + m := &Messenger{&http.Client{Timeout: 10 * time.Second}, model, id} + 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{ - Prompt: blue + "You: " + reset, - HistoryFile: "/tmp/messenger_history.tmp", - }) - if err != nil { - fmt.Println("Error:", err) - return + histFile := "/tmp/messenger_temp.tmp" + if id != "" { + histFile = fmt.Sprintf("/tmp/messenger_%s.tmp", id) } + + rl, _ := readline.NewEx(&readline.Config{Prompt: blue + "You: " + reset, HistoryFile: histFile}) defer rl.Close() + first := id == "" + for { input, err := rl.Readline() - if err != nil || strings.TrimSpace(input) == "quit" || strings.TrimSpace(input) == "exit" { + if err != nil || input == "quit" || input == "exit" { break } - - input = strings.TrimSpace(input) - if input == "" { + if input = strings.TrimSpace(input); input == "" { continue } - response, err := messenger.Send(input) + response, err := m.Send(input) if err != nil { fmt.Println("Error:", err) 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.Println("Goodbye!") -} + }