Production-oriented AI projects across workflow automation, agentic systems and enterprise RAG.
Cloud provider
AI Application
AI Workflow Automation
Production-grade AI applications using Amazon Bedrock, Anthropic Claude
and OpenAI models to automate operational workflows and provide
intelligent organisational assistance.
Primary objective
Turn repetitive operational tasks into intelligent,
auditable AI-assisted workflows.
AWS Architecture
Request → AI reasoning → enterprise services → governed response
Production architecture
Staff / User
Web, mobile or internal portal
API Gateway / FastAPI
Authentication · validation · routing
Amazon Bedrock
Claude · GPT · Guardrails · Model routing
AI Workflows
Summarisation · drafting · classification
Business APIs
Operational systems · data services
Audit & Monitoring
CloudWatch · CloudTrail · logs
AI capabilities
✓ Claude-based reasoning
✓ GPT model integration
✓ Prompt orchestration
✓ Structured outputs
✓ Guardrails and validation
AWS services
Amazon Bedrock
Foundation models
Guardrails
Safety & policy
API Gateway
Edge routing
Lambda
Serverless compute
DynamoDB
State & history
Cognito
Authentication
CloudWatch
Metrics & logs
CloudTrail
Audit trail
Outcomes
✓ Reduced manual processing
✓ Faster information access
✓ Consistent AI-assisted outputs
✓ Auditable workflows
Sample code — governed Bedrock workflow
Invoke Claude on Amazon Bedrock with guardrails and structured output
workflows/summarize.pyPYTHON
import json, time, logging, boto3
from botocore.config import Config
from jsonschema import validate, ValidationError
log = logging.getLogger("workflows")
cw = boto3.client("cloudwatch")
bedrock = boto3.client(
"bedrock-runtime", region_name="us-east-1",
config=Config(retries={"max_attempts": 0}, read_timeout=30), # we retry ourselves
)
MODEL = "anthropic.claude-sonnet-4-20250514-v1:0"# Contract the model MUST satisfy — invalid output is rejected, not trusted
SCHEMA = {
"type": "object",
"required": ["summary", "priority", "next_action"],
"properties": {
"summary": {"type": "string", "maxLength": 600},
"priority": {"enum": ["low", "medium", "high"]},
"next_action": {"type": "string"},
},
"additionalProperties": False,
}
classGuardrailBlocked(Exception): ...
def_emit(name: str, value: float, unit="Count"):
cw.put_metric_data(Namespace="AI/Workflows",
MetricData=[{"MetricName": name, "Value": value, "Unit": unit}])
defsummarize_ticket(ticket: dict, *, max_retries: int = 3) -> dict:
prompt = f"""Summarise this operational ticket. Reply with ONLY JSON:
{{"summary": str, "priority": "low|medium|high", "next_action": str}}
Ticket: {json.dumps(ticket)}"""for attempt inrange(1, max_retries + 1):
started = time.perf_counter()
try:
resp = bedrock.invoke_model(
modelId=MODEL,
guardrailIdentifier="ops-guardrail", # PII redaction + denied topics
guardrailVersion="1",
body=json.dumps({
"anthropic_version": "bedrock-2023-05-31",
"max_tokens": 512, "temperature": 0,
"messages": [{"role": "user", "content": prompt}],
}),
)
payload = json.loads(resp["body"].read())
# Guardrail intervened → fail closed, never return raw model textif payload.get("amazon-bedrock-guardrailAction") == "INTERVENED":
_emit("GuardrailBlocked", 1)
raiseGuardrailBlocked("policy filter triggered")
data = json.loads(payload["content"][0]["text"])
validate(data, SCHEMA) # schema is the source of truth_emit("LatencyMs", (time.perf_counter() - started) * 1000, "Milliseconds")
_emit("Success", 1)
return data
except (json.JSONDecodeError, ValidationError) as e:
log.warning("invalid output (attempt %s): %s", attempt, e)
if attempt == max_retries:
_emit("ValidationFailure", 1)
raise
time.sleep(2 ** attempt * 0.2) # exponential backoff
Performance & scale
Throughput, concurrency & latency
Serverless fan-out — model inference dominates the wall clock
Representative targets
50–100 /s
Workflow throughput
horizontal Lambda fan-out per Region
1,000 concurrent
Default Lambda concurrency
reserved / provisioned raises the ceiling
1.2 s
p50 latency
~400 ms TTFT with streaming enabled
3.5 s
p95 latency
bounded output (max_tokens = 512)
$0.003–0.02 /run
Cost per workflow
scales with input + output tokens
Latency profile (single workflow)
TTFT
~0.4 s
p50
~1.2 s
p95
~3.5 s
Guardrails & controls
Safety, correctness & auditability
Fail closed — an unsafe or malformed answer is never returned
✕ Hard real-time paths needing sub-100 ms, deterministic responses.
✕ Jobs exceeding the 15-min Lambda limit — use Fargate / Step Functions.
✕ Exact legal/financial calculations where LLM variance is unacceptable.
✕ Very large context windows that inflate cost and latency per call.
✕ Air-gapped / offline environments without managed model access.
Agentic AI
Agentic AI Platform
Multi-step AI agents built with LangGraph, FastAPI and Model Context
Protocol, connecting language models to controlled enterprise tools
and operational systems.
Primary objective
Enable AI agents to reason, retrieve information and execute
controlled business operations.
Agent Architecture
LangGraph orchestration with MCP-based enterprise tools
User
Natural language request
FastAPI
API & authentication
LangGraph
Agent state & orchestration
Amazon Bedrock
Foundation model reasoning
MCP Gateway
Tool discovery & routing
Client Tools
Records & care information
Staff Tools
Workforce & availability
Scheduling Tools
Visits & operational actions
01
Understand
Classify the user's request and determine required capabilities.
02
Reason
LangGraph manages multi-step model reasoning and state.
03
Use tools
MCP exposes controlled business capabilities to the agent.
04
Execute
High-impact operations require validation and approval.
AWS services
Amazon Bedrock
Agent reasoning
ECS Fargate
Agent & MCP runtime
API Gateway
Entry point
Step Functions
Approval flows
DynamoDB
Agent memory
Amazon SQS
Async tool jobs
VPC / PrivateLink
Private tools
IAM
Scoped tool access
Sample code — LangGraph agent with MCP tools
A ReAct agent reasoning on Bedrock, calling controlled enterprise tools over MCP
agent/graph.pyPYTHON
import os
from langgraph.prebuilt import create_react_agent
from langgraph.checkpoint.memory import MemorySaver
from langgraph.errors import GraphRecursionError
from langchain_aws import ChatBedrockConverse
from langchain_mcp_adapters.client import MultiServerMCPClient
# Full step-by-step tracing + eval in LangSmith (zero code changes downstream)
os.environ["LANGSMITH_TRACING"] = "true"
os.environ["LANGSMITH_PROJECT"] = "agentic-platform-prod"
llm = ChatBedrockConverse(model="anthropic.claude-sonnet-4-20250514-v1:0", temperature=0)
# Only these tools may ever be bound — everything else is ignored
ALLOWED = {"list_visits", "get_client", "check_availability", "schedule_visit"}
WRITE_TOOLS = {"schedule_visit"} # require human approval before executingasync defbuild_agent():
mcp = MultiServerMCPClient({
"scheduling": {"url": "https://mcp.internal/scheduling", "transport": "streamable_http"},
"records": {"url": "https://mcp.internal/records", "transport": "streamable_http"},
})
tools = [t for t inawait mcp.get_tools() if t.name in ALLOWED] # allow-listreturncreate_react_agent(
llm, tools,
checkpointer=MemorySaver(), # resumable state for approvals
interrupt_before=["tools"], # pause before any tool call
prompt="You are an operations assistant. Never invent data; cite tool results.",
)
async defrun(agent, message: str, thread_id: str, approve):
cfg = {"configurable": {"thread_id": thread_id}, "recursion_limit": 12} # loop captry:
state = await agent.ainvoke({"messages": [("user", message)]}, cfg)
# Paused on a pending tool call — gate writes through a humanwhile (pending := state.get("__interrupt__")):
call = state["messages"][-1].tool_calls[0]
if call["name"] in WRITE_TOOLS andnotawaitapprove(call):
return"Action declined by reviewer."
state = await agent.ainvoke(None, cfg) # resume the graphreturn state["messages"][-1].content
except GraphRecursionError:
return"Stopped: step budget exceeded before reaching an answer."
Performance & scale
Throughput, concurrency & latency
Multi-step loops — latency is the sum of model + tool round-trips
Representative targets
3–8 steps
Tool calls per task
capped at 12 by the recursion limit
10–30 /task
Concurrent sessions
per Fargate task; ECS autoscales horizontally
4–9 s
p50 end-to-end
grows with the number of reasoning steps
~20 s
p95 end-to-end
long tool calls offloaded via SQS
$0.05–0.30 /task
Cost per task
multi-turn context accumulates tokens
Where the time goes (typical 5-step task)
Reasoning
~55%
Tool I/O
~35%
Overhead
~10%
Guardrails & controls
Bounded autonomy
The agent can reason freely but acts only within hard limits
Tool allow-listing
Only vetted tools are bound; IAM-scoped MCP servers behind PrivateLink.
Human-in-the-loop
Write / high-impact actions pause on an interrupt and require approval.
Loop & step limits
Recursion limit + per-tool timeouts stop runaway or stuck reasoning.
Token & cost budgets
Per-task ceilings abort long chains before they run up spend.
Reversible / dry-run
Prefer idempotent, reversible actions; simulate before committing.
LangSmith tracing & eval
Every reasoning step and tool call is traced in LangSmith for replay, latency breakdowns and offline evaluation.
When agents earn their keep
→ Tasks needing dynamic, multi-step decisions across several tools.
→ Read-heavy investigation with a few gated, approved writes.
→ Workflows whose exact steps vary per request and can't be pre-scripted.
→ Well-defined, reliable tool APIs the agent can compose.
→ Latency budgets measured in seconds, not milliseconds.
When an agent is the wrong tool
✕ Fixed, deterministic pipelines — a plain workflow is cheaper and safer.
✕ Strict low-latency paths that can't absorb multiple round-trips.
✕ High-volume, cost-sensitive traffic where per-task token spend hurts.
✕ Irreversible actions without a reliable human approval gate.
✕ Flaky or unpredictable tool APIs the agent can't recover from.
Retrieval-Augmented Generation
Enterprise RAG Platform
A grounded knowledge platform that combines enterprise documents,
operational information and foundation models to provide accurate,
context-aware answers with source citations.
Primary objective
Give users reliable answers grounded in trusted enterprise
documentation and live operational data.
RAG Architecture
Documents + operational data → retrieval → grounded generation
→ Doc-level access control across mixed-sensitivity content.
When to reach for something else
✕ Tiny corpora that fit in the prompt — just pass the context directly.
✕ Reasoning/synthesis far beyond the documents themselves.
✕ Real-time data not yet indexed — call the live API instead.
✕ Poor source quality: garbage in, confidently-cited garbage out.
✕ Ultra-low-latency (<500 ms) paths where retrieval + generation is too slow.
Model Context Protocol
MCP Tool Platform
A governed layer that exposes enterprise systems as standard MCP
tools, resources and prompts — so any agent or model can safely
reuse the same controlled capabilities over one protocol.
Primary objective
Decouple tools from models: build capabilities once, govern them
centrally, reuse them across every AI application.
AuthN/Z, private networking & rate limiting at the edge
Scheduling Server
Tools · resources · prompts
Records Server
Read-scoped client data
Search Server
Knowledge & retrieval
ECS Fargate
Server runtime
DynamoDB
Operational data
IAM
Per-tool scope
LangSmith
Tool tracing
Tools
Model-invocable, typed actions (read & gated write) with JSON-schema
inputs the client validates before calling.
Resources
Addressable, read-only context (documents, records) the host can
attach to a prompt without a side-effecting call.
Prompts
Reusable, parameterised prompt templates published by the server so
every client invokes a capability the same way.
AWS services & tooling
MCP Servers
Tool layer
ECS Fargate
Server runtime
API Gateway
Edge + rate limit
PrivateLink
Private transport
Cognito
Session auth
IAM
Per-tool scope
DynamoDB
Backing data
LangSmith
Tool tracing & eval
Sample code — a governed MCP tool server
Typed tools over streamable HTTP, with read/write separation and scope checks
mcp_servers/scheduling.pyPYTHON
from mcp.server.fastmcp import FastMCP, Context
import boto3
mcp = FastMCP("scheduling")
visits = boto3.resource("dynamodb").Table("visits")
classNotAuthorised(Exception): ...
def_require(ctx: Context, scope: str):
# Scopes come from the Cognito token the client presented at connectif scope notin ctx.session.scopes:
raiseNotAuthorised(f"missing scope: {scope}")
# READ tool — typed signature IS the input schema the client validates@mcp.tool()defcheck_availability(staff_id: str, date: str, ctx: Context) -> list[str]:
"""Return open time slots for a staff member on YYYY-MM-DD."""_require(ctx, "scheduling:read")
rows = visits.query(KeyConditionExpression=Key("staff").eq(staff_id)).get("Items", [])
booked = {r["slot"] for r in rows if r["date"] == date}
return [s for s in SLOTS if s notin booked]
# WRITE tool — separate scope; host gates it behind human approval@mcp.tool()defschedule_visit(staff_id: str, client_id: str, slot: str, ctx: Context) -> dict:
"""Book a visit. Side-effecting — requires the scheduling:write scope."""_require(ctx, "scheduling:write")
visits.put_item(
Item={"staff": staff_id, "client": client_id, "slot": slot},
ConditionExpression="attribute_not_exists(slot)", # idempotent
)
return {"status": "booked", "staff": staff_id, "slot": slot}
if __name__ == "__main__":
mcp.run(transport="streamable-http") # fronted by API Gateway + PrivateLink
Performance & scale
Throughput, concurrency & latency
A thin, stateless protocol layer — backend calls dominate
Representative targets
40–150 ms
Tool call overhead
protocol + auth, excluding backend work
500+ /s
Calls per task
stateless Fargate tasks scale horizontally
HTTP + stdio
Transports
streamable HTTP remote · stdio local
$0 tokens
Model cost here
compute only — no inference at this layer
N clients
Reuse factor
one server serves every MCP-capable host
Guardrails & controls
Governance at the tool boundary
The server, not the model, is the enforcement point
Scoped auth per tool
Cognito token scopes gate each tool; IAM roles scope each server's reach.
Typed input validation
Tool signatures are the schema — malformed calls are rejected before execution.
Read / write separation
Side-effecting tools carry a distinct scope the host can gate behind approval.
Private networking
Servers stay off the public internet — reached only via VPC / PrivateLink.
Rate limits & quotas
API Gateway throttles per client so one host can't exhaust a tool.
End-to-end tracing
Every tool call is traced in LangSmith & CloudWatch for audit and eval.
When MCP earns its place
→ Multiple agents or apps need to share the same governed tools.
→ You want tools decoupled from any one model or framework.
→ Enterprise systems must sit behind a single controlled boundary.
→ Central auth, rate limiting and audit for tool access are required.
→ Tools evolve independently and should version without touching clients.
When it's overkill
✕ A single app with a couple of internal functions — just call them.
✕ Ultra-low-latency inline logic where a network hop isn't justified.
✕ No cross-app reuse — the protocol overhead buys you nothing.
✕ Throwaway prototypes where a standard interface adds friction.
✕ Pure data retrieval already well served by a managed RAG pipeline.
Composable LLM Chains
LangChain Pipelines
Deterministic, composable LLM pipelines built with LangChain
Expression Language (LCEL) — prompt, model and parser wired into
typed, testable units that stream, batch and fall back for free.
Primary objective
Turn one-off prompt calls into reusable, structured pipelines with
predictable inputs, outputs and cost.