feat: implement file upload handler and related functionality

- Added file upload handler to process both multipart and raw file uploads.
- Implemented parsing logic for upload requests, including handling file metadata.
- Introduced SaveFileDecodedInput structure for handling decoded file uploads.
- Created unit tests for file upload parsing and validation.

feat: add metadata retry configuration and functionality

- Introduced MetadataRetryConfig to the application configuration.
- Implemented MetadataRetryer to handle retrying metadata extraction for thoughts.
- Added new tool for retrying failed metadata extractions.
- Updated thought metadata structure to include status and timestamps for metadata processing.

fix: enhance metadata normalization and error handling

- Updated metadata normalization functions to track status and errors.
- Improved handling of metadata extraction failures during thought updates and captures.
- Ensured that metadata status is correctly set during various operations.

refactor: streamline file saving logic in FilesTool

- Refactored Save method in FilesTool to utilize new SaveDecoded method.
- Simplified project and thought ID resolution logic during file saving.
This commit is contained in:
2026-03-30 22:57:21 +02:00
parent 7f2b2b9fee
commit 72b4f7ce3d
21 changed files with 890 additions and 126 deletions

View File

@@ -277,6 +277,28 @@ func (db *DB) UpdateThought(ctx context.Context, id uuid.UUID, content string, e
return db.GetThought(ctx, id)
}
func (db *DB) UpdateThoughtMetadata(ctx context.Context, id uuid.UUID, metadata thoughttypes.ThoughtMetadata) (thoughttypes.Thought, error) {
metadataBytes, err := json.Marshal(metadata)
if err != nil {
return thoughttypes.Thought{}, fmt.Errorf("marshal updated metadata: %w", err)
}
tag, err := db.pool.Exec(ctx, `
update thoughts
set metadata = $2::jsonb,
updated_at = now()
where guid = $1
`, id, metadataBytes)
if err != nil {
return thoughttypes.Thought{}, fmt.Errorf("update thought metadata: %w", err)
}
if tag.RowsAffected() == 0 {
return thoughttypes.Thought{}, pgx.ErrNoRows
}
return db.GetThought(ctx, id)
}
func (db *DB) DeleteThought(ctx context.Context, id uuid.UUID) error {
tag, err := db.pool.Exec(ctx, `delete from thoughts where guid = $1`, id)
if err != nil {
@@ -309,6 +331,58 @@ func (db *DB) RecentThoughts(ctx context.Context, projectID *uuid.UUID, limit in
return db.ListThoughts(ctx, filter)
}
func (db *DB) ListThoughtsPendingMetadataRetry(ctx context.Context, limit int, projectID *uuid.UUID, includeArchived bool, olderThanDays int) ([]thoughttypes.Thought, error) {
args := make([]any, 0, 4)
conditions := []string{
"(metadata->>'metadata_status' = 'pending' or metadata->>'metadata_status' = 'failed')",
}
if !includeArchived {
conditions = append(conditions, "archived_at is null")
}
if projectID != nil {
args = append(args, *projectID)
conditions = append(conditions, fmt.Sprintf("project_id = $%d", len(args)))
}
if olderThanDays > 0 {
args = append(args, olderThanDays)
conditions = append(conditions, fmt.Sprintf("coalesce(nullif(metadata->>'metadata_last_attempted_at', '')::timestamptz, created_at) <= now() - ($%d * interval '1 day')", len(args)))
}
query := `
select guid, content, metadata, project_id, archived_at, created_at, updated_at
from thoughts
where ` + strings.Join(conditions, " and ")
args = append(args, limit)
query += fmt.Sprintf(" order by coalesce(nullif(metadata->>'metadata_last_attempted_at', '')::timestamptz, created_at) asc limit $%d", len(args))
rows, err := db.pool.Query(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("list thoughts pending metadata retry: %w", err)
}
defer rows.Close()
thoughts := make([]thoughttypes.Thought, 0, limit)
for rows.Next() {
var thought thoughttypes.Thought
var metadataBytes []byte
if err := rows.Scan(&thought.ID, &thought.Content, &metadataBytes, &thought.ProjectID, &thought.ArchivedAt, &thought.CreatedAt, &thought.UpdatedAt); err != nil {
return nil, fmt.Errorf("scan pending metadata retry thought: %w", err)
}
if err := json.Unmarshal(metadataBytes, &thought.Metadata); err != nil {
return nil, fmt.Errorf("decode pending metadata retry thought: %w", err)
}
thoughts = append(thoughts, thought)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("iterate pending metadata retry thoughts: %w", err)
}
return thoughts, nil
}
func (db *DB) SearchSimilarThoughts(ctx context.Context, embedding []float32, embeddingModel string, threshold float64, limit int, projectID *uuid.UUID, excludeID *uuid.UUID) ([]thoughttypes.SearchResult, error) {
args := []any{pgvector.NewVector(embedding), threshold, embeddingModel}
conditions := []string{