Files
enkly-ai/services/knowledge/internal/application/service.go
enkly 79ce8dc4c0
Some checks failed
Go CI Pipeline / build_and_test (push) Failing after 22m0s
Init Project
2026-10-30 18:06:55 +01:00

96 lines
3.1 KiB
Go

// Package application holds knowledge's use cases — the content of
// internal/rag/store.go's IndexFile/Search methods, now split from
// their SQL (internal/infrastructure/postgres) so this package depends
// only on domain.DocumentRepository/domain.Embedder.
package application
import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"enkly/services/knowledge/internal/domain"
)
type Service struct {
Documents domain.DocumentRepository
Embedder domain.Embedder
}
func hashContent(content string) string {
sum := sha256.Sum256([]byte(content))
return hex.EncodeToString(sum[:])
}
// upsertChunk replaces rag.Store.UpsertChunk: embeds a single chunk and
// stores it, skipping the embedding call entirely if the chunk's
// content hasn't changed since it was last indexed.
func (s *Service) upsertChunk(ctx context.Context, filePath string, chunkIndex int, content string) error {
hash := hashContent(content)
existing, err := s.Documents.ExistingContentHash(ctx, filePath, chunkIndex)
if err != nil {
return err
}
if existing == hash {
return nil // unchanged, skip re-embedding
}
vec, err := s.Embedder.Embed(ctx, content)
if err != nil {
return fmt.Errorf("knowledge: embed %s chunk %d: %w", filePath, chunkIndex, err)
}
if len(vec) != domain.EmbeddingDim {
return fmt.Errorf("knowledge: embedding model returned %d-dimensional vectors but the schema declares vector(%d) — "+
"use a %d-dim model (the default nomic-embed-text is) or change the column and re-index",
len(vec), domain.EmbeddingDim, domain.EmbeddingDim)
}
if err := s.Documents.Upsert(ctx, filePath, chunkIndex, content, hash, vec); err != nil {
return fmt.Errorf("knowledge: upsert %s chunk %d: %w", filePath, chunkIndex, err)
}
return nil
}
// IndexFile replaces rag.Store.IndexFile.
func (s *Service) IndexFile(ctx context.Context, filePath, content string) (indexed, skipped int, err error) {
chunks := domain.ChunkText(content)
if err := s.Documents.DeleteChunksFrom(ctx, filePath, len(chunks)); err != nil {
return 0, 0, fmt.Errorf("knowledge: prune stale chunks for %s: %w", filePath, err)
}
for i, chunk := range chunks {
before, _ := s.Documents.ExistingContentHash(ctx, filePath, i)
if err := s.upsertChunk(ctx, filePath, i, chunk); err != nil {
return indexed, skipped, err
}
if before == hashContent(chunk) {
skipped++
} else {
indexed++
}
}
return indexed, skipped, nil
}
// PruneMissing replaces rag.Store.pruneMissing.
func (s *Service) PruneMissing(ctx context.Context, keepFilePaths []string) (int, error) {
return s.Documents.PruneMissing(ctx, keepFilePaths)
}
// Search replaces rag.Store.Search.
func (s *Service) Search(ctx context.Context, query string, topK int) ([]domain.CodeChunk, error) {
if topK <= 0 {
topK = 6
}
vec, err := s.Embedder.Embed(ctx, query)
if err != nil {
return nil, fmt.Errorf("knowledge: embed query: %w", err)
}
return s.Documents.Search(ctx, vec, topK)
}
// ListIndexedFiles replaces db.ListIndexedFiles.
func (s *Service) ListIndexedFiles(ctx context.Context, limit int) ([]string, error) {
return s.Documents.ListIndexedFiles(ctx, limit)
}