355 lines
11 KiB
C#
Executable File
355 lines
11 KiB
C#
Executable File
using System.Text.Json;
|
|
using MarketAlly.AIPlugin;
|
|
|
|
namespace MarketAlly.AIPlugin.Context
|
|
{
|
|
/// <summary>
|
|
/// Plugin for deleting context entries from storage.
|
|
/// Handles both individual entry deletion and bulk operations.
|
|
/// </summary>
|
|
[AIPlugin("ContextDeletion", "Delete context entries from storage with support for individual and bulk operations")]
|
|
public class ContextDeletionPlugin : IAIPlugin
|
|
{
|
|
[AIParameter("ID of the context entry to delete", required: true)]
|
|
public string EntryId { get; set; } = "";
|
|
|
|
[AIParameter("Project path where context is stored", required: false)]
|
|
public string? ProjectPath { get; set; }
|
|
|
|
[AIParameter("Type of deletion: 'single', 'bulk', 'by_tag', 'by_type', 'by_date_range'", required: false)]
|
|
public string DeletionType { get; set; } = "single";
|
|
|
|
[AIParameter("Additional criteria for bulk deletion (JSON format)", required: false)]
|
|
public string? DeletionCriteria { get; set; }
|
|
|
|
[AIParameter("Confirm deletion (must be true to proceed)", required: false)]
|
|
public bool Confirm { get; set; } = false;
|
|
|
|
public IReadOnlyDictionary<string, Type> SupportedParameters => new Dictionary<string, Type>
|
|
{
|
|
["entryId"] = typeof(string),
|
|
["entryid"] = typeof(string),
|
|
["projectPath"] = typeof(string),
|
|
["projectpath"] = typeof(string),
|
|
["deletionType"] = typeof(string),
|
|
["deletiontype"] = typeof(string),
|
|
["deletionCriteria"] = typeof(string),
|
|
["deletioncriteria"] = typeof(string),
|
|
["confirm"] = typeof(bool)
|
|
};
|
|
|
|
public async Task<AIPluginResult> ExecuteAsync(IReadOnlyDictionary<string, object> parameters)
|
|
{
|
|
try
|
|
{
|
|
// Extract parameters
|
|
var entryId = parameters["entryId"].ToString()!;
|
|
var projectPath = parameters.TryGetValue("projectPath", out var pp) ? pp?.ToString() : null;
|
|
var deletionType = parameters.TryGetValue("deletionType", out var dt) ? dt.ToString()!.ToLower() : "single";
|
|
var deletionCriteria = parameters.TryGetValue("deletionCriteria", out var dc) ? dc?.ToString() : null;
|
|
var confirm = parameters.TryGetValue("confirm", out var c) ? Convert.ToBoolean(c) : false;
|
|
|
|
if (!confirm)
|
|
{
|
|
return new AIPluginResult(new { Error = "Deletion not confirmed" },
|
|
"Deletion requires explicit confirmation. Set 'confirm' parameter to true.");
|
|
}
|
|
|
|
var storagePath = await GetStoragePathAsync(projectPath);
|
|
|
|
return deletionType switch
|
|
{
|
|
"single" => await DeleteSingleEntryAsync(entryId, storagePath),
|
|
"bulk" => await DeleteBulkEntriesAsync(deletionCriteria, storagePath),
|
|
"by_tag" => await DeleteByTagAsync(entryId, storagePath), // entryId as tag name
|
|
"by_type" => await DeleteByTypeAsync(entryId, storagePath), // entryId as type name
|
|
"by_date_range" => await DeleteByDateRangeAsync(deletionCriteria, storagePath),
|
|
_ => new AIPluginResult(new { Error = "Invalid deletion type" },
|
|
$"Unknown deletion type: {deletionType}")
|
|
};
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return new AIPluginResult(ex, "Failed to delete context entry/entries");
|
|
}
|
|
}
|
|
|
|
private async Task<string> GetStoragePathAsync(string? projectPath)
|
|
{
|
|
if (string.IsNullOrEmpty(projectPath))
|
|
{
|
|
projectPath = Directory.GetCurrentDirectory();
|
|
}
|
|
|
|
var contextDir = Path.Combine(projectPath, ".context");
|
|
return contextDir;
|
|
}
|
|
|
|
private async Task<AIPluginResult> DeleteSingleEntryAsync(string entryId, string storagePath)
|
|
{
|
|
try
|
|
{
|
|
var deletedFromFiles = 0;
|
|
var filesProcessed = 0;
|
|
|
|
// Get all context files
|
|
var contextFiles = Directory.GetFiles(storagePath, "context-*.json")
|
|
.Where(f => !f.EndsWith("context-index.json"))
|
|
.ToList();
|
|
|
|
foreach (var filePath in contextFiles)
|
|
{
|
|
filesProcessed++;
|
|
var fileContent = await File.ReadAllTextAsync(filePath);
|
|
var entries = JsonSerializer.Deserialize<List<StoredContextEntry>>(fileContent);
|
|
|
|
if (entries == null) continue;
|
|
|
|
var originalCount = entries.Count;
|
|
entries.RemoveAll(e => e.Id == entryId);
|
|
|
|
if (entries.Count < originalCount)
|
|
{
|
|
// Entry was found and removed
|
|
var updatedJson = JsonSerializer.Serialize(entries, new JsonSerializerOptions
|
|
{
|
|
WriteIndented = true,
|
|
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
|
});
|
|
|
|
await File.WriteAllTextAsync(filePath, updatedJson);
|
|
deletedFromFiles++;
|
|
|
|
// Update the index
|
|
await RemoveFromIndexAsync(entryId, storagePath);
|
|
|
|
return new AIPluginResult(new
|
|
{
|
|
Success = true,
|
|
EntryId = entryId,
|
|
DeletedFrom = Path.GetFileName(filePath),
|
|
RemainingEntries = entries.Count,
|
|
Operation = "single_deletion"
|
|
}, $"Successfully deleted entry {entryId}");
|
|
}
|
|
}
|
|
|
|
return new AIPluginResult(new
|
|
{
|
|
Success = false,
|
|
EntryId = entryId,
|
|
FilesSearched = filesProcessed,
|
|
Operation = "single_deletion"
|
|
}, $"Entry {entryId} not found in any context files");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return new AIPluginResult(ex, $"Failed to delete entry {entryId}");
|
|
}
|
|
}
|
|
|
|
private async Task<AIPluginResult> DeleteBulkEntriesAsync(string? criteriaJson, string storagePath)
|
|
{
|
|
try
|
|
{
|
|
if (string.IsNullOrEmpty(criteriaJson))
|
|
{
|
|
return new AIPluginResult(new { Error = "Bulk deletion requires criteria" },
|
|
"Provide deletion criteria as JSON with fields like 'type', 'priority', 'tags', 'olderThan'");
|
|
}
|
|
|
|
var criteria = JsonSerializer.Deserialize<BulkDeletionCriteria>(criteriaJson);
|
|
if (criteria == null)
|
|
{
|
|
return new AIPluginResult(new { Error = "Invalid criteria format" },
|
|
"Failed to parse deletion criteria JSON");
|
|
}
|
|
|
|
var totalDeleted = 0;
|
|
var filesProcessed = 0;
|
|
var deletedEntries = new List<string>();
|
|
|
|
var contextFiles = Directory.GetFiles(storagePath, "context-*.json")
|
|
.Where(f => !f.EndsWith("context-index.json"))
|
|
.ToList();
|
|
|
|
foreach (var filePath in contextFiles)
|
|
{
|
|
filesProcessed++;
|
|
var fileContent = await File.ReadAllTextAsync(filePath);
|
|
var entries = JsonSerializer.Deserialize<List<StoredContextEntry>>(fileContent);
|
|
|
|
if (entries == null) continue;
|
|
|
|
var originalCount = entries.Count;
|
|
var toDelete = entries.Where(e => MatchesCriteria(e, criteria)).ToList();
|
|
|
|
foreach (var entry in toDelete)
|
|
{
|
|
entries.Remove(entry);
|
|
deletedEntries.Add(entry.Id);
|
|
totalDeleted++;
|
|
}
|
|
|
|
if (entries.Count < originalCount)
|
|
{
|
|
var updatedJson = JsonSerializer.Serialize(entries, new JsonSerializerOptions
|
|
{
|
|
WriteIndented = true,
|
|
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
|
});
|
|
|
|
await File.WriteAllTextAsync(filePath, updatedJson);
|
|
}
|
|
}
|
|
|
|
// Update index for all deleted entries
|
|
foreach (var entryId in deletedEntries)
|
|
{
|
|
await RemoveFromIndexAsync(entryId, storagePath);
|
|
}
|
|
|
|
return new AIPluginResult(new
|
|
{
|
|
Success = true,
|
|
TotalDeleted = totalDeleted,
|
|
FilesProcessed = filesProcessed,
|
|
DeletedEntries = deletedEntries.Take(10).ToList(), // Show first 10
|
|
Criteria = criteria,
|
|
Operation = "bulk_deletion"
|
|
}, $"Successfully deleted {totalDeleted} entries matching criteria");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return new AIPluginResult(ex, "Failed to perform bulk deletion");
|
|
}
|
|
}
|
|
|
|
private async Task<AIPluginResult> DeleteByTagAsync(string tag, string storagePath)
|
|
{
|
|
var criteria = new BulkDeletionCriteria { Tags = new List<string> { tag } };
|
|
var criteriaJson = JsonSerializer.Serialize(criteria);
|
|
return await DeleteBulkEntriesAsync(criteriaJson, storagePath);
|
|
}
|
|
|
|
private async Task<AIPluginResult> DeleteByTypeAsync(string type, string storagePath)
|
|
{
|
|
var criteria = new BulkDeletionCriteria { Type = type };
|
|
var criteriaJson = JsonSerializer.Serialize(criteria);
|
|
return await DeleteBulkEntriesAsync(criteriaJson, storagePath);
|
|
}
|
|
|
|
private async Task<AIPluginResult> DeleteByDateRangeAsync(string? criteriaJson, string storagePath)
|
|
{
|
|
try
|
|
{
|
|
if (string.IsNullOrEmpty(criteriaJson))
|
|
{
|
|
return new AIPluginResult(new { Error = "Date range deletion requires criteria" },
|
|
"Provide criteria with 'olderThan' or 'newerThan' dates");
|
|
}
|
|
|
|
var criteria = JsonSerializer.Deserialize<BulkDeletionCriteria>(criteriaJson);
|
|
if (criteria == null)
|
|
{
|
|
return new AIPluginResult(new { Error = "Invalid date criteria" },
|
|
"Failed to parse date range criteria");
|
|
}
|
|
|
|
return await DeleteBulkEntriesAsync(criteriaJson, storagePath);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return new AIPluginResult(ex, "Failed to delete by date range");
|
|
}
|
|
}
|
|
|
|
private bool MatchesCriteria(StoredContextEntry entry, BulkDeletionCriteria criteria)
|
|
{
|
|
// Check type
|
|
if (!string.IsNullOrEmpty(criteria.Type) &&
|
|
!entry.Type.Equals(criteria.Type, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Check priority
|
|
if (!string.IsNullOrEmpty(criteria.Priority) &&
|
|
!entry.Priority.Equals(criteria.Priority, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Check tags
|
|
if (criteria.Tags?.Any() == true)
|
|
{
|
|
var hasMatchingTag = criteria.Tags.Any(tag =>
|
|
entry.Tags.Any(entryTag => entryTag.Equals(tag, StringComparison.OrdinalIgnoreCase)));
|
|
|
|
if (!hasMatchingTag) return false;
|
|
}
|
|
|
|
// Check date range
|
|
if (criteria.OlderThan.HasValue && entry.Timestamp >= criteria.OlderThan.Value)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (criteria.NewerThan.HasValue && entry.Timestamp <= criteria.NewerThan.Value)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Check project path
|
|
if (!string.IsNullOrEmpty(criteria.ProjectPath) &&
|
|
!entry.ProjectPath.Equals(criteria.ProjectPath, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private async Task RemoveFromIndexAsync(string entryId, string storagePath)
|
|
{
|
|
try
|
|
{
|
|
var indexPath = Path.Combine(storagePath, "context-index.json");
|
|
|
|
if (!File.Exists(indexPath)) return;
|
|
|
|
var indexContent = await File.ReadAllTextAsync(indexPath);
|
|
var indexEntries = JsonSerializer.Deserialize<List<ContextIndexEntry>>(indexContent);
|
|
|
|
if (indexEntries == null) return;
|
|
|
|
indexEntries.RemoveAll(e => e.Id == entryId);
|
|
|
|
var updatedIndexJson = JsonSerializer.Serialize(indexEntries, new JsonSerializerOptions
|
|
{
|
|
WriteIndented = true
|
|
});
|
|
|
|
await File.WriteAllTextAsync(indexPath, updatedIndexJson);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// Log but don't fail the deletion if index update fails
|
|
Console.WriteLine($"Warning: Failed to update index after deletion: {ex.Message}");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Criteria for bulk deletion operations
|
|
/// </summary>
|
|
public class BulkDeletionCriteria
|
|
{
|
|
public string? Type { get; set; }
|
|
public string? Priority { get; set; }
|
|
public List<string>? Tags { get; set; }
|
|
public DateTime? OlderThan { get; set; }
|
|
public DateTime? NewerThan { get; set; }
|
|
public string? ProjectPath { get; set; }
|
|
}
|
|
} |