Build a Quickstart Generator with Connectors and Web Search

ConnectorsAgents

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:

StepToolsWhat the model can access
1NoneTraining data only
2Web searchBlog posts, Stack Overflow, release notes
3Context7 connectorOfficial Polars documentation
4BothDocs + web — the model picks the best source per sub-topic
5Filtered connectorA single doc-retrieval tool (skip the resolver)

API status: This notebook uses client.beta.connectors and client.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-key

Option 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 --quiet

Import 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))

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:

  1. Create the connector pointing at Context7's MCP endpoint
  2. Register credentials (empty, since Context7 is public — but the record must exist)
  3. 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:

StepToolsStrengthsWeaknesses
1NoneFast, no setupMay have outdated syntax, no citations
2Web searchCurrent info, community tips, migration adviceMay surface low-quality sources
3Context7 connectorAuthoritative API docs, correct signaturesMisses community wisdom and gotchas
4BothBest of both — accurate docs + practical tipsSlightly longer response time
5Filtered connectorPrecise — skips unnecessary tool callsRequires 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.