/** * RemoteBrain - rbrain * * This file is licensed under the Affero General Public License version 3 or * later. See the COPYING file. * * @author Paolo Lulli * @copyright Paolo Lulli 2026 */ package main import ( "bufio" "bytes" "crypto/tls" "database/sql" "encoding/json" "errors" "fmt" "net/http" "os" "path" "rbrain/internal/config" "rbrain/internal/encoding" "rbrain/internal/ollama" "rbrain/internal/queries" "rbrain/internal/service" "strings" "time" ) type RemoteChatApp struct { Config config.RemoteChatConfiguration } func main() { configPath := os.Getenv("HOME") + "/.config/rbrain/config.json" if _, err := os.Stat(configPath); errors.Is(err, os.ErrNotExist) { fmt.Printf("Config file %s doesn't exist\n", configPath) os.Exit(1) } cfg := config.GetClientConfig(configPath) var c = RemoteChatApp{Config: cfg} model := cfg.Model if len(os.Args) > 1 { model = os.Args[1] } db, err := sql.Open("sqlite", cfg.DbFile) if err != nil { fmt.Fprintf(os.Stderr, "Cannot open database: %v\n", err) os.Exit(1) } defer db.Close() if err := queries.InitSchema(db); err != nil { fmt.Fprintf(os.Stderr, "Cannot initialise schema: %v\n", err) os.Exit(1) } fmt.Printf("\nRemote Brain\n") fmt.Printf(" model : %s\n", model) fmt.Printf(" database : %s\n", cfg.DbFile) fmt.Printf("%s\n\n", strings.Repeat("─", 64)) scanner := bufio.NewScanner(os.Stdin) ch := make(chan service.Result, 1) for { fmt.Print("[Request]: ") if !scanner.Scan() { break } input := strings.TrimSpace(scanner.Text()) if input == "" { continue } switch { case input == ":quit" || input == ":exit" || input == ":q": fmt.Println("Exiting") return case input == ":history": printHistory(db) continue case strings.HasPrefix(input, ":model "): model = strings.TrimPrefix(input, ":model ") fmt.Printf("Model switched to: %s\n\n", model) continue } fmt.Print(" [Bot]: Processing...") c.ProcessMessage(db, input, model, ch) res := <-ch fmt.Print("\r [Bot]: ") if res.Err != nil { fmt.Printf("Error: %v\n\n", res.Err) } else { fmt.Printf("%s\n\n", strings.TrimSpace(res.Text)) } } if err := scanner.Err(); err != nil { fmt.Fprintf(os.Stderr, "Scanner: %v\n", err) } } func printHistory(db *sql.DB) { rows, err := queries.GetRequestsHistory(db) if err != nil { fmt.Printf("History error: %v\n", err) return } fmt.Printf("\n%s\n", strings.Repeat("─", 64)) fmt.Println("Last conversations (oldest first)") fmt.Printf("%s\n", strings.Repeat("─", 64)) defer rows.Close() for rows.Next() { var message, response, createdAt string if err := rows.Scan(&message, &response, &createdAt); err != nil { fmt.Printf("History error: %v\n", err) return } if t, err := time.Parse("2006-01-02 15:04:05", createdAt); err == nil { createdAt = t.Format("Jan 02 2006 15:04:05") } fmt.Printf("\ntime: %s\n", createdAt) fmt.Printf("query: %s\n", encoding.DecodeB64(message)) fmt.Printf("response %s\n", encoding.DecodeB64(response)) } fmt.Printf("%s\n", strings.Repeat("─", 64)) } func (c *RemoteChatApp) CallOllama(prompt, model string) (string, error) { payload, _ := json.Marshal(ollama.OllamaRequest{ Model: model, Prompt: prompt, Stream: false, }) resp := c.ApiPost(c.Config.Endpoint, bytes.NewBuffer(payload).String()) return resp, nil } // processMessage saves the request, calls Ollama, and saves the response — // all in a separate goroutine. It sends a single result on ch when done. func (c *RemoteChatApp) ProcessMessage(db *sql.DB, message, model string, ch chan<- service.Result) { go func() { requestID, err := queries.SaveRequest(db, message) if err != nil { ch <- service.Result{Err: err} return } response, err := c.CallOllama(message, model) if err != nil { ch <- service.Result{Err: err} return } if err := queries.SaveResponse(db, requestID, response); err != nil { ch <- service.Result{Err: err} return } ch <- service.Result{Text: response} }() } func (c *RemoteChatApp) secureHttpClient(key string, cert string) (client *http.Client) { x509cert, err := tls.LoadX509KeyPair(cert, key) if err != nil { fmt.Printf("Error loading X509 certificate: %s and/or key: %s", cert, key) panic(err.Error()) } certs := []tls.Certificate{x509cert} if len(certs) == 0 { client = &http.Client{} return } insecureSkipVerify := false if c.Config.TlsVerifyServer == "false" { insecureSkipVerify = true } tr := &http.Transport{ TLSClientConfig: &tls.Config{Certificates: certs, InsecureSkipVerify: insecureSkipVerify}, } client = &http.Client{Transport: tr} return } func (c *RemoteChatApp) ApiPost(endpoint string, body string) string { var client *http.Client if c.Config.Insecure == "true" { client = &http.Client{} } else { cert := path.Join(c.Config.TlsCertificate) certKey := path.Join(c.Config.TlsKeyFile) client = c.secureHttpClient(certKey, cert) } req, err := http.NewRequest("POST", endpoint, bytes.NewBuffer([]byte(body))) req.Header.Set("Content-Type", "application/json; charset=UTF-8") if err != nil { fmt.Println("Unable to make POST request", err) os.Exit(1) } req.Header.Add("Accept", "* / *") resp, err := client.Do(req) if err != nil { fmt.Println(err) os.Exit(1) } var r ollama.OllamaResponse if err := json.NewDecoder(resp.Body).Decode(&r); err != nil { return "" } defer resp.Body.Close() return r.Response }