<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Mohsin Sheikhani</title>
    <description>The latest articles on DEV Community by Mohsin Sheikhani (@mohsinsheikhani).</description>
    <link>https://dev.to/mohsinsheikhani</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3205660%2F86f13820-8822-4969-8c94-661bd5847686.png</url>
      <title>DEV Community: Mohsin Sheikhani</title>
      <link>https://dev.to/mohsinsheikhani</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mohsinsheikhani"/>
    <language>en</language>
    <item>
      <title>An AI Gateway Is Not a Router. It's a Governance Layer.</title>
      <dc:creator>Mohsin Sheikhani</dc:creator>
      <pubDate>Mon, 13 Jul 2026 12:24:31 +0000</pubDate>
      <link>https://dev.to/mohsinsheikhani/an-ai-gateway-is-not-a-router-its-a-governance-layer-2j79</link>
      <guid>https://dev.to/mohsinsheikhani/an-ai-gateway-is-not-a-router-its-a-governance-layer-2j79</guid>
      <description>&lt;p&gt;AI gateway sounds like a router: something that picks a model, maybe falls back to a backup when one is down, and gets out of the way. That's the least interesting thing a gateway does.&lt;/p&gt;

&lt;p&gt;The actual job is governance. Cost control, security, and audit. Routing is one line item on that list, not the headline.&lt;/p&gt;

&lt;p&gt;This post walks through all four and shows the exact config behind each.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five services, five ways to get it wrong
&lt;/h2&gt;

&lt;p&gt;Picture a company with five things calling LLMs directly: a support bot, a RAG pipeline, an internal LangGraph agent, a nightly batch summarizer, and a customer-facing chat feature. No gateway. Each one talks to the provider on its own.&lt;/p&gt;

&lt;p&gt;Here's what that actually means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every service carries its own copy of the provider API key. A leak in the batch summarizer is a leak of the entire budget.&lt;/li&gt;
&lt;li&gt;Rate limiting and retries are whatever each team got around to. Usually that means "none" until the first incident.&lt;/li&gt;
&lt;li&gt;Nobody has a live view of spend. Finance learns about a runaway agent when the invoice arrives, not when the loop starts burning tokens at 2am.&lt;/li&gt;
&lt;li&gt;PII scanning is per-team folklore. Some scrub, some don't, nobody agrees on what counts.&lt;/li&gt;
&lt;li&gt;Logging is five different formats, if it exists at all.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You don't have one problem five times. You have five slightly different, half-finished versions of the same five problems. Put them all in front of an auditor and none of them hold up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Collapse it into one control plane
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv52ohxf84kw5u6g39110.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv52ohxf84kw5u6g39110.png" alt="AI Gateway to enforce Governance | AI Engineer - Mohsin Sheikhani" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One gateway. Every service talks to it with an OpenAI-compatible API, and every rule gets enforced in that one box before a request ever reaches a provider. The four controls below all live inside it.&lt;/p&gt;

&lt;p&gt;I used &lt;a href="https://docs.litellm.ai/" rel="noopener noreferrer"&gt;LiteLLM&lt;/a&gt; as the proxy. The theme running through all four controls: don't rebuild what the platform already does well, just wire it correctly and make it enforce.&lt;/p&gt;

&lt;h2&gt;
  
  
  Control 1: cost governance that counts tokens, not requests
&lt;/h2&gt;

&lt;p&gt;A normal API gateway counts requests. For LLMs that's the wrong unit, and it's wrong in a way that hurts. A ten-word prompt and a ten-thousand-token agent trajectory both register as "one request" while costing wildly different amounts. If you want to govern spend, you track tokens.&lt;/p&gt;

&lt;p&gt;And you track them per identity, not as one global number. Per API key, per team, per customer. A single global counter tells you the ship is sinking but not which compartment.&lt;/p&gt;

&lt;p&gt;So the design is hierarchical budgets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;team&lt;/strong&gt; budget. This is the unit that gets enforced. Every user belongs to a team.&lt;/li&gt;
&lt;li&gt;Rolled up into an &lt;strong&gt;org / customer&lt;/strong&gt; budget above it.&lt;/li&gt;
&lt;li&gt;The request gets rejected &lt;em&gt;before&lt;/em&gt; it hits the provider if either level is over.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Users still exist as identities, so spend is attributed to a person (the audit trail in Control 4 needs that). But there's no enforced budget on the user tier, and that's on purpose. LiteLLM only enforces a user's own budget when that user has no team, so for anyone on a team it does nothing. The hierarchy that actually bites is team then org. The user is for attribution.&lt;/p&gt;

&lt;p&gt;Budgets reset on a window you pick: daily, weekly, monthly. This is the control that keeps one runaway agent loop from becoming a five-figure surprise. If any of your traffic is agentic rather than one-shot completions, that's the failure mode that will eventually find you.&lt;/p&gt;

&lt;h3&gt;
  
  
  Two planes, and LiteLLM owns the money
&lt;/h3&gt;

&lt;p&gt;I didn't rebuild token tracking. Per-model pricing and pre-flight spend accounting is the one thing LiteLLM genuinely does well, so the system splits into two planes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Identity plane (mine).&lt;/strong&gt; Cognito plus an &lt;code&gt;auth&lt;/code&gt; service. Source of truth for who the caller is: &lt;code&gt;Org → Team → User&lt;/code&gt;, in Postgres.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost plane (LiteLLM).&lt;/strong&gt; Budgets, spend, and reject-before-spend all live in LiteLLM's own DB, on its &lt;code&gt;Organization&lt;/code&gt; and &lt;code&gt;Team&lt;/code&gt; records. I never store a dollar or a token myself.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The two planes meet at two points: provisioning and request time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Provisioning.&lt;/strong&gt; Seed the budgeted Org and Team records in LiteLLM with a &lt;code&gt;max_budget&lt;/code&gt; and a reset window, the team linked to its org. The key detail is that the IDs are the auth DB's own primary keys, not names. Names collide and aren't stable, so they're never the join key.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# provision.py — mirror each Org/Team into LiteLLM with a budget.
&lt;/span&gt;&lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;LITELLM_URL&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/organization/new&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;_headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;organization_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;org&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;      &lt;span class="c1"&gt;# our Postgres PK, stringified
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;organization_alias&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;org&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;# display name only, not a key
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;max_budget&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ORG_BUDGET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;            &lt;span class="c1"&gt;# e.g. 500
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;budget_duration&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BUDGET_DURATION&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# e.g. "30d"
&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;LITELLM_URL&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/team/new&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;_headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;team_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;team&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;team_alias&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;team&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;organization_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;team&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;org_id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="c1"&gt;# links team -&amp;gt; org so the org budget rolls up
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;max_budget&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TEAM_BUDGET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           &lt;span class="c1"&gt;# e.g. 100
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;budget_duration&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BUDGET_DURATION&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Full file: &lt;a href="https://github.com/mohsinsheikhani/llm-gateway-governance/blob/main/services/gateway/provision.py" rel="noopener noreferrer"&gt;&lt;code&gt;services/gateway/provision.py&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Request time.&lt;/strong&gt; Callers send their Cognito JWT to the gateway. LiteLLM runs a &lt;code&gt;custom_auth&lt;/code&gt; hook that verifies the JWT and resolves it to &lt;code&gt;user_id / team_id / org_id&lt;/code&gt;, using the same IDs provisioning used. LiteLLM then checks the team and org budgets and rejects before calling the provider if either is over.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# custom_auth.py — the last few lines of the hook.
# Verify the Cognito access token, resolve the sub to our team/org PKs, and
# hand LiteLLM an identity. The IDs match what provision.py created, so the
# budget lookup lands on the right records.
&lt;/span&gt;&lt;span class="n"&gt;claims&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_verify_access_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;team_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;org_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_resolve_identity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;claims&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sub&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;UserAPIKeyAuth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;claims&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sub&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;   &lt;span class="c1"&gt;# Cognito sub
&lt;/span&gt;    &lt;span class="n"&gt;team_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;team_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;         &lt;span class="c1"&gt;# our Postgres PK
&lt;/span&gt;    &lt;span class="n"&gt;org_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;org_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Full file: &lt;a href="https://github.com/mohsinsheikhani/llm-gateway-governance/blob/main/services/gateway/custom_auth.py" rel="noopener noreferrer"&gt;&lt;code&gt;services/gateway/custom_auth.py&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Wiring the hook in is two lines of proxy config. The second one matters more than it looks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config.yaml&lt;/span&gt;
&lt;span class="na"&gt;general_settings&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;custom_auth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;custom_auth.user_api_key_auth&lt;/span&gt;
  &lt;span class="c1"&gt;# Without this, the hook authenticates but the budgets are never checked.&lt;/span&gt;
  &lt;span class="na"&gt;custom_auth_run_common_checks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Full file: &lt;a href="https://github.com/mohsinsheikhani/llm-gateway-governance/blob/main/services/gateway/config.yaml" rel="noopener noreferrer"&gt;&lt;code&gt;services/gateway/config.yaml&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;One question worth answering directly: why a custom hook instead of LiteLLM's JWT-to-virtual-key mapping? Because the native mapping is an Enterprise feature. The open-source &lt;code&gt;custom_auth&lt;/code&gt; hook does the same job, turning a Cognito identity into a governed LiteLLM identity, in about twenty lines you write yourself.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F71xd0a76sb6ms56iiagd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F71xd0a76sb6ms56iiagd.png" alt="LLM Cost Governance | AI Engineer - Mohsin Sheikhani" width="800" height="681"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The demo shrinks a team budget, logs in a real user, and loops until the gateway answers &lt;code&gt;HTTP 429 Budget has been exceeded! Team=1&lt;/code&gt;. That 429 comes before any provider call, which is the whole point.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu7onfa48wgi0mupa59a3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu7onfa48wgi0mupa59a3.png" alt="LLM Cost Governance Demo | AI Engineer - Mohsin Sheikhani" width="800" height="545"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Control 2: security at the edge
&lt;/h2&gt;

&lt;p&gt;An API gateway checks &lt;em&gt;who&lt;/em&gt; is calling. An AI gateway also has to check &lt;em&gt;what&lt;/em&gt; they're sending, and that second check is the one people skip.&lt;/p&gt;

&lt;p&gt;A prompt is free text, and services stuff whatever they have into it. The support bot pastes in the customer's email. The agent quotes a tool result that happens to contain a card number. Send that as-is and the PII lands in a third-party provider's logs. That's a data-egress incident, and the uncomfortable part is that nobody did anything obviously wrong to cause it.&lt;/p&gt;

&lt;p&gt;So security at the edge is two layers. The &lt;em&gt;who&lt;/em&gt; is already handled by Control 1: the same &lt;code&gt;custom_auth&lt;/code&gt; hook rejects any request without a valid JWT. The new part is the &lt;em&gt;what&lt;/em&gt;: inspect the prompt's content before it leaves the network, and mask PII pre-flight.&lt;/p&gt;

&lt;h3&gt;
  
  
  Presidio as a sidecar, masking before the provider call
&lt;/h3&gt;

&lt;p&gt;I didn't write a PII scanner either. LiteLLM ships a guardrail that integrates &lt;a href="https://microsoft.github.io/presidio/" rel="noopener noreferrer"&gt;Microsoft Presidio&lt;/a&gt;, so I stood Presidio up as two sidecars and wired it in. The config is small, and every knob in it is a decision:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config.yaml&lt;/span&gt;
&lt;span class="na"&gt;guardrails&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;guardrail_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;presidio-pii"&lt;/span&gt;
    &lt;span class="na"&gt;litellm_params&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;guardrail&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;presidio&lt;/span&gt;
      &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pre_call"&lt;/span&gt;        &lt;span class="c1"&gt;# scrub the request BEFORE the provider is called&lt;/span&gt;
      &lt;span class="na"&gt;default_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;        &lt;span class="c1"&gt;# every request, not opt-in — opt-in isn't enforcement&lt;/span&gt;
      &lt;span class="na"&gt;presidio_language&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;en"&lt;/span&gt;
      &lt;span class="na"&gt;pii_entities_config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;PERSON&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MASK"&lt;/span&gt;         &lt;span class="c1"&gt;# names  -&amp;gt; &amp;lt;PERSON&amp;gt;&lt;/span&gt;
        &lt;span class="na"&gt;EMAIL_ADDRESS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MASK"&lt;/span&gt;  &lt;span class="c1"&gt;# emails -&amp;gt; &amp;lt;EMAIL_ADDRESS&amp;gt;&lt;/span&gt;
        &lt;span class="na"&gt;CREDIT_CARD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MASK"&lt;/span&gt;    &lt;span class="c1"&gt;# cards  -&amp;gt; &amp;lt;CREDIT_CARD&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Full file: &lt;a href="https://github.com/mohsinsheikhani/llm-gateway-governance/blob/main/services/gateway/config.yaml" rel="noopener noreferrer"&gt;&lt;code&gt;services/gateway/config.yaml&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Three things worth calling out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mask, don't block.&lt;/strong&gt; Detected entities are replaced with typed placeholders and the now-clean request continues. The caller still gets a useful answer, the provider never sees the raw PII.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;default_on: true&lt;/code&gt;.&lt;/strong&gt; The scan runs on every request. An opt-in guardrail is a suggestion, not a control.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fail-closed, for free.&lt;/strong&gt; Because &lt;code&gt;pii_entities_config&lt;/code&gt; is set, LiteLLM's Presidio integration raises if the analyzer is unreachable or errors. A degraded scanner rejects traffic instead of forwarding it unscrubbed, so a misconfiguration or an outage can't quietly leak PII.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The two Presidio containers run right next to the gateway in the same compose file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# docker-compose.yaml&lt;/span&gt;
&lt;span class="na"&gt;presidio-analyzer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;     &lt;span class="c1"&gt;# detects entities with an NLP model (slow first boot)&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mcr.microsoft.com/presidio-analyzer:latest&lt;/span&gt;
  &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5002:3000"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;presidio-anonymizer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;   &lt;span class="c1"&gt;# applies the masking&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mcr.microsoft.com/presidio-anonymizer:latest&lt;/span&gt;
  &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5001:3000"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Full file: &lt;a href="https://github.com/mohsinsheikhani/llm-gateway-governance/blob/main/docker-compose.yaml" rel="noopener noreferrer"&gt;&lt;code&gt;docker-compose.yaml&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3yxxxuyoamgmsb9kvg81.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3yxxxuyoamgmsb9kvg81.png" alt="LLM Security at the edge | AI Engineer - Mohsin Sheikhani" width="800" height="734"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The demo sends a prompt full of PII and asks the model to echo it back verbatim, so you can see exactly what left the network: only the placeholders come back. Stop the analyzer and re-run, and the request is rejected instead of leaked.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwz1n4mfs3xkf3vimj46h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwz1n4mfs3xkf3vimj46h.png" alt="LLM Security at the edge Demo | AI Engineer - Mohsin Sheikhani" width="800" height="712"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Control 3: routing and failover
&lt;/h2&gt;

&lt;p&gt;The caller shouldn't have to know which model to use, and shouldn't ever see a provider outage. Both are the gateway's job, and they sit at opposite ends of a request's timeline.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Routing is a cost decision made &lt;em&gt;before&lt;/em&gt; the call.&lt;/strong&gt; Not every prompt needs the expensive model. "What's the capital of France?" and a distributed-systems design question both look like one request, but sending the first to gpt-4o is money set on fire.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Failover is a reliability decision made &lt;em&gt;after&lt;/em&gt; a call fails.&lt;/strong&gt; A provider throws a 500 or times out. Instead of handing that to the caller, the gateway retries on a healthy backup.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Unlike the first two controls, there's no hook code here. LiteLLM does both natively, so Control 3 is configuration. The work was in picking a mechanism that isn't smoke and mirrors.&lt;/p&gt;

&lt;h3&gt;
  
  
  Routing: the complexity router
&lt;/h3&gt;

&lt;p&gt;LiteLLM ships two content-based routers. A &lt;em&gt;semantic&lt;/em&gt; one embeds every prompt and matches it against example utterances, which costs an embedding call per request. A &lt;em&gt;complexity&lt;/em&gt; one scores keywords, code, and reasoning markers with rules, in under a millisecond, no extra call. I used the complexity router. It's free per request and deterministic, and paying an embedding call to decide how to save money undercuts the whole point.&lt;/p&gt;

&lt;p&gt;Callers hit one logical model, &lt;code&gt;smart-router&lt;/code&gt;, and the gateway maps the prompt to a cost tier:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config.yaml&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;model_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;smart-router&lt;/span&gt;
  &lt;span class="na"&gt;litellm_params&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;auto_router/complexity_router&lt;/span&gt;
    &lt;span class="na"&gt;complexity_router_config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;tiers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;SIMPLE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gpt-4o-mini&lt;/span&gt;      &lt;span class="c1"&gt;# greetings, definitions -&amp;gt; cheapest&lt;/span&gt;
        &lt;span class="na"&gt;MEDIUM&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gpt-4o-mini&lt;/span&gt;      &lt;span class="c1"&gt;# standard queries       -&amp;gt; still cheap&lt;/span&gt;
        &lt;span class="na"&gt;COMPLEX&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gpt-4o&lt;/span&gt;          &lt;span class="c1"&gt;# technical, multi-part   -&amp;gt; pricier&lt;/span&gt;
        &lt;span class="na"&gt;REASONING&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;o3-mini&lt;/span&gt;       &lt;span class="c1"&gt;# chain-of-thought        -&amp;gt; reasoning model&lt;/span&gt;
      &lt;span class="na"&gt;default_model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gpt-4o-mini&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fniv2bi4lajgdpu6uanpq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fniv2bi4lajgdpu6uanpq.png" alt="LLM Routing Demo | AI Engineer - Mohsin Sheikhani" width="800" height="635"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo9q1vjs22eow35fhgaoo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo9q1vjs22eow35fhgaoo.png" alt="LLM Routing Demo | AI Engineer - Mohsin Sheikhani" width="800" height="660"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Failover
