minimal working version of backend and cli
This commit is contained in:
@@ -5,19 +5,21 @@ import (
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
DSN string // e.g. mysql: "user:pass@tcp(127.0.0.1:3306)/mind?parseTime=true"; sqlite: "file:mind.db?_pragma=busy_timeout(5000)"
|
||||
DSN string
|
||||
Driver string // "mysql" or "sqlite"
|
||||
JWTSecret string
|
||||
Port string // default 8080
|
||||
Port string
|
||||
LlamaURL string
|
||||
}
|
||||
|
||||
func getenv(k, def string) string { v := os.Getenv(k); if v == "" { return def }; return v }
|
||||
|
||||
func Load() Config {
|
||||
return Config{
|
||||
DSN: getenv("DB_DSN", "file:mind.db?_pragma=busy_timeout(5000)"),
|
||||
Driver: getenv("DB_DRIVER", "sqlite"),
|
||||
JWTSecret: getenv("JWT_SECRET", "devsecret"),
|
||||
Port: getenv("PORT", "8080"),
|
||||
}
|
||||
return Config{
|
||||
DSN: getenv("DB_DSN", "file:mind.db?_pragma=busy_timeout(5000)"),
|
||||
Driver: getenv("DB_DRIVER", "sqlite"),
|
||||
JWTSecret: getenv("JWT_SECRET", "devsecret"),
|
||||
Port: getenv("PORT", "8080"),
|
||||
LlamaURL: getenv("LLAMA_URL", "http://localhost:8081"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,3 +86,29 @@ func (r *Repo) LatestParent(ctx context.Context, childID int64) (models.Node, bo
|
||||
}
|
||||
return n, true, nil
|
||||
}
|
||||
|
||||
func (r *Repo) DeleteBranch(ctx context.Context, convID int64, name string) error {
|
||||
_, err := r.db.ExecContext(ctx, `DELETE FROM branches WHERE conversation_id=? AND name=?`, convID, name)
|
||||
return err
|
||||
}
|
||||
|
||||
// ListBranches returns all branches for a conversation, newest first.
|
||||
func (r *Repo) ListBranches(ctx context.Context, convID int64) ([]models.Branch, error) {
|
||||
rows, err := r.db.QueryContext(ctx, `
|
||||
SELECT id, conversation_id, name, head_node_id
|
||||
FROM branches
|
||||
WHERE conversation_id=?
|
||||
ORDER BY id DESC`, convID)
|
||||
if err != nil { return nil, err }
|
||||
defer rows.Close()
|
||||
|
||||
var out []models.Branch
|
||||
for rows.Next() {
|
||||
var b models.Branch
|
||||
if err := rows.Scan(&b.ID, &b.ConversationID, &b.Name, &b.HeadNodeID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, b)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
@@ -5,8 +5,20 @@ import (
|
||||
"mind/internal/models"
|
||||
)
|
||||
|
||||
type ForkReq struct { ConversationID int64; Name string; HeadNodeID int64 }
|
||||
type ForkReq struct {
|
||||
ConversationID int64 `json:"conversation_id"`
|
||||
Name string `json:"name"`
|
||||
HeadNodeID int64 `json:"head_node_id"`
|
||||
}
|
||||
|
||||
func (g *Glue) ForkBranch(ctx context.Context, fr ForkReq) (models.Branch, error) {
|
||||
return g.repo.CreateOrGetBranch(ctx, fr.ConversationID, fr.Name, fr.HeadNodeID)
|
||||
}
|
||||
|
||||
func (g *Glue) DeleteBranch(ctx context.Context, conversationID int64, name string) error {
|
||||
return g.repo.DeleteBranch(ctx, conversationID, name)
|
||||
}
|
||||
|
||||
func (g *Glue) ListBranches(ctx context.Context, conversationID int64) ([]models.Branch, error) {
|
||||
return g.repo.ListBranches(ctx, conversationID)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,34 @@
|
||||
package glue
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"mind/internal/config"
|
||||
"mind/internal/db"
|
||||
"mind/internal/models"
|
||||
)
|
||||
|
||||
type Glue struct {
|
||||
repo *db.Repo
|
||||
cfg config.Config
|
||||
}
|
||||
|
||||
// NewGlue now receives cfg
|
||||
func NewGlue(r *db.Repo, cfg config.Config) *Glue { return &Glue{repo: r, cfg: cfg} }
|
||||
|
||||
func (g *Glue) CreateConversation(ctx context.Context, ownerID int64, title string) (int64, error) {
|
||||
return g.repo.CreateConversation(ctx, ownerID, title)
|
||||
}
|
||||
|
||||
func (g *Glue) ListConversations(ctx context.Context, ownerID int64) ([]models.Conversation, error) {
|
||||
return g.repo.ListConversations(ctx, ownerID)
|
||||
}
|
||||
|
||||
type CompletionReq struct {
|
||||
ConversationID int64 `json:"conversation_id"`
|
||||
BranchName string `json:"branch"`
|
||||
@@ -11,29 +36,78 @@ type CompletionReq struct {
|
||||
}
|
||||
|
||||
type CompletionResp struct {
|
||||
PromptNodeID int64 `json:"prompt_node_id"`
|
||||
AnswerNodeID int64 `json:"answer_node_id"`
|
||||
Answer string `json:"answer"`
|
||||
PromptNodeID int64 `json:"prompt_node_id"`
|
||||
AnswerNodeID int64 `json:"answer_node_id"`
|
||||
Answer string `json:"answer"`
|
||||
}
|
||||
|
||||
// llama request/response (support two common shapes)
|
||||
type llamaReq struct {
|
||||
Prompt string `json:"prompt"`
|
||||
}
|
||||
type llamaResp struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
func (g *Glue) callLlama(ctx context.Context, prompt string) (string, error) {
|
||||
if g.cfg.LlamaURL == "" {
|
||||
return "", errors.New("LLAMA_URL not set")
|
||||
}
|
||||
body, _ := json.Marshal(llamaReq{Prompt: prompt})
|
||||
url := g.cfg.LlamaURL + "/completion"
|
||||
|
||||
httpc := &http.Client{Timeout: 45 * time.Second}
|
||||
|
||||
req, _ := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err := httpc.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
dec := json.NewDecoder(resp.Body)
|
||||
var a llamaResp
|
||||
if err := dec.Decode(&a); err == nil && a.Content != "" {
|
||||
return a.Content, nil
|
||||
}
|
||||
|
||||
return "", errors.New("unexpected llama response")
|
||||
}
|
||||
|
||||
// For v0 we stub the answer as a simple echo with a prefix.
|
||||
func (g *Glue) AppendCompletion(ctx context.Context, req CompletionReq) (CompletionResp, error) {
|
||||
b, err := g.repo.GetBranch(ctx, req.ConversationID, req.BranchName)
|
||||
if err != nil { return CompletionResp{}, err }
|
||||
if err != nil {
|
||||
return CompletionResp{}, err
|
||||
}
|
||||
|
||||
// 1) create user prompt node
|
||||
// 1) user prompt node
|
||||
promptID, err := g.repo.CreateNode(ctx, req.ConversationID, "user", req.Prompt)
|
||||
if err != nil { return CompletionResp{}, err }
|
||||
if err := g.repo.Link(ctx, b.HeadNodeID, promptID); err != nil { return CompletionResp{}, err }
|
||||
if err != nil {
|
||||
return CompletionResp{}, err
|
||||
}
|
||||
if err := g.repo.Link(ctx, b.HeadNodeID, promptID); err != nil {
|
||||
return CompletionResp{}, err
|
||||
}
|
||||
|
||||
// 2) real llama call (fallback to stub if it fails)
|
||||
answerText, err := g.callLlama(ctx, req.Prompt)
|
||||
if err != nil {
|
||||
answerText = "(stub) You said: " + req.Prompt
|
||||
}
|
||||
|
||||
// 2) create assistant answer node (stub)
|
||||
answerText := "(stub) You said: " + req.Prompt
|
||||
answerID, err := g.repo.CreateNode(ctx, req.ConversationID, "assistant", answerText)
|
||||
if err != nil { return CompletionResp{}, err }
|
||||
if err := g.repo.Link(ctx, promptID, answerID); err != nil { return CompletionResp{}, err }
|
||||
if err != nil {
|
||||
return CompletionResp{}, err
|
||||
}
|
||||
if err := g.repo.Link(ctx, promptID, answerID); err != nil {
|
||||
return CompletionResp{}, err
|
||||
}
|
||||
|
||||
// 3) move branch head
|
||||
if err := g.repo.MoveBranchHead(ctx, b.ID, answerID); err != nil { return CompletionResp{}, err }
|
||||
if err := g.repo.MoveBranchHead(ctx, b.ID, answerID); err != nil {
|
||||
return CompletionResp{}, err
|
||||
}
|
||||
|
||||
return CompletionResp{PromptNodeID: promptID, AnswerNodeID: answerID, Answer: answerText}, nil
|
||||
}
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
package glue
|
||||
|
||||
import (
|
||||
"context"
|
||||
"mind/internal/db"
|
||||
"mind/internal/models"
|
||||
)
|
||||
|
||||
type Glue struct { repo *db.Repo }
|
||||
|
||||
func NewGlue(r *db.Repo) *Glue { return &Glue{repo: r} }
|
||||
|
||||
func (g *Glue) CreateConversation(ctx context.Context, ownerID int64, title string) (int64, error) {
|
||||
return g.repo.CreateConversation(ctx, ownerID, title)
|
||||
}
|
||||
|
||||
func (g *Glue) ListConversations(ctx context.Context, ownerID int64) ([]models.Conversation, error) {
|
||||
return g.repo.ListConversations(ctx, ownerID)
|
||||
}
|
||||
@@ -30,13 +30,51 @@ func (s *server) conversations(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// POST /branches {conversation_id, name, head_node_id}
|
||||
// GET /branches?conversation_id=...
|
||||
// DELETE /branches {conversation_id, name}
|
||||
func (s *server) branches(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost { w.WriteHeader(405); return }
|
||||
var in glue.ForkReq
|
||||
if err := json.NewDecoder(r.Body).Decode(&in); err != nil { writeJSON(w, 400, map[string]string{"error":"bad json"}); return }
|
||||
b, err := s.glue.ForkBranch(r.Context(), in)
|
||||
if err != nil { writeJSON(w, 500, map[string]string{"error": err.Error()}); return }
|
||||
writeJSON(w, 200, b)
|
||||
switch r.Method {
|
||||
case http.MethodPost:
|
||||
var in struct {
|
||||
ConversationID int64 `json:"conversation_id"`
|
||||
Name string `json:"name"`
|
||||
HeadNodeID int64 `json:"head_node_id"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
||||
writeJSON(w, 400, map[string]string{"error": "bad json"}); return
|
||||
}
|
||||
b, err := s.glue.ForkBranch(r.Context(), glue.ForkReq{
|
||||
ConversationID: in.ConversationID,
|
||||
Name: in.Name,
|
||||
HeadNodeID: in.HeadNodeID,
|
||||
})
|
||||
if err != nil { writeJSON(w, 500, map[string]string{"error": err.Error()}); return }
|
||||
writeJSON(w, 200, b)
|
||||
|
||||
case http.MethodGet:
|
||||
qs := r.URL.Query().Get("conversation_id")
|
||||
if qs == "" { writeJSON(w, 400, map[string]string{"error":"conversation_id required"}); return }
|
||||
convID, _ := strconv.ParseInt(qs, 10, 64)
|
||||
out, err := s.glue.ListBranches(r.Context(), convID)
|
||||
if err != nil { writeJSON(w, 500, map[string]string{"error": err.Error()}); return }
|
||||
writeJSON(w, 200, out)
|
||||
|
||||
case http.MethodDelete:
|
||||
var in struct {
|
||||
ConversationID int64 `json:"conversation_id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
||||
writeJSON(w, 400, map[string]string{"error":"bad json"}); return
|
||||
}
|
||||
if err := s.glue.DeleteBranch(r.Context(), in.ConversationID, in.Name); err != nil {
|
||||
writeJSON(w, 500, map[string]string{"error": err.Error()}); return
|
||||
}
|
||||
writeJSON(w, 200, map[string]string{"ok":"true"})
|
||||
|
||||
default:
|
||||
w.WriteHeader(405)
|
||||
}
|
||||
}
|
||||
|
||||
// POST /completion {conversation_id, branch, prompt}
|
||||
|
||||
Reference in New Issue
Block a user