feat: sqlite persistence with goose migrations, flashcard schema, zap logger

- Add SQLite as authoritative store alongside Qdrant (best-effort indexing)
- Use goose for embedded SQL migrations
- Schema matches flashcard format: id(INTEGER PK), note, repos, host, tags, user, context, created
- Replace std log with zap structured logger
- Split registerTools into registerStoreKnowledge + registerSearchKnowledge
- Qdrant index uses SQLite rowid directly as point ID (no SHA256 hash)
- Search enriches Qdrant results from SQLite by ID
- Lowercase auth header before prefix check
- Always handle json.MarshalIndent errors
- NixOS: StateDirectory, sqlitePath to /var/lib/st-pierre/st-pierre.sqlite
This commit is contained in:
didier committed 2026-06-02 10:01:28 -04:00
1 parent 5ac11631d4
commit 39c6185b70
8 files changed
+366 -94

No files matched your search

+29 -51
View File
@@ -2,16 +2,30 @@ package main
import (
"context"
"crypto/sha256"
"encoding/json"
"fmt"
"strconv"
"github.com/google/uuid"
"github.com/qdrant/go-client/qdrant"
)
const collectionName = "knowledge"
type entry struct {
ID int64 `json:"id"`
Note string `json:"note"`
Repos []string `json:"repos,omitempty"`
Host string `json:"host,omitempty"`
Tags []string `json:"tags,omitempty"`
User string `json:"user,omitempty"`
Context string `json:"context,omitempty"`
Created string `json:"created"`
}
type searchResult struct {
Entry entry `json:",inline"`
Score float32 `json:"score"`
}
type qdrantStore struct {
client *qdrant.Client
embedder *embedder
@@ -33,7 +47,7 @@ func (s *qdrantStore) ensureCollection(ctx context.Context) error {
err = s.client.CreateCollection(ctx, &qdrant.CreateCollection{
CollectionName: collectionName,
VectorsConfig: qdrant.NewVectorsConfig(&qdrant.VectorParams{
Size: 1536, // text-embedding-3-small dimension
Size: 1536,
Distance: qdrant.Distance_Cosine,
}),
})
@@ -44,63 +58,32 @@ func (s *qdrantStore) ensureCollection(ctx context.Context) error {
return nil
}
type storedKnowledge struct {
ID string `json:"id"`
Content string `json:"content"`
Metadata map[string]any `json:"metadata,omitempty"`
}
type searchResult struct {
ID string `json:"id"`
Content string `json:"content"`
Score float32 `json:"score"`
Metadata map[string]any `json:"metadata,omitempty"`
}
func (s *qdrantStore) store(ctx context.Context, content string, metadata map[string]any) (*storedKnowledge, error) {
vec, err := s.embedder.embedText(ctx, content)
func (s *qdrantStore) index(ctx context.Context, id int64, note string) error {
vec, err := s.embedder.embedText(ctx, note)
if err != nil {
return nil, fmt.Errorf("embed text: %w", err)
return fmt.Errorf("embed text: %w", err)
}
id := uuid.New().String()
payload := map[string]*qdrant.Value{
"content": qdrant.NewValueString(content),
}
if metadata != nil {
metaBytes, err := json.Marshal(metadata)
if err != nil {
return nil, fmt.Errorf("marshal metadata: %w", err)
}
payload["_metadata"] = qdrant.NewValueString(string(metaBytes))
}
idHash := sha256.Sum256([]byte(id))
idNum := uint64(0)
for i := 0; i < 8; i++ {
idNum = (idNum << 8) | uint64(idHash[i])
"id": qdrant.NewValueString(strconv.FormatInt(id, 10)),
"content": qdrant.NewValueString(note),
}
_, err = s.client.Upsert(ctx, &qdrant.UpsertPoints{
CollectionName: collectionName,
Points: []*qdrant.PointStruct{
{
Id: qdrant.NewIDNum(idNum),
Id: qdrant.NewIDNum(uint64(id)),
Vectors: qdrant.NewVectors(vec...),
Payload: payload,
},
},
})
if err != nil {
return nil, fmt.Errorf("upsert point: %w", err)
return fmt.Errorf("upsert point: %w", err)
}
return &storedKnowledge{
ID: id,
Content: content,
Metadata: metadata,
}, nil
return nil
}
func (s *qdrantStore) search(ctx context.Context, query string, limit uint64) ([]searchResult, error) {
@@ -130,14 +113,9 @@ func (s *qdrantStore) search(ctx context.Context, query string, limit uint64) ([
}
if payload := pt.GetPayload(); payload != nil {
if v, ok := payload["content"]; ok {
r.ID = fmt.Sprintf("%d", pt.GetId().GetNum())
r.Content = v.GetStringValue()
}
if v, ok := payload["_metadata"]; ok {
var meta map[string]any
if err := json.Unmarshal([]byte(v.GetStringValue()), &meta); err == nil {
r.Metadata = meta
if v, ok := payload["id"]; ok {
if n, err := strconv.ParseInt(v.GetStringValue(), 10, 64); err == nil {
r.Entry.ID = n
}
}
}