[7' read]
Build a Quickstart Generator with Connectors and Web Search
You want to learn Polars, the fast DataFrame library for Python. You could read the docs, skim blog posts, and piece together a quickstart yourself — or you could let Mistral do it for you, with access to the right tools.
This notebook sends the same prompt four times, each time with a different tool configuration, so you can see how output quality improves as you give the model better sources:
| Step | Tools | What the model can access |
|---|---|---|
| 1 | None | Training data only |
| 2 | Web search | Blog posts, Stack Overflow, release notes |
| 3 | Context7 connector | Official Polars documentation |
| 4 | Both | Docs + web — the model picks the best source per sub-topic |
| 5 | Filtered connector | A single doc-retrieval tool (skip the resolver) |
API status: This notebook uses
client.beta.connectorsandclient.beta.conversations. These are beta endpoints and may change. See the Connectors documentation for the latest API reference.
Run cells top-to-bottom. A TypeScript version of the Conversations API is covered in the reference cookbook.
Prerequisites#
To complete this notebook, you will need:
- Python 3.9 or later
- A Mistral account and API key
Environment setup#
Install the Mistral Python SDK by running the cell below.
To complete this cookbook, you'll need a Mistral API key. In Studio, navigate to the API keys section, choose Private and shared connectors for Connector access scope and create a new API key.
Set it before running the client cell using one of these options:
Option 1 — environment variable (recommended for local use):
MISTRAL_API_KEY=your-mistral-api-keyOption 2 — enter it when prompted: if MISTRAL_API_KEY is not already set in your environment, the next code cell will display a secure input field where you can paste your key directly.
%pip install mistralai --quietImport the SDK and create the client. If MISTRAL_API_KEY is not set as an environment variable, a secure input prompt will appear.
import getpass
import os
from mistralai.client import Mistral
if not os.environ.get("MISTRAL_API_KEY"):
os.environ["MISTRAL_API_KEY"] = getpass.getpass("Mistral API key: ")
client = Mistral(api_key=os.environ["MISTRAL_API_KEY"])Step 1 — Baseline: no tools#
We start by asking the model to generate a Polars quickstart with no tools at all. The model can only draw on its training data, so anything that changed after the knowledge cutoff will be missing or wrong.
We define PROMPT once and reuse it for steps 1 through 4 so the comparison is fair.
PROMPT = (
"Write a Polars quickstart for a developer who knows pandas. "
"Cover: installation, reading a CSV, filtering rows, groupby aggregation, "
"and lazy evaluation. End with 3 gotchas when migrating from pandas. "
"Include runnable code examples."
)
print("--- Step 1: Baseline (no tools) ---\n")
response = await client.beta.conversations.start_async(
model="mistral-medium-latest",
inputs=[{"role": "user", "content": PROMPT}],
)
for output in response.outputs:
if getattr(output, "type", None) == "message.output":
content = output.content
if isinstance(content, str):
print(content)
else:
print("".join(getattr(c, "text", str(c)) for c in content))Step 2 — Web search#
Adding web_search as a built-in tool lets the model pull in current information — recent blog posts, Stack Overflow answers, and release notes. No connector setup required.
Compare the output with Step 1: you should see more up-to-date syntax and community tips.
print("--- Step 2: Web search ---\n")
response = await client.beta.conversations.start_async(
model="mistral-medium-latest",
inputs=[{"role": "user", "content": PROMPT}],
tools=[{"type": "web_search"}],
)
for output in response.outputs:
if getattr(output, "type", None) == "message.output":
content = output.content
if isinstance(content, str):
print(content)
else:
print("".join(getattr(c, "text", str(c)) for c in content))Step 3 — Context7 connector: official docs#
Context7 is an MCP server that serves up-to-date documentation for popular open-source libraries. It requires no authentication, making it a good first connector.
This step has three parts:
- Create the connector pointing at Context7's MCP endpoint
- Register credentials (empty, since Context7 is public — but the record must exist)
- List tools to see what the connector exposes (we will use these names in Step 5)
CONTEXT7_URL = "https://mcp.context7.com/mcp"
connector = await client.beta.connectors.create_async(
name="quickstart_context7",
description="Context7 connector — library documentation lookup",
server=CONTEXT7_URL,
visibility="private",
)
print(f"Created: {connector.name} (id={connector.id})")
await client.beta.connectors.create_or_update_user_credentials_async(
connector_id_or_name=connector.name,
name=f"{connector.name}-default",
credentials={"headers": {}},
is_default=True,
)
print(f"Credentials registered for {connector.name}")
tools_list = await client.beta.connectors.list_tools_async(
connector_id_or_name=connector.name,
)
print(f"\nTools exposed by {connector.name}:")
DOC_TOOL_NAME = None
for tool in tools_list:
print(f" - {tool.name}: {tool.description}")
if "documentation" in (tool.description or "").lower() or "doc" in tool.name.lower():
DOC_TOOL_NAME = tool.name
if DOC_TOOL_NAME:
print(f"\nDoc-retrieval tool for Step 5: {DOC_TOOL_NAME}")
else:
print("\nNo doc-retrieval tool auto-detected — check tool names above and set DOC_TOOL_NAME manually.")Create an agent with the Context7 connector attached. We use an agent instead of the conversations API so we can stream events and watch connector tool calls (tool.execution.started, tool.execution.delta, tool.execution.done) happen in real time. The instructions tell the model to use the connector rather than relying on training data alone.
connector_agent = await client.beta.agents.create_async(
name="quickstart_context7_agent",
model="mistral-medium-latest",
instructions=(
"You are a helpful programming assistant. "
"When asked about a library, always use the Context7 connector to look up "
"the official documentation before answering. Do not rely on training data alone."
),
tools=[{"type": "connector", "connector_id": connector.id}],
)
print(f"Agent ready: {connector_agent.name} (id={connector_agent.id})")Now use the agent in a conversation. The model gets access to official Polars documentation, so you should see authoritative API references — but it may miss community tips and migration advice that lives on blogs.
print("--- Step 3: Context7 connector (official docs) ---\n")
conversation_id = None
async for event in await client.beta.conversations.start_stream_async(
agent_id=connector_agent.id,
inputs=[{"role": "user", "content": PROMPT}],
timeout_ms=300_000,
):
data = event.data
event_type = getattr(data, "type", None)
if event_type == "conversation.response.started":
conversation_id = data.conversation_id
elif event_type == "message.output.delta":
print(".", end="", flush=True)
else:
name = getattr(data, "name", "")
print(f"\n[{event_type}]{' ' + name if name else ''}")
messages = await client.beta.conversations.get_messages_async(
conversation_id=conversation_id
)
last_output = next(
(m for m in reversed(messages.messages)
if getattr(m, "type", None) == "message.output"),
None,
)
if last_output:
print("\n")
content = last_output.content
if isinstance(content, str):
print(content)
else:
print("".join(getattr(c, "text", str(c)) for c in content))Step 4 — Both tools combined#
This is the payoff. We update the agent again, this time giving it both web search and the Context7 connector. The model can pull official documentation for accurate API examples and web results for community wisdom, migration gotchas, and recent release notes. It decides which source to use for each sub-topic.
Compare this output with Steps 1-3 — the combined version is noticeably richer.
print("--- Step 4: Web search + Context7 connector ---\n")
combined_agent = await client.beta.agents.update_async(
agent_id=connector_agent.id,
tools=[
{"type": "web_search"},
{"type": "connector", "connector_id": connector.id},
],
)
conversation_id = None
async for event in await client.beta.conversations.start_stream_async(
agent_id=combined_agent.id,
inputs=[{"role": "user", "content": PROMPT}],
timeout_ms=300_000,
):
data = event.data
event_type = getattr(data, "type", None)
if event_type == "conversation.response.started":
conversation_id = data.conversation_id
elif event_type == "message.output.delta":
print(".", end="", flush=True)
else:
name = getattr(data, "name", "")
print(f"\n[{event_type}]{' ' + name if name else ''}")
messages = await client.beta.conversations.get_messages_async(
conversation_id=conversation_id
)
last_output = next(
(m for m in reversed(messages.messages)
if getattr(m, "type", None) == "message.output"),
None,
)
if last_output:
print("\n")
content = last_output.content
if isinstance(content, str):
print(content)
else:
print("".join(getattr(c, "text", str(c)) for c in content))Step 5 — Tool filtering#
Context7 exposes multiple tools: a resolver (to find the library ID from a name) and a doc-retrieval tool (to fetch pages by library ID). If you already know the library ID, you can skip the resolver by using tool_configuration.include to restrict the connector to just the doc-retrieval tool.
We update the agent one last time, replacing its tools with a single filtered connector. This step also uses a different, more focused prompt that pre-specifies the Polars library so the resolver is unnecessary.
FILTERED_PROMPT = (
"Using the Polars documentation, explain lazy evaluation in Polars. "
"Cover: what LazyFrame is, how to build a lazy query with .lazy(), "
"how .collect() triggers execution, and when to prefer lazy over eager. "
"Include a runnable before/after code example."
)
if DOC_TOOL_NAME:
print(f"--- Step 5: Filtered connector (only {DOC_TOOL_NAME}) ---\n")
filtered_agent = await client.beta.agents.update_async(
agent_id=combined_agent.id,
tools=[
{
"type": "connector",
"connector_id": connector.id,
"tool_configuration": {
"include": [DOC_TOOL_NAME],
},
},
],
)
conversation_id = None
async for event in await client.beta.conversations.start_stream_async(
agent_id=filtered_agent.id,
inputs=[{"role": "user", "content": FILTERED_PROMPT}],
timeout_ms=300_000,
):
data = event.data
event_type = getattr(data, "type", None)
if event_type == "conversation.response.started":
conversation_id = data.conversation_id
elif event_type == "message.output.delta":
print(".", end="", flush=True)
else:
name = getattr(data, "name", "")
print(f"\n[{event_type}]{' ' + name if name else ''}")
messages = await client.beta.conversations.get_messages_async(
conversation_id=conversation_id
)
last_output = next(
(m for m in reversed(messages.messages)
if getattr(m, "type", None) == "message.output"),
None,
)
if last_output:
print("\n")
content = last_output.content
if isinstance(content, str):
print(content)
else:
print("".join(getattr(c, "text", str(c)) for c in content))
else:
print("Skipped — DOC_TOOL_NAME was not set. Set it manually from the tool list in Step 3.")Comparison#
Here is how each configuration performed:
| Step | Tools | Strengths | Weaknesses |
|---|---|---|---|
| 1 | None | Fast, no setup | May have outdated syntax, no citations |
| 2 | Web search | Current info, community tips, migration advice | May surface low-quality sources |
| 3 | Context7 connector | Authoritative API docs, correct signatures | Misses community wisdom and gotchas |
| 4 | Both | Best of both — accurate docs + practical tips | Slightly longer response time |
| 5 | Filtered connector | Precise — skips unnecessary tool calls | Requires knowing tool names upfront |
Cleanup#
Delete the agent and connector when you are done. Since we reused a single agent across Steps 2-5 (updating its tools each time), there is only one agent to clean up.
await client.beta.agents.delete_async(agent_id=connector_agent.id)
print(f"Agent deleted: {connector_agent.name}")
result = await client.beta.connectors.delete_async(connector_id=connector.id)
print(f"Connector deleted: {connector.name} — {result.message}")Summary#
This notebook demonstrated how adding tools to a Mistral conversation progressively improves output quality — from a baseline response using only training data, through web search and a documentation connector, to combining both for the richest result.
What you built:
- A Polars quickstart generator that improves with each tool added
- A Context7 connector for fetching official library documentation
- A filtered tool configuration that skips unnecessary connector tools
Mistral features used:
- Connectors (beta)
- Conversations API (beta)
- Agents API (beta) — used to stream tool execution events
- Web search built-in tool
- Tool filtering (
tool_configuration.include)
Other services:
- Context7 — MCP server for open-source library documentation
View your Connectors in Studio.