minimal working version of backend and cli
This commit is contained in:
434
backend/cli.py
Executable file
434
backend/cli.py
Executable file
@@ -0,0 +1,434 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
MIND CLI (v0) — supports Ops 1–8 via REST calls to the Go backend.
|
||||
|
||||
Assumptions:
|
||||
- API base: http://localhost:8080 (change with \\set api ...)
|
||||
- llama.cpp server: http://localhost:8081 (change with \\set llama ...)
|
||||
- Auth: dev token ("Bearer dev"). Adjust as needed.
|
||||
|
||||
Core usage model:
|
||||
- Free text (no leading '\\') calls /completion (Op 2 helper) on the active conversation/branch.
|
||||
- Backslash commands perform explicit ops.
|
||||
|
||||
Operations → Commands mapping
|
||||
1) Start conversation → \\new <title> <owner_id>
|
||||
2) Commit (prompt/answer or node)→ free text → /completion; or \\node <user|assistant> <content> [parent_id]
|
||||
3) Fork branch → \\branch <name> [head_node_id]
|
||||
4) Delete branch or node → \\delbranch <name> | \\delnode <node_id>
|
||||
5) Extract subset as new tree → \\extract <title> <node_id ...>
|
||||
6) Merge two branches → \\merge <left> <right> <left-first|right-first>
|
||||
7) Detach branch to new convo → \\detach <branch>
|
||||
8) Append/copy tree to another → \\append-tree <src_conv> <src_branch> [dst_conv [dst_branch]]
|
||||
|
||||
Helpful extras:
|
||||
- \\listconvs <owner_id>
|
||||
- \\listbranches <conversation_id>
|
||||
- \\linearize (active conv/branch)
|
||||
- \\llama <prompt> (debug llama-server directly)
|
||||
- \\useconv <id> ; \\usebranch <name> ; \\who ; \\quit
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import shlex
|
||||
import sys
|
||||
from typing import Dict, Any, List
|
||||
|
||||
import requests
|
||||
|
||||
DEFAULT_API = os.environ.get("MIND_API", "http://localhost:8080")
|
||||
DEFAULT_LLAMA = os.environ.get("LLAMA_API", "http://localhost:8081")
|
||||
|
||||
auth_headers = lambda: {"Authorization": "Bearer dev", "Content-Type": "application/json"}
|
||||
|
||||
class State:
|
||||
def __init__(self):
|
||||
self.api = DEFAULT_API.rstrip('/')
|
||||
self.llama = DEFAULT_LLAMA.rstrip('/')
|
||||
self.conversation_id = None # int
|
||||
self.branch = None # str
|
||||
|
||||
def as_dict(self):
|
||||
return {
|
||||
"api": self.api,
|
||||
"llama": self.llama,
|
||||
"conversation_id": self.conversation_id,
|
||||
"branch": self.branch,
|
||||
}
|
||||
|
||||
|
||||
def pretty(obj: Any) -> str:
|
||||
try:
|
||||
return json.dumps(obj, indent=2, ensure_ascii=False)
|
||||
except Exception:
|
||||
return str(obj)
|
||||
|
||||
|
||||
def post_json(url: str, payload: Dict[str, Any]) -> Dict[str, Any]:
|
||||
r = requests.post(url, headers=auth_headers(), data=json.dumps(payload), timeout=60)
|
||||
r.raise_for_status()
|
||||
if r.text.strip() == "":
|
||||
return {}
|
||||
return r.json()
|
||||
|
||||
|
||||
def delete_json(url: str, payload: Dict[str, Any] | None = None) -> Dict[str, Any]:
|
||||
r = requests.delete(url, headers=auth_headers(), data=json.dumps(payload) if payload else None, timeout=60)
|
||||
r.raise_for_status()
|
||||
return r.json() if r.text.strip() else {}
|
||||
|
||||
|
||||
def get_json(url: str, params: Dict[str, Any]) -> Dict[str, Any] | List[Any]:
|
||||
r = requests.get(url, headers=auth_headers(), params=params, timeout=60)
|
||||
r.raise_for_status()
|
||||
ct = r.headers.get('Content-Type','')
|
||||
if 'application/json' in ct:
|
||||
return r.json()
|
||||
return {"raw": r.text}
|
||||
|
||||
|
||||
# ---------- Commands ----------
|
||||
|
||||
def cmd_help():
|
||||
print(__doc__)
|
||||
|
||||
|
||||
def cmd_set(st: State, args):
|
||||
if len(args) < 2:
|
||||
print("usage: \\set <api|llama> <URL>")
|
||||
return
|
||||
key, val = args[0], args[1]
|
||||
if key == 'api':
|
||||
st.api = val.rstrip('/')
|
||||
elif key == 'llama':
|
||||
st.llama = val.rstrip('/')
|
||||
else:
|
||||
print("unknown key; use 'api' or 'llama'")
|
||||
return
|
||||
print("ok:", pretty(st.as_dict()))
|
||||
|
||||
|
||||
# (1) Start conversation
|
||||
|
||||
def cmd_new(st: State, args):
|
||||
if len(args) < 2:
|
||||
print("usage: \\new <title> <owner_id>")
|
||||
return
|
||||
title = args[0]
|
||||
try:
|
||||
owner_id = int(args[1])
|
||||
except ValueError:
|
||||
print("owner_id must be an integer")
|
||||
return
|
||||
out = post_json(f"{st.api}/conversations", {"title": title, "owner_id": owner_id})
|
||||
cid = out.get("id")
|
||||
st.conversation_id = cid
|
||||
print("created conversation:", cid)
|
||||
|
||||
|
||||
def cmd_listconvs(st: State, args):
|
||||
if not args:
|
||||
print("usage: \\listconvs <owner_id>")
|
||||
return
|
||||
owner_id = int(args[0])
|
||||
out = get_json(f"{st.api}/conversations", {"owner_id": owner_id})
|
||||
print(pretty(out))
|
||||
|
||||
|
||||
# (3) Fork branch
|
||||
|
||||
def cmd_branch(st: State, args):
|
||||
if st.conversation_id is None:
|
||||
print("set a conversation first: \\useconv <id> or \\new <title> <owner_id>")
|
||||
return
|
||||
if len(args) < 1:
|
||||
print("usage: \\branch <name> [head_node_id]")
|
||||
return
|
||||
name = args[0]
|
||||
head = int(args[1]) if len(args) > 1 else 0
|
||||
payload = {
|
||||
"conversation_id": st.conversation_id,
|
||||
"name": name,
|
||||
"head_node_id": head
|
||||
}
|
||||
out = post_json(f"{st.api}/branches", payload)
|
||||
st.branch = out.get("name", name)
|
||||
print("active branch:", st.branch, pretty(out))
|
||||
|
||||
|
||||
def cmd_usebranch(st: State, args):
|
||||
if not args:
|
||||
print("usage: \\usebranch <name>")
|
||||
return
|
||||
st.branch = args[0]
|
||||
print("active branch:", st.branch)
|
||||
|
||||
|
||||
def cmd_listbranches(st: State, args):
|
||||
if not args:
|
||||
if st.conversation_id is None:
|
||||
print("usage: \\listbranches <conversation_id> (or set active with \\useconv)")
|
||||
return
|
||||
conv = st.conversation_id
|
||||
else:
|
||||
conv = int(args[0])
|
||||
out = get_json(f"{st.api}/branches", {"conversation_id": conv})
|
||||
print(pretty(out))
|
||||
|
||||
|
||||
def cmd_useconv(st: State, args):
|
||||
if not args:
|
||||
print("usage: \\useconv <conversation_id>")
|
||||
return
|
||||
st.conversation_id = int(args[0])
|
||||
print("active conversation:", st.conversation_id)
|
||||
|
||||
|
||||
# (2) Commit helpers
|
||||
|
||||
def cmd_node(st: State, args):
|
||||
"""Create a single node (explicit Op 2). usage: \\node <user|assistant> <content> [parent_id]"""
|
||||
if st.conversation_id is None:
|
||||
print("set active conversation first")
|
||||
return
|
||||
if len(args) < 2:
|
||||
print("usage: \\node <user|assistant> <content> [parent_id]")
|
||||
return
|
||||
author = args[0]
|
||||
content = args[1]
|
||||
parent_id = int(args[2]) if len(args) > 2 else None
|
||||
payload = {
|
||||
"conversation_id": st.conversation_id,
|
||||
"author_kind": author,
|
||||
"content": content
|
||||
}
|
||||
if parent_id is not None:
|
||||
payload["parent_id"] = parent_id
|
||||
out = post_json(f"{st.api}/nodes", payload)
|
||||
print(pretty(out))
|
||||
|
||||
|
||||
def send_prompt(st: State, line: str):
|
||||
# Free text → /completion (with auto-branch-create retry)
|
||||
if st.conversation_id is None or not st.branch:
|
||||
print("need active conversation and branch (\\new/\\useconv and \\branch/\\usebranch)")
|
||||
return
|
||||
payload = {
|
||||
"conversation_id": st.conversation_id,
|
||||
"branch": st.branch,
|
||||
"prompt": line.strip(),
|
||||
}
|
||||
try:
|
||||
out = post_json(f"{st.api}/completion", payload)
|
||||
except requests.HTTPError as e:
|
||||
body = e.response.text if e.response is not None else ""
|
||||
# If branch doesn't exist yet, create it and retry once
|
||||
if e.response is not None and e.response.status_code in (400, 404, 500) and "branch" in body.lower():
|
||||
_ = post_json(f"{st.api}/branches", {
|
||||
"conversation_id": st.conversation_id,
|
||||
"name": st.branch,
|
||||
"head_node_id": 0
|
||||
})
|
||||
out = post_json(f"{st.api}/completion", payload)
|
||||
else:
|
||||
raise
|
||||
print("assistant:\n", out.get("answer"))
|
||||
|
||||
|
||||
# (4) Delete branch / node
|
||||
def cmd_delbranch(st: State, args):
|
||||
if st.conversation_id is None:
|
||||
print("set active conversation first")
|
||||
return
|
||||
if not args:
|
||||
print("usage: \\delbranch <name>")
|
||||
return
|
||||
name = args[0]
|
||||
out = delete_json(f"{st.api}/branches", {"conversation_id": st.conversation_id, "name": name})
|
||||
if st.branch == name:
|
||||
st.branch = None
|
||||
print("deleted branch:", name, pretty(out))
|
||||
|
||||
|
||||
def cmd_delnode(st: State, args):
|
||||
if not args:
|
||||
print("usage: \\delnode <node_id>")
|
||||
return
|
||||
node_id = int(args[0])
|
||||
out = delete_json(f"{st.api}/nodes/{node_id}")
|
||||
print(pretty(out))
|
||||
|
||||
|
||||
# (5) Extract selected nodes to new conversation
|
||||
|
||||
def cmd_extract(st: State, args):
|
||||
if len(args) < 2:
|
||||
print("usage: \\extract <title> <node_id ...>")
|
||||
return
|
||||
title = args[0]
|
||||
node_ids = [int(x) for x in args[1:]]
|
||||
payload = {"conversation_id": st.conversation_id, "title": title, "node_ids": node_ids}
|
||||
out = post_json(f"{st.api}/extract", payload)
|
||||
new_cid = out.get("new_conversation_id")
|
||||
print("extracted to conversation:", new_cid, pretty(out))
|
||||
|
||||
|
||||
# (6) Merge two branches (concat order)
|
||||
|
||||
def cmd_merge(st: State, args):
|
||||
if st.conversation_id is None:
|
||||
print("set active conversation first")
|
||||
return
|
||||
if len(args) < 3:
|
||||
print("usage: \\merge <left_branch> <right_branch> <left-first|right-first>")
|
||||
return
|
||||
left, right, order = args[0], args[1], args[2]
|
||||
payload = {"conversation_id": st.conversation_id, "left_branch": left, "right_branch": right, "order": order}
|
||||
out = post_json(f"{st.api}/merge", payload)
|
||||
print("merge result:", pretty(out))
|
||||
|
||||
|
||||
# (7) Detach branch → new conversation
|
||||
|
||||
def cmd_detach(st: State, args):
|
||||
if st.conversation_id is None:
|
||||
print("set active conversation first")
|
||||
return
|
||||
if not args:
|
||||
print("usage: \\detach <branch>")
|
||||
return
|
||||
payload = {"conversation_id": st.conversation_id, "branch": args[0]}
|
||||
out = post_json(f"{st.api}/detach", payload)
|
||||
print("detached to:", pretty(out))
|
||||
|
||||
|
||||
# (8) Append/copy tree
|
||||
|
||||
def cmd_append_tree(st: State, args):
|
||||
if len(args) < 2:
|
||||
print("usage: \\append-tree <src_conv> <src_branch> [dst_conv [dst_branch]]")
|
||||
return
|
||||
src_conv = int(args[0])
|
||||
src_branch = args[1]
|
||||
dst_conv = int(args[2]) if len(args) > 2 else st.conversation_id
|
||||
dst_branch = args[3] if len(args) > 3 else st.branch
|
||||
payload = {"src_conversation_id": src_conv, "src_branch": src_branch, "dst_conversation_id": dst_conv, "dst_branch": dst_branch}
|
||||
out = post_json(f"{st.api}/append-tree", payload)
|
||||
print("append-tree result:", pretty(out))
|
||||
|
||||
|
||||
# Linearize & llama & misc
|
||||
|
||||
def cmd_linearize(st: State):
|
||||
if st.conversation_id is None or not st.branch:
|
||||
print("need active conversation and branch (\\useconv / \\usebranch)")
|
||||
return
|
||||
out = get_json(f"{st.api}/linearize", {"conversation_id": st.conversation_id, "branch": st.branch})
|
||||
if isinstance(out, dict):
|
||||
print("==== Transcript ====")
|
||||
print(out.get("text", ""))
|
||||
else:
|
||||
print(pretty(out))
|
||||
|
||||
|
||||
def cmd_llama(st: State, args):
|
||||
if not args:
|
||||
print("usage: \\llama <prompt>")
|
||||
return
|
||||
prompt = " ".join(args)
|
||||
payload = {"prompt": prompt}
|
||||
try:
|
||||
r = requests.post(f"{st.llama}/completion", headers={"Content-Type": "application/json"}, data=json.dumps(payload), timeout=60)
|
||||
r.raise_for_status()
|
||||
data = r.json()
|
||||
text = None
|
||||
if isinstance(data, dict):
|
||||
if 'completion' in data:
|
||||
text = data['completion']
|
||||
elif 'content' in data and isinstance(data['content'], list) and data['content']:
|
||||
seg = data['content'][0]
|
||||
if isinstance(seg, dict) and 'text' in seg:
|
||||
text = seg['text']
|
||||
print("llama says:", text or pretty(data))
|
||||
except Exception as e:
|
||||
print("llama error:", e)
|
||||
|
||||
|
||||
def cmd_who(st: State):
|
||||
print(pretty(st.as_dict()))
|
||||
|
||||
|
||||
# ---------- REPL ----------
|
||||
|
||||
def repl():
|
||||
st = State()
|
||||
print("MIND CLI — type \\help for commands. Free text sends /completion.")
|
||||
while True:
|
||||
try:
|
||||
line = input("> ").strip()
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
print()
|
||||
break
|
||||
if not line:
|
||||
continue
|
||||
if line.startswith("\\"):
|
||||
parts = shlex.split(line[1:])
|
||||
if not parts:
|
||||
continue
|
||||
cmd, *args = parts
|
||||
cmd = cmd.lower()
|
||||
try:
|
||||
if cmd == 'help':
|
||||
cmd_help()
|
||||
elif cmd == 'set':
|
||||
cmd_set(st, args)
|
||||
elif cmd == 'new':
|
||||
cmd_new(st, args)
|
||||
elif cmd == 'listconvs':
|
||||
cmd_listconvs(st, args)
|
||||
elif cmd == 'listbranches':
|
||||
cmd_listbranches(st, args)
|
||||
elif cmd == 'useconv':
|
||||
cmd_useconv(st, args)
|
||||
elif cmd == 'branch':
|
||||
cmd_branch(st, args)
|
||||
elif cmd == 'usebranch':
|
||||
cmd_usebranch(st, args)
|
||||
elif cmd == 'node':
|
||||
cmd_node(st, args)
|
||||
elif cmd == 'delbranch':
|
||||
cmd_delbranch(st, args)
|
||||
elif cmd == 'delnode':
|
||||
cmd_delnode(st, args)
|
||||
elif cmd == 'extract':
|
||||
cmd_extract(st, args)
|
||||
elif cmd == 'merge':
|
||||
cmd_merge(st, args)
|
||||
elif cmd == 'detach':
|
||||
cmd_detach(st, args)
|
||||
elif cmd in ('append-tree', 'appendtree', 'append'):
|
||||
cmd_append_tree(st, args)
|
||||
elif cmd == 'linearize':
|
||||
cmd_linearize(st)
|
||||
elif cmd == 'llama':
|
||||
cmd_llama(st, args)
|
||||
elif cmd == 'who':
|
||||
cmd_who(st)
|
||||
elif cmd in ('quit', 'exit', 'q'):
|
||||
break
|
||||
else:
|
||||
print("unknown command, try \\help")
|
||||
except requests.HTTPError as e:
|
||||
try:
|
||||
print("HTTP", e.response.status_code, e.response.text)
|
||||
except Exception:
|
||||
print("HTTP error:", e)
|
||||
except Exception as e:
|
||||
print("error:", e)
|
||||
else:
|
||||
send_prompt(st, line)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
repl()
|
||||
@@ -26,7 +26,7 @@ func main() {
|
||||
}
|
||||
repo := db.NewRepo(database)
|
||||
|
||||
g := glue.NewGlue(repo)
|
||||
g := glue.NewGlue(repo, cfg)
|
||||
r := httpapi.NewRouter(logger, cfg, g)
|
||||
|
||||
srv := &http.Server{
|
||||
|
||||
@@ -5,10 +5,11 @@ 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 }
|
||||
@@ -19,5 +20,6 @@ func Load() Config {
|
||||
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"`
|
||||
@@ -16,24 +41,73 @@ type CompletionResp struct {
|
||||
Answer string `json:"answer"`
|
||||
}
|
||||
|
||||
// For v0 we stub the answer as a simple echo with a prefix.
|
||||
// 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")
|
||||
}
|
||||
|
||||
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)
|
||||
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}
|
||||
|
||||
BIN
backend/mind.db
BIN
backend/mind.db
Binary file not shown.
Reference in New Issue
Block a user