diff --git a/EmailSearch/AIAgentConfig.json b/EmailSearch/AIAgentConfig.json
new file mode 100644
index 0000000..aacf5f2
--- /dev/null
+++ b/EmailSearch/AIAgentConfig.json
@@ -0,0 +1,17 @@
+{
+ "anthropicApiKey": "",
+ "model": "claude-sonnet-4-20250514",
+ "watchFolder": "Inbox",
+
+ "gotifyUrl": "http://gotify.local:8080",
+ "gotifyToken": "YOUR_GOTIFY_APP_TOKEN",
+
+ "userPriorities": [
+ "Emails from my boss or direct team members",
+ "Urgent requests that need immediate attention",
+ "Customer inquiries and support tickets",
+ "Meeting changes and calendar updates",
+ "Security alerts from legitimate services (Microsoft, Google, etc.)",
+ "Invoices and payment confirmations from known vendors"
+ ]
+}
diff --git a/EmailSearch/Agents/AIAgentRunner.cs b/EmailSearch/Agents/AIAgentRunner.cs
new file mode 100644
index 0000000..7d21f06
--- /dev/null
+++ b/EmailSearch/Agents/AIAgentRunner.cs
@@ -0,0 +1,100 @@
+using System.Text.Json;
+
+namespace EmailSearch.Agents;
+
+///
+/// Entry point for running the AI Email Agent standalone.
+///
+public static class AIAgentRunner
+{
+ public static async Task RunAsync(CancellationToken cancellationToken = default)
+ {
+ Console.WriteLine("╔═══════════════════════════════════════════════════════════╗");
+ Console.WriteLine("║ AI EMAIL AGENT - Powered by Claude ║");
+ Console.WriteLine("╚═══════════════════════════════════════════════════════════╝");
+ Console.WriteLine();
+
+ var config = LoadConfig();
+
+ if (string.IsNullOrEmpty(config.AnthropicApiKey))
+ {
+ Console.WriteLine("ERROR: Anthropic API key not configured!");
+ Console.WriteLine();
+ Console.WriteLine("Set your API key via:");
+ Console.WriteLine(" 1. Environment variable: ANTHROPIC_API_KEY");
+ Console.WriteLine(" 2. Config file: AIAgentConfig.json");
+ Console.WriteLine();
+ return;
+ }
+
+ Console.WriteLine($"Model: {config.Model}");
+ Console.WriteLine($"Watching: {config.WatchFolder}");
+ Console.WriteLine($"Gotify: {(string.IsNullOrEmpty(config.GotifyUrl) ? "(not configured)" : config.GotifyUrl)}");
+ Console.WriteLine();
+ Console.WriteLine("User priorities:");
+ foreach (var p in config.UserPriorities)
+ Console.WriteLine($" • {p}");
+ Console.WriteLine();
+ Console.WriteLine("Press Ctrl+C to stop...");
+ Console.WriteLine();
+
+ using var agent = new AIEmailAgent(config);
+ await agent.StartAsync(cancellationToken);
+
+ try
+ {
+ await Task.Delay(Timeout.Infinite, cancellationToken);
+ }
+ catch (OperationCanceledException) { }
+
+ await agent.StopAsync(CancellationToken.None);
+ }
+
+ private static AIAgentConfig LoadConfig()
+ {
+ var configPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "AIAgentConfig.json");
+
+ AIAgentConfig? config = null;
+
+ if (File.Exists(configPath))
+ {
+ try
+ {
+ var json = File.ReadAllText(configPath);
+ config = JsonSerializer.Deserialize(json, new JsonSerializerOptions
+ {
+ PropertyNameCaseInsensitive = true
+ });
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Warning: Failed to load config: {ex.Message}");
+ }
+ }
+
+ config ??= new AIAgentConfig();
+
+ // Override with environment variables if set
+ config.AnthropicApiKey = Environment.GetEnvironmentVariable("ANTHROPIC_API_KEY")
+ ?? config.AnthropicApiKey;
+ config.GotifyUrl = Environment.GetEnvironmentVariable("GOTIFY_URL")
+ ?? config.GotifyUrl;
+ config.GotifyToken = Environment.GetEnvironmentVariable("GOTIFY_TOKEN")
+ ?? config.GotifyToken;
+
+ // Default priorities if none configured
+ if (config.UserPriorities.Count == 0)
+ {
+ config.UserPriorities = new List
+ {
+ "Emails from my boss or direct team members",
+ "Urgent or time-sensitive requests",
+ "Meeting changes and calendar updates",
+ "Security alerts from legitimate services",
+ "Customer inquiries that need quick response"
+ };
+ }
+
+ return config;
+ }
+}
diff --git a/EmailSearch/Agents/AIEmailAgent.cs b/EmailSearch/Agents/AIEmailAgent.cs
new file mode 100644
index 0000000..2abccec
--- /dev/null
+++ b/EmailSearch/Agents/AIEmailAgent.cs
@@ -0,0 +1,685 @@
+using System.Net.Http.Headers;
+using System.Text;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Microsoft.Extensions.Hosting;
+using NetOffice.OutlookApi;
+using NetOffice.OutlookApi.Enums;
+using EmailSearch.SpamDetection;
+using OutlookApp = NetOffice.OutlookApi.Application;
+using Exception = System.Exception;
+
+namespace EmailSearch.Agents;
+
+///
+/// A true AI Agent that uses Claude to reason about emails and decide what actions to take.
+/// The LLM is in the decision loop - it evaluates each email and chooses actions dynamically.
+///
+public class AIEmailAgent : BackgroundService
+{
+ private readonly AIAgentConfig _config;
+ private readonly HttpClient _httpClient;
+ private readonly SpamDetector _spamDetector;
+ private OutlookApp? _outlookApp;
+ private _NameSpace? _namespace;
+ private MAPIFolder? _watchFolder;
+ private Items? _items;
+
+ // Tools the agent can use
+ private readonly List _tools;
+
+ public AIEmailAgent(AIAgentConfig config)
+ {
+ _config = config;
+ _httpClient = new HttpClient();
+ _httpClient.DefaultRequestHeaders.Add("x-api-key", config.AnthropicApiKey);
+ _httpClient.DefaultRequestHeaders.Add("anthropic-version", "2023-06-01");
+ _spamDetector = new SpamDetector();
+
+ // Define tools the agent can use
+ _tools = DefineTools();
+ }
+
+ private List DefineTools()
+ {
+ return new List
+ {
+ new AgentTool
+ {
+ Name = "send_notification",
+ Description = "Send a push notification to the user's phone via Gotify. Use for important emails that need immediate attention.",
+ InputSchema = new
+ {
+ type = "object",
+ properties = new
+ {
+ title = new { type = "string", description = "Short notification title" },
+ message = new { type = "string", description = "Notification body with key details" },
+ priority = new { type = "integer", description = "1-10, where 10 is most urgent. Use 8+ for truly urgent, 5-7 for important, 1-4 for informational" }
+ },
+ required = new[] { "title", "message", "priority" }
+ }
+ },
+ new AgentTool
+ {
+ Name = "move_to_folder",
+ Description = "Move the email to a specific Outlook folder. Use for organizing emails (e.g., move spam to Junk, receipts to Archive).",
+ InputSchema = new
+ {
+ type = "object",
+ properties = new
+ {
+ folder = new { type = "string", description = "Target folder: Junk, Archive, or any custom folder name" },
+ reason = new { type = "string", description = "Brief reason for moving" }
+ },
+ required = new[] { "folder", "reason" }
+ }
+ },
+ new AgentTool
+ {
+ Name = "flag_email",
+ Description = "Flag the email for follow-up with a category. Use when the email needs future attention but not immediate notification.",
+ InputSchema = new
+ {
+ type = "object",
+ properties = new
+ {
+ category = new { type = "string", description = "Category to apply: 'Follow Up', 'Review', 'Waiting', 'Important'" },
+ reason = new { type = "string", description = "Why this needs follow-up" }
+ },
+ required = new[] { "category" }
+ }
+ },
+ new AgentTool
+ {
+ Name = "log_observation",
+ Description = "Log an observation about the email without taking action. Use when the email is noted but requires no action.",
+ InputSchema = new
+ {
+ type = "object",
+ properties = new
+ {
+ observation = new { type = "string", description = "What you observed about this email" }
+ },
+ required = new[] { "observation" }
+ }
+ },
+ new AgentTool
+ {
+ Name = "analyze_spam",
+ Description = "Run detailed spam analysis on the email to get risk score and red flags. Use when you're unsure if an email is legitimate.",
+ InputSchema = new
+ {
+ type = "object",
+ properties = new
+ {
+ include_details = new { type = "boolean", description = "Include detailed feature breakdown" }
+ },
+ required = new string[] { }
+ }
+ }
+ };
+ }
+
+ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
+ {
+ Log("AI Email Agent starting...");
+ Log($"Model: {_config.Model}");
+ Log($"Watching: {_config.WatchFolder}");
+
+ try
+ {
+ InitializeOutlook();
+ Log("Connected to Outlook. AI Agent ready and watching for emails...");
+ Log("");
+
+ while (!stoppingToken.IsCancellationRequested)
+ {
+ await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
+ }
+ }
+ catch (Exception ex)
+ {
+ Log($"Error: {ex.Message}");
+ }
+ }
+
+ private void InitializeOutlook()
+ {
+ _outlookApp = new OutlookApp();
+ _namespace = _outlookApp.GetNamespace("MAPI");
+
+ _watchFolder = _config.WatchFolder.Equals("Inbox", StringComparison.OrdinalIgnoreCase)
+ ? _namespace.GetDefaultFolder(OlDefaultFolders.olFolderInbox)
+ : FindFolder(_namespace, _config.WatchFolder);
+
+ if (_watchFolder == null)
+ throw new Exception($"Folder '{_config.WatchFolder}' not found");
+
+ _items = (Items)_watchFolder.Items;
+ _items.ItemAddEvent += OnNewEmailReceived;
+ }
+
+ private void OnNewEmailReceived(object item)
+ {
+ if (item is not MailItem mail)
+ return;
+
+ // Run agent processing on a background thread
+ _ = Task.Run(async () =>
+ {
+ try
+ {
+ await ProcessEmailWithAgent(mail);
+ }
+ catch (Exception ex)
+ {
+ Log($"Agent error: {ex.Message}");
+ }
+ });
+ }
+
+ ///
+ /// The core agentic loop - sends email to Claude, executes tool calls, continues until done.
+ ///
+ private async Task ProcessEmailWithAgent(MailItem mail)
+ {
+ var subject = mail.Subject ?? "(No Subject)";
+ var sender = mail.SenderName ?? mail.SenderEmailAddress ?? "Unknown";
+ var senderEmail = mail.SenderEmailAddress ?? "";
+ var body = TruncateBody(mail.Body ?? "", 2000);
+ var receivedTime = mail.ReceivedTime;
+
+ Log($"");
+ Log($"═══════════════════════════════════════════════════════════");
+ Log($"NEW EMAIL: {subject}");
+ Log($"From: {sender} <{senderEmail}>");
+ Log($"Time: {receivedTime}");
+ Log($"═══════════════════════════════════════════════════════════");
+
+ // Build context for the agent
+ var systemPrompt = BuildSystemPrompt();
+ var userMessage = BuildEmailContext(mail, subject, sender, senderEmail, body);
+
+ var messages = new List
+ {
+ new AgentMessage { Role = "user", Content = userMessage }
+ };
+
+ // Agentic loop - keep going until Claude stops calling tools
+ var iteration = 0;
+ const int maxIterations = 5;
+
+ while (iteration < maxIterations)
+ {
+ iteration++;
+ Log($"");
+ Log($"[Agent thinking... iteration {iteration}]");
+
+ var response = await CallClaudeAsync(systemPrompt, messages);
+
+ if (response == null)
+ {
+ Log("[Agent] Failed to get response from Claude");
+ break;
+ }
+
+ // Process the response
+ var hasToolUse = false;
+ var toolResults = new List();
+
+ foreach (var content in response.Content)
+ {
+ if (content.Type == "text" && !string.IsNullOrWhiteSpace(content.Text))
+ {
+ Log($"[Agent] {content.Text}");
+ }
+ else if (content.Type == "tool_use" && content.Input.HasValue)
+ {
+ hasToolUse = true;
+ Log($"[Agent calling tool: {content.Name}]");
+
+ // Execute the tool
+ var result = await ExecuteToolAsync(content.Name!, content.Input.Value, mail);
+ toolResults.Add(new ToolResultContent
+ {
+ Type = "tool_result",
+ ToolUseId = content.Id!,
+ Content = result
+ });
+ }
+ }
+
+ // If no tool calls, agent is done
+ if (!hasToolUse)
+ {
+ Log($"[Agent complete]");
+ break;
+ }
+
+ // Add assistant response and tool results to continue the conversation
+ messages.Add(new AgentMessage
+ {
+ Role = "assistant",
+ Content = response.Content
+ });
+
+ messages.Add(new AgentMessage
+ {
+ Role = "user",
+ Content = toolResults.Cast