The problem: A model is only one component of an AI application. Python still has to prepare input, retrieve evidence, call the model, validate its output, control tools, preserve state, handle failure, and return a result.
What you will learn: This capstone connects your Python skills to embeddings, RAG, vector databases, agents, tool calling, structured outputs, Pydantic, LangChain, LangGraph, and MCP. Nothing needs to remain unexplained magic: you will see what ordinary Python is doing underneath and where each abstraction begins, so you can choose frameworks deliberately.
1. Python and Machine Learning Libraries
An AI application is larger than its model. Python commonly coordinates the parts around that model:
user input
↓ validate
retrieve data or call tools
↓ construct model input
model API or local model
↓ validate output
store, evaluate, observe, and show result
Libraries hide difficult implementation details, but they do not remove the need to understand data shapes, failures, security, and evaluation. This module connects familiar Python building blocks to each box in that flow before it introduces frameworks.
You won’t train models from scratch in this course — that’s a separate, much deeper topic. But you now have exactly the Python foundation (functions, classes, NumPy, exception handling) that libraries like PyTorch and scikit-learn are built on top of. When you see PyTorch code like:
import torch
x = torch.tensor([1.0, 2.0, 3.0])
y = x * 2
This tensor is similar to a NumPy array, but it is not literally a NumPy array. It has familiar shapes and vector operations plus features such as accelerator support and automatic differentiation, which training uses to calculate how parameters should change.
🤖 The takeaway: you’re not starting from zero when you eventually open a PyTorch tutorial — the array/vector mental model transfers directly.
2. Python and Embeddings
Meaning Represented as Numbers
An embedding is a list of numbers (a vector) that represents the meaning of a piece of text — produced by a model trained so that similar meanings end up as similar (nearby) vectors.
# Calling an embedding API (conceptually — actual SDK call shown for real use)
import numpy as np
def fake_embed(text):
"""Stand-in for a real embedding API call — returns a small vector."""
# A real call looks like:
# response = client.embeddings.create(model="text-embedding-3-small", input=text)
# return response.data[0].embedding
np.random.seed(len(text)) # deterministic "fake" embedding for this demo
return np.random.rand(8)
embedding = fake_embed("Python is great for AI")
print(embedding.shape)
print(embedding)
Expected Output (deterministic given the fake seed):
(8,)
[0.29399155 0.90340192 0.30224445 0.52975037 0.19311129 0.20549821
0.98815385 0.35836585 ...]
Connect Embeddings to NumPy Vectors
Recall Module 9: an embedding is a NumPy array. “Finding relevant
documents” is computing cosine similarity between vectors — the exact
cosine_similarity() function you already wrote.
🤖 How Is This Used in AI? Every semantic search, every RAG system, every recommendation engine built on text starts here: text → embedding model → vector → stored and compared using the vector math from Module 9.
3. Python and RAG (Retrieval-Augmented Generation)
What Is It?
RAG means: retrieve relevant information first, then ask the model to answer using that retrieved information — instead of relying purely on what the model already “knows” from training.
Why Does It Exist?
LLMs have a training cutoff and can’t know about your private documents, recent events, or company-specific data. RAG bridges that gap by injecting relevant, current, specific information directly into the prompt.
Here is the visual architecture of a standard RAG pipeline, separating offline ingestion from online query retrieval:
graph TD
subgraph Offline["Data Ingestion (Offline)"]
docs[Raw Docs] --> chunk[Chunking]
chunk --> embed[Embedding Model]
embed --> vecDB[(Vector DB)]
end
subgraph Online["Query Retrieval & Generation (Online)"]
query[User Query] --> queryEmbed[Embed Query]
queryEmbed --> search[Similarity Search]
vecDB -.-> search
search --> retrieve[Top-K Chunks]
retrieve --> prompt[Augmented Prompt]
query --> prompt
prompt --> LLM[LLM Generation]
LLM --> answer[Final Answer]
end
A rough RAG pipeline, built entirely from what you already know
import numpy as np
documents = [
"Python is widely used for building AI applications.",
"RAG combines document retrieval with LLM generation.",
"Bananas are a good source of potassium.",
]
def fake_embed(text):
np.random.seed(abs(hash(text)) % (10 ** 6))
return np.random.rand(8)
def cosine_similarity(a, b):
return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
# 1. Embed all documents once (recall Module 9 + Module 4 - functions)
doc_embeddings = [(doc, fake_embed(doc)) for doc in documents]
# 2. Embed the incoming query
query = "How is Python used in AI?"
query_embedding = fake_embed(query)
# 3. Retrieve: rank documents by similarity (Module 9's dot product / Module 4's sorted+lambda)
ranked = sorted(
doc_embeddings,
key=lambda pair: cosine_similarity(query_embedding, pair[1]),
reverse=True,
)
top_docs = [doc for doc, _ in ranked[:2]]
# 4. Build the augmented prompt (Module 1's f-strings)
context = "\n".join(top_docs)
prompt = f"""Answer the question using only this context.
Context:
{context}
Question: {query}
"""
print(prompt)
# 5. Generate: send `prompt` to an LLM API (Module 11) to get the final answer
Expected Output (shape, exact ranking may vary with the fake hash-based embedding):
Answer the question using only this context.
Context:
Python is widely used for building AI applications.
RAG combines document retrieval with LLM generation.
Question: How is Python used in AI?
🧠 The Big Realization
Every step of this pipeline is a concept from an earlier module:
| RAG step | Python concept | Module |
|---|---|---|
| Store documents | Lists / dicts | 2 |
| Embed text | NumPy arrays | 9 |
| Rank by relevance | sorted() + lambda + dot product | 4, 9 |
| Build final prompt | f-strings | 1 |
| Call the model | HTTP / SDK call | 11 |
| Handle failures | try/except, retries | 6, 11 |
RAG isn’t a mysterious new discipline — it’s a specific arrangement of everything you already know.
4. Python and Vector Databases
What Is It?
A vector database is a specialized system for storing millions (or billions) of embeddings and finding the closest matches to a query extremely fast — far faster than the manual “compare every document” loop above, which becomes impractically slow at scale.
# Conceptual shape of working with a vector DB client (e.g. Pinecone, Chroma, Weaviate)
# results = vector_db.query(vector=query_embedding, top_k=3)
# for match in results:
# print(match["score"], match["metadata"]["text"])
🧠 Intuition: Recall Module 3’s nested-loop query-matching example —
comparing every query against every document with plain Python. A vector
database does conceptually the same thing (find nearest vectors), but
with specialized indexing (like HNSW or IVF — beyond this course’s scope)
that makes it fast at massive scale, instead of the O(N × M) slowdown
nested loops hit.
🤖 How Is This Used in AI? The documents list in our mini-RAG example
above would, in a real system, be millions of chunks stored in a vector
database — with .query() replacing our manual sorted(...) ranking
step.
5. Python and AI Agents
What Is It?
An AI agent is a system where an LLM doesn’t just answer once — it decides what to do next, potentially calling tools, checking results, and looping until it has a satisfactory final answer.
Here is the visual cycle of a ReAct (Reasoning + Action) agent loop, running iteratively until a final answer is resolved:
graph TD
Start([Receive User Goal]) --> Thought["Thought: Analyze state & history"]
Thought --> Action["Action: Decide next step (call tool / respond)"]
Action --> Call{Is it a tool call?}
Call -->|Yes: Tool Call| Exec[Execute Tool]
Exec --> Observation[Observation: Gather tool output]
Observation --> Thought
Call -->|No: Final Answer| Done([Return Answer to User])
The core agent loop, built from what you know
def run_agent(question, tools, max_steps=3):
history = [{"role": "user", "content": question}] # Module 2: list of dicts
for step in range(max_steps): # Module 3: for loop
# Ask the model what to do next (simplified — normally an actual LLM call)
decision = decide_next_action(question, step) # Module 4: function
if decision["action"] == "final_answer": # Module 3: if/elif
return decision["text"]
elif decision["action"] == "use_tool":
tool_name = decision["tool"]
if tool_name in tools:
try: # Module 6: exceptions
result = tools[tool_name](decision["input"])
history.append({"role": "tool", "content": result})
except Exception as e:
history.append({"role": "tool", "content": f"Error: {e}"})
return "Agent reached max steps without a final answer."
def decide_next_action(question, step):
"""Stand-in for a real LLM call that decides the agent's next move."""
if step == 0:
return {"action": "use_tool", "tool": "search", "input": question}
return {"action": "final_answer", "text": f"Final answer for: {question}"}
tools = {
"search": lambda q: f"search results for '{q}'",
}
print(run_agent("What is retrieval-augmented generation?", tools))
Expected Output:
Final answer for: What is retrieval-augmented generation?
🧠 The Big Realization
This loop is an agent, at its conceptual core: a while/for loop
(Module 3), a growing list of messages (Module 2), functions representing
tools (Module 4), try/except around tool calls that might fail
(Module 6). Real agent frameworks add a lot of polish — but the skeleton
is exactly this.
6. Python and Tool Calling
What Is It?
Tool calling (also called “function calling”) is how an LLM API lets the model itself decide which function to call and with what arguments — your code executes the actual function, then sends the result back.
# The shape of a tool definition sent to an LLM API
tool_definition = {
"name": "get_weather",
"description": "Get the current weather for a location",
"input_schema": {
"type": "object",
"properties": {
"location": {"type": "string", "description": "City name"}
},
"required": ["location"],
},
}
def get_weather(location: str) -> str:
return f"The weather in {location} is sunny, 22°C."
# Conceptual flow:
# 1. Send `tool_definition` + the user's question to the model
# 2. Model responds: "call get_weather with location='Paris'"
# 3. Your code actually runs: get_weather("Paris")
# 4. Send the result back to the model for a final natural-language answer
tool_call_from_model = {"name": "get_weather", "input": {"location": "Paris"}}
if tool_call_from_model["name"] == "get_weather":
result = get_weather(**tool_call_from_model["input"]) # Module 4: **kwargs unpacking!
print(result)
Expected Output:
The weather in Paris is sunny, 22°C.
🧠 Notice get_weather(**tool_call_from_model["input"]) — this is exactly
Module 4’s **kwargs unpacking, used to call a real Python function using
arguments the model itself decided on.
7. Python and Structured Outputs
What Is It?
Instead of letting an LLM return free-form text, you ask it to return data in a specific, guaranteed shape — a JSON object matching a schema you define — so your code can reliably use the result without fragile text-parsing.
# Without structured output — fragile, must parse free text
raw_response = "The sentiment is positive, with a confidence of about 85%."
# Parsing this reliably in code is error-prone and brittle.
# With structured output — the model returns exactly this shape:
structured_response = {
"sentiment": "positive",
"confidence": 0.85,
}
print(structured_response["sentiment"], structured_response["confidence"])
Expected Output:
positive 0.85
🤖 How Is This Used in AI? Structured outputs are essential the moment your code needs to act on a model’s response programmatically (routing, storing in a database, triggering another function) rather than just displaying text to a human.
8. Python and Pydantic
What Problem Pydantic Solves
An LLM can return JSON that’s shaped correctly but contains invalid
values — a confidence of "very high" instead of a number, or a missing
required field. Pydantic validates that structured output actually matches
what your code expects, before your application trusts it.
from pydantic import BaseModel, ValidationError
from typing import Optional
class SentimentResult(BaseModel):
sentiment: str
confidence: float
flagged: Optional[bool] = None
# Valid data — works fine
valid_data = {"sentiment": "positive", "confidence": 0.85}
result = SentimentResult(**valid_data)
print(result)
# Real API response validation from a raw JSON string
json_string = '{"sentiment": "positive", "confidence": 0.85}'
result_from_string = SentimentResult.model_validate_json(json_string)
print(f"Parsed text string successfully: {result_from_string.sentiment}")
# Invalid data from a misbehaving model — Pydantic catches it immediately
invalid_data = {"sentiment": "positive", "confidence": "very high"}
try:
SentimentResult(**invalid_data)
except ValidationError as e:
print("Validation failed:", e.errors()[0]["msg"])
Expected Output:
sentiment='positive' confidence=0.85 flagged=None
Parsed text string successfully: positive
Validation failed: Input should be a valid number, unable to parse string as a number
🧠 Intuition
Recall Module 10’s @dataclass — Pydantic’s BaseModel is that same idea,
plus automatic validation. A dataclass trusts you; a Pydantic model
checks you.
The realistic flow
LLM
↓
Structured JSON (the model's raw output)
↓
Pydantic model (SentimentResult(**json_data))
↓
Validated Python object (guaranteed correct types)
↓
Application logic (safe to use without extra checks)
🤖 Why this is much safer than blindly trusting LLM output: Models occasionally produce malformed or unexpected output — a missing field, a wrong type, an out-of-range value. Pydantic turns “hope the model got it right” into “guaranteed correct, or a clear error you can catch and handle” (recall Module 6’s exception handling, applied here to validation errors specifically).
9. Python and LangChain
Now that you’ve built a rough retriever, a rough agent loop, and rough tool-calling by hand, LangChain will look familiar rather than magical.
# Conceptual LangChain shape — NOT executable without installation/setup
# from langchain_anthropic import ChatAnthropic
# from langchain_core.prompts import ChatPromptTemplate
#
# model = ChatAnthropic(model="claude-sonnet-4-6")
# prompt = ChatPromptTemplate.from_template("Answer using context: {context}\n\n{question}")
# chain = prompt | model
# response = chain.invoke({"context": "...", "question": "..."})
🧠 What LangChain actually is, underneath: classes wrapping API calls
(your LLMClient from Module 5), functions chained together
(your pipeline functions from Module 4), and structured data flowing
between them (your dicts and Pydantic models). LangChain organizes and
standardizes these patterns across many providers and use cases — it
doesn’t invent new concepts you haven’t already touched.
This is exactly the promised connection from earlier in the course: “This is exactly the kind of Python code that frameworks such as LangChain/LangGraph help organize.”
10. Python and LangGraph
Where LangChain often chains steps linearly, LangGraph models an
application as a graph of steps, with explicit branching, loops, and
state — a more structured version of the run_agent() loop you wrote by
hand in Section 5.
# Conceptual shape — NOT executable without installation/setup
# from langgraph.graph import StateGraph
#
# graph = StateGraph(AgentState)
# graph.add_node("retrieve", retrieve_documents)
# graph.add_node("generate", generate_answer)
# graph.add_edge("retrieve", "generate")
🧠 Intuition: Your run_agent() loop’s if/elif branching
(Module 3) and growing history list (Module 2) are a graph of steps,
just written by hand instead of declared explicitly as nodes and edges.
LangGraph makes that structure visible and manageable as agent logic grows
more complex than a simple loop can cleanly express.
11. Python and MCP
MCP (Model Context Protocol) standardizes how an AI application connects to external tools and data sources — instead of every app writing custom integration code for every tool, MCP defines a common protocol both sides agree to speak.
graph LR
subgraph Host Application ["Host Application (e.g. Claude Desktop, Cursor)"]
Client[MCP Client]
end
subgraph Protocol ["Protocol (JSON-RPC)"]
direction LR
Client <-->|Standard Messages| Server[MCP Server]
end
subgraph External Tools ["External Resources"]
Server <--> Filesystem[(Local Filesystem)]
Server <--> Database[(Postgres / sqlite)]
Server <--> Search[Web Search API]
end
🧠 Intuition: Recall Module 5’s BaseTool class with a shared run()
interface, and Module 11’s HTTP request/response mechanics. MCP is
essentially that same idea — a shared, standardized interface for
tools — applied consistently across the whole AI ecosystem, so a tool
built for one AI application can work with any MCP-compatible client
without custom glue code. Instead of writing custom API connection clients
for GitHub, Slack, and your local database, you run standard MCP servers
that the host application’s client queries natively using JSON-RPC requests.
12. Putting Everything Together
The full picture, one more time, now completely understood
User
↓
Python application (Modules 1–5: variables, functions, classes)
↓
Environment variables (Module 8: .env, secure API keys)
↓
API call (Module 11: HTTP, headers, JSON body)
↓
JSON response (Module 7: JSON ↔ dict)
↓
Pydantic validation (Module 14: BaseModel, guaranteed-safe data)
↓
Data processing (Modules 2, 4, 9: collections, functions, NumPy/Pandas)
↓
Async operations (Module 13: concurrent calls, if there are many)
↓
Logging (Module 12: what happened, safely recorded)
↓
AI/LLM (the actual model, called via your Python code)
↓
Final response
Where each concept lives in this flow
- Variables & data types (1) — configuration: model name, temperature, thresholds.
- Collections (2) — documents, chat history, search results as lists/dicts.
- Control flow (3) — deciding relevance, routing agent actions, retrying.
- Functions (4) — every discrete pipeline step: clean, embed, search, generate.
- OOP (5) — model wrapper classes, tool classes, an
AgentorRAGSystemcomposed of parts. - Exceptions (6) — surviving API failures, invalid input, timeouts.
- Files & JSON (7) — loading documents, saving results, the literal shape of every API request/response.
- Modules & environments (8) — organizing code, securely holding API keys.
- NumPy & Pandas (9) — embeddings, similarity math, evaluation datasets.
- Advanced Python (10) — generators streaming tokens, decorators wrapping retries, dataclasses/Pydantic-adjacent structure.
- APIs (11) — the actual mechanism of calling any AI model at all.
- Logging (12) — the only record of what a production AI service did.
- Async (13) — running many slow AI/tool calls concurrently instead of one at a time.
- This module (14) — the AI-specific concepts (RAG, agents, tool calling, Pydantic validation) that everything above assembles into.
Start with the Smallest Useful Abstraction
For one model call, an official SDK plus an ordinary Python function is often the clearest design. A framework becomes valuable when the application truly needs reusable retrieval chains, durable agent state, branching workflows, or observability. Frameworks change quickly, so isolate provider-specific code and pin tested versions.
MCP is a protocol that lets an AI application discover and invoke tools or read resources through a standard interface. It is not a model, database, or safety system by itself.
Model Output Is Untrusted Input
A model can suggest a tool name and arguments, but your Python application owns the real decision:
model proposes tool call
↓
validate shape and allowed values
↓
check user permission and business rules
↓
ask for approval if the action is sensitive
↓
execute with least privilege → record result
Structured output can guarantee a shape such as {city: string} when the
provider supports strict schemas. It cannot guarantee that the city is real,
that a factual answer is correct, or that an action is authorized. Validate
meaning as well as structure, and never place raw model-generated text into a
shell command or database query.
Module Summary
You’ve now seen exactly how embeddings, RAG, vector databases, agents, tool calling, structured outputs, and Pydantic validation are built from Python concepts you already deeply understand — and how LangChain, LangGraph, and MCP organize those same patterns at framework scale rather than inventing anything fundamentally new.
AI Connection
This entire module is the AI connection — every concept here is a direct, traceable assembly of the thirteen modules before it. That’s the whole point of this course: not to memorize “AI terms,” but to be able to look at RAG code, agent code, or a LangChain snippet and genuinely understand what it’s doing and why, because you’ve already built rough versions of all of it yourself.
Mini Practice
- Extend the mini-RAG pipeline in Section 3 to return the top 3 documents instead of 2, and print each with its similarity score.
- Define a Pydantic model
ToolCallResultwithtool_name: str,success: bool, andoutput: Optional[str], and validate one correct and one intentionally invalid example. - Extend the
run_agent()function in Section 5 to log (Module 12) each step it takes, using an appropriate logging level. - Rewrite the agent loop’s tool-calling step to run two independent tools
concurrently using
asyncio.gather(Module 13). - In your own words, explain to someone who has never coded before: what is RAG, why does it need Python, and which three Python concepts from this course does it lean on the most?
You’ve gone from print("hello world") to building — by hand, using
nothing but the Python you now understand — a rough retriever, a rough
agent loop, tool calling, and validated structured output. That is
genuinely most of what real AI application code is doing underneath its
frameworks.
Two practical modules remain, rounding out what a real AI codebase needs beyond the model-facing logic itself:
Next: Module 15 — Unit Testing and Mock Testing — testing this kind of AI code reliably, without hitting a live API on every test run.
- Author
- TechByteByByte Editorial Team
- Reviewed by
- TechByteByByte Admin
- Published
- Last reviewed