You've already forked agentic-coding-workflow
295 lines
9.4 KiB
Go
295 lines
9.4 KiB
Go
package codeindex
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
"github.com/pgvector/pgvector-go"
|
|
|
|
codeindexsqlc "github.com/yan1h/agent-coding-workflow/internal/codeindex/sqlc"
|
|
"github.com/yan1h/agent-coding-workflow/internal/infra/errs"
|
|
)
|
|
|
|
// ChunkRow is a fully-populated chunk ready for bulk insert (embedding included).
|
|
type ChunkRow struct {
|
|
FilePath string
|
|
Lang string
|
|
StartLine int
|
|
EndLine int
|
|
Content string
|
|
ContentSHA []byte
|
|
TokenCount int
|
|
Embedding []float32
|
|
}
|
|
|
|
// Repository is the codeindex persistence contract.
|
|
type Repository interface {
|
|
InsertRun(ctx context.Context, r *Run) (*Run, error)
|
|
GetRun(ctx context.Context, id uuid.UUID) (*Run, error)
|
|
GetRunByCommit(ctx context.Context, wsID uuid.UUID, commitSHA, model string) (*Run, error)
|
|
GetLatestCompletedRun(ctx context.Context, wsID uuid.UUID) (*Run, error)
|
|
MarkRunning(ctx context.Context, id uuid.UUID) error
|
|
MarkCompleted(ctx context.Context, id uuid.UUID, files, chunks int) error
|
|
MarkFailed(ctx context.Context, id uuid.UUID, errMsg string) error
|
|
MarkStale(ctx context.Context, wsID uuid.UUID) error
|
|
MarkStuckStale(ctx context.Context, runningBefore time.Time) ([]uuid.UUID, error)
|
|
// ReplaceChunks deletes any existing chunks for run then bulk-inserts rows.
|
|
ReplaceChunks(ctx context.Context, runID, wsID uuid.UUID, commitSHA string, rows []ChunkRow) error
|
|
// SearchKNN returns the top-k chunks of the latest completed run for wsID
|
|
// ordered by cosine similarity to queryVec, with optional path/lang filters.
|
|
SearchKNN(ctx context.Context, wsID uuid.UUID, queryVec []float32, topK int, pathPrefix, lang string) ([]Snippet, error)
|
|
}
|
|
|
|
// PgRepository is the production Repository on a pgxpool.Pool.
|
|
type PgRepository struct {
|
|
pool *pgxpool.Pool
|
|
q *codeindexsqlc.Queries
|
|
}
|
|
|
|
// NewPostgresRepository builds a PgRepository.
|
|
func NewPostgresRepository(pool *pgxpool.Pool) *PgRepository {
|
|
return &PgRepository{pool: pool, q: codeindexsqlc.New(pool)}
|
|
}
|
|
|
|
var _ Repository = (*PgRepository)(nil)
|
|
|
|
func pgUUID(u uuid.UUID) pgtype.UUID { return pgtype.UUID{Bytes: u, Valid: true} }
|
|
|
|
func pgUUIDPtr(u *uuid.UUID) pgtype.UUID {
|
|
if u == nil {
|
|
return pgtype.UUID{}
|
|
}
|
|
return pgtype.UUID{Bytes: *u, Valid: true}
|
|
}
|
|
|
|
func uuidPtr(p pgtype.UUID) *uuid.UUID {
|
|
if !p.Valid {
|
|
return nil
|
|
}
|
|
id := uuid.UUID(p.Bytes)
|
|
return &id
|
|
}
|
|
|
|
func runFromRow(r codeindexsqlc.CodeIndexRun) *Run {
|
|
out := &Run{
|
|
ID: uuid.UUID(r.ID.Bytes),
|
|
WorkspaceID: uuid.UUID(r.WorkspaceID.Bytes),
|
|
WorktreeID: uuidPtr(r.WorktreeID),
|
|
Branch: r.Branch,
|
|
CommitSHA: r.CommitSha,
|
|
Status: r.Status,
|
|
EmbeddingEndpointID: uuidPtr(r.EmbeddingEndpointID),
|
|
EmbeddingModel: r.EmbeddingModel,
|
|
Dims: int(r.Dims),
|
|
FilesIndexed: int(r.FilesIndexed),
|
|
ChunksIndexed: int(r.ChunksIndexed),
|
|
Error: r.Error,
|
|
}
|
|
if r.CreatedAt.Valid {
|
|
out.CreatedAt = r.CreatedAt.Time
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (r *PgRepository) InsertRun(ctx context.Context, run *Run) (*Run, error) {
|
|
row, err := r.q.InsertRun(ctx, codeindexsqlc.InsertRunParams{
|
|
ID: pgUUID(run.ID),
|
|
WorkspaceID: pgUUID(run.WorkspaceID),
|
|
WorktreeID: pgUUIDPtr(run.WorktreeID),
|
|
Branch: run.Branch,
|
|
CommitSha: run.CommitSHA,
|
|
Status: StatusPending,
|
|
EmbeddingEndpointID: pgUUIDPtr(run.EmbeddingEndpointID),
|
|
EmbeddingModel: run.EmbeddingModel,
|
|
Dims: int32(run.Dims),
|
|
})
|
|
if err != nil {
|
|
return nil, errs.Wrap(err, errs.CodeInternal, "insert code index run")
|
|
}
|
|
return runFromRow(row), nil
|
|
}
|
|
|
|
func (r *PgRepository) GetRun(ctx context.Context, id uuid.UUID) (*Run, error) {
|
|
row, err := r.q.GetRun(ctx, pgUUID(id))
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return nil, errs.New(errs.CodeNotFound, "code index run not found")
|
|
}
|
|
return nil, errs.Wrap(err, errs.CodeInternal, "get code index run")
|
|
}
|
|
return runFromRow(row), nil
|
|
}
|
|
|
|
func (r *PgRepository) GetRunByCommit(ctx context.Context, wsID uuid.UUID, commitSHA, model string) (*Run, error) {
|
|
row, err := r.q.GetRunByCommit(ctx, codeindexsqlc.GetRunByCommitParams{
|
|
WorkspaceID: pgUUID(wsID),
|
|
CommitSha: commitSHA,
|
|
EmbeddingModel: model,
|
|
})
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return nil, nil
|
|
}
|
|
return nil, errs.Wrap(err, errs.CodeInternal, "get run by commit")
|
|
}
|
|
return runFromRow(row), nil
|
|
}
|
|
|
|
func (r *PgRepository) GetLatestCompletedRun(ctx context.Context, wsID uuid.UUID) (*Run, error) {
|
|
row, err := r.q.GetLatestCompletedRun(ctx, pgUUID(wsID))
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return nil, nil
|
|
}
|
|
return nil, errs.Wrap(err, errs.CodeInternal, "get latest completed run")
|
|
}
|
|
return runFromRow(row), nil
|
|
}
|
|
|
|
func (r *PgRepository) MarkRunning(ctx context.Context, id uuid.UUID) error {
|
|
if err := r.q.MarkRunRunning(ctx, pgUUID(id)); err != nil {
|
|
return errs.Wrap(err, errs.CodeInternal, "mark run running")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *PgRepository) MarkCompleted(ctx context.Context, id uuid.UUID, files, chunks int) error {
|
|
if err := r.q.MarkRunCompleted(ctx, codeindexsqlc.MarkRunCompletedParams{
|
|
ID: pgUUID(id),
|
|
FilesIndexed: int32(files),
|
|
ChunksIndexed: int32(chunks),
|
|
}); err != nil {
|
|
return errs.Wrap(err, errs.CodeInternal, "mark run completed")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *PgRepository) MarkFailed(ctx context.Context, id uuid.UUID, errMsg string) error {
|
|
msg := errMsg
|
|
if err := r.q.MarkRunFailed(ctx, codeindexsqlc.MarkRunFailedParams{
|
|
ID: pgUUID(id),
|
|
Error: &msg,
|
|
}); err != nil {
|
|
return errs.Wrap(err, errs.CodeInternal, "mark run failed")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *PgRepository) MarkStale(ctx context.Context, wsID uuid.UUID) error {
|
|
if err := r.q.MarkRunsStale(ctx, pgUUID(wsID)); err != nil {
|
|
return errs.Wrap(err, errs.CodeInternal, "mark runs stale")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *PgRepository) MarkStuckStale(ctx context.Context, runningBefore time.Time) ([]uuid.UUID, error) {
|
|
rows, err := r.q.MarkStuckRunsStale(ctx, pgtype.Timestamptz{Time: runningBefore, Valid: true})
|
|
if err != nil {
|
|
return nil, errs.Wrap(err, errs.CodeInternal, "mark stuck runs stale")
|
|
}
|
|
out := make([]uuid.UUID, 0, len(rows))
|
|
for _, p := range rows {
|
|
out = append(out, uuid.UUID(p.Bytes))
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// ReplaceChunks deletes prior chunks for the run and bulk-inserts the given rows
|
|
// in a single transaction. The embedding column uses the pgvector text literal.
|
|
func (r *PgRepository) ReplaceChunks(ctx context.Context, runID, wsID uuid.UUID, commitSHA string, rows []ChunkRow) error {
|
|
tx, err := r.pool.Begin(ctx)
|
|
if err != nil {
|
|
return errs.Wrap(err, errs.CodeInternal, "begin chunk tx")
|
|
}
|
|
defer func() { _ = tx.Rollback(ctx) }()
|
|
|
|
if _, err := tx.Exec(ctx, `DELETE FROM code_chunks WHERE run_id = $1`, pgUUID(runID)); err != nil {
|
|
return errs.Wrap(err, errs.CodeInternal, "delete prior chunks")
|
|
}
|
|
|
|
const insertSQL = `INSERT INTO code_chunks (
|
|
id, run_id, workspace_id, commit_sha, file_path, lang,
|
|
start_line, end_line, content, content_sha, token_count, embedding
|
|
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12::vector)`
|
|
|
|
batch := &pgx.Batch{}
|
|
for _, row := range rows {
|
|
var lang *string
|
|
if row.Lang != "" {
|
|
l := row.Lang
|
|
lang = &l
|
|
}
|
|
var tok *int32
|
|
if row.TokenCount > 0 {
|
|
t := int32(row.TokenCount)
|
|
tok = &t
|
|
}
|
|
batch.Queue(insertSQL,
|
|
pgUUID(uuid.New()), pgUUID(runID), pgUUID(wsID), commitSHA,
|
|
row.FilePath, lang, int32(row.StartLine), int32(row.EndLine),
|
|
row.Content, row.ContentSHA, tok, pgvector.NewVector(row.Embedding),
|
|
)
|
|
}
|
|
br := tx.SendBatch(ctx, batch)
|
|
for range rows {
|
|
if _, err := br.Exec(); err != nil {
|
|
_ = br.Close()
|
|
return errs.Wrap(err, errs.CodeInternal, "insert chunk")
|
|
}
|
|
}
|
|
if err := br.Close(); err != nil {
|
|
return errs.Wrap(err, errs.CodeInternal, "close chunk batch")
|
|
}
|
|
if err := tx.Commit(ctx); err != nil {
|
|
return errs.Wrap(err, errs.CodeInternal, "commit chunk tx")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// SearchKNN runs cosine-distance KNN scoped to the latest completed run for the
|
|
// workspace. Score is 1 - cosine_distance (so higher is more similar). Optional
|
|
// path_prefix / lang filters narrow the candidate set.
|
|
func (r *PgRepository) SearchKNN(ctx context.Context, wsID uuid.UUID, queryVec []float32, topK int, pathPrefix, lang string) ([]Snippet, error) {
|
|
if topK <= 0 {
|
|
topK = 10
|
|
}
|
|
q := `
|
|
WITH latest AS (
|
|
SELECT id FROM code_index_runs
|
|
WHERE workspace_id = $1 AND status = 'completed'
|
|
ORDER BY created_at DESC LIMIT 1
|
|
)
|
|
SELECT c.file_path, c.start_line, c.end_line, c.content, c.commit_sha,
|
|
1 - (c.embedding <=> $2::vector) AS score
|
|
FROM code_chunks c
|
|
JOIN latest ON c.run_id = latest.id
|
|
WHERE ($3::text = '' OR c.file_path LIKE $3 || '%')
|
|
AND ($4::text = '' OR c.lang = $4)
|
|
ORDER BY c.embedding <=> $2::vector
|
|
LIMIT $5`
|
|
rows, err := r.pool.Query(ctx, q, pgUUID(wsID), pgvector.NewVector(queryVec), pathPrefix, lang, int32(topK))
|
|
if err != nil {
|
|
return nil, errs.Wrap(err, errs.CodeInternal, "knn search")
|
|
}
|
|
defer rows.Close()
|
|
var out []Snippet
|
|
for rows.Next() {
|
|
var s Snippet
|
|
var start, end int32
|
|
if err := rows.Scan(&s.FilePath, &start, &end, &s.Content, &s.CommitSHA, &s.Score); err != nil {
|
|
return nil, errs.Wrap(err, errs.CodeInternal, "scan knn row")
|
|
}
|
|
s.StartLine, s.EndLine = int(start), int(end)
|
|
out = append(out, s)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, errs.Wrap(err, errs.CodeInternal, "knn rows")
|
|
}
|
|
return out, nil
|
|
}
|