Task 6 · 8 tasks
Structured output
Make the agent return a typed Pydantic DepartmentReport instead of a wall of text.
The “Messy Handwriting” problem
Alice asked for a department report and got three paragraphs of prose. Her dashboard needs JSON with a known shape. Time to give the agent a contract.
Build it
Return a typed report
Challenge
Define a
DepartmentReportwith a title, a summary, a list ofDepartmententries (name and active employees) and an integertotal_employees, reject a report whose total is not the sum of its departments, and make the agent return one.Work in
phase1/starter/t6_structured_output.py(look forTODO; an unfinished one prints[starter] TODO …) and run it withuv run bootcamp.py phase1 t6 --starter. The reference solution isphase1/t6_structured_output.py;uv run bootcamp.py phase1 t6runs it.Hint 1
Strands can validate the final answer against a Pydantic model. See Strands: structured output.
Hint 2
Pass
structured_output_model=DepartmentReportwhen you call the agent and readresult.structured_output.Field(description=...)tells the model what each field means (say “every department, including those with no active employees”).Hint 3
A Pydantic
@model_validator(mode="after")can check fields against each other; raiseValueErrorwith a clear message. Strands sends that message back to the model as the tool's error, and the model tries again.Solution
Paste each block below into
phase1/starter/t6_structured_output.pyin place of the definition with the same name (the TODO), including any import line that comes with it. Nothing else in the file changes.phase1/starter/t6_structured_output.py · replace Department, DepartmentReport, make_responder()class Department(BaseModel): """One department row of the report.""" name: str = Field(description="Department name, as in the departments table") active_employees: int = Field(description="Employees with status = 'active' in this department (0 if none)") class DepartmentReport(BaseModel): """Department report for DataStream Corp.""" title: str = Field(description="Report title") summary: str = Field(description="Executive summary: only facts from the query results, no reasons or plans") departments: list[Department] = Field( description="Every department in the departments table, including those with no active employees" ) total_employees: int = Field(description="Active employees in the whole company") @model_validator(mode="after") def total_matches_departments(self) -> "DepartmentReport": """Reject a total that is not the sum of the departments; Strands sends the error back so the model retries.""" counted = sum(department.active_employees for department in self.departments) if counted != self.total_employees: raise ValueError(f"total_employees is {self.total_employees}, but the departments add up to {counted}") return self def make_responder(agent: Agent): def respond(prompt: str) -> DepartmentReport: report = agent(prompt, structured_output_model=DepartmentReport).structured_output print(report.model_dump_json(indent=2)) return report return respondRun it
terminal · your starter fileuv run bootcamp.py phase1 t6 --starter "Generate a department report for DataStream Corp"terminal · reference solutionuv run bootcamp.py phase1 t6 "Generate a department report for DataStream Corp"Experiments
- Watch the validator reject a bad report without spending a token:In a real run the same message comes back asterminal
uv run python -c "import sys; sys.path.insert(0, 'phase1'); from t6_structured_output import DepartmentReport; DepartmentReport(title='t', summary='s', total_employees=100, departments=[{'name': 'HR', 'active_employees': 84}])"[tool] DepartmentReport error: ... total_employees is ..., but the departments add up to ..., followed by a second[tool] DepartmentReportcall with the fixed report. - Add a
budgettoDepartment. Does the agent run another query to fill it? - Add a field the data can't support, like
customer_satisfaction. What does the model put there? WouldOptionalplus a clear description help?
- Watch the validator reject a bad report without spending a token:
Check your work
Phase 1 has no automated test: you check it by running the task and looking for the result below.
The [tool] query_db lines count status = 'active', then a [tool] DepartmentReport line hands over the report. The printed JSON lists all seven departments with their active employees (Engineering 481, Sales 360, Marketing 180, HR 84, Finance 60, Operations 36 and Research 0: 1,201 in total), and total_employees is 1201, the sum of the department counts. A missing Research row, 483 for Engineering or a total of 1207 means the agent counted the wrong rows: the report validates but is wrong.
Failed? Run the same prompt on the reference (drop --starter). If the reference fails too, it's the model: rerun or try another MODEL_ID (uv run bootcamp.py llm models).
Under the hood
Strands turns the Pydantic model into a tool schema and asks the model to call it as its final step, then validates the arguments with Pydantic, including your model_validator. If validation fails, the error is fed back as the tool result so the model can retry. You get a real Python object, not text you have to parse.