Task 0 · 8 tasks
Setup check
Clone the repo, connect to AWS, and confirm your database copy and model access before Alice's first request lands.
The “Getting Started” challenge
Welcome to DataStream Corp, a 1,200-person tech company whose CEO, Alice Chen, wants an AI assistant that actually knows the business. You are the developer she hired to build it.
Before you write a line of agent code, make sure the plumbing works: the code, your AWS identity, the company database and a route to a model.
Build it
Clone the repository
terminal · macOS / Linux / Windowsgit clone --branch bootcamp-participant --single-branch https://github.com/Opsfleet/agentic-workshop-aws.git datastream-bootcamp cd datastream-bootcampInstall the tools
You need uv, Terraform (Phase 2 drives it for you; you never edit it) and the AWS CLI v2.
terminal · macOS / Linuxcurl -LsSf https://astral.sh/uv/install.sh | sh brew tap hashicorp/tap brew install hashicorp/tap/terraform awscli # Terraform >= 1.9 uv --version && terraform -version && aws --versionterminal · Windows PowerShellpowershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" winget install Hashicorp.Terraform Amazon.AWSCLI uv --version; terraform -version; aws --versionConnect to AWS with SSO
Your instructor gives you an SSO start URL and a region. Create a named profile, log in and check who you are:
terminal · macOS / Linux / Windowsaws configure sso # paste the start URL and region; name the profile, e.g. bootcamp aws sso login --profile bootcamp aws sts get-caller-identity --profile bootcampThe last command prints your SSO identity. SSO sessions expire after some hours; just run
aws sso login --profile bootcampagain.Sync the project and create your .env
terminal · macOS / Linuxuv sync cp .env.example .envterminal · Windows PowerShelluv sync Copy-Item .env.example .envFill in
PARTICIPANTwith the name your instructor gave you (it starts blank on purpose) andAWS_PROFILE=bootcamp, the profile you just created.AWS_PROFILEis required when you use SSO; leave it blank only if your credentials come from environment variables.BOOTCAMP_VERBOSE=1(or--verboseanywhere on a command) shows full terraform output and debug logs in Phase 2..env (copied from .env.example)# Your bootcamp name, exactly as your instructor registered it (lowercase letters/digits, e.g. jdoe). Required. PARTICIPANT= # Your SSO profile, e.g. bootcamp (the name the guide uses for `aws configure sso`). Required for SSO users; # leave blank only when your credentials come from environment variables. AWS_PROFILE= # Bootcamp region (wins over any AWS_REGION exported in your shell). BOOTCAMP_REGION=us-east-1 MODEL_ID=gpt-6-luna # Optional: override the role to assume (default bootcamp-participant-<PARTICIPANT>); "none" = use AWS_PROFILE as-is. BOOTCAMP_ROLE_ARN= # Optional agent capabilities (need stage >= 4; apply with `uv run bootcamp.py deploy`): # run_python tool on the AWS-managed AgentCore Code Interpreter, browse_web tool on the AWS-managed AgentCore Browser. ENABLE_CODE_INTERPRETER=false ENABLE_BROWSER=false # Bedrock Guardrail on every agent model call (prompt-attack filter, email/phone masking) + tool-output screening. ENABLE_GUARDRAILS=false # Optional: 1 = full terraform output and debug logs (same as --verbose). BOOTCAMP_VERBOSE=0From here on, every
bootcamp.pycommand starts from your SSO profile and automatically assumes the rolebootcamp-participant-<name>. That role is what the platform recognises you by. Credentials refresh automatically, so long chats don't hit a one-hour limit, andaws loginprofiles work too. If something is wrong, the CLI prints a one-line fix, for exampleaws sso login --profile bootcampwhen your SSO session has expired.Meet your model gateway
Every model call in this bootcamp goes through the Opsfleet LiteLLM gateway. You never call Bedrock directly: your role can't. And there is no API key: each request carries a presigned AWS identity, and the gateway maps your role to your personal budget (default $1), which is plenty for the cheap default model: all of Phase 1 usually costs $0.10-0.25 and all of Phase 2 about $0.05 on
gpt-6-luna.terminaluv run bootcamp.py llmIf this prints
LITELLM_BASE_URL,MODELS, yourMODEL_IDand yourBUDGET(spent of max), the role assumption and keyless identity both work. If your budget runs out, model calls fail with HTTP 429budget_exceeded; ask your instructor for a top-up.Model When to use it gpt-6-lunaDefault. OpenAI GPT-6 Luna on Bedrock: small, fast, cheap. claude-sonnet-5-5Set MODEL_IDin.envwhen routing or reasoning gets hard. Costs more of your budget.claude-haiku-5-5Anthropic's small, fast model: an alternative to gpt-6-lunafor quick, cheap calls.Run the setup check
terminaluv run bootcamp.py phase1 t0It downloads your own copy of the DataStream SQLite database into
phase1/, counts employees, checks your AWS identity, builds a test agent and makes a real model call through the gateway.
Check your work
Phase 1 has no automated test: you check it by running the task and looking for the result below.
Every line starts with PASS, including LiteLLM gateway, ending with Setup complete, ready for Task 1. You should see roughly 1,200 employees and departments such as Engineering, Sales, Marketing, HR, Finance and Operations.
uv run bootcamp.py phase1 t0Under the hood
bootcamp.py phase1 t0 runs phase1/t0_setup.py with your .env loaded and your participant role assumed. The shared helpers live in phase1/common.py; the one that matters most is the model factory:
MODEL_ID = os.getenv("MODEL_ID", "gpt-6-luna")
def make_model() -> OpenAIModel:
"""Every task talks to the model through the Opsfleet LiteLLM gateway (keyless, budgeted per participant)."""
return litellm_gateway.make_model(MODEL_ID)The heavy lifting is in shared/litellm_gateway.py, which Phase 2 reuses unchanged:
class StsIdentityAuth(httpx.Auth):
"""Adds a fresh presigned STS identity URL to each request (cached until shortly before it expires).
Short-lived (2 min) because the URL is a bearer credential for your LLM budget until it expires.
The URL must be signed for GET: STS is a query-protocol API, so boto3 presigns for POST by default and the
gateway's GET replay would fail with SignatureDoesNotMatch.
"""
def __init__(self, session: boto3.Session | None = None, region: str | None = None) -> None:
self._session = session or boto3.Session()
self._region = region or self._session.region_name or os.getenv("AWS_REGION", "us-east-1")
self._url = ""
self._expires_at = 0.0
def identity_url(self) -> str:
if time.time() > self._expires_at - REFRESH_MARGIN_SECONDS:
sts = self._session.client("sts", region_name=self._region)
self._url = sts.generate_presigned_url("get_caller_identity", ExpiresIn=PRESIGN_SECONDS, HttpMethod="GET")
self._expires_at = time.time() + PRESIGN_SECONDS
return self._url
def auth_flow(self, request: httpx.Request):
request.headers[IDENTITY_HEADER] = self.identity_url()
yield request
class GatewayModel(OpenAIModel):
"""Strands OpenAIModel bound to the gateway.
Strands opens (and closes) a fresh OpenAI client per request from `_resolve_client_args()`, so the keyless
httpx client must be created per request too; the STS signer (and its cached URL) is shared.
"""
def __init__(self, base_url: str, api_key: str | None = None, session: boto3.Session | None = None, **config):
super().__init__(
client_args={"base_url": f"{base_url.rstrip('/')}/v1", "api_key": api_key or KEYLESS_PLACEHOLDER}, **config
)
self._auth = None if api_key else StsIdentityAuth(session)
def _resolve_client_args(self) -> dict:
args = dict(self.client_args)
if self._auth:
args["http_client"] = httpx.AsyncClient(auth=self._auth, timeout=600)
return args
async def stream(self, *args: Any, **kwargs: Any) -> AsyncGenerator[StreamEvent]:
"""Stream like OpenAIModel, but raise `EmptyModelResponseError` if the reply carried no output at all."""
produced = False
async for event in super().stream(*args, **kwargs):
produced = produced or has_output(event)
yield event
if not produced:
raise EmptyModelResponseError("the model returned an empty response (no text, tool call or tokens)")
def make_model(model_id: str | None = None, base_url: str | None = None, **params) -> OpenAIModel:
"""Strands model routed through the gateway; reads MODEL_ID / LITELLM_BASE_URL / LITELLM_API_KEY from env."""
return GatewayModel(
base_url or os.environ["LITELLM_BASE_URL"],
os.getenv("LITELLM_API_KEY"),
model_id=model_id or os.getenv("MODEL_ID", "gpt-6-luna"),
params=params or None,
)Strands' OpenAIModel speaks the OpenAI chat-completions API, which LiteLLM exposes and translates to Bedrock. GatewayModel is a thin subclass: instead of a key, its StsIdentityAuth hook adds the X-Amz-Sts-Identity-Url header, a presigned STS GetCallerIdentity URL signed with your own credentials. The gateway replays it, learns you are bootcamp-participant-alice, and bills alice's budget. LITELLM_BASE_URL is filled in by the CLI; LITELLM_API_KEY is optional, only for tools that can't do keyless auth.