# TASK 2.2: Implement AnomalyDetectorAgent with LLM + MCP Integration

## Summary

Implement the anomaly detection agent that uses LLM reasoning with MCP tools to detect anomalies. The agent is fully generic - it receives a MonitorSpec (plain text instructions) and MCP tools (database access), and relies on the `UseFunctionInvocation()` middleware to handle the agentic tool-calling loop automatically.

## Dependencies

- TASK 1.4 (McpToolProvider for MCP tools)
- TASK 2.1 (IAnomalyDetectorAgent interface, result types)

## Architecture

```
MonitorSpec.SystemPrompt  ──►  LLM (Claude via Bedrock)
                                    │
                      UseFunctionInvocation() middleware
                        handles tool loop automatically
                                    │
                                    ├── calls mysql_query (MCP tool) ──► MySQL DB
                                    ├── calls mysql_query again...
                                    ├── reasons about results...
                                    └── calls emit_conclusion ──► AnomalyDetectionResult
```

### How UseFunctionInvocation() Works

The `FunctionInvokingChatClient` middleware (added via `UseFunctionInvocation()` on the `IChatClient` pipeline) intercepts `GetResponseAsync` calls and:
1. Sends the request to the inner chat client (LLM)
2. If the LLM response contains tool call requests (`FunctionCallContent`), the middleware **automatically invokes** the corresponding `AITool` functions
3. Appends the tool results to the conversation and sends it back to the LLM
4. Repeats until the LLM produces a response with no tool calls (or max iterations reached)
5. Returns the final `ChatResponse` with the full conversation history

This means the agent code does **NOT** need a manual `for` loop - a single `GetResponseAsync` call handles the entire multi-turn tool-calling conversation.

### Agent Flow

1. System prompt from MonitorSpec + user message with analysis time
2. Single `GetResponseAsync` call with all tools (MCP + emit_conclusion)
3. Middleware handles the entire loop: LLM → tool calls → tool results → LLM → ...
4. When LLM calls `emit_conclusion`, the tool captures the structured result
5. When LLM stops calling tools, `GetResponseAsync` returns
6. Agent checks if `emit_conclusion` was called and returns the result

## Files to Create

### 1. `backend/Kds.Ai.AnomalyDetector/Agents/ConclusionHolder.cs`

A simple mutable wrapper to capture the `emit_conclusion` tool result. This avoids the `ref` closure bug (closures cannot capture `ref` parameters).

```csharp
namespace Kds.Ai.AnomalyDetector.Agents;

/// <summary>
/// Mutable holder for the agent's conclusion, captured by the emit_conclusion tool closure.
/// </summary>
internal sealed class ConclusionHolder
{
    public AnomalyDetectionResult? Result { get; set; }
}
```

### 2. `backend/Kds.Ai.AnomalyDetector/Agents/AnomalyDetectorAgent.cs`