&lt;/h3&gt;

&lt;p&gt;LiteLLM's &lt;code&gt;fallbacks&lt;/code&gt; list reroutes a failed request to a backup. I wired it so the demo can't cheat: a deployment named &lt;code&gt;flaky-provider&lt;/code&gt; points its endpoint at a closed port, so it always fails with a connection error, standing in for a provider outage.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config.yaml&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;model_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;flaky-provider&lt;/span&gt;
  &lt;span class="na"&gt;litellm_params&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;openai/gpt-4o-mini&lt;/span&gt;
    &lt;span class="na"&gt;api_key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;os.environ/OPENAI_API_KEY&lt;/span&gt;
    &lt;span class="na"&gt;api_base&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://localhost:1&lt;/span&gt;     &lt;span class="c1"&gt;# nothing listening -&amp;gt; connection refused&lt;/span&gt;

&lt;span class="na"&gt;litellm_settings&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;fallbacks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[{&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;flaky-provider"&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-4o-mini"&lt;/span&gt;&lt;span class="pi"&gt;]}]&lt;/span&gt;
  &lt;span class="na"&gt;num_retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
  &lt;span class="na"&gt;request_timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Full file: &lt;a href="https://github.com/mohsinsheikhani/llm-gateway-governance/blob/main/services/gateway/config.yaml" rel="noopener noreferrer"&gt;&lt;code&gt;services/gateway/config.yaml&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A call to &lt;code&gt;flaky-provider&lt;/code&gt; fails fast, the fallback sends it to a healthy &lt;code&gt;gpt-4o-mini&lt;/code&gt;, and the caller gets a normal 200.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdi1ep4pp2ns7h22x4pd8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdi1ep4pp2ns7h22x4pd8.png" alt="LLM Failover Demo | AI Engineer - Mohsin Sheikhani" width="799" height="572"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftgyxxflr0igvf0assznz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftgyxxflr0igvf0assznz.png" alt="LLM Failover Demo | AI Engineer - Mohsin Sheikhani" width="800" height="651"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One thing I deliberately left out: cascading.&lt;/strong&gt; Cascading means calling the cheap model, inspecting its answer, and escalating if it isn't good enough. It gates on the &lt;em&gt;response&lt;/em&gt;, so no LiteLLM router does it, because they all decide from the prompt before the call. It's a real technique, but it's a different shape (a &lt;code&gt;post_call&lt;/code&gt; hook with a quality gate) and belongs in its own control, not smuggled into this one.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjj7lm3z9ar9awrfxg4zg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjj7lm3z9ar9awrfxg4zg.png" alt="LLM Routing and Failover | AI Engineer - Mohsin Sheikhani" width="800" height="833"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The demo sends three prompts through one &lt;code&gt;smart-router&lt;/code&gt; endpoint, then resolves the &lt;code&gt;x-litellm-model-id&lt;/code&gt; response header to prove they landed on three different models, and finishes by surviving the &lt;code&gt;flaky-provider&lt;/code&gt; outage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Control 4: the audit trail
&lt;/h2&gt;

&lt;p&gt;The first three controls each make a decision per request: who you are, whether the prompt is safe to send, which model runs it, whether you're over budget. To a security or finance team, none of that is worth much without a record. The audit trail is the record. For every request: who called, what model ran, how many tokens, what it cost, how long it took, and the field everyone forgets, whether it was allowed or rejected.&lt;/p&gt;

&lt;h3&gt;
  
  
  Trace to Langfuse, success and failure, tagged with identity
&lt;/h3&gt;

&lt;p&gt;No logging pipeline here either. LiteLLM traces every call to &lt;a href="https://langfuse.com/" rel="noopener noreferrer"&gt;Langfuse&lt;/a&gt; natively: model, tokens, cost, latency, input, and output, out of the box. Three choices turn that from observability into an audit control, and all three are in this block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config.yaml&lt;/span&gt;
&lt;span class="na"&gt;litellm_settings&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;success_callback&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;langfuse"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;   &lt;span class="c1"&gt;# completed calls&lt;/span&gt;
  &lt;span class="na"&gt;failure_callback&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;langfuse"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;   &lt;span class="c1"&gt;# rejections and errors — the events that matter most&lt;/span&gt;
  &lt;span class="na"&gt;langfuse_default_tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;user_api_key_user_id&lt;/span&gt;         &lt;span class="c1"&gt;# Cognito sub&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;user_api_key_team_id&lt;/span&gt;         &lt;span class="c1"&gt;# our Postgres PKs, straight from custom_auth&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;user_api_key_org_id&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Full file: &lt;a href="https://github.com/mohsinsheikhani/llm-gateway-governance/blob/main/services/gateway/config.yaml" rel="noopener noreferrer"&gt;&lt;code&gt;services/gateway/config.yaml&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Both callbacks.&lt;/strong&gt; An audit trail that only records successes hides the exact events that matter: the over-budget 429 from Control 1, a provider failure. Send both.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Identity tags.&lt;/strong&gt; Out of the box a trace tells you what ran and what it cost, but not who. The tags pull the identity resolved by &lt;code&gt;custom_auth&lt;/code&gt; in Control 1 straight onto every trace, server-side, so the caller can't forge them. Now you can filter the whole trail to one team and see everything it spent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PII already handled.&lt;/strong&gt; Control 2 masks PII before the provider call, and the trace stores that same scrubbed text. The audit log doesn't reintroduce the leak by parking raw PII in a third-party tool.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One thing this is not: a SIEM. Langfuse is observability. What you get is a queryable, per-request trail you can filter and export, a genuine first cut of an audit story, not a compliance-grade immutable log pipeline. Worth saying plainly so nobody oversells it in a meeting.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F09it55a63j9jvxu7ou2i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F09it55a63j9jvxu7ou2i.png" alt="Audit trial | AI Engineer - Mohsin Sheikhani" width="800" height="659"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The demo generates one success and one failure, both traced and tagged with the caller. Open the Traces view, filter on &lt;code&gt;user_api_key_team_id:1&lt;/code&gt;, and you see everything that team spent, rejections included.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;That's the four controls. Identity and cost, content security, routing and failover, and the audit trail. Each one a working LiteLLM configuration with a demo, and together they collapse five inconsistent per-service versions of these concerns into one control plane.&lt;/p&gt;

&lt;p&gt;The through-line is simple: an AI gateway earns its place by governing, not routing. Routing is the easy 10%. The budgets, the PII masking, the identity-tagged audit trail — that's the part that keeps you out of the incident channel and in front of the auditor with something that holds up.&lt;/p&gt;

&lt;p&gt;Full source, config, and demos: &lt;strong&gt;&lt;a href="https://github.com/mohsinsheikhani/llm-gateway-governance" rel="noopener noreferrer"&gt;github.com/mohsinsheikhani/ai-gateway-governance&lt;/a&gt;&lt;/strong&gt;. If you'd wire any of it differently, I'd genuinely like to hear it in the comments.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>security</category>
      <category>aiops</category>
    </item>
    <item>
      <title>How to Use Claude Code with Qwen models for Free (Linux)</title>
      <dc:creator>Mohsin Sheikhani</dc:creator>
      <pubDate>Sat, 03 Jan 2026 20:27:44 +0000</pubDate>
      <link>https://dev.to/mohsinsheikhani/how-to-use-claude-code-with-qwen-models-for-free-linux-1fc4</link>
      <guid>https://dev.to/mohsinsheikhani/how-to-use-claude-code-with-qwen-models-for-free-linux-1fc4</guid>
      <description>&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Qwen CLI installed and authenticated&lt;/li&gt;
&lt;li&gt;Node.js v18+ installed&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 1: Install Claude, Claude Code Router and Qwen Code
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install -g @qwen-code/qwen-code@latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install -g @anthropic-ai/claude-code @musistudio/claude-code-router
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Extract Your Access Token
&lt;/h3&gt;

&lt;p&gt;Replace LINUX_USER with your Linux username.&lt;/p&gt;

&lt;p&gt;Open &lt;code&gt;/home/LINUX_USER/.qwen/oauth_creds.json&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;It should look something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "access_token": "YOUR_QWEN_ACCESS_TOKEN_HERE",
  "token_type": "Bearer",
  "refresh_token": "YOUR_QWEN_REFRESH_TOKEN_HERE",
  "resource_url": "portal.qwen.ai",
  "expiry_date": 1764876220290
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Copy the &lt;code&gt;access_token&lt;/code&gt; value.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Create router config
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cat &amp;gt; ~/.claude-code-router/config.json &amp;lt;&amp;lt; 'EOF'
{
  "LOG": true,
  "LOG_LEVEL": "info",
  "HOST": "127.0.0.1",
  "PORT": 3456,
  "API_TIMEOUT_MS": 600000,
  "Providers": [
    {
      "name": "qwen",
      "api_base_url": "https://portal.qwen.ai/v1/chat/completions",
      "api_key": "$QWEN_ACCESS_TOKEN",
      "models": [
        "qwen3-coder-plus",
        "qwen3-coder-plus",
        "qwen3-coder-plus"
      ]
    }
  ],
  "Router": {
    "default": "qwen,qwen3-coder-plus",
    "background": "qwen,qwen3-coder-plus",
    "think": "qwen,qwen3-coder-plus",
    "longContext": "qwen,qwen3-coder-plus",
    "longContextThreshold": 60000,
    "webSearch": "qwen,qwen3-coder-plus"
  }
}
EOF
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Verify file was created
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cat ~/.claude-code-router/config.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 4: Set your Access Token
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;echo 'export QWEN_ACCESS_TOKEN="YOUR_QWEN_ACCESS_TOKEN_HERE"' &amp;gt;&amp;gt; ~/.zshrc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;source ~/.zshrc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 5: Verify Setup
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;claude --version        # Should show: Claude Code v2.x.x
ccr version             # Should show version number
echo $QWEN_ACCESS_TOKEN # Should show your token
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 6: Start Using
&lt;/h3&gt;

&lt;p&gt;Restart the router server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ccr restart
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run Claude Code with Qwen models:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ccr code
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt; hi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Token Refresh (When you get 401 errors)
&lt;/h3&gt;

&lt;p&gt;Your OAuth token expires. Refresh it by:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Re-authenticating your QWEN CODE CLI: If already logged in and the access_token matches in both &lt;code&gt;config.json&lt;/code&gt; and &lt;code&gt;oauth_creds.json&lt;/code&gt;, delete the &lt;code&gt;oauth_creds.json&lt;/code&gt; file and run &lt;code&gt;qwen&lt;/code&gt; to initiate re-authentication.&lt;/li&gt;
&lt;li&gt;Update the api_key in your config.json with the new access_token: &lt;code&gt;nano ~/.claude-code-router/config.json&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Restart: &lt;code&gt;ccr restart&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Hopefully&lt;/strong&gt; this will help you learn Claude Code for Free 💖&lt;/p&gt;

</description>
      <category>linux</category>
      <category>llm</category>
      <category>node</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Building Production Multi-Agent Systems: My Experience with Amazon Bedrock AgentCore, and AWS Strands Agents</title>
      <dc:creator>Mohsin Sheikhani</dc:creator>
      <pubDate>Mon, 29 Sep 2025 18:34:47 +0000</pubDate>
      <link>https://dev.to/mohsinsheikhani/building-production-multi-agent-systems-my-experience-with-amazon-bedrock-agentcore-and-aws-41h2</link>
      <guid>https://dev.to/mohsinsheikhani/building-production-multi-agent-systems-my-experience-with-amazon-bedrock-agentcore-and-aws-41h2</guid>
      <description>&lt;h3&gt;
  
  
  The 2 AM Hotel Booking Nightmare
&lt;/h3&gt;

&lt;p&gt;Picture this: It's 2 AM, you're frantically booking a hotel room for a business trip that got moved up by three days. You find the perfect room, book it, then realize six hours later that your flight got cancelled.&lt;br&gt;
Now you need to cancel the booking, but wait, what's the cancellation policy? Will there be fees? Can you modify instead of cancel? And why is customer service only available during business hours when your life operates on chaos-time?&lt;/p&gt;

&lt;p&gt;If this sounds familiar, you've experienced the fundamental problem with traditional hotel booking systems: they treat complex, multi-step workflows as isolated transactions. But real life isn't transactional, it's conversational, contextual, and full of "what-ifs."&lt;/p&gt;

&lt;p&gt;This is the story of how I built an AI system that doesn't just book hotels, it thinks like a seasoned concierge who remembers your preferences, understands hotel policies, and can handle the messy reality of travel planning.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why Traditional Hotel Booking Systems Miss the Mark
&lt;/h2&gt;

&lt;p&gt;Most hotel booking platforms treat you like you're ordering a pizza: pick your toppings (dates, location, price range), pay, and done. But hotel booking isn't pizza ordering, it's relationship management.&lt;/p&gt;

&lt;p&gt;Think about what a great hotel concierge does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Understands complex policies&lt;/strong&gt; and explains them in plain English
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handles changes gracefully&lt;/strong&gt; without making you start over&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coordinates multiple services&lt;/strong&gt; (booking, modifications, cancellations, notifications)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remembers your preferences&lt;/strong&gt; from previous stays&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Provides proactive guidance&lt;/strong&gt; based on your specific situation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Traditional systems fail because they're built around databases and forms, not conversations and context. They force users to navigate separate interfaces for search, booking, modification, and support, each requiring you to re-explain your situation.&lt;/p&gt;

&lt;p&gt;The result? Frustrated customers, abandoned bookings, and support teams drowning in "simple" requests that require human intelligence to resolve.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Multi-Agent Vision
&lt;/h2&gt;

&lt;p&gt;What if instead of building another booking form, I built a team of AI specialists that work together like a hotel's back-office staff?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Search Specialist&lt;/strong&gt; who knows every hotel's availability and can compare options intelligently&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Booking Specialist&lt;/strong&gt; who understands reservation lifecycles and policy implications
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Policy Advisor&lt;/strong&gt; who can explain complex terms and check compliance before actions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Communications Specialist&lt;/strong&gt; who handles confirmations and keeps everyone informed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Supervisor&lt;/strong&gt; who orchestrates the team and maintains conversation context&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2gnk621dmj064c33vbjh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2gnk621dmj064c33vbjh.png" alt="Multi-Agent Architecture, Amazon Bedrock AgentCore, AWS Strands Agents, AWS Lambda - Mohsin Sheikhani" width="800" height="432"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This isn't just a technical architecture, it's a business model that scales human-like service.&lt;/p&gt;
&lt;h2&gt;
  
  
  Enter Amazon Bedrock AgentCore: The Foundation for Intelligent Orchestration
&lt;/h2&gt;

&lt;p&gt;Here's where the vision meets reality. Amazon Bedrock AgentCore isn't just another AI service, it's the infrastructure that makes multi-agent systems production-ready.&lt;/p&gt;

&lt;p&gt;The challenge with building agent teams isn't creating individual agents (that's the easy part). The real complexity lies in:&lt;/p&gt;

&lt;p&gt;AgentCore solves these with three key services that work together:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Memory Service&lt;/strong&gt;: Persistent conversation context that survives sessions. Your booking agent remembers you mentioned you prefer ground floor rooms, even if you come back next week.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gateway Service&lt;/strong&gt;: Secure, managed access to external tools and APIs. No more wrestling with authentication, rate limiting, or connection management.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Runtime Service&lt;/strong&gt;: Scalable agent execution with built-in observability. Your agents run reliably in production without you managing infrastructure.&lt;/p&gt;

&lt;p&gt;But here's what makes it powerful: these services are designed to work together. Memory informs decision-making, Gateway enables action-taking, and Runtime orchestrates it all.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Architecture That Changes Everything
&lt;/h2&gt;

&lt;p&gt;Instead of building a monolithic booking system, I created a supervisor-agent architecture where specialized agents collaborate through AgentCore's components:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2bal3of52xrbdxx7vxiq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2bal3of52xrbdxx7vxiq.png" alt="Multi-Agent Architecture, Amazon Bedrock AgentCore, AgentCore Identity, AWS Strands Agents, MCP, A2A, AWS Lambda - Mohsin Sheikhani" width="800" height="177"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The supervisor doesn't just route requests—it maintains context, orchestrates multi-step workflows, and ensures policy compliance before any action is taken.&lt;/p&gt;
&lt;h2&gt;
  
  
  Building the Agent Team: From Search to Confirmation
&lt;/h2&gt;

