using System.Text.Json;
using System.Text.RegularExpressions;
using MarketAlly.AIPlugin;
namespace MarketAlly.AIPlugin.Context
{
///
/// Plugin for searching through stored context to find relevant information from previous conversations.
/// Provides intelligent search across conversation history, decisions, and code changes.
///
[AIPlugin("ContextSearch", "Searches through stored context and conversation history to find relevant information from previous discussions")]
public class ContextSearchPlugin : IAIPlugin
{
[AIParameter("Search query or keywords to find in context", required: true)]
public string Query { get; set; } = "";
[AIParameter("Type of context to search: 'all', 'conversation', 'decision', 'codechange', 'insight', 'milestone'", required: false)]
public string ContextType { get; set; } = "all";
[AIParameter("Project path to search context for", required: false)]
public string? ProjectPath { get; set; }
[AIParameter("Maximum number of results to return", required: false)]
public int MaxResults { get; set; } = 10;
[AIParameter("Search priority: 'all', 'high', 'medium', 'low'", required: false)]
public string Priority { get; set; } = "all";
[AIParameter("Number of days back to search (0 for all time)", required: false)]
public int DaysBack { get; set; } = 0;
[AIParameter("Tags to filter by (comma-separated)", required: false)]
public string? Tags { get; set; }
[AIParameter("Include full content in results (otherwise just summaries)", required: false)]
public bool IncludeContent { get; set; } = true;
public IReadOnlyDictionary SupportedParameters => new Dictionary
{
["query"] = typeof(string),
["contextType"] = typeof(string),
["contexttype"] = typeof(string),
["projectPath"] = typeof(string),
["projectpath"] = typeof(string),
["maxResults"] = typeof(int),
["maxresults"] = typeof(int),
["priority"] = typeof(string),
["daysBack"] = typeof(int),
["daysback"] = typeof(int),
["tags"] = typeof(string),
["includeContent"] = typeof(bool),
["includecontent"] = typeof(bool)
};
public async Task ExecuteAsync(IReadOnlyDictionary parameters)
{
try
{
// Validate required parameters
if (!parameters.ContainsKey("query") || string.IsNullOrWhiteSpace(parameters["query"].ToString()))
{
return new AIPluginResult(new ArgumentException("Missing or empty query parameter"),
"The 'query' parameter is required and cannot be empty");
}
// Extract parameters
var query = parameters["query"].ToString()!;
var contextType = parameters.TryGetValue("contextType", out var ct) ? ct.ToString()!.ToLower() : "all";
var projectPath = parameters.TryGetValue("projectPath", out var pp) ? pp?.ToString() : null;
var maxResults = parameters.TryGetValue("maxResults", out var mr) ? Convert.ToInt32(mr) : 10;
var priority = parameters.TryGetValue("priority", out var p) ? p.ToString()!.ToLower() : "all";
var daysBack = parameters.TryGetValue("daysBack", out var db) ? Convert.ToInt32(db) : 0;
var tags = parameters.TryGetValue("tags", out var t) ? t?.ToString() : null;
var includeContent = parameters.TryGetValue("includeContent", out var ic) ? Convert.ToBoolean(ic) : true;
// Get storage path
var storagePath = await GetStoragePathAsync(projectPath);
if (!Directory.Exists(storagePath))
{
return new AIPluginResult(new
{
Results = new List(),
TotalFound = 0,
Message = "No context storage found"
}, "No stored context found. Use ContextStorage plugin to store information first.");
}
// Search through context
var searchResults = await SearchContextAsync(storagePath, query, contextType, priority, daysBack, tags, maxResults, includeContent);
return new AIPluginResult(new
{
Query = query,
Results = searchResults.Results,
TotalFound = searchResults.TotalFound,
SearchParameters = new
{
ContextType = contextType,
Priority = priority,
DaysBack = daysBack,
Tags = tags,
MaxResults = maxResults,
IncludeContent = includeContent
},
Message = $"Found {searchResults.TotalFound} relevant context entries"
}, $"Found {searchResults.TotalFound} context entries matching '{query}'");
}
catch (Exception ex)
{
return new AIPluginResult(ex, "Failed to search context");
}
}
private async Task GetStoragePathAsync(string? projectPath)
{
if (string.IsNullOrEmpty(projectPath))
{
projectPath = Directory.GetCurrentDirectory();
}
return Path.Combine(projectPath, ".context");
}
private async Task SearchContextAsync(string storagePath, string query, string contextType,
string priority, int daysBack, string? tags, int maxResults, bool includeContent)
{
var results = new List();
var totalFound = 0;
// First, try to use the index for faster searching
var indexPath = Path.Combine(storagePath, "context-index.json");
List indexEntries = new();
if (File.Exists(indexPath))
{
try
{
var indexJson = await File.ReadAllTextAsync(indexPath);
var index = JsonSerializer.Deserialize>(indexJson);
if (index != null)
{
indexEntries = index;
}
}
catch
{
// Index corrupted, fall back to file scanning
}
}
// Filter index entries based on search criteria
var filteredIndex = FilterIndexEntries(indexEntries, contextType, priority, daysBack, tags);
// Search through index entries first
var indexResults = SearchIndexEntries(filteredIndex, query);
// Load full content for matching entries
var loadedResults = new List();
foreach (var indexResult in indexResults.Take(maxResults))
{
var fullEntry = await LoadFullContextEntryAsync(storagePath, indexResult.IndexEntry);
if (fullEntry != null)
{
var searchResult = new ContextSearchResult
{
Id = fullEntry.Id,
Type = fullEntry.Type,
Summary = fullEntry.Summary,
Content = includeContent ? fullEntry.Content : null,
Tags = fullEntry.Tags,
Priority = fullEntry.Priority,
Timestamp = fullEntry.Timestamp,
ProjectPath = fullEntry.ProjectPath,
Relevance = indexResult.Relevance,
MatchedTerms = indexResult.MatchedTerms,
Metadata = fullEntry.Metadata
};
loadedResults.Add(searchResult);
}
}
// If index search didn't find enough results, fall back to full file search
if (loadedResults.Count < maxResults)
{
var fileResults = await SearchContextFilesAsync(storagePath, query, contextType, priority, daysBack, tags,
maxResults - loadedResults.Count, includeContent, loadedResults.Select(r => r.Id).ToHashSet());
loadedResults.AddRange(fileResults.Results);
totalFound += fileResults.TotalFound;
}
// Sort by relevance and timestamp
results = loadedResults.OrderByDescending(r => r.Relevance)
.ThenByDescending(r => r.Timestamp)
.Take(maxResults)
.ToList();
return new SearchResults
{
Results = results,
TotalFound = Math.Max(totalFound, results.Count)
};
}
private List FilterIndexEntries(List entries, string contextType,
string priority, int daysBack, string? tags)
{
var filtered = entries.AsEnumerable();
// Filter by type
if (contextType != "all")
{
filtered = filtered.Where(e => e.Type.Equals(contextType, StringComparison.OrdinalIgnoreCase));
}
// Filter by priority
if (priority != "all")
{
filtered = filtered.Where(e => e.Priority.Equals(priority, StringComparison.OrdinalIgnoreCase));
}
// Filter by date
if (daysBack > 0)
{
var cutoffDate = DateTime.UtcNow.AddDays(-daysBack);
filtered = filtered.Where(e => e.Timestamp >= cutoffDate);
}
// Filter by tags
if (!string.IsNullOrEmpty(tags))
{
var searchTags = tags.Split(',').Select(t => t.Trim().ToLower()).ToList();
filtered = filtered.Where(e => e.Tags.Any(tag => searchTags.Contains(tag.ToLower())));
}
return filtered.ToList();
}
private List SearchIndexEntries(List entries, string query)
{
var results = new List();
var queryTerms = ExtractSearchTerms(query);
foreach (var entry in entries)
{
var relevance = CalculateRelevance(entry, queryTerms);
if (relevance > 0)
{
results.Add(new IndexSearchResult
{
IndexEntry = entry,
Relevance = relevance,
MatchedTerms = GetMatchedTerms(entry, queryTerms)
});
}
}
return results.OrderByDescending(r => r.Relevance).ToList();
}
private async Task LoadFullContextEntryAsync(string storagePath, ContextIndexEntry indexEntry)
{
try
{
var filePath = Path.Combine(storagePath, indexEntry.FileName);
if (!File.Exists(filePath))
{
return null;
}
var json = await File.ReadAllTextAsync(filePath);
var entries = JsonSerializer.Deserialize>(json);
return entries?.FirstOrDefault(e => e.Id == indexEntry.Id);
}
catch
{
return null;
}
}
private async Task SearchContextFilesAsync(string storagePath, string query, string contextType,
string priority, int daysBack, string? tags, int maxResults, bool includeContent, HashSet excludeIds)
{
var results = new List();
var queryTerms = ExtractSearchTerms(query);
// Get all context files
var contextFiles = Directory.GetFiles(storagePath, "context-*.json")
.Where(f => !Path.GetFileName(f).Equals("context-index.json"))
.ToList();
foreach (var file in contextFiles)
{
try
{
var json = await File.ReadAllTextAsync(file);
var entries = JsonSerializer.Deserialize>(json);
if (entries == null) continue;
foreach (var entry in entries)
{
// Skip if already included
if (excludeIds.Contains(entry.Id)) continue;
// Apply filters
if (!PassesFilters(entry, contextType, priority, daysBack, tags)) continue;
// Calculate relevance
var relevance = CalculateRelevance(entry, queryTerms);
if (relevance > 0)
{
results.Add(new ContextSearchResult
{
Id = entry.Id,
Type = entry.Type,
Summary = entry.Summary,
Content = includeContent ? entry.Content : null,
Tags = entry.Tags,
Priority = entry.Priority,
Timestamp = entry.Timestamp,
ProjectPath = entry.ProjectPath,
Relevance = relevance,
MatchedTerms = GetMatchedTerms(entry, queryTerms),
Metadata = entry.Metadata
});
}
}
}
catch
{
// Skip corrupted files
}
}
return new SearchResults
{
Results = results.OrderByDescending(r => r.Relevance)
.ThenByDescending(r => r.Timestamp)
.Take(maxResults)
.ToList(),
TotalFound = results.Count
};
}
private bool PassesFilters(StoredContextEntry entry, string contextType, string priority, int daysBack, string? tags)
{
// Type filter
if (contextType != "all" && !entry.Type.Equals(contextType, StringComparison.OrdinalIgnoreCase))
return false;
// Priority filter
if (priority != "all" && !entry.Priority.Equals(priority, StringComparison.OrdinalIgnoreCase))
return false;
// Date filter
if (daysBack > 0 && entry.Timestamp < DateTime.UtcNow.AddDays(-daysBack))
return false;
// Tags filter
if (!string.IsNullOrEmpty(tags))
{
var searchTags = tags.Split(',').Select(t => t.Trim().ToLower()).ToList();
if (!entry.Tags.Any(tag => searchTags.Contains(tag.ToLower())))
return false;
}
return true;
}
private List ExtractSearchTerms(string query)
{
// Extract meaningful terms from the query
var terms = new List();
// Split by common delimiters and clean up
var rawTerms = Regex.Split(query.ToLower(), @"[\s,;.!?]+")
.Where(t => t.Length > 2) // Ignore very short terms
.Where(t => !IsStopWord(t))
.ToList();
terms.AddRange(rawTerms);
// Also add the full query for exact phrase matching
if (query.Length > 5)
{
terms.Add(query.ToLower());
}
return terms.Distinct().ToList();
}
private bool IsStopWord(string word)
{
var stopWords = new HashSet { "the", "and", "or", "but", "in", "on", "at", "to", "for", "of", "with", "by", "is", "are", "was", "were", "be", "been", "have", "has", "had", "do", "does", "did", "will", "would", "could", "should", "may", "might", "can", "this", "that", "these", "those" };
return stopWords.Contains(word);
}
private double CalculateRelevance(ContextIndexEntry entry, List queryTerms)
{
return CalculateRelevanceCommon(entry.Summary, entry.Tags, queryTerms);
}
private double CalculateRelevance(StoredContextEntry entry, List queryTerms)
{
var summaryRelevance = CalculateRelevanceCommon(entry.Summary, entry.Tags, queryTerms);
// Also check content for full entries
var contentRelevance = 0.0;
foreach (var term in queryTerms)
{
if (entry.Content.Contains(term, StringComparison.OrdinalIgnoreCase))
{
contentRelevance += 0.3; // Content matches are worth less than summary/tag matches
}
}
return summaryRelevance + contentRelevance;
}
private double CalculateRelevanceCommon(string summary, List tags, List queryTerms)
{
var relevance = 0.0;
var summaryLower = summary.ToLower();
var tagsLower = tags.Select(t => t.ToLower()).ToList();
foreach (var term in queryTerms)
{
// Exact matches in summary are highly relevant
if (summaryLower.Contains(term))
{
relevance += term.Length > 10 ? 2.0 : 1.0; // Longer terms are more significant
}
// Tag matches are also highly relevant
if (tagsLower.Any(tag => tag.Contains(term)))
{
relevance += 1.5;
}
}
return relevance;
}
private List GetMatchedTerms(ContextIndexEntry entry, List queryTerms)
{
return GetMatchedTermsCommon(entry.Summary, entry.Tags, queryTerms);
}
private List GetMatchedTerms(StoredContextEntry entry, List queryTerms)
{
var matched = GetMatchedTermsCommon(entry.Summary, entry.Tags, queryTerms);
// Also check content
foreach (var term in queryTerms)
{
if (entry.Content.Contains(term, StringComparison.OrdinalIgnoreCase) && !matched.Contains(term))
{
matched.Add(term);
}
}
return matched;
}
private List GetMatchedTermsCommon(string summary, List tags, List queryTerms)
{
var matched = new List();
var summaryLower = summary.ToLower();
var tagsLower = tags.Select(t => t.ToLower()).ToList();
foreach (var term in queryTerms)
{
if (summaryLower.Contains(term) || tagsLower.Any(tag => tag.Contains(term)))
{
matched.Add(term);
}
}
return matched;
}
}
// Supporting classes for search results
public class SearchResults
{
public List Results { get; set; } = new();
public int TotalFound { get; set; }
}
public class ContextSearchResult
{
public string Id { get; set; } = "";
public string Type { get; set; } = "";
public string Summary { get; set; } = "";
public string? Content { get; set; }
public List Tags { get; set; } = new();
public string Priority { get; set; } = "";
public DateTime Timestamp { get; set; }
public string ProjectPath { get; set; } = "";
public double Relevance { get; set; }
public List MatchedTerms { get; set; } = new();
public Dictionary Metadata { get; set; } = new();
}
public class IndexSearchResult
{
public ContextIndexEntry IndexEntry { get; set; } = new();
public double Relevance { get; set; }
public List MatchedTerms { get; set; } = new();
}
}