Contact Center Agent & Test/Eval Pipeline
A production Connect + Lex + Bedrock contact center agent, backed by a git- and upload-driven eval pipeline that gates every prompt, knowledge base, or test-case change before it ships.
The problem & requirements
- Handle voice/chat contacts via Amazon Connect, routed through Lex for intent, escalating to a Bedrock agent for complex or RAG-based answers
- Engineers add or edit test cases two ways: upload a file (.xlsx/.json/.jsonl/.csv) through the React/AppSync frontend, or edit in the codebase via a git diff on test-case files
- Either path triggers an automated eval run. DeepEval metrics, a RAG groundedness checker, and a KB retrieval check, per agent the test case is associated with
- Results (scores, turns, ground truth, Bedrock Guardrails info) are queryable in CI logs, S3, DynamoDB, and the frontend dashboard
- Traceability: every test run ties to a specific git commit/diff or uploaded file version
- CI/CD gate: pipeline flags or blocks deployment on score regression
- CI-vendor agnostic: works across GitLab-CI, GitHub Actions, and CodeBuild, not locked to one platform
- Isolation: CI eval traffic against Bedrock must not compete with production contact center traffic
- Auditability: CloudWatch logs and CloudFormation-provisioned infra, reproducible end to end
Scale & constraints
A typical PR touches ~200 test cases, each running 3 checks against an average of 2 associated agents.
API design
Data model
| Entity | Key fields |
|---|---|
| TestRun | test_run_id, source (upload/git_diff), source_ref, status, created_at, triggered_by |
| TestCase | test_case_id, test_run_id, agent_id, input, ground_truth, conversation_turns[] |
| TestResult | test_case_id, test_run_id, agent_id, deepeval_scores{}, rag_claims_score, kb_retrieval_score, guardrails_info{}, latency_ms, pass_fail |
| Agent config | agent_id, bedrock_model_id, kb_id, prompt_version, connect_flow_id |
source_ref on TestRun is what makes a score regression traceable back to the exact commit or upload that caused it. Without it, "the eval score dropped" has no owner.
Architecture
A test case either gets uploaded through the React/AppSync frontend or added via a git commit; both paths land in S3, trigger a Lambda that creates the test run record, and hand off through SQS to a CI/CD test stage running three checks per associated agent.
Fig. 3a: Test entry (upload or git diff) converges on S3, runs through a CI test stage, and surfaces back in the same frontend.
Connect handles the channel and routing. Lex resolves simple intents directly, and complex or knowledge-dependent queries escalate to the Bedrock agent, which draws on both a knowledge base and Lambda-backed tools.
Fig. 3b: Lex resolves intent directly; complex queries escalate to the Bedrock agent for RAG-backed reasoning.
Key decisions & trade-offs
source_ref (commit SHA or upload version) plus a pinned bedrock_model_id and prompt_version on the agent config means a score regression is always attributable to a specific change, not a moving target.Lessons learned
- Making blocking the default CI gate, not an option, held up under real pressure to ship faster. Every time someone pushed for a non-blocking "just this once," the ~4 minute cost looked small next to what a bad prompt change could do to a live customer conversation.
- Storing Guardrails info as structured data, not just a pass/fail flag, turned out to be the single most useful debugging decision. Separating "failed because ungrounded" from "failed because guardrails blocked it" meant fixes went to the right place instead of a shared bucket of "eval failures."
- Decoupling test-run creation from execution via SQS paid off during a real CI outage. The queue just held the backlog instead of silently dropping requests, and everything caught up once the runners came back.
- I didn't plan for per-agent fan-out until it was already a problem. As more agents got added to the platform, the 1,200-call PR cost crept up without anyone deciding it should. A smoke-subset vs. full-suite split should've been part of the design from the first agent, not a cap added after CI got slow.
- Bedrock traffic isolation came after a scare, not before it. A large eval run competing with live contact volume during a peak window was the thing that forced separate rate-limit buckets. I'd provision that isolation on day one for anything touching a model shared with production.
- Agent config (model ID, KB, prompt version) started as loose parameters, not a versioned entity. Retrofitting
prompt_versiononto existing test results after the fact was more painful than just treating agent config as a first-class, versioned object from the start.