MarketAlly.AIPlugin.Extensions/MarketAlly.AIPlugin.Context/ContextStoragePlugin.cs

259 lines
8.2 KiB
C#
Executable File

using System.Text.Json;
using MarketAlly.AIPlugin;
namespace MarketAlly.AIPlugin.Context
{
/// <summary>
/// Plugin for storing conversation context and important information for future retrieval.
/// Allows Claude to persist key decisions, code changes, and discussion points across sessions.
/// </summary>
[AIPlugin("ContextStorage", "Stores conversation context, decisions, and important information for future retrieval across chat sessions")]
public class ContextStoragePlugin : IAIPlugin
{
[AIParameter("Type of context to store: 'conversation', 'decision', 'codechange', 'insight', 'milestone', or 'documentation'", required: true)]
public string ContextType { get; set; } = "conversation";
[AIParameter("The content/information to store", required: true)]
public string Content { get; set; } = "";
[AIParameter("Brief summary or title for this context entry", required: true)]
public string Summary { get; set; } = "";
[AIParameter("Tags to categorize this context (comma-separated)", required: false)]
public string? Tags { get; set; }
[AIParameter("Project path to associate this context with", required: false)]
public string? ProjectPath { get; set; }
[AIParameter("Priority level: 'low', 'medium', 'high', 'critical'", required: false)]
public string Priority { get; set; } = "medium";
[AIParameter("Additional metadata as JSON string", required: false)]
public string? Metadata { get; set; }
public IReadOnlyDictionary<string, Type> SupportedParameters => new Dictionary<string, Type>
{
["contextType"] = typeof(string),
["contexttype"] = typeof(string),
["content"] = typeof(string),
["summary"] = typeof(string),
["tags"] = typeof(string),
["projectPath"] = typeof(string),
["projectpath"] = typeof(string),
["priority"] = typeof(string),
["metadata"] = typeof(string)
};
public async Task<AIPluginResult> ExecuteAsync(IReadOnlyDictionary<string, object> parameters)
{
try
{
// Validate required parameters
if (!parameters.ContainsKey("content"))
{
return new AIPluginResult(new ArgumentException("Missing required parameter: content"),
"The 'content' parameter is required");
}
if (!parameters.ContainsKey("summary"))
{
return new AIPluginResult(new ArgumentException("Missing required parameter: summary"),
"The 'summary' parameter is required");
}
// Extract parameters
var contextType = parameters.TryGetValue("contextType", out var ct) ? ct.ToString()!.ToLower() : "conversation";
var content = parameters["content"].ToString()!;
var summary = parameters["summary"].ToString()!;
var tags = parameters.TryGetValue("tags", out var t) ? t?.ToString() : null;
var projectPath = parameters.TryGetValue("projectPath", out var pp) ? pp?.ToString() : null;
var priority = parameters.TryGetValue("priority", out var p) ? p.ToString()!.ToLower() : "medium";
var metadata = parameters.TryGetValue("metadata", out var m) ? m?.ToString() : null;
// Create context entry
var contextEntry = new StoredContextEntry
{
Id = Guid.NewGuid().ToString(),
Type = contextType,
Content = content,
Summary = summary,
Tags = !string.IsNullOrWhiteSpace(tags)
? tags.Split(',').Select(tag => tag.Trim()).Where(tag => !string.IsNullOrWhiteSpace(tag)).ToList()
: new List<string>(),
ProjectPath = projectPath ?? Directory.GetCurrentDirectory(),
Priority = priority,
Timestamp = DateTime.UtcNow,
Metadata = metadata != null ? JsonSerializer.Deserialize<Dictionary<string, object>>(metadata) : new Dictionary<string, object>()
};
// Store the context
var storagePath = await GetStoragePathAsync(projectPath);
var success = await StoreContextEntryAsync(contextEntry, storagePath);
if (success)
{
// Also update the quick access index
await UpdateContextIndexAsync(contextEntry, storagePath);
return new AIPluginResult(new
{
Success = true,
EntryId = contextEntry.Id,
StoredAt = storagePath,
Type = contextType,
Summary = summary,
Timestamp = contextEntry.Timestamp,
Message = "Context stored successfully"
}, $"Successfully stored {contextType} context: {summary}");
}
else
{
return new AIPluginResult(new { Success = false }, "Failed to store context");
}
}
catch (Exception ex)
{
return new AIPluginResult(ex, "Failed to store context");
}
}
private async Task<string> GetStoragePathAsync(string? projectPath)
{
if (string.IsNullOrEmpty(projectPath))
{
projectPath = Directory.GetCurrentDirectory();
}
// Create .context directory in project root
var contextDir = Path.Combine(projectPath, ".context");
if (!Directory.Exists(contextDir))
{
Directory.CreateDirectory(contextDir);
}
return contextDir;
}
private async Task<bool> StoreContextEntryAsync(StoredContextEntry entry, string storagePath)
{
try
{
// Store in monthly files to keep manageable file sizes
var fileName = $"context-{DateTime.UtcNow:yyyy-MM}.json";
var filePath = Path.Combine(storagePath, fileName);
List<StoredContextEntry> existingEntries = new();
// Load existing entries if file exists
if (File.Exists(filePath))
{
var existingJson = await File.ReadAllTextAsync(filePath);
var existing = JsonSerializer.Deserialize<List<StoredContextEntry>>(existingJson);
if (existing != null)
{
existingEntries = existing;
}
}
// Add new entry
existingEntries.Add(entry);
// Sort by timestamp (newest first)
existingEntries = existingEntries.OrderByDescending(e => e.Timestamp).ToList();
// Save back to file
var json = JsonSerializer.Serialize(existingEntries, new JsonSerializerOptions
{
WriteIndented = true,
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
});
await File.WriteAllTextAsync(filePath, json);
return true;
}
catch
{
return false;
}
}
private async Task UpdateContextIndexAsync(StoredContextEntry entry, string storagePath)
{
try
{
var indexPath = Path.Combine(storagePath, "context-index.json");
List<ContextIndexEntry> indexEntries = new();
// Load existing index
if (File.Exists(indexPath))
{
var indexJson = await File.ReadAllTextAsync(indexPath);
var existing = JsonSerializer.Deserialize<List<ContextIndexEntry>>(indexJson);
if (existing != null)
{
indexEntries = existing;
}
}
// Add new index entry
var indexEntry = new ContextIndexEntry
{
Id = entry.Id,
Type = entry.Type,
Summary = entry.Summary,
Tags = entry.Tags,
Priority = entry.Priority,
Timestamp = entry.Timestamp,
FileName = $"context-{entry.Timestamp:yyyy-MM}.json"
};
indexEntries.Add(indexEntry);
// Keep only the most recent 1000 entries in the index
indexEntries = indexEntries.OrderByDescending(e => e.Timestamp).Take(1000).ToList();
// Save index
var indexJsonString = JsonSerializer.Serialize(indexEntries, new JsonSerializerOptions
{
WriteIndented = true
});
await File.WriteAllTextAsync(indexPath, indexJsonString);
}
catch
{
// Index update failed, but main storage succeeded
}
}
}
/// <summary>
/// Represents a stored context entry with full content and metadata
/// </summary>
public class StoredContextEntry
{
public string Id { get; set; } = "";
public string Type { get; set; } = "";
public string Content { get; set; } = "";
public string Summary { get; set; } = "";
public List<string> Tags { get; set; } = new();
public string ProjectPath { get; set; } = "";
public string Priority { get; set; } = "";
public DateTime Timestamp { get; set; }
public Dictionary<string, object> Metadata { get; set; } = new();
}
/// <summary>
/// Represents an index entry for quick context lookup
/// </summary>
public class ContextIndexEntry
{
public string Id { get; set; } = "";
public string Type { get; set; } = "";
public string Summary { get; set; } = "";
public List<string> Tags { get; set; } = new();
public string Priority { get; set; } = "";
public DateTime Timestamp { get; set; }
public string FileName { get; set; } = "";
}
}