Ops AI Support Agent
Intelligent support system embedded in a Blazor Server ops platform — AI-based intent classification routes each message to RAG-powered documentation, live operational tool calls, or a self-service docs portal where non-technical users can author articles with AI-assisted formatting.
The Idea
Teams were keeping SOPs and task notes in personal docs, spreadsheets, and chat threads. There was no central place to find them, no consistent format, and no easy way to share them. At the same time, people needed quick answers from live operational data — status checks, file lookups, order queries — without digging through the system manually.
The goal was a single chat interface embedded in the existing ops dashboard that could handle both: surface the right documentation instantly, and query live data through tools — all without leaving the page.
AI-Based Intent Classification
The first version used keyword matching to route messages — detect "how do I" and send it to the KB, otherwise fall through to tools. It was unreliable, and it created a worse problem: routing everything through the tool pipeline caused conversation history poisoning when calls failed. A failed tool invocation left state in the history that broke subsequent messages.
The fix was to use Claude itself as the router — a lightweight, one-shot classification call that returns a single word before any tool or KB query happens:
public async Task<ChatIntent> ClassifyIntentAsync(
string message,
IEnumerable<string>? availableToolNames = null)
{
var toolsList = availableToolNames != null
? string.Join(", ", availableToolNames)
: "none";
var systemPrompt = $@"You are an intent classifier. Respond with EXACTLY one word.
Categories:
- KNOWLEDGE_BASE: how-to questions, processes, procedures
- OPERATIONAL: live data requests ({toolsList})
- DOCS_PORTAL: vague 'docs' mentions without specific question
- GENERAL: everything else";
// One-shot call — no tools, no history — returns a single token
var result = await _client.CompleteAsync(
new ChatMessage(ChatRole.User, message),
systemPrompt);
return result.ToUpperInvariant() switch
{
"KNOWLEDGE_BASE" => ChatIntent.KnowledgeBase,
"OPERATIONAL" => ChatIntent.Operational,
"DOCS_PORTAL" => ChatIntent.DocsPortal,
_ => ChatIntent.General,
};
} Classification happens before any conversation history is touched. The tool path only runs when the intent is actually operational — so a "how do I" question never enters the tool pipeline, and a failed tool call can't poison the context for the next message.
OnBeforeSend searches Bedrock KB → returns clickable article cards with snippet previews → user clicks → /docs/{articleId}
Falls through to Claude with MEAI tool providers registered for that dashboard — BOM status, ARCIN files, order releases, inventory queries
Navigates user to the /docs search portal rather than attempting a specific KB query
Standard Claude response — no tools, no KB, no routing overhead
What It Looks Like
Same chat window, same input box — but two different requests land on completely different paths under the hood. Content below is a recreated mockup for illustration; article titles and data are generic placeholders, not real system output.
Here's what I found:
Covers intake scanning, condition grading, and restock routing for returned pallets before they re-enter inventory...
Read full article| Order | Status | ETA |
|---|---|---|
| 48213 | Released | Today, 3:40 PM |
Order 48213 released this morning and is on track for today's outbound window.
live tool call — GetOrderReleaseStatusInbound, shipping, and other operational dashboards share the same AI chat component. Each dashboard registers its own tool providers at startup — so the same component surfaces domain-specific tools per context without any changes to the shared component itself.
Runs before any KB query or tool call. Single fast inference, no tools, no history — returns KNOWLEDGE_BASE, OPERATIONAL, DOCS_PORTAL, or GENERAL. Prevents unnecessary tool pipeline invocations and keeps conversation history clean.
Retrieve API — semantic search, Titan Text Embeddings, auto-chunking. Per-warehouse metadata filtering. Returns ranked passages + S3 source URIs.
UI renders clickable cards (title + snippet). Clicking navigates to /docs/{articleId} where HTML is fetched from S3 and rendered inline.
C# methods decorated with [Description] exposed via Microsoft.Extensions.AI. GetOrderReleaseStatus, GetBomStatus, GetArcinFiles — each returns JSON that Claude formats into tables.
Tool call loop — Claude selects tool, MEAI executes C# function, returns JSON, Claude formats with tables and summaries. Streamed back to chat UI in real-time.
HTML documentation with embedded base64 screenshots. /global/ for shared docs, /warehouse-{id}/ for location-specific content. Serves as Bedrock KB data source and direct fetch target for the /docs viewer. 10-minute IMemoryCache on fetches.
Admin saves or edits an article → S3 upload → StartIngestionJobAsync fires immediately. KB index is up to date within minutes. IAmazonBedrockAgent registered via standard AddAWSService DI pattern, credentials from the app's AWS profile.
The /docs Portal
Three routes, each doing different work. /docs is a KB-powered search page — results only appear after a query, not on load. /docs/{articleId} fetches the HTML article from S3, strips embedded styles, and renders it in a consistent viewer layout. /docs/new (and edit mode on article pages) is where the interesting part happens.
Non-technical ops supervisors were expected to author documentation. They knew the processes — they didn't know HTML. The solution was an AI Format button: paste raw notes into the Telerik editor, click Format, and Claude restructures it into consistent step-by-step HTML using a template prompt:
var prompt = @"Format into clean HTML using these patterns:
- <div class=""step""><div class=""step-number"">1</div>
<div class=""step-content"">...</div></div>
- <div class=""note""><strong>Note:</strong> text</div>
- <code> for system fields and buttons
- Return only HTML, no markdown fences";
// One-shot — clears history first so this doesn't bleed into chat
AiChatService.ClearHistory();
await foreach (var chunk in AiChatService.AskStreamingAsync(prompt, rawContent))
{
formattedContent += chunk;
StateHasChanged(); // Live preview updates as Claude streams
} The preview updates live as Claude streams the formatted HTML. Supervisors see the final output before saving — and once they save, the Bedrock KB sync fires automatically so the new doc is searchable within minutes.
Authorization for edit and create is gated by a Users.IsAdmin flag in the database rather than AD groups — more flexible for a tool used across multiple teams where IT involvement would slow things down.
Conversation History Poisoning
The most important reliability fix in the system wasn't a new feature — it was preventing failures from compounding. When a tool call failed mid-stream, the broken response stayed in conversation history. The next message from the user would pick up that poisoned context and fail the same way. The chat would spiral.
The fix is explicit history clearing on any error path:
// Guard at the start of every streaming call
if (hasError || _client == null)
{
_conversationHistory.Clear(); // Reset — don't carry poisoned state forward
yield return "⚠️ AI service unavailable. Please try again.";
yield break;
}
// After streaming completes
if (fullText.Length == 0)
{
_conversationHistory.Clear(); // Empty response = likely failed call
}
// AI Format button and other one-shot calls always clear first
public void ClearHistory() => _conversationHistory.Clear(); The intent classifier also helps here indirectly — by routing doc questions away from the tool pipeline entirely, there are far fewer opportunities for tool failures to enter the history in the first place.
Engineering Decisions
AI classifier over keyword matching. Keyword matching required maintaining a list of trigger phrases that was always incomplete. The AI classifier handles natural language variation ("walk me through", "steps for", "explain how") without a rules list, and adding a new intent category is a prompt change rather than a code change.
Managed Bedrock KB over a custom vector database. A self-managed option would require running an embedding pipeline, managing the vector index, and handling chunking logic. Bedrock Knowledge Bases handles all of that and exposes a single Retrieve API. The KB sync after save is a one-call operation rather than a re-indexing job to maintain.
HTML docs over Markdown. Most existing SOPs and process notes were authored in Word or Teams and contain screenshots. HTML with base64-embedded screenshots works natively in the browser and renders correctly in the /docs viewer without an external image host or transformation step. The AI format button produces HTML directly, so there's no conversion layer.
S3 over a database for doc storage. S3 is versioned, cheap, and works directly as a Bedrock data source. There's no ETL step to move docs from a database into a format Bedrock can ingest — upload to S3, trigger sync, done. The /docs viewer fetches articles directly from S3 without a separate read API.
Scale and Scope
| Dashboards with AI chat | 2+ — inbound, shipping, and others (shared component, extensible) |
| Intent categories | 4 — KNOWLEDGE_BASE, OPERATIONAL, DOCS_PORTAL, GENERAL |
| Knowledge base type | Managed RAG — Bedrock Knowledge Bases, Titan Text Embeddings |
| Tool framework | Microsoft.Extensions.AI — C# methods with [Description], per-dashboard providers |
| Doc format | HTML with embedded screenshots — no external image host needed |
| KB sync trigger | On save — StartIngestionJobAsync fires automatically after each article write |
| Doc cache TTL | 10 minutes — IMemoryCache on S3 fetches in the /docs viewer |
| Authorization model | DB flag — Users.IsAdmin, not AD groups — toggleable per user without IT |
| Secrets in code | Zero — Knowledge Base ID and Data Source ID in SSM Parameter Store |
My Role
Work project. I designed and built the full system end to end.
- Designed and implemented the AI-based intent classifier — replaced unreliable keyword matching with a one-shot Claude call that returns a single token
- Built the shared AI chat Blazor component with extensible MEAI tool provider registration per dashboard
- Integrated Amazon Bedrock Knowledge Bases for managed RAG — S3 data source, Titan embeddings, Retrieve API, per-warehouse metadata filtering
- Implemented automatic KB sync on article save via StartIngestionJobAsync
- Built the /docs portal — search, S3-backed article viewer with memory cache, /docs/new article creation with Telerik Editor
- Built the AI Format button — one-shot Claude call with template prompt, live streaming preview, ClearHistory isolation so formatting doesn't bleed into chat history
- Diagnosed and fixed conversation history poisoning — explicit ClearHistory on all error paths and after empty responses
- Built CDK infrastructure stack — S3 bucket, SSM parameters for KB ID and Data Source ID, IAM policies for Bedrock Retrieve and InvokeModel
Company and system names have been anonymized. Architecture and technical details are accurate.