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:
1 parent
5ac11631d4
commit
39c6185b70
8 files changed
+366
-94
No files matched your search
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user