```csharp
using System.Text.Json;
using Kds.Ai.AnomalyDetector.Mcp;
using Kds.Ai.Data.Entities;
using Kds.Ai.Data.Enums;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Logging;

namespace Kds.Ai.AnomalyDetector.Agents;

public sealed class AnomalyDetectorAgent : IAnomalyDetectorAgent
{
    private readonly IChatClient _chatClient;
    private readonly McpToolProvider _mcpToolProvider;
    private readonly ILogger<AnomalyDetectorAgent> _logger;

    public AnomalyDetectorAgent(
        IChatClient chatClient,
        McpToolProvider mcpToolProvider,
        ILogger<AnomalyDetectorAgent> logger)
    {
        _chatClient = chatClient;
        _mcpToolProvider = mcpToolProvider;
        _logger = logger;
    }

    public async Task<AnomalyDetectionResult> RunDetectionAsync(
        MonitorSpec spec,
        DateTimeOffset analysisTime,
        CancellationToken ct = default)
    {
        _logger.LogInformation(
            "Starting anomaly detection for monitor '{MonitorName}' at {AnalysisTime}",
            spec.Name, analysisTime);

        try
        {
            // 1. Get MCP tools (mysql_query etc.)
            var mcpTools = await _mcpToolProvider.GetToolsAsync(ct);

            // 2. Create the emit_conclusion tool for structured output
            var holder = new ConclusionHolder();
            var emitConclusionTool = CreateEmitConclusionTool(holder);

            // 3. Combine all tools
            var allTools = mcpTools.Concat([emitConclusionTool]).ToList();

            // 4. Build conversation
            var messages = new List<ChatMessage>
            {
                new(ChatRole.System, spec.SystemPrompt),
                new(ChatRole.User, $"Run analysis. Current time: {analysisTime:yyyy-MM-dd HH:mm:ss} UTC")
            };

            // 5. Single call - UseFunctionInvocation() middleware handles the agentic loop
            var response = await _chatClient.GetResponseAsync(
                messages, new ChatOptions { Tools = allTools }, ct);

            // 6. Extract token usage from the response
            var totalTokens = 0;
            if (response.Usage is not null)
                totalTokens = (response.Usage.InputTokenCount ?? 0) +
                              (response.Usage.OutputTokenCount ?? 0);

            // 7. Build reasoning trace from conversation history
            var reasoning = BuildReasoningTrace(response);

            // 8. Check if emit_conclusion was called
            if (holder.Result is not null)
            {
                return holder.Result with
                {
                    Reasoning = reasoning,
                    TotalTokens = totalTokens
                };
            }

            // Agent finished without calling emit_conclusion
            _logger.LogWarning(
                "Agent for '{MonitorName}' finished without calling emit_conclusion",
                spec.Name);

            return new AnomalyDetectionResult
            {
                AnomalyDetected = false,
                Reasoning = reasoning,
                TotalTokens = totalTokens,
                Warnings = ["Agent did not call emit_conclusion"]
            };
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Anomaly detection failed for monitor '{MonitorName}'", spec.Name);
            return AnomalyDetectionResult.Failed(ex.Message);
        }
    }

    private static AITool CreateEmitConclusionTool(ConclusionHolder holder)
    {
        return AIFunctionFactory.Create(
            (bool anomalyDetected, string? severity, string summary,
             string detailedAnalysis, string? findingsJson) =>
            {
                EventSeverity? parsedSeverity = null;
                if (!string.IsNullOrEmpty(severity) &&
                    Enum.TryParse<EventSeverity>(severity, true, out var sev))
                    parsedSeverity = sev;

                var findings = ParseFindings(findingsJson);

                holder.Result = new AnomalyDetectionResult
                {
                    AnomalyDetected = anomalyDetected,
                    Severity = parsedSeverity,
                    Summary = summary,
                    DetailedAnalysis = detailedAnalysis,
                    Findings = findings
                };

                return anomalyDetected
                    ? $"Conclusion recorded: {severity} severity - {summary}"
                    : $"Conclusion recorded: No anomaly - {summary}";
            },
            name: "emit_conclusion",
            description: "Report your final analysis conclusion with findings");
    }

    private static string BuildReasoningTrace(ChatResponse response)
    {
        var lines = new List<string>();

        foreach (var message in response.Messages)
        {
            if (message.Role == ChatRole.Assistant)
            {
                // Capture text content
                var text = message.Text;
                if (!string.IsNullOrWhiteSpace(text))
                    lines.Add($"[Assistant] {text}");

                // Capture tool calls
                foreach (var call in message.Contents.OfType<FunctionCallContent>())
                    lines.Add($"[Tool Call] {call.Name}({TruncateArgs(call.Arguments)})");
            }
            else if (message.Role == ChatRole.Tool)
            {
                // Capture tool results (truncated)
                foreach (var result in message.Contents.OfType<FunctionResultContent>())
                {
                    var resultText = result.Result?.ToString() ?? "";
                    var truncated = resultText.Length > 500
                        ? resultText[..500] + "..."
                        : resultText;
                    lines.Add($"[Tool Result] {result.CallId}: {truncated}");
                }
            }
        }

        return string.Join("\n", lines);
    }

    private static List<AnomalyFinding> ParseFindings(string? findingsJson)
    {
        if (string.IsNullOrEmpty(findingsJson)) return [];
        try
        {
            return JsonSerializer.Deserialize<List<AnomalyFinding>>(findingsJson) ?? [];
        }
        catch
        {
            return [];
        }
    }

    private static string TruncateArgs(IDictionary<string, object?>? args)
    {
        if (args is null) return "{}";
        var json = JsonSerializer.Serialize(args);
        return json.Length > 200 ? json[..200] + "..." : json;
    }
}
```

## Key Implementation Details

