Moving terminal-messenger into its own repository
This commit is contained in:
@@ -1,210 +0,0 @@
|
|||||||
# Terminal Messenger
|
|
||||||
|
|
||||||
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 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
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
- Go 1.16 or higher
|
|
||||||
- Internet connection
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
1. **Clone or download the project**
|
|
||||||
|
|
||||||
2. **Install dependencies**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
go get github.com/chzyer/readline
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Build the application (optional)**
|
|
||||||
```bash
|
|
||||||
go build -o messenger main.go
|
|
||||||
```
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
### Starting the application
|
|
||||||
|
|
||||||
**Using go run:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
go run main.go
|
|
||||||
```
|
|
||||||
|
|
||||||
**Or if you built the binary:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./messenger
|
|
||||||
```
|
|
||||||
|
|
||||||
### First time usage
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
- **↑ (Up arrow)** - Navigate to previous input
|
|
||||||
- **↓ (Down arrow)** - Navigate to next input
|
|
||||||
- **←/→ (Left/Right arrows)** - Move cursor within current input
|
|
||||||
- **Ctrl+A** - Jump to beginning of line
|
|
||||||
- **Ctrl+E** - Jump to end of line
|
|
||||||
- **Ctrl+C** or type `quit`/`exit` - Exit application
|
|
||||||
|
|
||||||
## 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:
|
|
||||||
|
|
||||||
```
|
|
||||||
https://router.ivastudio.verint.live/ProxyScript/run/67bca862210071627d32ef12/current/basic_messenger
|
|
||||||
```
|
|
||||||
|
|
||||||
To change the endpoint, modify the `apiURL` constant in `main.go`.
|
|
||||||
|
|
||||||
### Request format
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"input": "your message here",
|
|
||||||
"model": "model name",
|
|
||||||
"previous_response_id": "previous response ID for context",
|
|
||||||
"metadata": {
|
|
||||||
"channel": "text"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Response format
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": "response-id",
|
|
||||||
"object": "response",
|
|
||||||
"created_at": 1234567890,
|
|
||||||
"status": "completed",
|
|
||||||
"model": "Model Name",
|
|
||||||
"output": [
|
|
||||||
{
|
|
||||||
"type": "message",
|
|
||||||
"id": "message-id",
|
|
||||||
"role": "assistant",
|
|
||||||
"content": [
|
|
||||||
{
|
|
||||||
"type": "output_text",
|
|
||||||
"text": "Assistant response here",
|
|
||||||
"annotations": []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Project Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
.
|
|
||||||
├── main.go # Main application file (~160 lines)
|
|
||||||
├── go.mod # Go module file
|
|
||||||
└── README.md # This file
|
|
||||||
```
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Connection errors
|
|
||||||
|
|
||||||
If you encounter connection errors, check:
|
|
||||||
|
|
||||||
- Your internet connection
|
|
||||||
- The endpoint URL is accessible
|
|
||||||
- Firewall settings aren't blocking the connection
|
|
||||||
|
|
||||||
### Module errors
|
|
||||||
|
|
||||||
If you get "module not found" errors:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
go mod tidy
|
|
||||||
```
|
|
||||||
|
|
||||||
### History file permissions
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
- [github.com/chzyer/readline](https://github.com/chzyer/readline) -
|
|
||||||
For input history and line editing
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
This project is provided as-is for educational and development purposes.
|
|
||||||
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
Feel free to submit issues, fork the repository, and create pull requests
|
|
||||||
for any improvements.:> [!WARNING]
|
|
||||||
|
|
||||||
>
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
module messenger
|
|
||||||
|
|
||||||
go 1.24.6
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/chzyer/readline v1.5.1 // indirect
|
|
||||||
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5 // indirect
|
|
||||||
)
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
|
|
||||||
github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI=
|
|
||||||
github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
|
|
||||||
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
|
|
||||||
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5 h1:y/woIyUBFbpQGKS0u1aHF/40WUDnek3fPOyD08H5Vng=
|
|
||||||
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
@@ -1,243 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/chzyer/readline"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
apiURL = "https://router.ivastudio.verint.live/ProxyScript/run/67bca862210071627d32ef12/current/basic_messenger"
|
|
||||||
blue = "\033[34m"
|
|
||||||
green = "\033[32m"
|
|
||||||
reset = "\033[0m"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Request struct {
|
|
||||||
Input string `json:"input"`
|
|
||||||
Model string `json:"model"`
|
|
||||||
PreviousResponseID string `json:"previous_response_id,omitempty"`
|
|
||||||
Metadata map[string]string `json:"metadata"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Response struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
Output []struct {
|
|
||||||
Text string `json:"text"`
|
|
||||||
} `json:"output"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Messenger struct {
|
|
||||||
client *http.Client
|
|
||||||
model string
|
|
||||||
prevID string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Messenger) Send(input string) ([]string, error) {
|
|
||||||
data, err := json.Marshal(Request{input, m.model, m.prevID, map[string]string{"channel": "text"}})
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to marshal request: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := m.client.Post(apiURL, "application/json", bytes.NewBuffer(data))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to send request: %w", err)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
// Check HTTP status code
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
body, _ := io.ReadAll(resp.Body)
|
|
||||||
return nil, fmt.Errorf("HTTP %d: %s - Response body: %s", resp.StatusCode, resp.Status, string(body))
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to read response body: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var r Response
|
|
||||||
if err := json.Unmarshal(body, &r); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to parse JSON response: %w - Raw body: %s", err, string(body))
|
|
||||||
}
|
|
||||||
|
|
||||||
m.prevID = r.ID
|
|
||||||
|
|
||||||
// Detailed validation of response structure
|
|
||||||
if r.Status != "completed" {
|
|
||||||
return nil, fmt.Errorf("response status is '%s' (expected 'completed') - Response ID: %s, Raw body: %s", r.Status, r.ID, string(body))
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(r.Output) == 0 {
|
|
||||||
return nil, fmt.Errorf("response has no output - Response ID: %s, Status: %s, Output length: %d, Raw body: %s", r.ID, r.Status, len(r.Output), string(body))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collect all Output Text values
|
|
||||||
var texts []string
|
|
||||||
for i, content := range r.Output {
|
|
||||||
if content.Text == "" {
|
|
||||||
fmt.Printf("Warning: Output[%d] has empty text field\n", i)
|
|
||||||
}
|
|
||||||
texts = append(texts, content.Text)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(texts) == 0 {
|
|
||||||
return nil, fmt.Errorf("no text content found in response - Response ID: %s, Output items: %d", r.ID, len(r.Output))
|
|
||||||
}
|
|
||||||
|
|
||||||
return texts, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
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) })
|
|
||||||
|
|
||||||
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() {
|
|
||||||
var id, model string
|
|
||||||
|
|
||||||
fmt.Println("\n░▀█▀░█░█░█▀█░░░█▄█░█▀▀░█▀▀░█▀▀░█▀▀░█▀█░█▀▀░█▀▀░█▀▄\n░░█░░▀▄▀░█▀█░░░█░█░█▀▀░▀▀█░▀▀█░█▀▀░█░█░█░█░█▀▀░█▀▄\n░▀▀▀░░▀░░▀░▀░░░▀░▀░▀▀▀░▀▀▀░▀▀▀░▀▀▀░▀░▀░▀▀▀░▀▀▀░▀░▀")
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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("Use ↑/↓ for history, 'quit' to exit\n")
|
|
||||||
|
|
||||||
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 || input == "quit" || input == "exit" {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if input = strings.TrimSpace(input); input == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
responses, err := m.Send(input)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("\n"+reset+"Error sending message:\n%s\n\n", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Join all responses into a single string
|
|
||||||
response := strings.Join(responses, "\n")
|
|
||||||
|
|
||||||
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!")
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user