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