### UseFunctionInvocation() Middleware (NOT in this file)

The `UseFunctionInvocation()` middleware is registered on the `IChatClient` pipeline in DI (see Files to Modify below and TASK 5.2 for full DI wiring). It wraps the inner chat client with `FunctionInvokingChatClient` which:
- Automatically invokes tools when the LLM requests them
- Feeds tool results back to the LLM
- Loops until the LLM produces a final text response (no more tool calls)
- Has a configurable `MaximumIterationsPerRequest` (default is sufficient; can be tuned if needed)

This means the agent itself is simple - one `GetResponseAsync` call does all the work.

### ConclusionHolder Pattern

The `emit_conclusion` tool needs to capture a result that the agent can read after `GetResponseAsync` returns. Since closures cannot capture `ref` parameters, we use a simple mutable wrapper class (`ConclusionHolder`) that the closure writes to and the agent reads from.

### emit_conclusion Tool

This is the only tool we define in code. It provides a structured way for the LLM to report results that map to `AnomalyDetectionResult`. The system prompt in MonitorSpec should instruct the agent to use this tool.

### Reasoning Trace from ChatResponse

With `UseFunctionInvocation()`, the returned `ChatResponse` contains the full conversation history (all messages including assistant tool calls and tool results). `BuildReasoningTrace` extracts this into a human-readable log for debugging.

### No Domain Logic in Code

- No prep-time specific code
- No query builders
- No typed DTOs for query results
- The LLM handles all data interpretation

## Files to Modify

### 3. `backend/Kds.Ai.AgentsCommon/DependencyInjectionExtensions.cs`

Add `UseFunctionInvocation()` to the `IChatClient` pipeline. This must be added here (not in AnomalyDetector module) because `AgentsCommon` is where `AddChatClient` is called:

```csharp
public static IHostApplicationBuilder AddAgentsCommonModule(this IHostApplicationBuilder builder)
{
    builder.AddOptions<AwsBedrockCredentials>();
    builder.Services.AddChatClient(provider =>
    {
        var settings = provider.GetRequiredService<IOptions<AwsBedrockCredentials>>();
        var accessKey = settings.Value.AccessKeyId;
        var secretKey = settings.Value.SecretAccessKey;
        var runtime = new AmazonBedrockRuntimeClient(accessKey, secretKey, RegionEndpoint.USWest2);
        var modelId = settings.Value.ModelId;
        var client = runtime.AsIChatClient(modelId);
        return client;
    })
    .UseFunctionInvocation();  // <-- ADD THIS: enables automatic tool calling for all agents

    return builder;
}
```

**Note:** `AddChatClient` returns a `ChatClientBuilder`, so `UseFunctionInvocation()` chains directly. This enables automatic tool invocation for any agent that injects `IChatClient`.

### 4. `backend/Kds.Ai.AnomalyDetector/DependencyInjectionExtensions.cs`

Register the agent:

```csharp
builder.Services.AddScoped<IAnomalyDetectorAgent, AnomalyDetectorAgent>();
```

## Acceptance Criteria

- [ ] `AnomalyDetectorAgent` implements `IAnomalyDetectorAgent`
- [ ] Agent receives MCP tools from `McpToolProvider`
- [ ] Agent adds `emit_conclusion` tool for structured output
- [ ] `UseFunctionInvocation()` middleware registered on `IChatClient` pipeline in `AgentsCommon`
- [ ] Single `GetResponseAsync` call (no manual agentic loop)
- [ ] `ConclusionHolder` wrapper used instead of `ref` parameter
- [ ] Token usage extracted from response
- [ ] Reasoning trace built from full `ChatResponse.Messages` conversation history
- [ ] Error handling prevents crashes
- [ ] No domain-specific (prep-time, KDSDev) code
- [ ] Solution builds without errors

## Notes

- The `FunctionInvokingChatClient` has a default `MaximumIterationsPerRequest` that should be sufficient. If agents hit the limit, it can be configured via the `configure` callback on `UseFunctionInvocation()`.
- `ChatResponse.Messages` contains the full multi-turn conversation after `UseFunctionInvocation()` resolves all tool calls. This is how we build the reasoning trace.
- `ChatResponse.Usage` aggregates token usage across all iterations handled by the middleware.
- MonitorSpec.SystemPrompt should instruct the agent to call `emit_conclusion` when done.
