feat(cmd): add main entrypoint wiring TUI, API, scheduler, and llama.cpp
This commit is contained in:
151
cmd/divad/main.go
Normal file
151
cmd/divad/main.go
Normal file
@@ -0,0 +1,151 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user