AI-powered operations assistant for an industrial rental marketplace (dumpsters, portable toilets, fencing). Helps ops staff triage issues by querying orders, checking customer sentiment, and finding active rentals — all through natural language.
echo "ANTHROPIC_API_KEY=sk-ant-..." > .env
docker compose up --buildBackend: .NET 10, ASP.NET Core Minimal API, Entity Framework Core (SQLite), Anthropic C# SDK (official)
Frontend: React 19, TypeScript, Vite, Tailwind CSS v4, shadcn/ui, @agenisea/sse-kit (npm)
LLM: Claude via Anthropic API — raw Messages API with OutputConfig (constrained JSON decoding), prompt caching, multi-tool support
Testing: xUnit, FluentAssertions, Microsoft.Extensions.AI.Evaluation (with custom InjectionResistanceEvaluator)
Deployment: Docker Compose (split services: nginx frontend + .NET backend + optional observability)
| Tool | Description | Example |
|---|---|---|
lookup_order |
Order details by code — status, product, tonnage, access/gate codes, dates | "What's the status of ORD-5353?" |
get_order_sentiment |
Sentiment analysis from customer messages — overall score + flagged messages | "How's the customer feeling about ORD-9910?" |
find_active_orders |
All active rentals for a company — joins users to orders by company name | "Show me active orders for Chase Construction" |
Claude can call multiple tools in a single turn (e.g., "Give me the full picture on ORD-9910" triggers both order lookup and sentiment analysis).
The agent calls the Anthropic Messages API directly with tool definitions and OutputConfig for constrained JSON decoding. CSV data is loaded into SQLite at startup and queried through C# tool methods. The LLM never sees raw data in its context, so this scales to millions of rows.
Responses stream via .NET 10's native Server-Sent Events API (TypedResults.ServerSentEvents with Channel<SseItem<string>>) so the user sees real-time progress:
event: thinking -> data: {"message": "Processing your request..."}
event: tool_call -> data: {"message": "Looking up order ORD-5353..."}
event: complete -> data: {"message": "...", "details": [...], "sessionId": "..."}
Multi-turn conversations are supported via sessionId — the agent resolves references like "it", "that order", "that company" across turns. Conversation history is stored as plain text matching what the user sees in the chat, with automatic trimming on user/assistant pair boundaries to prevent context window overflow.
Browser -> ChatRequest -> POST /api/chat -> ASP.NET Core (native SSE)
|
ChatPipelineService
(injection detection, history, validation)
|
AgentRunner
(provider-agnostic tool loop)
|
IModelClient
(provider boundary)
|
Claude selects tool(s)
|
+------+------+
| OpsPlugin |
| (EF Core) |
+------+------+
|
AgentResponse
{ message, details[] }
(flat OutputConfig schema)
|
+-----------+-----------+
| |
Plain text history SSE -> Browser
(conversation store) (thinking/tool_call/complete)
Clean Architecture layers:
OpsAgent.Core— Domain entities, schemas (records), interfaces (IModelClient,IConversationStore), telemetry, injection detection. No dependencies.OpsAgent.Infrastructure— EF Core, repositories,AnthropicModelClient(provider adapter),AnthropicTurnBuilder, conversation store, OTel spans.Testing/subfolder containsDeterministicModelClientfor E2E tests.OpsAgent.Agent—AgentRunner(provider-agnostic tool loop),ChatPipelineService(orchestration), system prompt, structured output, tool dispatch.OpsAgent.Api— Composition root, endpoints, DI wiring, rate limiting, SSE concurrency middleware, OTel SDK config.
Defense-in-depth against prompt injection and data leakage:
| Layer | What it does |
|---|---|
| Input detection | InjectionDetector — regex patterns + typoglycemia fuzzy matching, per-session rate limiting |
| Tool argument validation | Regex allowlists (ORD-\d{4} for order codes, alphanumeric for company names) |
| Tool output sanitization | TelemetryRedactor.SanitizeToolOutput — strips instruction-like tags, wraps injection phrases |
| System prompt hardening | Dedicated Security Rules section with data boundary markers (<tool_data> tags) |
| Output validation | StructuredOutput.ValidateResponse — checks for leaked tool names, system prompt fragments |
| Telemetry redaction | PII scanning (email, phone, SSN, CC, API keys), company names hashed, user messages never logged |
| Tier | Count | LLM | What |
|---|---|---|---|
| Unit tests | 95 | No | Repositories, plugin methods, schema sync, injection detection, PII scanning, output validation, history trimming, pipeline orchestration, tool dispatch |
| Integration tests | 10 | No | Real HTTP pipeline via WebApplicationFactory — SSE streaming, session continuity, rate limiting, tool-call flow, injection threshold |
| Playwright E2E | 9 | No | Browser automation — chat flow, tool status, session continuity, error handling, rate limiting, page reload |
| Tool evals | 7 | No | Deterministic tool output against seeded SQLite |
| Agent evals | 10 | Yes | Full pipeline: order lookup, sentiment, company search, off-topic rejection, multi-tool, prompt injection (DAN jailbreak, system prompt extraction, data exfiltration) |
Security evals use Microsoft.Extensions.AI.Evaluation with a custom InjectionResistanceEvaluator that scores responses 1-5 for prompt leak and persona adoption. Results are stored to disk and can be viewed as an HTML report.
# Unit + integration tests (safe, no API key)
cd backend && dotnet test tests/OpsAgent.Tests
# Tool evals (safe, no API key)
cd backend && dotnet test evals/OpsAgent.Evals --filter "Category=ToolEval"
# Agent evals (requires ANTHROPIC_API_KEY)
cd backend && ANTHROPIC_API_KEY=... dotnet test evals/OpsAgent.Evals --filter "Category=Integration"
# Playwright E2E (starts own backend in Testing mode, no API key)
cd frontend && pnpm test:e2e
# All backend tests except agent evals
cd backend && dotnet test --filter "Category!=Integration"User (10) Order (20) Product (3)
+----------+ 1:N +-----------------+ N:1 +------------------+
| id |<----------| user_id (FK) |------->| id |
| username | | code "ORD-" | | name |
| is_active| | status | | included_tonnage |
+----------+ | waste_type_id | | _quantity |
| access_details | +------------------+
| conversation_id-|
| start/end_date |
+-----------------+
|
1:N | (via conversation_id)
v
Message (60)
+-----------------+
| conversation_id |
| message |
| sentiment_label |
| created_on |
+-----------------+
All seed data is synthetic (generated UUIDs, @example.com emails, generic company names).
# Dev: frontend (nginx) + backend (.NET) — 2 containers
docker compose up --build
# Lean: + Jaeger tracing UI at :16686 — 3 containers
docker compose --profile lean up --build
# Full: + Langfuse AI observability at :3100 — 8 containers
docker compose --profile full up --build
# Both: dual export (Jaeger + Langfuse) — 9 containers
docker compose --profile lean --profile full up --build| Service | Port | Profile |
|---|---|---|
| Frontend (nginx) | 3000 | always |
| Backend (.NET) | 8000 | always |
| Jaeger UI | 16686 | lean |
| Langfuse UI | 3100 | full |
For Langfuse first-time setup, see ARCHITECTURE_INSIGHTS.md.
Note: docker compose down -v deletes the Langfuse database and all stored credentials. Use docker compose down (without -v) to preserve data across restarts. After a -v reset, create a new Langfuse account and API keys via the UI at :3100 and update .env.
# Backend (run from backend/)
cd backend && dotnet build
export ANTHROPIC_API_KEY=sk-ant-...
cd backend && dotnet run --project src/OpsAgent.Api
# Frontend (separate terminal)
cd frontend && pnpm install && pnpm dev
# Dev server proxies /api -> localhost:8000backend/
Dockerfile # .NET backend container (non-root)
src/
OpsAgent.Core/ # Domain: entities, schemas, interfaces, telemetry, injection detection
OpsAgent.Infrastructure/ # Data: EF Core, repos, AnthropicModelClient, AnthropicTurnBuilder, conversation store
AI/ # Provider adapters (Anthropic)
Testing/ # DeterministicModelClient for E2E tests (inert in production)
OpsAgent.Agent/ # App: AgentRunner, ChatPipelineService, system prompt, tool dispatch
OpsAgent.Api/ # API: endpoints, DI, rate limiting, SSE concurrency middleware, OTel SDK
tests/
OpsAgent.Tests/ # Unit + integration tests (xUnit + FluentAssertions)
IntegrationTests/ # WebApplicationFactory HTTP pipeline tests
TestDoubles/ # Shared FakeModelClient
evals/
OpsAgent.Evals/ # Tool evals + agent evals (Microsoft.Extensions.AI.Evaluation)
data/ # CSV seed files
frontend/
Dockerfile # nginx frontend container (non-root)
nginx.conf # Reverse proxy + rate limiting + SSE support
e2e/ # Playwright browser tests
src/
components/chat/ # Chat UI (window, messages, input, copy)
components/chat/agent-response/ # Detail card rendering (opaque text blocks)
hooks/ # SSE streaming hook (@agenisea/sse-kit)
types/ # API type definitions
docker-compose.yml # Profile-based: dev, lean (Jaeger), full (Langfuse)
For deep-dive architectural decisions, trade-offs, and lessons learned, see ARCHITECTURE_INSIGHTS.md.
Built by Agenisea™ 🪼