&lt;p&gt;Each agent in the system is a specialist, but they're not working in isolation. Here's how the team comes together:&lt;/p&gt;
&lt;h3&gt;
  
  
  The Search &amp;amp; Discovery Agent
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Role&lt;/strong&gt;: The research specialist who knows every hotel's availability and pricing.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/mohsinsheikhani/multi-agent-hotel-assistant/blob/main/app/src/agents/search_discovery.py" rel="noopener noreferrer"&gt;Link To Repo&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8vy0wps1etvw2ta0jcuk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8vy0wps1etvw2ta0jcuk.png" alt="Amazon Bedrock AgentCore, AgentCore Identity, AgentCore Gateway, AWS Strands Agents, MCP, A2A, AWS Lambda - Mohsin Sheikhani" width="798" height="174"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This agent doesn't just return search results, it understands context. When you ask for "hotels near the conference center," it factors in your previous preferences, compares pricing across dates, and highlights amenities that matter to business travelers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SearchDiscoveryAgent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseAgent&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Agent responsible for hotel search and discovery operations&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;super&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;9001&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_agent_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SearchDiscoveryAgent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_agent_description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Handles hotel search, availability checking, and price comparisons using AgentCore Lambda tools&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Reservation Agent
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Role&lt;/strong&gt;: The booking specialist who manages the entire reservation lifecycle.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/mohsinsheikhani/multi-agent-hotel-assistant/blob/main/app/src/agents/reservation.py" rel="noopener noreferrer"&gt;Link To Repo&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxlhaqrj1zfhyboe1usuf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxlhaqrj1zfhyboe1usuf.png" alt="Amazon Bedrock AgentCore, AgentCore Identity, AgentCore Gateway, AWS Strands Agents, MCP, A2A, AWS Lambda - Mohsin Sheikhani" width="799" height="177"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But here's where it gets interesting, this agent is policy-aware. Before making any booking, modification, or cancellation, it checks with the Guest Advisory Agent to understand implications.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ReservationAgent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseAgent&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Agent responsible for managing hotel reservations&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;super&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;9002&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_agent_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ReservationAgent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_agent_description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Handles the full lifecycle of hotel room reservations, including booking, updating, canceling, and fetching past reservations for a guest.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Guest Advisory Agent
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Role&lt;/strong&gt;: The policy expert who bridges business rules and user actions.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/mohsinsheikhani/multi-agent-hotel-assistant/blob/main/app/src/agents/guest_advisory.py" rel="noopener noreferrer"&gt;Link To Repo&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjtvfiwl71vcqvwapei2w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjtvfiwl71vcqvwapei2w.png" alt="Amazon Bedrock AgentCore, AgentCore Identity, AgentCore Gateway, AWS Strands Agents, MCP, A2A, AWS Lambda, Amazon Bedrock Knowledge Base - Mohsin Sheikhani" width="800" height="174"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This agent has access to a knowledge base of hotel policies, cancellation terms, and booking rules. It doesn't just recite policies—it explains them in context and calculates real implications.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GuestAdvisoryAgent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseAgent&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Agent responsible for providing hotel policies and advisory information&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;super&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;9003&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_agent_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GuestAdvisoryAgent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_agent_description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Provides hotel policies, rules, and advisory information including cancellation policies, check-in/out procedures, and general hotel guidelines.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Notification Agent
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Role&lt;/strong&gt;: The communications specialist who keeps everyone informed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/mohsinsheikhani/multi-agent-hotel-assistant/blob/main/app/src/agents/notification.py" rel="noopener noreferrer"&gt;Link To Repo&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5wf0u31pp4bq5ejnyc60.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5wf0u31pp4bq5ejnyc60.png" alt="Amazon Bedrock AgentCore, AWS Strands Agents, Strands Agents Tools, MCP, A2A, AWS Lambda - Mohsin Sheikhani" width="799" height="331"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every booking, modification, or cancellation triggers appropriate notifications. But it's not just email templates, it's contextual communication that includes next steps and relevant details.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;NotificationAgent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseAgent&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Agent responsible for handling booking notifications and communications&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;super&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;9004&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_agent_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NotificationAgent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_agent_description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Handles booking confirmations, modifications, cancellations, and other communication needs for hotel reservations.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Supervisor's Secret Weapon: Memory
&lt;/h2&gt;

&lt;p&gt;Here's what makes this system truly intelligent—the supervisor agent uses AgentCore's Memory service to maintain conversation context across sessions.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/mohsinsheikhani/multi-agent-hotel-assistant/blob/main/app/src/core/memory.py" rel="noopener noreferrer"&gt;Link To Repo&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MemoryManager&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Manages Bedrock AgentCore memory operations&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;region_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;memory_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MemoryClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;region_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;region_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;memory_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;memory_name&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;memory_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize_memory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Initialize or retrieve existing memory&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MemoryHookProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HookProvider&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Provides memory hooks for agent lifecycle events&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;memory_client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MemoryClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;memory_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;memory_client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;memory_client&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;memory_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;memory_id&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;on_agent_initialized&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AgentInitializedEvent&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Load recent conversation history when agent starts&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="bp"&gt;...&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Memory load error: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;on_message_added&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MessageAddedEvent&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Store messages in memory&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="bp"&gt;...&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Memory save error: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;register_hooks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;registry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;HookRegistry&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Register memory hooks&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="n"&gt;registry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MessageAddedEvent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;on_message_added&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;registry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AgentInitializedEvent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;on_agent_initialized&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Conversation context persists across sessions
# Agents remember preferences, previous bookings, ongoing requests
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you return to continue a booking conversation from yesterday, the system doesn't just remember what you said—it remembers the context, your preferences, and where you left off in the process.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Magic of Agent Orchestration: How Complex Workflows Become Simple Conversations
&lt;/h2&gt;

&lt;p&gt;Here's where the supervisor agent earns its keep. It doesn't just route requests, it orchestrates intelligent workflows that would normally require multiple customer service interactions.&lt;/p&gt;

&lt;h3&gt;
  
  
  A Real Workflow in Action
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;User&lt;/strong&gt;: "I need to cancel my booking for next week, but I'm worried about fees."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Traditional System&lt;/strong&gt;: Navigate to "My Bookings" → Find booking → Click cancel → Read policy wall of text → Call customer service for clarification → Wait on hold → Explain situation → Get transferred → Explain again...&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Our Multi-Agent System&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Supervisor&lt;/strong&gt; identifies this as a policy-sensitive cancellation request&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guest Advisory Agent&lt;/strong&gt; retrieves specific cancellation policy for this booking&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reservation Agent&lt;/strong&gt; calculates exact fees and alternatives&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Supervisor&lt;/strong&gt; presents clear options: "Cancelling now incurs a $50 fee, but modifying dates is free until tomorrow"&lt;/li&gt;
&lt;li&gt;User chooses, &lt;strong&gt;Reservation Agent&lt;/strong&gt; executes, &lt;strong&gt;Notification Agent&lt;/strong&gt; confirms&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All in one conversation. No navigation, no transfers, no re-explaining.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Orchestration Code That Makes It Possible
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Supervisor's intelligent routing with context awareness
&lt;/span&gt;&lt;span class="n"&gt;system_prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
You are the Supervisor Agent for a multi-agent hotel booking system.

Policy-aware behavior:
- Before performing any booking, modification, or cancellation, check relevant hotel policies (using the Guest Advisory Agent or Knowledge Base) to determine if there are penalties, restrictions, or special conditions.
- Present the user with a summary of what will happen and the relevant policy (e.g., &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Cancelling within 24 hours will incur a 20% fee&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;).
- Ask for explicit confirmation before taking action.
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="c1"&gt;# The supervisor maintains context through AgentCore Memory
&lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;bedrock_model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;# A2A communication tools
&lt;/span&gt;    &lt;span class="n"&gt;system_prompt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_get_system_prompt&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;hooks&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;MemoryHookProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;memory_manager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;memory_id&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
    &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;actor_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;actor_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session_id&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.com/mohsinsheikhani/multi-agent-hotel-assistant/blob/main/app/src/core/supervisor.py" rel="noopener noreferrer"&gt;Link To Repo&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Why This Architecture Scales
&lt;/h3&gt;

&lt;p&gt;Each agent runs independently on its own port, communicating through Agent-to-Agent (A2A) protocols. This means:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkbkuobqvqtwdn1safiy2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkbkuobqvqtwdn1safiy2.png" alt="Amazon Bedrock AgentCore, AgentCore Identity, AgentCore Gateway, AWS Strands Agents, MCP, A2A, AWS Lambda - Mohsin Sheikhani" width="800" height="375"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fault isolation&lt;/strong&gt;: One agent's issues don't cascade&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Independent scaling&lt;/strong&gt;: High-demand agents can scale separately&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Easy updates&lt;/strong&gt;: Modify one agent without touching others&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clear responsibilities&lt;/strong&gt;: Each agent has a single, well-defined purpose&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Production Reality: Memory, Monitoring, and the Details That Matter
&lt;/h2&gt;

&lt;p&gt;Building a demo is one thing. Building something that handles real customer conversations at 3 AM when your infrastructure is under load? That's where AgentCore's production features become essential.&lt;/p&gt;

&lt;h3&gt;
  
  
  Memory That Actually Works
&lt;/h3&gt;

&lt;p&gt;The breakthrough isn't just that the system remembers, it's how it remembers intelligently.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Memory hooks that maintain context across sessions
&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MemoryHookProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HookProvider&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;on_agent_initialized&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AgentInitializedEvent&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# Load last 10 conversation turns when agent starts
&lt;/span&gt;        &lt;span class="n"&gt;recent_turns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;memory_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_last_k_turns&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;memory_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;memory_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
            &lt;span class="n"&gt;actor_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;actor_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
            &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
            &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="c1"&gt;# Inject context into agent's system prompt
&lt;/span&gt;        &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;system_prompt&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s"&gt;Recent conversation:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a customer returns after a week to modify their booking, the system doesn't just remember the booking ID, it remembers they mentioned preferring ground floor rooms, that they're traveling for a conference, and that they were concerned about cancellation policies.&lt;/p&gt;

&lt;h3&gt;
  
  
  Error Handling That Prevents Disasters
&lt;/h3&gt;

&lt;p&gt;In production, things fail. APIs timeout, agents crash, networks hiccup. The difference between a good system and a great one is graceful degradation:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/mohsinsheikhani/multi-agent-hotel-assistant/blob/main/app/src/core/supervisor.py" rel="noopener noreferrer"&gt;Link To Repo&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Supervisor handles agent failures gracefully
&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;process_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Process a user request through the supervisor agent&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Processing request: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke_async&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Request processed successfully&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Error processing request: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Observability That Tells the Real Story
&lt;/h3&gt;

&lt;p&gt;AgentCore's built-in observability means you can see exactly what's happening:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which agent handled each request&lt;/li&gt;
&lt;li&gt;How long each step took&lt;/li&gt;
&lt;li&gt;Where conversations get stuck&lt;/li&gt;
&lt;li&gt;Which policies cause the most confusion&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This isn't just monitoring, it's business intelligence about how customers actually interact with your system.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Authentication Reality
&lt;/h3&gt;

&lt;p&gt;Real systems need real security. AgentCore handles the complexity:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/mohsinsheikhani/multi-agent-hotel-assistant/blob/main/app/src/utils/auth.py" rel="noopener noreferrer"&gt;Link To Repo&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Cognito integration for secure gateway access
&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TokenManager&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_fresh_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# Client credentials flow with proper scoping
&lt;/span&gt;        &lt;span class="c1"&gt;# Automatic token refresh and error handling
&lt;/span&gt;        &lt;span class="c1"&gt;# No security vulnerabilities from DIY auth
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Business Impact: What This Actually Means for Hotels and Customers
&lt;/h2&gt;

&lt;p&gt;The technical architecture is impressive, but let's talk about what really matters, business outcomes.&lt;/p&gt;

&lt;h3&gt;
  
  
  For Hotels: Operational Efficiency at Scale
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Before&lt;/strong&gt;: Customer calls about cancellation policy → Agent looks up booking → Checks policy document → Calculates fees → Explains options → Customer decides → Agent processes → Sends confirmation.&lt;br&gt;
&lt;strong&gt;Average handling time: 8-12 minutes.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;After&lt;/strong&gt;: Customer asks about cancellation → System instantly knows booking details, policy implications, and alternatives → Presents clear options → Customer decides → Action executed automatically.&lt;br&gt;
&lt;strong&gt;Average handling time: 1-2 minutes.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's not just efficiency, that's 4x capacity increase with the same support team.&lt;/p&gt;
&lt;h3&gt;
  
  
  For Customers: Intelligence That Feels Personal
&lt;/h3&gt;

&lt;p&gt;The system doesn't just remember your booking, it remembers your story:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"I see you're traveling for the same conference as last year. Would you like the same room type?"&lt;/li&gt;
&lt;li&gt;"Based on your previous concern about cancellation fees, I've found options with flexible policies."&lt;/li&gt;
&lt;li&gt;"Your flight was delayed last time, should I book a late check-in for this trip?"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This isn't marketing personalization, it's operational intelligence that makes every interaction feel like talking to someone who actually knows you.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Competitive Advantage
&lt;/h3&gt;

&lt;p&gt;While competitors are still building better search interfaces, this system is solving the real problem: &lt;strong&gt;complex travel decisions require conversation, not just transactions.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Hotels using this approach can offer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;24/7 intelligent support&lt;/strong&gt; without 24/7 staffing costs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Proactive policy guidance&lt;/strong&gt; that prevents booking mistakes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Seamless modification workflows&lt;/strong&gt; that retain customers instead of losing them&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contextual upselling&lt;/strong&gt; based on actual preferences, not generic algorithms&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  ROI That Actually Matters
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Support cost reduction&lt;/strong&gt;: 4x efficiency gain on routine inquiries&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Booking completion rates&lt;/strong&gt;: Fewer abandoned bookings due to policy confusion&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Customer retention&lt;/strong&gt;: Seamless modification experience vs. starting over elsewhere&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Upselling effectiveness&lt;/strong&gt;: Context-aware recommendations vs. generic offers&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  The Future of Conversational Commerce: Where This Goes Next
&lt;/h2&gt;

&lt;p&gt;This hotel booking system is just the beginning. The patterns we've established, supervisor orchestration, policy-aware workflows, persistent memory, apply to any complex business process that currently requires human intervention.&lt;/p&gt;
&lt;h3&gt;
  
  
  Beyond Hotel Booking
&lt;/h3&gt;

&lt;p&gt;Imagine applying this architecture to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Insurance claims processing&lt;/strong&gt; with policy specialists and damage assessors&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Financial planning&lt;/strong&gt; with investment advisors and compliance experts
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Healthcare coordination&lt;/strong&gt; with specialists who understand your medical history&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enterprise procurement&lt;/strong&gt; with budget analysts and vendor specialists&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each domain gets a team of AI specialists who collaborate intelligently, remember context, and handle complexity gracefully.&lt;/p&gt;
&lt;h1&gt;
  
  
  The AgentCore Foundation: Why This Changes Everything
&lt;/h1&gt;

&lt;p&gt;Amazon Bedrock AgentCore solved the infrastructure problems that kill most multi-agent projects. Here's how each component transformed this hotel booking system from concept to reality.&lt;/p&gt;
&lt;h2&gt;
  
  
  AgentCore Gateway: Your Lambda Functions Become Agent Tools
&lt;/h2&gt;

&lt;p&gt;The breakthrough moment came when I realized I could keep all my business logic in familiar AWS Lambda functions while making them accessible to agents through the Model Context Protocol (MCP).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Problem&lt;/strong&gt;: Agents need access to real business systems, hotel inventory databases, booking APIs, policy engines. Traditionally, this means complex authentication, API management, and security concerns.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AgentCore Gateway's Solution&lt;/strong&gt;: Your Lambda functions become agent tools automatically.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F585b4f3ky38qdwyv3jem.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F585b4f3ky38qdwyv3jem.png" alt="Amazon Bedrock AgentCore, AgentCore Identity, AgentCore Gateway, AWS Strands Agents, MCP, A2A, AWS Lambda - Mohsin Sheikhani" width="800" height="282"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;APIGatewayProxyEvent&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;APIGatewayProxyResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Received event:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;city&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;queryResp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;dynamo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;QueryCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
          &lt;span class="na"&gt;TableName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tableName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;KeyConditionExpression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;...,&lt;/span&gt;
          &lt;span class="na"&gt;ExpressionAttributeValues&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="p"&gt;...&lt;/span&gt;
          &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;scanResp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;dynamo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ScanCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
          &lt;span class="na"&gt;TableName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tableName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Error:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Internal Server Error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Gateway handles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Authentication and authorization&lt;/strong&gt; with fine-grained access control&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate limiting and throttling&lt;/strong&gt; to protect your backend systems&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Request/response transformation&lt;/strong&gt; between agent protocols and your APIs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MCP protocol abstraction&lt;/strong&gt; so your Lambdas don't need to know about agents&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This means your hotel search, booking, and policy engines remain standard AWS Lambda functions, but agents can invoke them as naturally as calling any other tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  AgentCore Identity: Security That Scales
&lt;/h2&gt;

