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(); } }