You've already forked agentic-coding-workflow
bugfix
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
package projectmemory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/yan1h/agent-coding-workflow/internal/infra/errs"
|
||||
"github.com/yan1h/agent-coding-workflow/internal/infra/llm"
|
||||
)
|
||||
|
||||
// EmbedderSelector resolves the configured platform embedder, or a typed error
|
||||
// when embeddings are disabled. Implemented in app.go (shared with codeindex).
|
||||
type EmbedderSelector interface {
|
||||
Embedder(ctx context.Context) (emb llm.Embedder, endpointID uuid.UUID, model string, dims int, err error)
|
||||
}
|
||||
|
||||
// Service is the projectmemory application service. Scope enforcement (project
|
||||
// access) is performed by callers (MCP tools / supervisor) before invoking it.
|
||||
type Service interface {
|
||||
Write(ctx context.Context, in WriteInput) (*Entry, error)
|
||||
Search(ctx context.Context, q Query) ([]Entry, error)
|
||||
List(ctx context.Context, projectID uuid.UUID, kind string) ([]Entry, error)
|
||||
Delete(ctx context.Context, id uuid.UUID) error
|
||||
RenderAgentsMD(ctx context.Context, projectID uuid.UUID, wsID *uuid.UUID) (string, error)
|
||||
}
|
||||
|
||||
type service struct {
|
||||
repo Repository
|
||||
embedSel EmbedderSelector
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
// NewService constructs the projectmemory Service. embedSel may be nil (no
|
||||
// embeddings → keyword fallback only); log may be nil.
|
||||
func NewService(repo Repository, embedSel EmbedderSelector, log *slog.Logger) Service {
|
||||
if log == nil {
|
||||
log = slog.Default()
|
||||
}
|
||||
return &service{repo: repo, embedSel: embedSel, log: log}
|
||||
}
|
||||
|
||||
func (s *service) Write(ctx context.Context, in WriteInput) (*Entry, error) {
|
||||
if !IsValidKind(in.Kind) {
|
||||
return nil, errs.New(errs.CodeInvalidInput, "invalid memory kind")
|
||||
}
|
||||
if in.Title == "" || in.Body == "" {
|
||||
return nil, errs.New(errs.CodeInvalidInput, "title and body required")
|
||||
}
|
||||
// Best-effort embed of title+body; null embedding when disabled/unavailable.
|
||||
var model *string
|
||||
var vec []float32
|
||||
if emb, _, m, _, err := s.embedder(ctx); err == nil {
|
||||
vecs, eerr := emb.Embed(ctx, m, []string{in.Title + "\n\n" + in.Body})
|
||||
if eerr != nil {
|
||||
s.log.Warn("projectmemory.embed_failed", "err", eerr.Error())
|
||||
} else if len(vecs) == 1 {
|
||||
vec = vecs[0]
|
||||
model = &m
|
||||
}
|
||||
}
|
||||
entry, err := s.repo.Insert(ctx, in, model)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if vec != nil && model != nil {
|
||||
if err := s.repo.SetEmbedding(ctx, entry.ID, vec, *model); err != nil {
|
||||
// Non-fatal: the entry exists and is keyword-searchable.
|
||||
s.log.Warn("projectmemory.set_embedding_failed", "id", entry.ID, "err", err.Error())
|
||||
} else {
|
||||
entry.EmbeddingModel = model
|
||||
}
|
||||
}
|
||||
return entry, nil
|
||||
}
|
||||
|
||||
func (s *service) Search(ctx context.Context, q Query) ([]Entry, error) {
|
||||
if q.Text == "" {
|
||||
return nil, errs.New(errs.CodeInvalidInput, "query text required")
|
||||
}
|
||||
if q.Kind != "" && !IsValidKind(q.Kind) {
|
||||
return nil, errs.New(errs.CodeInvalidInput, "invalid memory kind")
|
||||
}
|
||||
// Vector path when an embedder is configured; ILIKE/tags fallback otherwise.
|
||||
if emb, _, model, _, err := s.embedder(ctx); err == nil {
|
||||
vecs, eerr := emb.Embed(ctx, model, []string{q.Text})
|
||||
if eerr == nil && len(vecs) == 1 {
|
||||
res, kerr := s.repo.SearchKNN(ctx, q.ProjectID, vecs[0], q.Kind, q.TopK)
|
||||
if kerr != nil {
|
||||
return nil, kerr
|
||||
}
|
||||
// If the project has no embedded entries yet, fall back to keyword.
|
||||
if len(res) > 0 {
|
||||
return res, nil
|
||||
}
|
||||
} else if eerr != nil {
|
||||
s.log.Warn("projectmemory.search_embed_failed", "err", eerr.Error())
|
||||
}
|
||||
}
|
||||
return s.repo.SearchKeyword(ctx, q.ProjectID, q.Kind, q.Text, q.TopK)
|
||||
}
|
||||
|
||||
func (s *service) List(ctx context.Context, projectID uuid.UUID, kind string) ([]Entry, error) {
|
||||
if kind != "" && !IsValidKind(kind) {
|
||||
return nil, errs.New(errs.CodeInvalidInput, "invalid memory kind")
|
||||
}
|
||||
return s.repo.ListByProject(ctx, projectID, kind)
|
||||
}
|
||||
|
||||
func (s *service) Delete(ctx context.Context, id uuid.UUID) error {
|
||||
return s.repo.Delete(ctx, id)
|
||||
}
|
||||
|
||||
func (s *service) RenderAgentsMD(ctx context.Context, projectID uuid.UUID, wsID *uuid.UUID) (string, error) {
|
||||
entries, err := s.repo.ListByProject(ctx, projectID, "")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return renderAgentsMD(entries, wsID), nil
|
||||
}
|
||||
|
||||
// embedder returns the selector result, or an error when no selector is set.
|
||||
func (s *service) embedder(ctx context.Context) (llm.Embedder, uuid.UUID, string, int, error) {
|
||||
if s.embedSel == nil {
|
||||
return nil, uuid.Nil, "", 0, llm.ErrEmbeddingsUnsupported
|
||||
}
|
||||
return s.embedSel.Embedder(ctx)
|
||||
}
|
||||
Reference in New Issue
Block a user