&lt;p&gt;Multi-agent systems create complex security challenges. When the Supervisor Agent needs to call the Reservation Agent, which then calls the Policy Agent, how do you maintain security context across the entire chain?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Traditional Approach&lt;/strong&gt;: Custom authentication between every agent pair, token management nightmares, and security vulnerabilities.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AgentCore Identity&lt;/strong&gt;: Centralized identity management with automatic token handling.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Agents authenticate once, communicate securely forever
&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TokenManager&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scope_string&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;resource_server_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/gateway:read &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;resource_server_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/gateway:write&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_fresh_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="c1"&gt;# AgentCore handles the OAuth2 client credentials flow
&lt;/span&gt;        &lt;span class="c1"&gt;# Automatic token refresh and scope validation
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Identity service provides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;OAuth2 client credentials flow&lt;/strong&gt; with automatic token refresh&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fine-grained scopes&lt;/strong&gt; for different agent capabilities&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Centralized policy management&lt;/strong&gt; across your entire agent ecosystem&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit trails&lt;/strong&gt; for every agent interaction&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flv5fpsredzczqgqyousp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flv5fpsredzczqgqyousp.png" alt="Amazon Bedrock AgentCore, AgentCore Identity, AgentCore Gateway, AWS Strands Agents, MCP, A2A, AWS Lambda - Mohsin Sheikhani" width="800" height="215"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  AgentCore Memory: Context That Persists and Scales
&lt;/h2&gt;

&lt;p&gt;Here's where the magic happens. Traditional chatbots lose context between sessions. AgentCore Memory makes agents truly intelligent by maintaining conversation context that survives sessions, scales across millions of users, and enables sophisticated reasoning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Architecture&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fax0uk34w5xd1bueps3cf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fax0uk34w5xd1bueps3cf.png" alt="Amazon Bedrock AgentCore, AgentCore Identity, AgentCore Gateway, AgentCore Memory, AWS Strands Agents, MCP, A2A, AWS Lambda - Mohsin Sheikhani" width="703" height="567"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What This Enables&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cross-session continuity&lt;/strong&gt;: Customer returns next week, agent remembers their preferences&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-agent context sharing&lt;/strong&gt;: Reservation Agent knows what Search Agent discovered&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Intelligent reasoning&lt;/strong&gt;: "Based on your previous concern about cancellation fees..."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scalable storage&lt;/strong&gt;: Millions of conversations with configurable retention policies&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The Business Impact&lt;/strong&gt;: Customers don't repeat themselves. Agents make contextual decisions. Conversations feel natural, not transactional.&lt;/p&gt;

&lt;h2&gt;
  
  
  AgentCore Runtime: Production-Ready Agent Execution
&lt;/h2&gt;

&lt;p&gt;Running one agent in development is easy. Running a team of agents in production, handling failures gracefully, scaling under load, and maintaining performance—that's where most multi-agent projects die.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AgentCore Runtime's Production Features&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BedrockAgentCoreApp&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# Agents that handle production realities
&lt;/span&gt;&lt;span class="nd"&gt;@app.entrypoint&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;send_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Main entry point for the hotel booking system&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;question&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;question&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;No question provided&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;supervisor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;process_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Failed to process request: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Failed to process request: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Runtime Capabilities&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Automatic scaling&lt;/strong&gt; based on demand&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Health monitoring&lt;/strong&gt; with automatic recovery&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resource management&lt;/strong&gt; to prevent runaway processes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Load balancing&lt;/strong&gt; across agent instances&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Graceful degradation&lt;/strong&gt; when components fail&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feklmzz6v4t1ieiaf3vv1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feklmzz6v4t1ieiaf3vv1.png" alt="Amazon Bedrock AgentCore, AgentCore Identity, AgentCore Gateway, AWS Strands Agents, MCP, A2A, AWS Lambda - Mohsin Sheikhani" width="703" height="287"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Result&lt;/strong&gt;: Your agents run reliably in production without you managing infrastructure complexity.&lt;/p&gt;

&lt;h2&gt;
  
  
  AgentCore Observability: Intelligence You Can See
&lt;/h2&gt;

&lt;p&gt;The most sophisticated multi-agent system is useless if you can't understand what's happening inside it. AgentCore Observability provides deep insights into agent behavior, performance, and business impact.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What You Can See&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Agent interaction flows&lt;/strong&gt;: Which agent handled each step of a complex workflow&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance metrics&lt;/strong&gt;: Response times, success rates, resource utilization&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Business intelligence&lt;/strong&gt;: Which policies cause confusion, where customers get stuck&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Error patterns&lt;/strong&gt;: Systematic issues before they become customer problems&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fos4pghwdjbsdzkc9gwlc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fos4pghwdjbsdzkc9gwlc.png" alt="Amazon Bedrock AgentCore, AgentCore Observability, AWS Strands Agents, MCP, A2A, AWS Lambda - Mohsin Sheikhani" width="800" height="282"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Business Value&lt;/strong&gt;: You don't just run agents, you optimize them based on real usage patterns and customer behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Architecture Scales
&lt;/h2&gt;

&lt;p&gt;Each AgentCore service solves a specific production challenge:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gateway&lt;/strong&gt;: Tool access without security complexity&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Identity&lt;/strong&gt;: Authentication without custom code&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory&lt;/strong&gt;: Context without storage management&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runtime&lt;/strong&gt;: Reliability without infrastructure management&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observability&lt;/strong&gt;: Insights without custom monitoring&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Together, they create a foundation where you focus on business logic while AgentCore handles the production complexity that typically kills multi-agent projects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building Your Own Agent Team
&lt;/h2&gt;

&lt;p&gt;Ready to experiment? The architecture is surprisingly approachable:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Start with the supervisor pattern&lt;/strong&gt; - One agent that orchestrates others&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add memory integration&lt;/strong&gt; - Context that persists across sessions
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build specialized agents&lt;/strong&gt; - Each with a single, clear responsibility&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use A2A communication&lt;/strong&gt; - Agents that collaborate, not compete&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deploy on AgentCore&lt;/strong&gt; - Production-ready from day one&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The complete implementation is available on GitHub, including deployment scripts and documentation for getting started with your own multi-agent system.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;What complex workflow in your domain could benefit from conversational intelligence? The tools are ready, the question is what you'll build with them.&lt;/strong&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  GitHub Repo
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://github.com/mohsinsheikhani/multi-agent-hotel-assistant" rel="noopener noreferrer"&gt;Multi-Agent Hotel Assistant&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This project was built for "AWS AI Engineering Month: Building with Agents". The combination of Amazon Bedrock AgentCore's Memory, Gateway, and Runtime services with the Strands Agents framework creates a powerful foundation for production multi-agent systems.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>agentaichallenge</category>
      <category>bedrock</category>
      <category>ai</category>
    </item>
    <item>
      <title>A Practical Guide to MLOps on AWS: Demand Forecasting with Amazon Bedrock and Automated EC2 Pipelines (Phase 03)</title>
      <dc:creator>Mohsin Sheikhani</dc:creator>
      <pubDate>Thu, 10 Jul 2025 17:19:58 +0000</pubDate>
      <link>https://dev.to/mohsinsheikhani/a-practical-guide-to-mlops-on-aws-demand-forecasting-with-amazon-bedrock-and-automated-ec2-3c4c</link>
      <guid>https://dev.to/mohsinsheikhani/a-practical-guide-to-mlops-on-aws-demand-forecasting-with-amazon-bedrock-and-automated-ec2-3c4c</guid>
      <description>&lt;p&gt;In &lt;a href="https://dev.to/mohsinsheikhani/a-practical-guide-to-mlops-on-aws-transforming-raw-data-into-ai-ready-datasets-with-aws-glue-4lc9"&gt;Phase 02&lt;/a&gt;, we transformed raw user interaction events into structured, enriched datasets, organized across bronze, silver, and gold zones in S3, and made them query able through Glue + Athena.&lt;/p&gt;

&lt;p&gt;Now in Phase 03, we shift from preparing the data to putting it to work.&lt;/p&gt;

&lt;p&gt;This is where AI meets infrastructure:&lt;br&gt;
We’ll use Amazon Bedrock to predict product demand based on historical sales, and architect it the way real systems do.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fev7hspysppg0gacyu0cp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fev7hspysppg0gacyu0cp.png" alt="Demand Forecasting with Amazon Bedrock and Automated EC2 Pipelines" width="800" height="566"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Why does this matter?
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Forecasting is a batch job, not a real-time interaction.&lt;/li&gt;
&lt;li&gt;It needs compute, but we don’t want to keep EC2 running 24/7.&lt;/li&gt;
&lt;li&gt;So we’ll spin up an EC2 instance nightly, run a forecasting script that:

&lt;ul&gt;
&lt;li&gt;Reads gold-zone data&lt;/li&gt;
&lt;li&gt;Sends it to Bedrock&lt;/li&gt;
&lt;li&gt;Updates DynamoDB with new demand forecasts&lt;/li&gt;
&lt;li&gt;Shuts itself down to save cost&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of this is orchestrated via EventBridge and Lambda, forming a complete, automated, cost-efficient forecasting pipeline.&lt;/p&gt;

&lt;p&gt;Now, the question here is why we choose EC2 to run the forecasting job?&lt;/p&gt;

&lt;p&gt;Because in real-world ML systems, long-running batch jobs like forecasting are often too heavy for Lambda, may require more memory, longer runtimes, or even GPU-based instances. Using EC2 gives us:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Run larger forecasting workloads&lt;/li&gt;
&lt;li&gt;Use GPU-based instances (if needed)&lt;/li&gt;
&lt;li&gt;Keep costs low by shutting down after completion&lt;/li&gt;
&lt;li&gt;Full control over compute resources&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  How the Orchestration Works
&lt;/h3&gt;

&lt;p&gt;We’ve designed this system to be automated and cost-optimized:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Amazon EventBridge triggers a Lambda function nightly (e.g. every 24 hours)&lt;/li&gt;
&lt;li&gt;Lambda starts up an EC2 instance&lt;/li&gt;
&lt;li&gt;EC2 pulls cleaned sales data from S3 and runs a Python script&lt;/li&gt;
&lt;li&gt;The script:

&lt;ul&gt;
&lt;li&gt;Sends data to Amazon Bedrock to forecast the next 7 days&lt;/li&gt;
&lt;li&gt;Updates DynamoDB with the &lt;code&gt;forecasted_demand&lt;/code&gt; per product&lt;/li&gt;
&lt;li&gt;Shuts down the EC2 instance when the task is done&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;
  
  
  Step 1 - Provisioning EC2 Forecasting Instance
&lt;/h2&gt;

&lt;p&gt;In this step, we set up an Amazon EC2 instance that will run our demand forecasting script.&lt;/p&gt;

