Files
DivaCode/cmd/divad/main.go

152 lines
3.9 KiB
Go

package main
import (
"context"
"fmt"
"log/slog"
"net/http"
"os"
"os/signal"
"syscall"
"git.db123.ir/db123/divacode/internal/agent"
"git.db123.ir/db123/divacode/internal/channel"
"git.db123.ir/db123/divacode/internal/companion"
"git.db123.ir/db123/divacode/internal/config"
"git.db123.ir/db123/divacode/internal/llama"
"git.db123.ir/db123/divacode/internal/log"
"git.db123.ir/db123/divacode/internal/memory"
"git.db123.ir/db123/divacode/internal/storage"
"git.db123.ir/db123/divacode/internal/tools"
tea "github.com/charmbracelet/bubbletea"
)
func main() {
cfg := config.Load()
log.Init(cfg.LogLevel)
if err := cfg.Validate(); err != nil {
slog.Error("config validation failed", "error", err)
os.Exit(1)
}
slog.Info("starting divacode",
"llama_cpp_url", cfg.LlamaCppURL,
"db_path", cfg.DBPath,
"log_level", cfg.LogLevel,
)
db, err := storage.Open(cfg.DBPath)
if err != nil {
slog.Error("failed to open database", "path", cfg.DBPath, "error", err)
os.Exit(1)
}
defer db.Close()
if err := db.Migrate(); err != nil {
slog.Error("database migration failed", "error", err)
os.Exit(1)
}
slog.Info("database migrated", "path", cfg.DBPath)
llm := llama.NewClient(cfg.LlamaCppURL)
if err := llm.Health(); err != nil {
slog.Error("llama.cpp not reachable", "url", cfg.LlamaCppURL, "error", err)
os.Exit(1)
}
slog.Info("llama.cpp connected", "url", cfg.LlamaCppURL)
toolReg := tools.NewRegistry()
tools.RegisterBuiltins(toolReg)
memStore := memory.NewStore(db)
compEngine := companion.NewEngine(cfg, db)
ag := agent.New(cfg, llm, db, memStore, compEngine, toolReg)
if err := ag.NewConversation(); err != nil {
slog.Error("failed to start conversation", "error", err)
os.Exit(1)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
sched := agent.NewScheduler(ag, cfg.SchedulerInterval, cfg.DiaryHour)
go sched.Start(ctx)
matrix := channel.NewMatrixClient(cfg, ag)
go func() {
if err := matrix.Start(ctx); err != nil {
slog.Error("matrix client failed", "error", err)
}
}()
if cfg.APIEnabled {
go startAPI(cfg, ag)
}
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-sigCh
slog.Info("shutting down...")
sched.Stop()
cancel()
}()
p := tea.NewProgram(channel.NewTUIModel(ag), tea.WithAltScreen())
if _, err := p.Run(); err != nil {
slog.Error("tui error", "error", err)
os.Exit(1)
}
}
func startAPI(cfg *config.Config, ag *agent.Agent) {
mux := http.NewServeMux()
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"status":"ok"}`))
})
mux.HandleFunc("/v1/conversations", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodPost:
if err := ag.NewConversation(); err != nil {
http.Error(w, `{"error":"`+err.Error()+`"}`, http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"conversationId":"%s"}`, ag.ConversationID())
default:
http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed)
}
})
mux.HandleFunc("/v1/chat", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed)
return
}
msg := r.FormValue("message")
if msg == "" {
http.Error(w, `{"error":"message is required"}`, http.StatusBadRequest)
return
}
resp, err := ag.HandleMessage(r.Context(), msg)
if err != nil {
http.Error(w, `{"error":"`+err.Error()+`"}`, http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"response":"%s"}`, resp)
})
slog.Info("api server starting", "port", cfg.APIPort)
if err := http.ListenAndServe(":"+cfg.APIPort, mux); err != nil {
slog.Error("api server failed", "error", err)
}
}