- internal/llama/template.go: ChatML/Llama3 template engine with stop tokens - internal/agent/reasoning.go: parse <think>/<reasoning> tags from LLM output - companion/promptbuilder.go: output structured BuildResult, add date/time context - agent/agent.go: wire templates + reasoning split into message loop - storage: add reasoning TEXT column to messages table + migration - channel/tui.go: collapsible reasoning (tab toggle), mouse scroll, polished dimmed style - config: CHAT_TEMPLATE env var (chatml|llama3|none) - plan.md: mark Phase 2 complete, add themes TODO
152 lines
3.9 KiB
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(), tea.WithMouseCellMotion())
|
|
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)
|
|
}
|
|
}
|