&lt;p&gt;We’re using AWS CDK to provision this instance with the following characteristics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pulls aggregated sales data from the S3 Gold Zone (&lt;code&gt;forecast_ready/&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Sends this data to Amazon Bedrock to forecast product demand&lt;/li&gt;
&lt;li&gt;Updates the &lt;code&gt;forecasted_demand&lt;/code&gt; field in the DynamoDB Inventory Table&lt;/li&gt;
&lt;li&gt;Shuts itself down after the job is completed to avoid unnecessary costs&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Configuration Highlights
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Launched in a VPC with a public subnet (since it needs internet access for Bedrock and S3, we'll move it to private subnet in upcoming phase)&lt;/li&gt;
&lt;li&gt;Attached to an IAM role that allows:

&lt;ul&gt;
&lt;li&gt;Invoking Bedrock models (&lt;code&gt;bedrock:InvokeModel&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Reading from S3&lt;/li&gt;
&lt;li&gt;Writing to DynamoDB&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Bootstrapped via a user data script that:

&lt;ul&gt;
&lt;li&gt;Installs required dependencies (aws cli, etc.)&lt;/li&gt;
&lt;li&gt;Downloads the &lt;code&gt;inventory_forecaster.py&lt;/code&gt; script from S3&lt;/li&gt;
&lt;li&gt;Runs the script&lt;/li&gt;
&lt;li&gt;Terminates the instance once done&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Create a Dedicated File for EC2 Forecasting
&lt;/h4&gt;

&lt;p&gt;Follow the same pattern by organizing this inside a new folder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mkdir -p lib/constructs/common/compute/
touch lib/constructs/common/compute/forecast-instance.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then paste the following code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { Construct } from "constructs";
import {
  Instance,
  InstanceClass,
  InstanceSize,
  InstanceType,
  MachineImage,
  Vpc,
  SecurityGroup,
  Peer,
  Port,
} from "aws-cdk-lib/aws-ec2";
import {
  Role,
  ServicePrincipal,
  ManagedPolicy,
  PolicyStatement,
} from "aws-cdk-lib/aws-iam";
import { Bucket } from "aws-cdk-lib/aws-s3";
import { Table } from "aws-cdk-lib/aws-dynamodb";
import { aws_ec2 as ec2 } from "aws-cdk-lib";

interface ForecastEc2Props {
  vpc: Vpc;
  goldBucket: Bucket;
  dataAssetsBucket: Bucket;
  forecastTable: Table;
}

export class ForecastEc2Instance extends Construct {
  public readonly instance: Instance;

  constructor(scope: Construct, id: string, props: ForecastEc2Props) {
    super(scope, id);

    const { vpc, goldBucket, dataAssetsBucket, forecastTable } = props;

    const role = new Role(this, "ForecastEC2Role", {
      assumedBy: new ServicePrincipal("ec2.amazonaws.com"),
      managedPolicies: [
        ManagedPolicy.fromAwsManagedPolicyName("CloudWatchAgentServerPolicy"),
        ManagedPolicy.fromAwsManagedPolicyName("AmazonS3ReadOnlyAccess"),
        ManagedPolicy.fromAwsManagedPolicyName("AmazonDynamoDBFullAccess"),
      ],
    });

    role.addToPolicy(
      new PolicyStatement({
        actions: ["bedrock:InvokeModel"],
        resources: ["*"],
      })
    );

    role.addToPolicy(
      new PolicyStatement({
        actions: ["ec2:TerminateInstances"],
        resources: ["*"],
        conditions: {
          StringEquals: {
            "ec2:ResourceTag/Name": "ForecastEC2",
          },
        },
      })
    );

    const securityGroup = new SecurityGroup(this, "ForecastEC2SG", {
      vpc,
      description: "Allow EC2 to access S3/Bedrock/DynamoDB",
      allowAllOutbound: true,
    });

    securityGroup.addIngressRule(
      Peer.anyIpv4(),
      Port.tcp(22),
      "Allow SSH from anywhere"
    );

    const userData = ec2.UserData.forLinux();
    userData.addCommands(
      "sudo yum update -y",
      "sudo yum install -y python3 pip -y",
      "pip3 install boto3 pandas pyarrow",

      "cd /home/ec2-user",

      `curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"`,
      "unzip awscliv2.zip",
      "sudo ./aws/install --update",

      // Export environment variables to .bashrc or directly
      `echo 'export GOLD_BUCKET=${goldBucket.bucketName}' &amp;gt;&amp;gt; /etc/profile`,
      `echo 'export FORECAST_TABLE=${forecastTable.tableName}' &amp;gt;&amp;gt; /etc/profile`,
      "source /etc/profile",

      `aws s3 cp s3://${dataAssetsBucket.bucketName}/scripts/inventory_forecaster.py .`,
      "python3 ./inventory_forecaster.py",

      "shutdown now -h"
    );

    this.instance = new Instance(this, "ForecastEC2", {
      instanceName: "ForecastEC2",
      instanceType: InstanceType.of(InstanceClass.T3, InstanceSize.MEDIUM),
      machineImage: MachineImage.latestAmazonLinux2023(),
      vpc,
      securityGroup,
      role,
      userData,
      associatePublicIpAddress: true,
      vpcSubnets: {
        subnetType: ec2.SubnetType.PUBLIC,
      },
    });
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In your &lt;code&gt;retail-ai-insights-stack.ts&lt;/code&gt;, import the construct:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { ForecastEc2Instance } from "./constructs/common/compute/forecast-instance";
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And instantiate it like this (as you already had):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const forecastInstance = new ForecastEc2Instance(this, "ForecastingEc2", {
  vpc: vpc,
  goldBucket,
  dataAssetsBucket,
  forecastTable: dynamoConstruct.inventoryTable,
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Deploying the Forecasting Infrastructure
&lt;/h4&gt;

&lt;p&gt;Once the &lt;code&gt;ForecastEc2Instance&lt;/code&gt; construct is in place, don't do &lt;code&gt;cdk deploy&lt;/code&gt; for now.&lt;/p&gt;

&lt;p&gt;Go to AWS Console, and search for Amazon Bedrock, click on the Foundation Models &amp;gt; Model Catalog, on the Providers tab check mark the Anthropic&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fghy454cj62re9so5zrd0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fghy454cj62re9so5zrd0.png" alt="Amazon Bedrock Foundational Model Catalog - AWS Console" width="800" height="214"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Look for the &lt;code&gt;Claude 3.7 Sonnet&lt;/code&gt;, in my case it's on the first row, third column, click on it&lt;/p&gt;

&lt;p&gt;In the next screen, you'll see &lt;code&gt;Available to request&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhn63u3e6tjvpgocb7x6j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhn63u3e6tjvpgocb7x6j.png" alt="Request model access for Claude 3.7 Sonnet" width="800" height="149"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click on, &lt;code&gt;Request model access&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fysw5ur84ox5kbgpt4vme.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fysw5ur84ox5kbgpt4vme.png" alt="Request model access for Claude 3.7 Sonnet" width="799" height="130"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now, click for Enable specific models, and search for &lt;code&gt;Claude 3.7 Sonnet&lt;/code&gt;, check mark it, and at the very bottom click on &lt;code&gt;Next&lt;/code&gt;, and provide random details&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbzbr4i9ncm8dxy7clflk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbzbr4i9ncm8dxy7clflk.png" alt="Request model access for Claude 3.7 Sonnet - Put in details" width="800" height="415"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After a little while, you should see Access Granted&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F03p84x0ljv1izqz4oz83.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F03p84x0ljv1izqz4oz83.png" alt="Request access granted for Claude 3.7 Sonnet" width="797" height="95"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now that we've access to Bedrock model on our account, we should be good to deploy our resources with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cdk deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Verifying the Results
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Wait for the EC2 instance status to become "running" in the EC2 console.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx61qce1alxxlmrpj8bdb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx61qce1alxxlmrpj8bdb.png" alt="Amazon EC2 instances list" width="800" height="121"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Then watch it auto-terminate after the script completes execution.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4zf1zdaof1ik67k7b3qg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4zf1zdaof1ik67k7b3qg.png" alt="Amazon EC2 instances list - Status terminated" width="800" height="121"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Now head over to DynamoDB &amp;gt; Explore Items, and check your table.&lt;/li&gt;
&lt;li&gt;You’ll see that the &lt;code&gt;forecasted_demand&lt;/code&gt; field has been updated for four products (to save cost by avoidings extra calls to Bedrock).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flgtaouvg89i9vxiopref.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flgtaouvg89i9vxiopref.png" alt="Amazon DynamoDB - Explore items" width="799" height="284"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2 - Automating Forecasting with EventBridge &amp;amp; Lambda
&lt;/h2&gt;

&lt;p&gt;In a real-world scenario, you wouldn’t manually trigger forecasting jobs. Instead, you’d want these predictions to run nightly, every 24 hours, and only spin up compute when needed to save cost.&lt;/p&gt;

&lt;p&gt;To do that, we’ll use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Amazon EventBridge to define a scheduled rule (runs every night at 1:00 AM)&lt;/li&gt;
&lt;li&gt;AWS Lambda to start our EC2 forecasting instance&lt;/li&gt;
&lt;li&gt;EC2 itself terminates automatically after completing the prediction job&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Make a new file&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mkdir -p lib/constructs/events
touch lib/constructs/events/schedule-ec2-task.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Paste the code that provisions the Lambda, gives it permission to start the EC2 instance, and wires it into the EventBridge rule.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { Construct } from "constructs";
import * as cdk from "aws-cdk-lib";
import { aws_lambda as lambda, Duration } from "aws-cdk-lib";
import { Rule, Schedule } from "aws-cdk-lib/aws-events";
import { LambdaFunction } from "aws-cdk-lib/aws-events-targets";
import { PolicyStatement } from "aws-cdk-lib/aws-iam";
import {
  NodejsFunction,
  NodejsFunctionProps,
} from "aws-cdk-lib/aws-lambda-nodejs";
import path from "path";

export class ScheduleForecastTask extends Construct {
  constructor(scope: Construct, id: string, instanceId: string) {
    super(scope, id);

    const startInstanceLambdaProps: NodejsFunctionProps = {
      functionName: "StartInstanceLambda",
      runtime: lambda.Runtime.NODEJS_20_X,
      handler: "handler",
      memorySize: 128,
      entry: path.join(__dirname, "../../../lambda/start-instance/index.js"),
      timeout: cdk.Duration.seconds(10),
      environment: {
        INSTANCE_ID: instanceId,
      },
    };

    const startInstanceLambda = new NodejsFunction(
      this,
      "StartInstanceLambda",
      {
        ...startInstanceLambdaProps,
      }
    );

    startInstanceLambda.addToRolePolicy(
      new PolicyStatement({
        actions: ["ec2:StartInstances"],
        resources: [`arn:aws:ec2:*:*:instance/${instanceId}`],
      })
    );

    new Rule(this, "StartInstanceSchedule", {
      schedule: Schedule.cron({ minute: "0", hour: "1" }),
      targets: [new LambdaFunction(startInstanceLambda)],
    });
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's setup the lambda function&lt;/p&gt;

&lt;p&gt;Make up a file&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mkdir -p lambda/start-instance &amp;amp;&amp;amp; touch lambda/start-instance/index.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And paste the following code to trigger an ec2 instance&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { EC2Client, StartInstancesCommand } from "@aws-sdk/client-ec2";

const ec2 = new EC2Client({});

exports.handler = async (event) =&amp;gt; {
  console.info("Start Instance Lambda event", JSON.stringify(event, null, 2));

  const { instanceId } = event;

  try {
    const command = new StartInstancesCommand({
      InstanceIds: [instanceId],
    });

    await ec2.send(command);

    return { message: "Booting Instance command initiated" };
  } catch (error) {
    console.error("Failed to boot up the instance:", error);
    throw error;
  }
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, back in your &lt;code&gt;retail-ai-insights-stack.ts&lt;/code&gt;, import and call the construct like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;new ScheduleForecastTask(
  this,
  "ScheduleForecastTask",
  forecastInstance.instance.instanceId
);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once that done, deploy via&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cdk deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once deployed, your forecast job will run automatically every night, keeping your product demand predictions fresh and your compute costs optimized.&lt;/p&gt;

&lt;h3&gt;
  
  
  Wrapping Up Phase 3 – Scalable Forecasting, Zero Waste
&lt;/h3&gt;

&lt;p&gt;With this phase complete, we’ve automated a core business process, demand forecasting, in a way that’s:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AI-driven: Leveraging Amazon Bedrock for predictive insights.&lt;/li&gt;
&lt;li&gt;Cost-conscious: EC2 only runs when needed, then shuts down.&lt;/li&gt;
&lt;li&gt;Fully automated: Triggered nightly via EventBridge with no manual intervention.&lt;/li&gt;
&lt;li&gt;Production-ready: Clean orchestration, secure roles, real-time updates to DynamoDB.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This isn’t just about running a script. It’s about combining compute, AI, storage, and automation into a solution that mirrors how real companies make stocking decisions, every single day, with no human in the loop.&lt;/p&gt;

&lt;p&gt;Next up: Let’s use that same user interaction data to drive real-time product recommendations with Amazon Personalize.&lt;/p&gt;

&lt;h2&gt;
  
  
  Complete Code for the Third Phase
&lt;/h2&gt;

&lt;p&gt;To view the full code for the third phase, &lt;a href="https://github.com/mohsinsheikhani/retail-ai-insights/tree/main/03-demand-forecasting-with-bedrock" rel="noopener noreferrer"&gt;checkout the repository on GitHub&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;🚀 &lt;strong&gt;Follow me on &lt;a href="https://www.linkedin.com/in/mohsin-sheikhani/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; for more AWS content!&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>cloud</category>
      <category>mlops</category>
      <category>bedrock</category>
    </item>
    <item>
      <title>Hands-On with Amazon Bedrock Agents: Hotel Booking Assistant with Action Groups and Knowledge Bases</title>
      <dc:creator>Mohsin Sheikhani</dc:creator>
      <pubDate>Thu, 26 Jun 2025 13:06:25 +0000</pubDate>
      <link>https://dev.to/mohsinsheikhani/hands-on-with-amazon-bedrock-agents-hotel-booking-assistant-with-action-groups-and-knowledge-bases-1j</link>
      <guid>https://dev.to/mohsinsheikhani/hands-on-with-amazon-bedrock-agents-hotel-booking-assistant-with-action-groups-and-knowledge-bases-1j</guid>
      <description>&lt;p&gt;In this guide, I’ll walk through how I built a hotel room booking assistant using Amazon Bedrock Agents and AWS Lambda, combining large language models with real business logic.&lt;/p&gt;

&lt;p&gt;The goal? Let customers ask questions about hotel rooms, check availability, and book a room, all through natural conversation.&lt;/p&gt;

&lt;p&gt;This isn’t just another chatbot. Behind the scenes, it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pulls room descriptions from a knowledge base (S3 PDF)&lt;/li&gt;
&lt;li&gt;Checks real-time room availability via DynamoDB&lt;/li&gt;
&lt;li&gt;Books reservations using a serverless API&lt;/li&gt;
&lt;li&gt;And coordinates all of this using Bedrock Agents + Action Groups&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why this project matters
&lt;/h2&gt;

&lt;p&gt;If you're working with hotels, resorts, or any customer-facing business, conversational interfaces are becoming more than a nice-to-have, they're a competitive advantage. Instead of just answering FAQs, this agent can actually act: check, query, and write to your backend systems.&lt;/p&gt;

&lt;p&gt;This project is meant to show exactly how that’s possible, and how far Bedrock Agents have come.&lt;/p&gt;

&lt;h2&gt;
  
  
  We’ll create:
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsuu81b12dhj64twrkvxg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsuu81b12dhj64twrkvxg.png" alt="Hotel Booking Amazon Bedrock Agent Architecture" width="800" height="484"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An Amazon Bedrock Agent powered by Claude 3.5 Sonnet&lt;/li&gt;
&lt;li&gt;A Knowledge Base (PDF stored in S3) describing room types&lt;/li&gt;
&lt;li&gt;Two Action Groups (backed by Lambda + OpenAPI):

&lt;ul&gt;
&lt;li&gt;One to check room availability&lt;/li&gt;
&lt;li&gt;Another to book a reservation&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;A couple of DynamoDB tables for storing room and booking data&lt;/li&gt;
&lt;li&gt;And a simple walkthrough to connect it all together&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Let’s get into the build.
&lt;/h2&gt;

&lt;p&gt;Head over to the Amazon Bedrock console:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftclvoosj8wsfszcqs57i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftclvoosj8wsfszcqs57i.png" alt="Amazon Bedrock Agent Console" width="800" height="242"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Start by creating a new agent, give it a name and a short description , like so:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9yl5xn3kovxfes9h3fgs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9yl5xn3kovxfes9h3fgs.png" alt="Creating an Amazon Bedrock Agent" width="620" height="484"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once the agent is created, it's time to choose the foundation model it will use to generate responses. Click on &lt;code&gt;Select Model&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm9m665pz0fb8r4dof10z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm9m665pz0fb8r4dof10z.png" alt="Agent Builder" width="800" height="364"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Choose &lt;code&gt;Anthropic&lt;/code&gt; as the provider and select the &lt;code&gt;Claude 3.5 Sonnet&lt;/code&gt; model, then click Apply.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you don’t have access to this model yet, go to Model Access in the Bedrock console and request access for Claude 3.5 Sonnet.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fotb8ztsvm7ua8yir1yau.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fotb8ztsvm7ua8yir1yau.png" alt="Claude 3.5 Sonnet model selection" width="800" height="694"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You should now see the selected model listed in the Agent Builder:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6vza3kv6b8ynff5t7ck7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6vza3kv6b8ynff5t7ck7.png" alt="Agent Builder" width="800" height="379"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now, time for us to fill the &lt;code&gt;Instruction for Agent&lt;/code&gt; input box. Scroll down to the Instruction for Agent section. copy the instruction from this &lt;a href="https://github.com/mohsinsheikhani/bedrock-hotel-agent/blob/main/resources/AgentInstructions.txt" rel="noopener noreferrer"&gt;GitHub Link&lt;/a&gt;, and paste it into the instruction input box:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frwqg3pvkso96d3myshl2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frwqg3pvkso96d3myshl2.png" alt="Instruction for Amazon Bedrock Agent" width="800" height="546"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Next, expand &lt;code&gt;Additional Settings&lt;/code&gt; and make sure &lt;code&gt;User Input&lt;/code&gt; is enabled, this way the agent can ask clarification question from user to make a correct decision when needed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyajeli3gj378j2yzwbnj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyajeli3gj378j2yzwbnj.png" alt="User Input enable for Amazon Bedrock Agent" width="800" height="190"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now scroll back to the top, click &lt;code&gt;Save&lt;/code&gt;, and then &lt;code&gt;Prepare&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fki8uz6gco5ehfnu2p5nr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fki8uz6gco5ehfnu2p5nr.png" alt="Edit in Agent Builder" width="800" height="228"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Time for a quick test:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1soladi5vmbawrj1emxh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1soladi5vmbawrj1emxh.png" alt="Initial test for Amazon Bedrock Agent" width="686" height="427"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Notice how the agent uses the instructions to guide its answers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Clone the CDK Repository
&lt;/h3&gt;

&lt;p&gt;Now let’s set up the backend infrastructure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git clone https://github.com/mohsinsheikhani/bedrock-hotel-agent
cd bedrock-hotel-agent
npm install
cdk deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will provision:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;2 Lambda functions (check &amp;amp; book availability)&lt;/li&gt;
&lt;li&gt;2 DynamoDB tables&lt;/li&gt;
&lt;li&gt;1 S3 bucket (to store the knowledge base PDF)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To add sample room availability to the &lt;code&gt;HotelRoomAvailabilityTable&lt;/code&gt;, run the following scriptL&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;python3 ./scripts/insert-to-room-availability.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To verify, go to the DynamoDB Console, open the &lt;code&gt;HotelRoomAvailabilityTable&lt;/code&gt;, and click Explore Items:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffbxh5nflikojl9s3wnwy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffbxh5nflikojl9s3wnwy.png" alt="Explore Items on HotelRoomAvailabilityTable DynamoDB Table" width="529" height="408"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Add Domain Knowledge Using Bedrock Knowledge Base
&lt;/h3&gt;

&lt;p&gt;Right now, the agent knows how to help, thanks to the instructions we gave it, but it still doesn’t know what types of rooms exist or what amenities each offers.&lt;/p&gt;

&lt;p&gt;Let’s fix that.&lt;/p&gt;

&lt;p&gt;We’ll give the agent real hotel knowledge using Amazon Bedrock Knowledge Bases, backed by an S3-hosted PDF.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This is where Retrieval-Augmented Generation (RAG) comes into play. Instead of stuffing everything into the prompt, the model can now pull specific answers directly from documents, structured or unstructured, at runtime.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Start by uploading the &lt;code&gt;Hilton-Portfolio.pdf&lt;/code&gt; file (&lt;a href="https://github.com/mohsinsheikhani/bedrock-hotel-agent/blob/main/resources/Hilton-Portfolio.pdf" rel="noopener noreferrer"&gt;included in the repo&lt;/a&gt;) to the S3 bucket we provisioned with CDK: &lt;code&gt;agent-kb-assets&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwklka8judbc3831470o0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwklka8judbc3831470o0.png" alt="Uploaded file on S3 bucket for Amazon Bedrock Knowledge Bases" width="799" height="167"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Next, we’ll create a knowledge base and wire it to our agent so it can pull context from this PDF when answering questions.&lt;/p&gt;

&lt;p&gt;Head to the Amazon Bedrock Console, Look for &lt;code&gt;Knowledge Bases&lt;/code&gt; underneath &lt;code&gt;Builder tools&lt;/code&gt;, click on it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2qavj5maf5dyn9c6caxr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2qavj5maf5dyn9c6caxr.png" alt="Knowledge Bases on Amazon Bedrock console" width="799" height="341"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click &lt;code&gt;Create&lt;/code&gt; and choose the &lt;code&gt;Knowledge base with vector store&lt;/code&gt; option:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F36ct7e4a1y0nq5c220jv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F36ct7e4a1y0nq5c220jv.png" alt="Knowledge Base with vector store, Amazon Bedrock Agent" width="800" height="402"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Fill up the Knowledge Base details with a name, and choose S3 as the Data Source&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm76hj9x227hdab8ssj8j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm76hj9x227hdab8ssj8j.png" alt="Knowledge Base Details" width="800" height="440"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Next step, is to configure the Data Source and point to the S3 path where your PDF lives:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6vvkltnnk4046x4feokt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6vvkltnnk4046x4feokt.png" alt="Configure Knowledge Base Data Source for Amazon Bedrock Agent" width="800" height="440"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For Embeddings, select &lt;code&gt;Amazon Titan&lt;/code&gt;. For Vector Store, choose &lt;code&gt;Amazon OpenSearch Serverless&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This setup gives you serverless RAG with native AWS services:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fszxbhxc9bxuvth7fxjnw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fszxbhxc9bxuvth7fxjnw.png" alt="Configure data storage and processing, using Amazon Titan as the embedding model and Amazon OpenSearch Serverless as the vector data store" width="800" height="440"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click Next, review your configuration, then click &lt;code&gt;Create&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0igjiutdkhqpqgd6a9td.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0igjiutdkhqpqgd6a9td.png" alt="Amazon Bedrock Knowledge Base creation in-progress" width="799" height="200"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once it’s created, open your Knowledge Base and click &lt;code&gt;Sync&lt;/code&gt; to begin parsing the document and storing the chunks for semantic search:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8ohwl83s5wc1vrsmzjuj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8ohwl83s5wc1vrsmzjuj.png" alt="Syncing Knowledge Base with Amazon S3 Data source" width="799" height="288"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Return to the &lt;code&gt;Agent Builder&lt;/code&gt;, scroll down to the &lt;code&gt;Knowledge bases&lt;/code&gt; section:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvczo1kpoojxzz1sumwt3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvczo1kpoojxzz1sumwt3.png" alt="Knowledge Bases, Agent Builder" width="800" height="361"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click Add knowledge base, select the one you just created, and fill in the Instruction box with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;As an agent route any question by the user related to room type, room amenities, room description, hotel location to the knowledge bases.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5cjvxvqhle98e4qothbd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5cjvxvqhle98e4qothbd.png" alt="Add Knowledge Base" width="800" height="310"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click &lt;code&gt;Add&lt;/code&gt;, and you’ll see it appear in the list:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F737exxyx8nhlhh9hxq86.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F737exxyx8nhlhh9hxq86.png" alt="Added Knowledge Base to Amazon Bedrock Agent" width="799" height="141"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then, click Save, and Prepare your agent:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fki8uz6gco5ehfnu2p5nr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fki8uz6gco5ehfnu2p5nr.png" alt="Edit in Agent Builder" width="800" height="228"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now give the agent a prompt like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;What amenities are included in Embassy Suites by Hilton?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response should include real answers from the document you uploaded.&lt;br&gt;
But what’s even cooler?&lt;br&gt;
Scroll down and open the Orchestration trace. You’ll see the agent actively calling the Knowledge Base behind the scenes:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F02wc6l7km56h1tgsqsaz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F02wc6l7km56h1tgsqsaz.png" alt="Orchestration and Knowledge Base" width="800" height="720"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This confirms our RAG setup is working, the agent is now retrieval-aware and can ground its responses in real business content.&lt;/p&gt;
&lt;h4&gt;
  
  
  Clean Up the Knowledge Base (To Avoid Charges)
&lt;/h4&gt;

&lt;p&gt;Amazon Bedrock Knowledge Bases can incur ongoing charges, especially due to the underlying OpenSearch collection.&lt;br&gt;
At this point, if you're just experimenting or done testing:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go back to Agent Builder and remove the Knowledge Base from your agent.&lt;/li&gt;
&lt;li&gt;Then, head to Builder Tools → Knowledge Bases and delete the Knowledge Base itself.&lt;/li&gt;
&lt;li&gt;Finally, check the OpenSearch Service dashboard. If there's a collection still running, delete it too.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This will ensure you’re not billed unnecessarily going forward.&lt;/p&gt;
&lt;h3&gt;
  
  
  Action Group 01 - Room Availability Checks
&lt;/h3&gt;

&lt;p&gt;Now let’s move beyond answering questions and into actions.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Action Groups are just backend utilities which the Bedrock Agent uses to call external code on our behalf.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We'll start with a simple but critical one:&lt;br&gt;
Checking if a specific room is available for a customer’s requested dates.&lt;/p&gt;

&lt;p&gt;Go to your Agent Builder and scroll to Action groups.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftdqkr6zzz4700s1jje6e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftdqkr6zzz4700s1jje6e.png" alt="Action Group, Amazon Bedrock Agent" width="800" height="186"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click Add, give your Action Group a name, and configure it as follows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Action group type: Define with API schema&lt;/li&gt;
&lt;li&gt;Lambda function: Select the existing one: &lt;code&gt;RoomAvailabilityHandler&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faitg8jmvmj3q57iaqk7r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faitg8jmvmj3q57iaqk7r.png" alt="Create Action Group, Amazon Bedrock Agent" width="800" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For the API Schema, choose &lt;code&gt;Define via in-line schema editor&lt;/code&gt;.&lt;br&gt;
Then paste the OpenAPI schema from this &lt;a href="https://github.com/mohsinsheikhani/bedrock-hotel-agent/blob/main/resources/HotelRoomAvailability_OpenAPISchema.yaml" rel="noopener noreferrer"&gt;GitHub Link&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg3k2kqkdt8abcxix3rrw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg3k2kqkdt8abcxix3rrw.png" alt="Action Group Schema, Amazon Bedrock Agent" width="800" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click Create, and you’ll see it appear in your Action Groups list:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0meskcs77ej7jdszbigt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0meskcs77ej7jdszbigt.png" alt="Action Group List, Amazon Bedrock Agent" width="799" height="138"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;
  
  
  Give the Agent Permission to Call the Lambda
&lt;/h4&gt;

&lt;p&gt;By default, the agent can't invoke your Lambda function, we need to explicitly allow it.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to the &lt;code&gt;RoomAvailabilityHandler&lt;/code&gt; function in Lambda console.&lt;/li&gt;
&lt;li&gt;In the Configuration tab, scroll to Permissions &amp;gt; Resource-based policy statements&lt;/li&gt;
&lt;li&gt;Click Add permissions&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5fxkom10cbn8emwjvtc7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5fxkom10cbn8emwjvtc7.png" alt=" " width="799" height="411"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Fill it out like so:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2ra3xt103x911x5e3muc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2ra3xt103x911x5e3muc.png" alt="Assigning Permissions to Amazon Bedrock Agents to invoke a lambda function" width="799" height="411"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Head back to Agent Builder, and click Save and then Prepare:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fki8uz6gco5ehfnu2p5nr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fki8uz6gco5ehfnu2p5nr.png" alt="Edit in Agent Builder" width="800" height="228"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Try testing it out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Can you check the room availability for 2025-12-25?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You’ll see the agent reason through the prompt and call your Lambda:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0n44xmplcywqtav6v6wr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0n44xmplcywqtav6v6wr.png" alt="Testing Amazon Bedrock Agent" width="707" height="381"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And in the tracing panel, you'll notice that the agent invoked your Action Group exactly when it needed to:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Facfzzwfukdf96t5egrmk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Facfzzwfukdf96t5egrmk.png" alt="Orchestration and Knowledge Base" width="800" height="743"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That wraps up our first Action Group i.e. Room availability.&lt;/p&gt;

&lt;h3&gt;
  
  
  Action Group 02 - Room Booking
&lt;/h3&gt;

&lt;p&gt;Now that our agent can check availability, it’s time to let it book a room when the user is ready.&lt;/p&gt;

&lt;p&gt;We’ll create a second Action Group that invokes a Lambda function to store booking details in DynamoDB.&lt;/p&gt;

&lt;p&gt;Create another Action Group choosing the same steps i.e.:&lt;/p&gt;

&lt;p&gt;Just like before, go to Agent Builder &amp;gt; Action groups and click Add.&lt;/p&gt;

&lt;p&gt;Configure it with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Action group type: Define with API schema&lt;/li&gt;
&lt;li&gt;Lambda function: &lt;code&gt;HotelRoomBookingHandler&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;API Schema: Select in-line schema editor and paste the OpenAPI schema from this &lt;a href="https://github.com/mohsinsheikhani/bedrock-hotel-agent/blob/main/resources/HotelRoomBooking_OpenAPISchema.yaml" rel="noopener noreferrer"&gt;GitHub Link&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5pe0ltzdab64o1uxwwo7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5pe0ltzdab64o1uxwwo7.png" alt="Action Group Schema, Amazon Bedrock Agent" width="800" height="373"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2crw04kqdngoq6iqrhgp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2crw04kqdngoq6iqrhgp.png" alt="Action Group Schema, Amazon Bedrock Agent" width="800" height="567"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click Create, and you’ll see it appear in your Action Groups list:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fesjsb9aqme300lr26dpe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fesjsb9aqme300lr26dpe.png" alt="Action Group List, Amazon Bedrock Agent" width="800" height="183"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Give the Agent Permission to Call the Lambda
&lt;/h4&gt;

&lt;p&gt;Your agent can’t call this function until you give it explicit permission.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to the &lt;code&gt;HotelRoomBookingHandler&lt;/code&gt; function in Lambda console.&lt;/li&gt;
&lt;li&gt;In the Configuration tab, scroll to Permissions &amp;gt; Resource-based policy statements&lt;/li&gt;
&lt;li&gt;Click Add permissions&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fofiu5vlwyy9z4xtb89lc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fofiu5vlwyy9z4xtb89lc.png" alt="Lambda Resource-based policy statements" width="800" height="418"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6azobavz764vj9m04fee.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6azobavz764vj9m04fee.png" alt="Lambda Resource-based policy statements" width="800" height="178"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Use the same settings as shown:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnanm813zdu2pdm0z7j1y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnanm813zdu2pdm0z7j1y.png" alt="Assigning Permissions to Amazon Bedrock Agents to invoke a lambda function" width="800" height="370"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmksd00523kthxo49m8g2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmksd00523kthxo49m8g2.png" alt="Lambda Resource-based policy statements" width="800" height="141"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Back in Agent Builder, click Save and then Prepare to update the agent.&lt;/p&gt;

&lt;h3&gt;
  
  
  Test the Full Booking Flow
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1tlrkrldnpy9naa9fz7r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1tlrkrldnpy9naa9fz7r.png" alt="Amazon Bedrock Agents Demo" width="667" height="652"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkr4v9prb52463vu4l4af.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkr4v9prb52463vu4l4af.png" alt="Amazon Bedrock Agents Demo" width="662" height="618"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fae8y4flskl2nuaknb7m1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fae8y4flskl2nuaknb7m1.png" alt="Amazon Bedrock Agents Demo" width="662" height="618"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once the Booking is successful, verify it by going to DynamoDB, click on Explore Items and choose the &lt;code&gt;HotelRoomBookingTable&lt;/code&gt; and you should see the entry created via the Bedrock Agent.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1bl4myjaowuds7zmw8rg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1bl4myjaowuds7zmw8rg.png" alt="DynamoDB table for booking made by Amazon Bedrock Agent" width="800" height="383"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And that's it, you’ve now wired up an LLM agent that can check room availability and book hotel stays, powered by real APIs.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>cloud</category>
      <category>bedrockagents</category>
      <category>agentaichallenge</category>
    </item>
    <item>
      <title>A Practical Guide to MLOps on AWS: Transforming Raw Data into AI-Ready Datasets with AWS Glue (Phase 02)</title>
      <dc:creator>Mohsin Sheikhani</dc:creator>
      <pubDate>Thu, 12 Jun 2025 11:36:42 +0000</pubDate>
      <link>https://dev.to/mohsinsheikhani/a-practical-guide-to-mlops-on-aws-transforming-raw-data-into-ai-ready-datasets-with-aws-glue-4lc9</link>
      <guid>https://dev.to/mohsinsheikhani/a-practical-guide-to-mlops-on-aws-transforming-raw-data-into-ai-ready-datasets-with-aws-glue-4lc9</guid>
      <description>&lt;p&gt;In &lt;a href="https://dev.to/mohsinsheikhani/a-practical-guide-to-mlops-on-aws-streaming-data-ingestion-with-kinesis-firehose-phase-01-1bi7"&gt;Phase 01&lt;/a&gt;, we built the ingestion layer of our Retail AI Insights system. We streamed historical product interaction data into Amazon S3 (Bronze zone) and stored key product metadata with inventory information in DynamoDB.&lt;/p&gt;

&lt;p&gt;Now that we have raw data arriving reliably, it's time to clean, enrich, and organize it for downstream AI workflows.&lt;/p&gt;

&lt;h3&gt;
  
  
  Objective
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Transform raw event data from the Bronze zone into:&lt;/li&gt;
&lt;li&gt;Cleaned, analysis-ready Parquet files in the Silver zone&lt;/li&gt;
&lt;li&gt;Forecast-specific feature sets in the Gold zone under &lt;code&gt;/forecast_ready/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Recommendation-ready CSV files under &lt;code&gt;/recommendations_ready/&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpvcmvkoqz04t60li98az.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpvcmvkoqz04t60li98az.png" alt="Transforming Raw Data into AI-Ready Datasets with AWS Glue Architecture Diagram" width="800" height="534"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This will power:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Demand forecasting via Amazon Bedrock&lt;/li&gt;
&lt;li&gt;Personalized product recommendations using Amazon Personalize&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What We'll Build in This Phase
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;AWS Glue Jobs: Python scripts to clean, transform, and write data to the appropriate S3 zone&lt;/li&gt;
&lt;li&gt;AWS Glue Crawlers: Catalog metadata from S3 into tables for Athena &amp;amp; further processing&lt;/li&gt;
&lt;li&gt;AWS CDK Stack: Provisions all jobs, buckets, and crawlers&lt;/li&gt;
&lt;li&gt;Athena Queries: Run sanity checks on the transformed data&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Directory &amp;amp; Bucket Layout
&lt;/h2&gt;

&lt;p&gt;We'll now be working with the following S3 zones:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;retail-ai-bronze-zone/&lt;/code&gt; → Raw JSON from Firehose&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;retail-ai-silver-zone/cleaned_data/&lt;/code&gt; → Cleaned Parquet&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;retail-ai-gold-zone/forecast_ready/&lt;/code&gt; → Aggregated features for forecasting&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;retail-ai-gold-zone/recommendations_ready/&lt;/code&gt; → CSV with item metadata for Personalize&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You'll also notice a fourth bucket: &lt;code&gt;retail-ai-zone-assets/&lt;/code&gt;, this stores scripts, and training dataset.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1 - Creating Glue Resources via CDK
&lt;/h2&gt;

&lt;p&gt;Now that we've set up our storage zones and uploaded the required ETL scripts and datasets, it's time to define the Glue resources with AWS CDK.&lt;/p&gt;

&lt;p&gt;We'll create:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;3 Glue Jobs

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DataCleaningETLJob&lt;/strong&gt; → Cleans raw JSON into structured Parquet for the Silver Zone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ForecastGoldETLJob&lt;/strong&gt; → Transforms cleaned data with features for demand prediction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RecommendationGoldETLJob&lt;/strong&gt; → Prepares item metadata CSV for Amazon Personalize.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Four Crawlers&lt;/li&gt;
&lt;li&gt;Validate everything with Athena&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;From the project root, generate the construct file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mkdir -p lib/constructs/analytics &amp;amp;&amp;amp; touch lib/constructs/analytics/glue-resources.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make sure your local scripts/ and dataset/ directories are present, then upload them to your S3 assets bucket:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aws s3 cp ./scripts/sales_etl_script.py s3://retail-ai-zone-assets/scripts/
aws s3 cp ./scripts/forecast_gold_etl_script.py s3://retail-ai-zone-assets/scripts/
aws s3 cp ./scripts/user_interaction_etl_script.py s3://retail-ai-zone-assets/scripts/
aws s3 cp ./dataset/events_with_metadata.csv s3://retail-ai-zone-assets/dataset/
aws s3 cp ./scripts/inventory_forecaster.py s3://retail-ai-zone-assets/scripts/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Define Glue Jobs &amp;amp; Crawlers in CDK
&lt;/h3&gt;

&lt;p&gt;Now, open the &lt;code&gt;lib/constructs/analytics/glue-resources.ts&lt;/code&gt; file and define the full CDK logic to create:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A Glue job role with required permissions&lt;/li&gt;
&lt;li&gt;The three ETL jobs with their respective scripts&lt;/li&gt;
&lt;li&gt;Four crawlers with S3 targets pointing to Bronze, Silver, Forecast, and Recommendation zones&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Open the &lt;code&gt;lib/constructs/analytics/glue-resources.ts&lt;/code&gt; file, and add the following code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { Construct } from "constructs";
import * as cdk from "aws-cdk-lib";

import { Bucket } from "aws-cdk-lib/aws-s3";
import { CfnCrawler, CfnJob, CfnDatabase } from "aws-cdk-lib/aws-glue";
import {
  Role,
  ServicePrincipal,
  ManagedPolicy,
  PolicyStatement,
} from "aws-cdk-lib/aws-iam";

interface GlueProps {
  bronzeBucket: Bucket;
  silverBucket: Bucket;
  goldBucket: Bucket;
  dataAssetsBucket: Bucket;
}

export class GlueResources extends Construct {
  constructor(scope: Construct, id: string, props: GlueProps) {
    super(scope, id);

    const { bronzeBucket, silverBucket, goldBucket, dataAssetsBucket } = props;

    // Glue Database
    const glueDatabase = new CfnDatabase(this, "SalesDatabase", {
      catalogId: cdk.Stack.of(this).account,
      databaseInput: {
        name: "sales_data_db",
      },
    });

    // Create IAM Role for Glue
    const glueRole = new Role(this, "GlueServiceRole", {
      assumedBy: new ServicePrincipal("glue.amazonaws.com"),
    });

    bronzeBucket.grantRead(glueRole);
    silverBucket.grantReadWrite(glueRole);
    goldBucket.grantReadWrite(glueRole);

    glueRole.addToPolicy(
      new PolicyStatement({
        actions: ["s3:GetObject"],
        resources: [`${dataAssetsBucket.bucketArn}/*`],
      })
    );

    glueRole.addManagedPolicy(
      ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSGlueServiceRole")
    );

    // Glue Crawler (for Bronze Bucket)
    new CfnCrawler(this, "DataCrawlerBronze", {
      name: "DataCrawlerBronze",
      role: glueRole.roleArn,
      databaseName: glueDatabase.ref,
      targets: {
        s3Targets: [{ path: bronzeBucket.s3UrlForObject() }],
      },
      tablePrefix: "bronze_",
    });

    // Glue ETL Job
    new CfnJob(this, "DataCleaningETLJob", {
      name: "DataCleaningETLJob",
      role: glueRole.roleArn,
      command: {
        name: "glueetl",
        pythonVersion: "3",
        scriptLocation: dataAssetsBucket.s3UrlForObject(
          "scripts/sales_etl_script.py"
        ),
      },
      defaultArguments: {
        "--TempDir": silverBucket.s3UrlForObject("temp/"),
        "--job-language": "python",
        "--bronze_bucket": bronzeBucket.bucketName,
        "--silver_bucket": silverBucket.bucketName,
      },
      glueVersion: "3.0",
      maxRetries: 0,
      timeout: 10,
      workerType: "Standard",
      numberOfWorkers: 2,
    });

    // Glue Crawler (for Silver Bucket)
    new CfnCrawler(this, "DataCrawlerSilver", {
      name: "DataCrawlerSilver",
      role: glueRole.roleArn,
      databaseName: glueDatabase.ref,
      targets: {
        s3Targets: [
          {
            path: `${silverBucket.s3UrlForObject()}/cleaned_data/`,
          },
        ],
      },
      tablePrefix: "silver_",
    });

    // Glue Crawler (for Gold Bucket)
    new CfnCrawler(this, "DataCrawlerForecast", {
      name: "DataCrawlerForecast",
      role: glueRole.roleArn,
      databaseName: glueDatabase.ref,
      targets: {
        s3Targets: [{ path: `${goldBucket.s3UrlForObject()}/forecast_ready/` }],
      },
      tablePrefix: "gold_",
    });

    // Glue Crawler (for Gold Bucket)
    new CfnCrawler(this, "DataCrawlerRecommendations", {
      name: "DataCrawlerRecommendations",
      role: glueRole.roleArn,
      databaseName: glueDatabase.ref,
      targets: {
        s3Targets: [
          { path: `${goldBucket.s3UrlForObject()}/recommendations_ready/` },
        ],
      },
      tablePrefix: "gold_",
    });

    // Glue ETL Job to output forecast ready dataset
    new CfnJob(this, "ForecastGoldETLJob", {
      name: "ForecastGoldETLJob",
      role: glueRole.roleArn,
      command: {
        name: "glueetl",
        pythonVersion: "3",
        scriptLocation: dataAssetsBucket.s3UrlForObject(
          "scripts/forecast_gold_etl_script.py"
        ),
      },
      defaultArguments: {
        "--TempDir": silverBucket.s3UrlForObject("temp/"),
        "--job-language": "python",
        "--silver_bucket": silverBucket.bucketName,
        "--gold_bucket": goldBucket.bucketName,
      },
      glueVersion: "3.0",
      maxRetries: 0,
      timeout: 10,
      workerType: "Standard",
      numberOfWorkers: 2,
    });

    // Glue ETL Job to output recommendation ready dataset
    new CfnJob(this, "RecommendationGoldETLJob", {
      name: "RecommendationGoldETLJob",
      role: glueRole.roleArn,
      command: {
        name: "glueetl",
        pythonVersion: "3",
        scriptLocation: dataAssetsBucket.s3UrlForObject(
          "scripts/user_interaction_etl_script.py"
        ),
      },
      defaultArguments: {
        "--TempDir": silverBucket.s3UrlForObject("temp/"),
        "--job-language": "python",
        "--silver_bucket": silverBucket.bucketName,
        "--gold_bucket": goldBucket.bucketName,
      },
      glueVersion: "3.0",
      maxRetries: 0,
      timeout: 10,
      workerType: "Standard",
      numberOfWorkers: 2,
    });
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wire it up on the &lt;code&gt;retail-ai-insights-stack.ts&lt;/code&gt; file&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/**
 * Glue ETL Resources
 **/
new GlueResources(this, "GlueResources", {
  bronzeBucket,
  silverBucket,
  goldBucket,
  dataAssetsBucket,
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once deployed via &lt;code&gt;cdk deploy&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to AWS Glue &amp;gt; ETL Jobs - You should see:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgcviikq2qlizdk6vul2u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgcviikq2qlizdk6vul2u.png" alt="AWS Glue Studio" width="800" height="267"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to AWS Glue &amp;gt; Data Catalog &amp;gt; Crawlers – Ensure four crawlers exist:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwntiecqwoefxdjt7bwjw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwntiecqwoefxdjt7bwjw.png" alt="AWS Glue Crawlers" width="800" height="194"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2 - Run Glue Jobs to Transform Raw Data
&lt;/h3&gt;

&lt;p&gt;Now that our Glue jobs and crawlers are deployed, let’s walk through how we run the ETL flow across the Bronze, Silver, and Gold zones.&lt;/p&gt;

&lt;h4&gt;
  
  
  Locate Raw Data in Bronze Bucket
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;Go to the Amazon S3 Console, open the &lt;code&gt;retail-ai-bronze-zone bucket&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Drill down through the directories until you see the file, note the tree structure, in my case it's &lt;code&gt;dataset/2025/05/26/20&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Copy this full prefix path.&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  Update the ETL Script Input Path
&lt;/h4&gt;

&lt;p&gt;Open the &lt;code&gt;sales_etl_script.py&lt;/code&gt; inside VSCode.&lt;br&gt;
On line 36, update the input_path variable to reflect the directory path you just copied:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;input_path = f"s3://{bronze_bucket}/dataset/2025/05/26/20/"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Re-upload the modified script to your S3 data-assets bucket:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aws s3 cp ./scripts/sales_etl_script.py s3://retail-ai-zone-assets/scripts/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because versioning is enabled on the bucket, this will replace the previous file while preserving version history.&lt;/p&gt;

&lt;h4&gt;
  
  
  Run the ETL Jobs
&lt;/h4&gt;

&lt;p&gt;Now let’s kick off the transformation pipeline:&lt;/p&gt;

&lt;p&gt;Run &lt;code&gt;DataCleaningETLJob&lt;/code&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Go to AWS Glue Console &amp;gt; ETL Jobs.&lt;/li&gt;
&lt;li&gt;Select the &lt;code&gt;DataCleaningETLJob&lt;/code&gt; and click Run Job.&lt;/li&gt;
&lt;li&gt;This job will:

&lt;ul&gt;
&lt;li&gt;Read raw JSON data from the Bronze bucket.&lt;/li&gt;
&lt;li&gt;Clean, cast, and convert it to Parquet.&lt;/li&gt;
&lt;li&gt;Store the results in the &lt;code&gt;retail-ai-silver-zone&lt;/code&gt; bucket under &lt;code&gt;cleaned_data/&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F214xah13td2wnigk6hp2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F214xah13td2wnigk6hp2.png" alt="Running AWS Glue Job" width="800" height="152"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once successful, navigate to the &lt;code&gt;retail-ai-silver-zone&lt;/code&gt; bucket and confirm:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa7f7ksmgz4thzmvnu3sg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa7f7ksmgz4thzmvnu3sg.png" alt="S3 Bucket for Silver Zone" width="800" height="327"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Run &lt;code&gt;ForecastGoldETLJob&lt;/code&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Go to AWS Glue Console &amp;gt; ETL Jobs.&lt;/li&gt;
&lt;li&gt;Select the &lt;code&gt;ForecastGoldETLJob&lt;/code&gt; and click Run Job.&lt;/li&gt;
&lt;li&gt;This job will:

&lt;ul&gt;
&lt;li&gt;Read the cleaned data from &lt;code&gt;retail-ai-silver-zone/cleaned_data/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Aggregate daily sales&lt;/li&gt;
&lt;li&gt;Output the transformed data to &lt;code&gt;retail-ai-gold-zone/forecast_ready/&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcts5otxav3gnk2x4i2u2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcts5otxav3gnk2x4i2u2.png" alt="Running AWS Glue Job" width="800" height="154"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once completed, visit the Gold bucket and confirm the forecast files are present in that directory.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flecq3inlpp3c7413tt6u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flecq3inlpp3c7413tt6u.png" alt="S3 Bucket for Gold Zone" width="800" height="327"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Run &lt;code&gt;RecommendationGoldETLJob&lt;/code&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Go to AWS Glue Console &amp;gt; ETL Jobs.&lt;/li&gt;
&lt;li&gt;Select the &lt;code&gt;RecommendationGoldETLJob&lt;/code&gt; and click Run Job.&lt;/li&gt;
&lt;li&gt;This job will:

&lt;ul&gt;
&lt;li&gt;Read cleaned product data from the Silver zone&lt;/li&gt;
&lt;li&gt;Output only the required item metadata in CSV format&lt;/li&gt;
&lt;li&gt;Save to &lt;code&gt;retail-ai-gold-zone/recommendations_ready/&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk0y556rnah8jkdqg94ya.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk0y556rnah8jkdqg94ya.png" alt="Running AWS Glue Job" width="800" height="148"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After the job runs successfully, go to the Gold bucket and verify the structure and CSV file.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdk94dhj62uvj4ph5c1k5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdk94dhj62uvj4ph5c1k5.png" alt="S3 Bucket for Gold Zone" width="800" height="185"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Run All Glue Crawlers
&lt;/h3&gt;

&lt;p&gt;Once the Glue crawlers are deployed, you’ll see four of them listed in the Glue Console &amp;gt; Data Catalog &amp;gt; Crawlers:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftcg3is7rhqu9sfwwou6l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftcg3is7rhqu9sfwwou6l.png" alt="AWS Glue Crawlers" width="800" height="194"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Select all four crawlers.&lt;/li&gt;
&lt;li&gt;Click Run.&lt;/li&gt;
&lt;li&gt;Once completed, look at the "Table changes on the last run" column  each should say "1 created".&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2vz0bys6gie08m6hmykd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2vz0bys6gie08m6hmykd.png" alt="AWS Glue Crawlers" width="800" height="189"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Validate Table Creation
&lt;/h4&gt;

&lt;p&gt;Navigate to Glue Console &amp;gt; Data Catalog &amp;gt; Databases &amp;gt; Tables. You should now see four new tables, each corresponding to a specific zone:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqzc3a1xqyuvn466xk748.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqzc3a1xqyuvn466xk748.png" alt="AWS Glue Data Catalog Tables" width="800" height="193"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each table has an automatically inferred schema, including columns like &lt;code&gt;user_id&lt;/code&gt;, &lt;code&gt;event_type&lt;/code&gt;, &lt;code&gt;timestamp&lt;/code&gt;, &lt;code&gt;price&lt;/code&gt;, &lt;code&gt;product_name&lt;/code&gt;, and more.&lt;/p&gt;

&lt;h3&gt;
  
  
  Query with Amazon Athena
&lt;/h3&gt;

&lt;p&gt;Now let’s run SQL queries against these tables:&lt;/p&gt;

&lt;p&gt;Open the Amazon Athena Console.&lt;/p&gt;

&lt;p&gt;If it's your first time, you’ll see a pop-up:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhe67mv82kgmzf12pg96g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhe67mv82kgmzf12pg96g.png" alt="AWS Athena, Output bucket configuration" width="794" height="35"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Choose your &lt;code&gt;retail-ai-zone-assets&lt;/code&gt; bucket.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7numhwljm8hrm3krrfb8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7numhwljm8hrm3krrfb8.png" alt="AWS Athena, Output bucket configuration" width="800" height="279"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click Save.&lt;/p&gt;

&lt;h4&gt;
  
  
  Sample Athena Query
&lt;/h4&gt;

&lt;p&gt;In the query editor, trying running simple SQL queries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Select * from sales_data_db.&amp;lt;TABLE_NAME&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Try this query on the &lt;code&gt;bronze_retail_ai_bronze_zone&lt;/code&gt; table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Select * from sales_data_db.bronze_retail_ai_bronze_zone
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fysz517su15r5shxscjif.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fysz517su15r5shxscjif.png" alt="AWS Athena query result" width="800" height="325"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Try this query on the &lt;code&gt;silver_cleaned_data&lt;/code&gt; table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Select * from sales_data_db.silver_cleaned_data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqxhy0vivi5050ugxvx0t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqxhy0vivi5050ugxvx0t.png" alt="AWS Athena query result" width="800" height="325"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Try this query on the &lt;code&gt;gold_forecast_ready&lt;/code&gt; table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Select * from sales_data_db.gold_forecast_ready
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0crx8codkaccaybup1l0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0crx8codkaccaybup1l0.png" alt="AWS Athena query result" width="800" height="325"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Try this query on the &lt;code&gt;gold_recommendations_ready&lt;/code&gt; table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Select * from sales_data_db.gold_recommendations_ready
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdrwgi0cej9p3lmhq6jk3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdrwgi0cej9p3lmhq6jk3.png" alt="AWS Athena query result" width="800" height="325"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What You’ve Just Built
&lt;/h2&gt;

&lt;p&gt;In this phase, you've gone beyond basic ETL. You’ve engineered a production-grade data lake with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Multi-zone architecture (Bronze, Silver, Gold)&lt;/li&gt;
&lt;li&gt;Automated ETL pipelines using AWS Glue&lt;/li&gt;
&lt;li&gt;Schema discovery and validation through Crawlers&lt;/li&gt;
&lt;li&gt;Interactive querying via Amazon Athena&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of this was done infrastructure-as-code first using AWS CDK, with clean separation of storage, processing, and access layers, exactly how real-world cloud data platforms are designed.&lt;/p&gt;

&lt;p&gt;But this isn’t just about organizing data. You’re now sitting on a foundation that’s:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AI-ready&lt;/li&gt;
&lt;li&gt;Model-friendly&lt;/li&gt;
&lt;li&gt;Cost-efficient&lt;/li&gt;
&lt;li&gt;And built for scale&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What’s Next?
&lt;/h3&gt;

&lt;p&gt;In Phase 3, we’ll unlock this data’s real potential, using Amazon Bedrock to power AI-based demand forecasting, running nightly on an EC2 instance and storing predictions back into our pipeline.&lt;/p&gt;

&lt;p&gt;You’ve built the rails, now it’s time to run intelligence through them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Complete Code for the Second Phase
&lt;/h2&gt;

&lt;p&gt;To view the full code for the second phase, &lt;a href="https://github.com/mohsinsheikhani/retail-ai-insights/tree/main/02-data-preparation" rel="noopener noreferrer"&gt;checkout the repository on GitHub&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;🚀 &lt;strong&gt;Follow me on &lt;a href="https://www.linkedin.com/in/mohsin-sheikhani/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; for more AWS content!&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>cloud</category>
      <category>mlops</category>
      <category>dataengineering</category>
    </item>
    <item>
      <title>A Practical Guide to MLOps on AWS: Streaming Data Ingestion with Kinesis Firehose (Phase 01)</title>
      <dc:creator>Mohsin Sheikhani</dc:creator>
      <pubDate>Sat, 07 Jun 2025 15:25:55 +0000</pubDate>
      <link>https://dev.to/mohsinsheikhani/a-practical-guide-to-mlops-on-aws-streaming-data-ingestion-with-kinesis-firehose-phase-01-1bi7</link>
      <guid>https://dev.to/mohsinsheikhani/a-practical-guide-to-mlops-on-aws-streaming-data-ingestion-with-kinesis-firehose-phase-01-1bi7</guid>
      <description>&lt;h2&gt;
  
  
  The Big Picture: Why This Project Matters
&lt;/h2&gt;

&lt;p&gt;In the age of AI-driven decisions, retail businesses are sitting on a goldmine of customer interaction data, but most are struggling to use it effectively.&lt;/p&gt;

&lt;p&gt;Imagine this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A customer browses your store but leaves without buying.&lt;/li&gt;
&lt;li&gt;You don't know what caught their eye.&lt;/li&gt;
&lt;li&gt;You don’t know what’s likely to sell tomorrow.&lt;/li&gt;
&lt;li&gt;You’re restocking based on gut feel, not data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the reality for many retailers.&lt;/p&gt;

&lt;p&gt;The goal of this project is to build a cloud-native, AI-enhanced retail analytics platform that solves two critical business problems:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What do you think we should stock next? → Predict demand using historical data and forecast which products need restocking.&lt;/li&gt;
&lt;li&gt;What should we recommend? → Use customer behavior to serve personalized product suggestions at runtime.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And we want to achieve this without maintaining complex infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: Real-Time, AI-Driven Retail Intelligence
&lt;/h2&gt;

&lt;p&gt;We'll build a modern data pipeline with these pillars:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Data Lake Architecture:&lt;/strong&gt; S3-based Bronze → Silver → Gold zones for raw, cleaned, and model-ready data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI at the Core:&lt;/strong&gt; Bedrock for forecasting, Personalize for recommendations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real-Time &amp;amp; Batch:&lt;/strong&gt; Lambda for on-demand actions, EC2 for nightly forecasting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MLOps Foundation:&lt;/strong&gt; Glue for ETL, EventBridge for orchestration, DynamoDB for fast lookup.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdq9p0itcq74iwovji8di.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdq9p0itcq74iwovji8di.png" alt="Cloud-Native AI Project on AWS for Product Forecasting and Product Recommendations" width="800" height="439"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is a full-stack, cloud-native project that covers everything from data ingestion to AI inference.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 01: Ingesting Events Like a Real-Time System
&lt;/h2&gt;

&lt;p&gt;Before anything else, we need &lt;strong&gt;data&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In this phase, we simulate a real-time ingestion pipeline by streaming historical customer interaction data into our system using Kinesis Firehose and Python.&lt;/p&gt;

&lt;h3&gt;
  
  
  What We’re Solving in Phase 1
&lt;/h3&gt;

&lt;p&gt;To build useful AI models, we first need user behavior data. This includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Product views&lt;/li&gt;
&lt;li&gt;Add-to-cart actions&lt;/li&gt;
&lt;li&gt;Purchases&lt;/li&gt;
&lt;li&gt;Timestamps and metadata&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But unlike traditional ETL pipelines, we want to simulate a real-time data flow, so that our system behaves like it would in production, even while testing locally.&lt;/p&gt;

&lt;h3&gt;
  
  
  How We’re Building It
&lt;/h3&gt;

&lt;p&gt;Here’s what we’re doing in this phase:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Stream JSON events to Kinesis Firehose, which writes them to an S3 Bronze bucket.&lt;/li&gt;
&lt;li&gt;Simultaneously write selected fields to DynamoDB to store product metadata and inventory.&lt;/li&gt;
&lt;li&gt;Set the foundation for AI-driven recommendations and forecasting later on.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F03oot0mxy3z92yinzlr8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F03oot0mxy3z92yinzlr8.png" alt="Data Ingestion with Kinesis Firehose" width="799" height="303"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Let’s Build It
&lt;/h3&gt;

&lt;p&gt;We’ll now walk through building this ingestion pipeline using AWS CDK, starting with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AWS CDK project setup&lt;/li&gt;
&lt;li&gt;Creating a DynamoDB table to store product info and initial inventory&lt;/li&gt;
&lt;li&gt;Creating the Base VPC&lt;/li&gt;
&lt;li&gt;Creating the S3 Buckets (bronze, silver, gold zones)&lt;/li&gt;
&lt;li&gt;Setting up Kinesis Firehose to deliver raw data to S3&lt;/li&gt;
&lt;li&gt;Writing Python scripts to simulate streaming events and populating DynamoDB&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 1 - Setting Up the AWS CDK Project
&lt;/h3&gt;

&lt;p&gt;To build this infrastructure in a clean, scalable way, we’re using AWS CDK (Cloud Development Kit). It allows us to define cloud infrastructure using familiar programming languages, in our case, TypeScript.&lt;/p&gt;

&lt;p&gt;We’ll organize everything using a modular folder structure that separates shared resources, analytics components, and common storage logic.&lt;/p&gt;

&lt;h3&gt;
  
  
  Initialize the CDK Project
&lt;/h3&gt;

&lt;p&gt;We start by creating and bootstrapping a CDK app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mkdir retail-ai-insights &amp;amp;&amp;amp; cd retail-ai-insights
cdk init app --language=typescript
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates a basic CDK project with boilerplate files like &lt;code&gt;cdk.json&lt;/code&gt;, &lt;code&gt;tsconfig.json&lt;/code&gt;, and a &lt;code&gt;lib/&lt;/code&gt; directory to organize stacks.&lt;/p&gt;

&lt;h4&gt;
  
  
  Organize Your Constructs
&lt;/h4&gt;

&lt;p&gt;Let’s build a modular file structure right from the start. Inside the &lt;code&gt;lib/&lt;/code&gt; directory, run the following one by one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cd lib/

# Shared Networking (VPC, Subnets, etc.)
mkdir -p constructs/shared/networking &amp;amp;&amp;amp; touch constructs/shared/networking/vpc.ts

# Analytics Pipeline (Firehose, Glue, etc.)
mkdir -p constructs/analytics &amp;amp;&amp;amp; touch constructs/analytics/firehose-stream.ts

# Common Storage (S3 Buckets, DynamoDB)
mkdir -p constructs/common/storage
touch constructs/common/storage/dynamodb-inventory.ts
touch constructs/common/storage/s3-bucket-factory.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once done, your project should look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;lib/
├── constructs/
│   ├── shared/
│   │   └── networking/
│   │       └── vpc.ts
│   ├── analytics/
│   │   └── firehose-stream.ts
│   └── common/
│       └── storage/
│           ├── dynamodb-inventory.ts
│           └── s3-bucket-factory.ts
└── retail-ai-insights-stack.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2 - Creating a DynamoDB Table for Product Inventory
&lt;/h3&gt;

&lt;p&gt;Now that our CDK project is set up, let’s provision a DynamoDB table that will store:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Product metadata (product_id, product_name, etc.)&lt;/li&gt;
&lt;li&gt;Current stock levels&lt;/li&gt;
&lt;li&gt;Forecasted demand (to be updated later)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This table will power real-time lookups during both recommendation generation and inventory management phases.&lt;/p&gt;

&lt;p&gt;Open &lt;code&gt;lib/constructs/common/storage/dynamodb-inventory.ts&lt;/code&gt; and add the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { Construct } from "constructs";

import * as dynamodb from "aws-cdk-lib/aws-dynamodb";
import * as cdk from "aws-cdk-lib";

export class DynamoDBInventory extends Construct {
  public readonly inventoryTable: dynamodb.Table;

  constructor(scope: Construct, id: string) {
    super(scope, id);

    this.inventoryTable = new dynamodb.Table(this, "RetailInventoryTable", {
      tableName: "RetailInventoryTable",
      partitionKey: { name: "product_id", type: dynamodb.AttributeType.STRING },
      billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
      encryption: dynamodb.TableEncryption.AWS_MANAGED,
      removalPolicy: cdk.RemovalPolicy.DESTROY,
    });
  }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now open &lt;code&gt;lib/retail-ai-insights-stack.ts&lt;/code&gt; and use the construct like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/**
* Retail Inventory Table
**/
const dynamoConstruct = new DynamoDBInventory(this, "DynamoDBInventory");
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To see the output, run&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cdk deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Navigate to the DynamoDB console&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1tco3fniticjt8jr6zik.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1tco3fniticjt8jr6zik.png" alt="List of DynamoDB" width="798" height="140"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This completes our DynamoDB setup, giving us a real-time-accessible source of truth for product stock, prices, and metadata.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3 - Creating the Base VPC
&lt;/h3&gt;

&lt;p&gt;We’ll keep it minimal and efficient by using:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;1 Availability Zones&lt;/li&gt;
&lt;li&gt;Public Subnets (for instance bootstrap, like downloading packages)&lt;/li&gt;
&lt;li&gt;Private Subnets with Egress (for EC2 forecasting to access Bedrock or S3 securely)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Open &lt;code&gt;lib/constructs/shared/networking/vpc.ts&lt;/code&gt; and add:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { Construct } from "constructs";
import { Vpc, SubnetType, NatProvider } from "aws-cdk-lib/aws-ec2";
import { StackProps } from "aws-cdk-lib";

export interface VpcResourceProps extends StackProps {
  maxAzs?: number;
}

export class VpcResource extends Construct {
  public readonly vpc: Vpc;

  constructor(scope: Construct, id: string, props: VpcResourceProps) {
    super(scope, id);

    this.vpc = new Vpc(this, "RetailForecastVpc", {
      vpcName: "RetailAIVPC",
      maxAzs: props.maxAzs ?? 1,
      natGatewayProvider: NatProvider.gateway(),
      natGateways: 0,
      subnetConfiguration: [
        {
          name: "PublicSubnet",
          subnetType: SubnetType.PUBLIC,
          cidrMask: 24,
        },
        {
          name: "PrivateSubnet",
          subnetType: SubnetType.PRIVATE_WITH_EGRESS,
          cidrMask: 24,
        },
      ],
    });
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In &lt;code&gt;lib/retail-ai-insights-stack.ts&lt;/code&gt;, add:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/**
* VPC Setup
**/
const { vpc } = new VpcResource(this, "RetailVpc", {});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's go run the&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cdk deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once done, navigate to the VPC console&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuzse80641jbbje0qlql4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuzse80641jbbje0qlql4.png" alt="List of VPC" width="799" height="118"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3n35doxuw9ojbge9e3we.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3n35doxuw9ojbge9e3we.png" alt="List of Subnets within a VPC" width="797" height="133"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9f6eh3vygjqo0btpkg8z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9f6eh3vygjqo0btpkg8z.png" alt="Route tables associated with a Subnet" width="798" height="143"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With this, we now have an isolated network environment to run our compute workloads with secure access to AWS services (via VPC endpoints, later on).&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4 - Creating the Storage Foundation (S3 Buckets)
&lt;/h3&gt;

&lt;p&gt;A production-grade data lake architecture often follows a multi-zone strategy to maintain a clean separation of data states:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Bronze:&lt;/strong&gt;  Raw data as ingested (e.g., event streams)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Silver:&lt;/strong&gt; Cleaned, filtered, and enriched data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gold:&lt;/strong&gt;    Aggregated or transformed data that's ready for ML/AI consumption&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In our case, we'll also include a &lt;code&gt;zone-assets&lt;/code&gt; bucket to store static datasets and job scripts that need to be referenced during ETL jobs.&lt;/p&gt;

&lt;p&gt;We’re creating multiple S3 buckets with similar configurations (like versioning, encryption, SSL-only access, and auto-deletion in dev environments), rather than duplicating logic, we’ll use a factory construct that makes it reusable and DRY.&lt;/p&gt;

&lt;p&gt;Open the file:&lt;br&gt;
&lt;code&gt;lib/constructs/common/storage/s3-bucket-factory.ts&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;And update it with the following code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { Construct } from "constructs";
import {
  Bucket,
  BlockPublicAccess,
  BucketEncryption,
} from "aws-cdk-lib/aws-s3";
import * as cdk from "aws-cdk-lib";

interface CustomS3BucketProps {
  bucketName: string;
}

export class S3BucketFactory extends Construct {
  public readonly bucket: Bucket;

  constructor(scope: Construct, id: string, props: CustomS3BucketProps) {
    super(scope, id);

    const { bucketName } = props;

    this.bucket = new Bucket(this, "S3Bucket", {
      bucketName,
      versioned: true,
      enforceSSL: true,
      autoDeleteObjects: true,
      blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
      encryption: BucketEncryption.S3_MANAGED,
      removalPolicy: cdk.RemovalPolicy.DESTROY,
    });
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now in your &lt;code&gt;retail-ai-insights-stack.ts&lt;/code&gt;, instantiate the factory construct like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/**
* Multi Zone Bucket
**/
const { bucket: bronzeBucket } = new S3BucketFactory(
  this,
  "BronzeDataLakeBucket",
  {
    bucketName: "retail-ai-bronze-zone",
  }
);

const { bucket: silverBucket } = new S3BucketFactory(
  this,
  "SilverDataLakeBucket",
  {
    bucketName: "retail-ai-silver-zone",
  }
);

const { bucket: goldBucket } = new S3BucketFactory(this, "GoldDataBucket", {
  bucketName: "retail-ai-gold-zone",
});

const { bucket: dataAssetsBucket } = new S3BucketFactory(
  this,
  "DataAssets",
  {
    bucketName: "retail-ai-zone-assets",
  }
);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Again, let's do&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cdk deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify the output by going to the S3 Bucket Console, you should see four of the buckets&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8tgvp1ythvnguae9t16a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8tgvp1ythvnguae9t16a.png" alt="General purpose S3 Buckets" width="800" height="203"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This sets up the core storage foundation that the rest of our ETL, forecasting, and recommendation workflows will depend on.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5 - Setting up Kinesis Firehose to deliver raw data to S3
&lt;/h3&gt;

&lt;p&gt;With our Bronze bucket in place, we’re now ready to stream raw event data into it using Amazon Kinesis Data Firehose. Firehose is a fully managed service for delivering real-time streaming data directly to destinations like Amazon S3.&lt;/p&gt;

&lt;p&gt;In our architecture, it enables our ingestion pipeline by capturing event streams and persisting them as raw JSON files in the Bronze zone.&lt;/p&gt;

&lt;p&gt;Oepn the file:&lt;br&gt;
&lt;code&gt;lib/constructs/analytics/firehose-stream.ts&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Add the following code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { Construct } from "constructs";

import { CfnDeliveryStream } from "aws-cdk-lib/aws-kinesisfirehose";
import { PolicyStatement, Role, ServicePrincipal } from "aws-cdk-lib/aws-iam";
import { Bucket } from "aws-cdk-lib/aws-s3";
import { LogGroup, LogStream } from "aws-cdk-lib/aws-logs";

interface FirehoseProps {
  destinationBucket: Bucket;
}

export class FirehoseToS3 extends Construct {
  public readonly deliveryStream: CfnDeliveryStream;

  constructor(scope: Construct, id: string, props: FirehoseProps) {
    super(scope, id);

    const logGroup = new LogGroup(this, "FirehoseLogGroup");
    const logStream = new LogStream(this, "FirehoseLogStream", {
      logGroup,
    });

    // IAM Role for Firehose to access S3
    const firehoseRole = new Role(this, "FirehoseRole", {
      assumedBy: new ServicePrincipal("firehose.amazonaws.com"),
    });

    props.destinationBucket.grantWrite(firehoseRole);

    firehoseRole.addToPolicy(
      new PolicyStatement({
        actions: [
          "logs:PutLogEvents",
          "logs:CreateLogGroup",
          "logs:CreateLogStream",
        ],
        resources: [logGroup.logGroupArn],
      })
    );

    // Firehose Delivery Stream
    this.deliveryStream = new CfnDeliveryStream(this, "DatasetFirehose", {
      deliveryStreamName: "firehose-to-s3",
      deliveryStreamType: "DirectPut",
      s3DestinationConfiguration: {
        bucketArn: props.destinationBucket.bucketArn,
        roleArn: firehoseRole.roleArn,
        prefix: "dataset/",
        bufferingHints: {
          intervalInSeconds: 60,
          sizeInMBs: 5,
        },
        compressionFormat: "UNCOMPRESSED",
        cloudWatchLoggingOptions: {
          enabled: true,
          logGroupName: logGroup.logGroupName,
          logStreamName: logStream.logStreamName,
        },
      },
    });
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;code&gt;lib/retail-ai-insights-stack.ts&lt;/code&gt; and wire it up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/**
* Firehose Stream
**/
new FirehoseToS3(this, "FirehoseToS3", {
  destinationBucket: bronzeBucket,
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Deploy and verify the output, this time go to the Kinesis Firehose console&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cdk deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnmum7iqthmafymz1i7zk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnmum7iqthmafymz1i7zk.png" alt="Kinesis Firehose Streams" width="798" height="125"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This completes our infrastructure deployment for our first phase. data.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6 - Simulating Real-Time Events Using Python Scripts
&lt;/h3&gt;

&lt;p&gt;Now that the infrastructure is in place, let’s simulate user activity by streaming mock sales data into our Firehose delivery stream and storing essential product metadata in DynamoDB.&lt;/p&gt;

&lt;h4&gt;
  
  
  What We’re Doing
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Sending user interaction events (like purchases, product views) into the Bronze zone via Firehose.&lt;/li&gt;
&lt;li&gt;Writing product-level information (with inventory and forecasted demand fields) directly into DynamoDB.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This gives us two distinct but complementary data flows: one for historical processing (S3), and one for operational lookups (DynamoDB).&lt;/p&gt;

&lt;h4&gt;
  
  
  Running the Simulation Scripts
&lt;/h4&gt;

&lt;p&gt;Clone the scripts directory directly into your project root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://github.com/mohsinsheikhani/retail-ai-insights/tree/main/scripts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  1. Stream to Firehose
&lt;/h4&gt;

&lt;p&gt;This script sends historical user events to Firehose in batches (simulating real-time behavior):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;python3 ./scripts/stream_to_firehose.py --stream-name firehose-to-s3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once it’s running:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Open the Kinesis Firehose Console&lt;/li&gt;
&lt;li&gt;Select the &lt;code&gt;firehose-to-s3&lt;/code&gt; stream&lt;/li&gt;
&lt;li&gt;Scroll down to the monitoring tab&lt;/li&gt;
&lt;li&gt;You’ll start to see metrics update (incoming bytes, delivery success, etc.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvtov7je2q5ttlrkpalef.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvtov7je2q5ttlrkpalef.png" alt="Firehose Stream Metrics" width="799" height="328"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then check your Bronze S3 bucket via the S3 Console:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Navigate into the bucket named &lt;code&gt;retail-ai-bronze-data&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;You should see a new folder under dataset/ containing your streamed JSON records.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frz0q15u23sjcmtt3rocm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frz0q15u23sjcmtt3rocm.png" alt="Bronze zone S3 Bucket for raw data" width="799" height="207"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  2. Populate DynamoDB
&lt;/h4&gt;

&lt;p&gt;This script writes a subset of product info to DynamoDB:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;python3 ./scripts/write_to_dynamodb.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It sends fields like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;product_id, product_name, category, price, rating
And auto-generates:&lt;/li&gt;
&lt;li&gt;current_stock (random between 30–70)&lt;/li&gt;
&lt;li&gt;forecasted_demand (initially set to 0)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once the script runs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Open DynamoDB Console&lt;/li&gt;
&lt;li&gt;Click Explore Items&lt;/li&gt;
&lt;li&gt;You’ll see all your ingested product records appear in the &lt;code&gt;RetailInventoryTable&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuib4ztdt5z8ssbh174e7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuib4ztdt5z8ssbh174e7.png" alt="List of items within a DynamoDB Table" width="800" height="486"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This completes our &lt;strong&gt;Phase 1: Ingestion Layer setup&lt;/strong&gt;, where we now have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Realistic product interaction data flowing into S3&lt;/li&gt;
&lt;li&gt;Fast-access inventory data in DynamoDB&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the next phase, we’ll build our ETL pipeline using AWS Glue to clean and enrich this data. Stay tuned!&lt;/p&gt;

&lt;h2&gt;
  
  
  Complete Code for the First Phase
&lt;/h2&gt;

&lt;p&gt;To view the full code for the first phase, &lt;a href="https://github.com/mohsinsheikhani/retail-ai-insights/tree/main/01-data-ingestion" rel="noopener noreferrer"&gt;checkout the repository on GitHub&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;🚀 &lt;strong&gt;Follow me on &lt;a href="https://www.linkedin.com/in/mohsin-sheikhani/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; for more AWS content!&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>serverless</category>
      <category>mlops</category>
      <category>dataengineering</category>
    </item>
  </channel>
</rss>
