feat(repos): add kb.yaml knowledge base config
KBConfig (.core/kb.yaml, checked in): - Wiki mirror: enabled flag, local dir, remote SSH base URL - Search: Qdrant host/port, collection (openbrain), Ollama URL, embed model (embeddinggemma), default top_k - Helpers: WikiRepoURL(name), WikiLocalPath(root, name) Data already lives in Qdrant (homelab-embedded) — this just configures the search client and local wiki clone paths. Co-Authored-By: Virgil <virgil@lethean.io> Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
f751b29170
commit
e4342454b8
2 changed files with 223 additions and 0 deletions
116
repos/kbconfig.go
Normal file
116
repos/kbconfig.go
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
package repos
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"forge.lthn.ai/core/go-io"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// KBConfig holds knowledge base configuration for a workspace.
|
||||
// Stored at .core/kb.yaml and checked into git.
|
||||
type KBConfig struct {
|
||||
Version int `yaml:"version"`
|
||||
Wiki WikiConfig `yaml:"wiki"`
|
||||
Search KBSearch `yaml:"search"`
|
||||
}
|
||||
|
||||
// WikiConfig controls local wiki mirror behaviour.
|
||||
type WikiConfig struct {
|
||||
// Enabled toggles wiki cloning on sync.
|
||||
Enabled bool `yaml:"enabled"`
|
||||
// Dir is the local directory for wiki clones, relative to .core/.
|
||||
Dir string `yaml:"dir"`
|
||||
// Remote is the SSH base URL for wiki repos (e.g. ssh://git@forge.lthn.ai:2223/core).
|
||||
// Repo wikis are at {Remote}/{name}.wiki.git
|
||||
Remote string `yaml:"remote"`
|
||||
}
|
||||
|
||||
// KBSearch configures vector search against the OpenBrain Qdrant collection.
|
||||
type KBSearch struct {
|
||||
// QdrantHost is the Qdrant server (gRPC).
|
||||
QdrantHost string `yaml:"qdrant_host"`
|
||||
// QdrantPort is the gRPC port.
|
||||
QdrantPort int `yaml:"qdrant_port"`
|
||||
// Collection is the Qdrant collection name.
|
||||
Collection string `yaml:"collection"`
|
||||
// OllamaURL is the Ollama API base URL for embedding queries.
|
||||
OllamaURL string `yaml:"ollama_url"`
|
||||
// EmbedModel is the Ollama model for embedding.
|
||||
EmbedModel string `yaml:"embed_model"`
|
||||
// TopK is the default number of results.
|
||||
TopK int `yaml:"top_k"`
|
||||
}
|
||||
|
||||
// DefaultKBConfig returns sensible defaults for knowledge base config.
|
||||
func DefaultKBConfig() *KBConfig {
|
||||
return &KBConfig{
|
||||
Version: 1,
|
||||
Wiki: WikiConfig{
|
||||
Enabled: true,
|
||||
Dir: "kb/wiki",
|
||||
Remote: "ssh://git@forge.lthn.ai:2223/core",
|
||||
},
|
||||
Search: KBSearch{
|
||||
QdrantHost: "qdrant.lthn.sh",
|
||||
QdrantPort: 6334,
|
||||
Collection: "openbrain",
|
||||
OllamaURL: "https://ollama.lthn.sh",
|
||||
EmbedModel: "embeddinggemma",
|
||||
TopK: 5,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// LoadKBConfig reads .core/kb.yaml from the given workspace root directory.
|
||||
// Returns defaults if the file does not exist.
|
||||
func LoadKBConfig(m io.Medium, root string) (*KBConfig, error) {
|
||||
path := filepath.Join(root, ".core", "kb.yaml")
|
||||
|
||||
if !m.Exists(path) {
|
||||
return DefaultKBConfig(), nil
|
||||
}
|
||||
|
||||
content, err := m.Read(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read kb config: %w", err)
|
||||
}
|
||||
|
||||
kb := DefaultKBConfig()
|
||||
if err := yaml.Unmarshal([]byte(content), kb); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse kb config: %w", err)
|
||||
}
|
||||
|
||||
return kb, nil
|
||||
}
|
||||
|
||||
// SaveKBConfig writes .core/kb.yaml to the given workspace root directory.
|
||||
func SaveKBConfig(m io.Medium, root string, kb *KBConfig) error {
|
||||
coreDir := filepath.Join(root, ".core")
|
||||
if err := m.EnsureDir(coreDir); err != nil {
|
||||
return fmt.Errorf("failed to create .core directory: %w", err)
|
||||
}
|
||||
|
||||
data, err := yaml.Marshal(kb)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal kb config: %w", err)
|
||||
}
|
||||
|
||||
path := filepath.Join(coreDir, "kb.yaml")
|
||||
if err := m.Write(path, string(data)); err != nil {
|
||||
return fmt.Errorf("failed to write kb config: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// WikiRepoURL returns the full clone URL for a repo's wiki.
|
||||
func (kb *KBConfig) WikiRepoURL(repoName string) string {
|
||||
return fmt.Sprintf("%s/%s.wiki.git", kb.Wiki.Remote, repoName)
|
||||
}
|
||||
|
||||
// WikiLocalPath returns the local path for a repo's wiki clone.
|
||||
func (kb *KBConfig) WikiLocalPath(root, repoName string) string {
|
||||
return filepath.Join(root, ".core", kb.Wiki.Dir, repoName)
|
||||
}
|
||||
107
repos/kbconfig_test.go
Normal file
107
repos/kbconfig_test.go
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
package repos
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"forge.lthn.ai/core/go-io"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// ── DefaultKBConfig ────────────────────────────────────────────────
|
||||
|
||||
func TestDefaultKBConfig_Good(t *testing.T) {
|
||||
kb := DefaultKBConfig()
|
||||
assert.Equal(t, 1, kb.Version)
|
||||
assert.True(t, kb.Wiki.Enabled)
|
||||
assert.Equal(t, "kb/wiki", kb.Wiki.Dir)
|
||||
assert.Contains(t, kb.Wiki.Remote, "forge.lthn.ai")
|
||||
assert.Equal(t, "qdrant.lthn.sh", kb.Search.QdrantHost)
|
||||
assert.Equal(t, 6334, kb.Search.QdrantPort)
|
||||
assert.Equal(t, "openbrain", kb.Search.Collection)
|
||||
assert.Equal(t, "embeddinggemma", kb.Search.EmbedModel)
|
||||
assert.Equal(t, 5, kb.Search.TopK)
|
||||
}
|
||||
|
||||
// ── Load / Save round-trip ─────────────────────────────────────────
|
||||
|
||||
func TestKBConfig_LoadSave_Good(t *testing.T) {
|
||||
m := io.NewMockMedium()
|
||||
_ = m.EnsureDir("/workspace/.core")
|
||||
|
||||
kb := DefaultKBConfig()
|
||||
kb.Search.TopK = 10
|
||||
kb.Search.Collection = "custom_brain"
|
||||
|
||||
err := SaveKBConfig(m, "/workspace", kb)
|
||||
require.NoError(t, err)
|
||||
|
||||
loaded, err := LoadKBConfig(m, "/workspace")
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 1, loaded.Version)
|
||||
assert.Equal(t, 10, loaded.Search.TopK)
|
||||
assert.Equal(t, "custom_brain", loaded.Search.Collection)
|
||||
assert.True(t, loaded.Wiki.Enabled)
|
||||
}
|
||||
|
||||
func TestKBConfig_Load_Good_NoFile(t *testing.T) {
|
||||
m := io.NewMockMedium()
|
||||
_ = m.EnsureDir("/workspace/.core")
|
||||
|
||||
kb, err := LoadKBConfig(m, "/workspace")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, DefaultKBConfig().Search.Collection, kb.Search.Collection)
|
||||
}
|
||||
|
||||
func TestKBConfig_Load_Good_PartialOverride(t *testing.T) {
|
||||
m := io.NewMockMedium()
|
||||
_ = m.Write("/workspace/.core/kb.yaml", `
|
||||
version: 1
|
||||
search:
|
||||
top_k: 20
|
||||
collection: my_brain
|
||||
`)
|
||||
|
||||
kb, err := LoadKBConfig(m, "/workspace")
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 20, kb.Search.TopK)
|
||||
assert.Equal(t, "my_brain", kb.Search.Collection)
|
||||
// Defaults preserved
|
||||
assert.True(t, kb.Wiki.Enabled)
|
||||
assert.Equal(t, "embeddinggemma", kb.Search.EmbedModel)
|
||||
}
|
||||
|
||||
func TestKBConfig_Load_Bad_InvalidYAML(t *testing.T) {
|
||||
m := io.NewMockMedium()
|
||||
_ = m.Write("/workspace/.core/kb.yaml", "{{{{broken")
|
||||
|
||||
_, err := LoadKBConfig(m, "/workspace")
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "failed to parse")
|
||||
}
|
||||
|
||||
// ── WikiRepoURL ────────────────────────────────────────────────────
|
||||
|
||||
func TestKBConfig_WikiRepoURL_Good(t *testing.T) {
|
||||
kb := DefaultKBConfig()
|
||||
url := kb.WikiRepoURL("go-scm")
|
||||
assert.Equal(t, "ssh://git@forge.lthn.ai:2223/core/go-scm.wiki.git", url)
|
||||
}
|
||||
|
||||
func TestKBConfig_WikiRepoURL_Good_CustomRemote(t *testing.T) {
|
||||
kb := &KBConfig{
|
||||
Wiki: WikiConfig{Remote: "ssh://git@git.example.com/org"},
|
||||
}
|
||||
url := kb.WikiRepoURL("my-repo")
|
||||
assert.Equal(t, "ssh://git@git.example.com/org/my-repo.wiki.git", url)
|
||||
}
|
||||
|
||||
// ── WikiLocalPath ──────────────────────────────────────────────────
|
||||
|
||||
func TestKBConfig_WikiLocalPath_Good(t *testing.T) {
|
||||
kb := DefaultKBConfig()
|
||||
path := kb.WikiLocalPath("/workspace", "go-scm")
|
||||
assert.Equal(t, "/workspace/.core/kb/wiki/go-scm", path)
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue