Everything this course built, in one script
This is the last lesson in the course, and it doesn't introduce a new concept so much as wire together concepts from all three tiers:
| From | Used here as |
|---|---|
Lesson 3, Settings | Global LLM/embedding config, set once |
Lessons 4-5, VectorStoreIndex + QueryEngine | Built twice, once per document set |
Lesson 10, output_cls | Applied at the agent level, not just one query engine |
| Lesson 14, tools | QueryEngineTool.from_defaults(), wrapping a query engine as a callable tool |
| Lessons 15-17, agents | FunctionAgent, reasoning over which tool(s) a question needs |
Two document sets, one agent that picks
The Nimbus policy documents (vacation, remote work, expense) and the Nimbus engineering documentation (engineering handbook + Cobalt-1 product FAQ, reused from 02_intermediate/13_multi_document_indexes/data/) are genuinely different domains, HR questions and engineering questions shouldn't search the same index. Each gets its own small VectorStoreIndex, wrapped as its own QueryEngineTool with a description telling the agent what it's for:
policy_tool = build_query_engine_tool(POLICY_DATA_DIR, name="hr_policies", description="...")engineering_tool = build_query_engine_tool(ENGINEERING_DATA_DIR, name="engineering_docs", description="...")FunctionAgent receives both tools and decides, per question, whether it needs one, the other, or both, the same tool-selection reasoning Lesson 14's predict_and_call() did for a single tool, now scaled to multiple tools and (if needed) multiple calls in sequence.
Structured output at the agent level
Lesson 10 showed output_cls on a single query engine, coercing one synthesized answer into a Pydantic model. FunctionAgent accepts the same output_cls keyword, applied to its *final* answer, after however many tool calls it took to get there:
class NimbusAnswer(BaseModel): answer: str document_sets_used: list[str] needs_human_followup: bool
agent = FunctionAgent(tools=[...], llm=Settings.llm, output_cls=NimbusAnswer, ...)document_sets_used and needs_human_followup aren't things a plain text answer would reliably self-report, having the schema force those fields is what makes them dependable to consume downstream (log which tools fired, route flagged answers to a human) rather than something you'd have to regex out of prose.
The code, piece by piece
result = await agent.run(question)FunctionAgent.run() is async (it's a Workflow under the hood, Lesson 19's mechanics), so this lesson wraps it in a small run_all() coroutine and calls asyncio.run() once at the bottom, rather than sprinkling await through a sync main().
parsed: NimbusAnswer = result.get_pydantic_model(NimbusAnswer)agent.run() returns an AgentOutput event, not the Pydantic model directly. .get_pydantic_model(NimbusAnswer) validates AgentOutput.structured_response against the model class and returns a real NimbusAnswer instance, or None with a warning if validation failed, the same "coerced and validated, not just text" guarantee Lesson 10 established.
def build_query_engine_tool(data_dir, name, description) -> QueryEngineTool: ... return QueryEngineTool.from_defaults(query_engine=query_engine, name=name, description=description)Factored into a function since this lesson needs the same build-index-then-wrap-as-tool steps twice, once per document set, identical to what Lesson 14 did for a single FunctionTool, just built from a QueryEngine instead of a plain Python function.
Checkpoint
QueryEngineTool.from_defaults(): wraps aQueryEngineas a tool, same idea as Lesson 14'sFunctionTool, built from an index instead of a plain function.FunctionAgent(tools=[...], output_cls=...): an agent that reasons over multiple tools across multiple document sets and returns a validated Pydantic object as its final answer, combining Lessons 10, 14, and 15-17 into one agent.agent.run()returns anAgentOutput; call.get_pydantic_model(YourModel)to get the validated structured result out of it.- Multi-document agentic RAG means giving an agent several narrowly-scoped retrieval tools and trusting it to pick the right one(s), rather than dumping every document into one big index and hoping retrieval alone sorts it out.
This is the last lesson in this course. Across 24 lessons you went from "what is a Document" to a multi-tool agent that searches across several document sets and hands back validated, structured answers, the same ingest -> index -> query loop from Lesson 1, now with evaluation, custom workflows, reranking, persistence, observability, and interoperability with the wider agent ecosystem layered on top. Congratulations on finishing the course.