> SpecStream is the live web data engine for teams and agents. This is the Markdown copy of https://specstream.ai/docs/integrations/mistral-agents-research. Every page, with what SpecStream does and costs: https://specstream.ai/llms.txt

# Mistral Agents API Research

Give a Mistral agent a research tool. You declare `research_report` as a function tool in the Agents API, beside Mistral's own `web_search`. When the model calls it, your code starts a SpecStream research report and returns the written answer from the live web, with a numbered list of the pages it cites and when each was read. A report has one fixed price: $0.10, $0.30 or $1.00 by depth.

[SpecStream for Mistral Agents API ResearchAgent frameworks & builders](https://specstream.ai/integrations/mistral-agents-research.md)

## What you need

-   A Mistral API key, and Python with the Mistral SDK and requests (`pip install mistralai requests`). The code uses the SDK's V2 imports, `from mistralai.client import ...`. On V1 of the SDK, import the same names from `mistralai`.
-   A SpecStream key. [Get your key](https://specstream.ai/workspace/search?utm_source=mistral-agents-research&utm_medium=howto) on the Search API screen of your SpecStream workspace. New accounts start with $10 of credit, no card.

1.  1

    ## 1. Set both keys

    Create a key in your SpecStream workspace, then set it and your Mistral key on the machine that runs your code. Neither key goes into a file.

    Terminal

    ```bash
    pip install mistralai requests
    
    export MISTRAL_API_KEY="your Mistral key"
    export SPECSTREAM_API_KEY="your SpecStream key"
    ```

2.  2

    ## 2. Write the research function

    Save this as `specstream_research.py`. `research_report` takes the question and a depth: `quick`, `standard` or `deep`. It starts the report with `POST /v1/research`, then reads `GET /v1/research/{id}` every 5 seconds for up to 7 minutes. A quick report takes about a minute, a standard report about four and a deep report about five, so the report usually comes back in the same call: the text with `[1]` markers, then a **Sources** list with each cited page's title, URL and read time.

    If the report needs longer, the function returns the report ID, which starts with `rr_`. `read_research_report` reads a report by that ID and costs nothing.

    Mistral runs a function tool in your environment, so the SpecStream key stays in your process and never reaches the model.

    specstream\_research.py

    ```python
    import hashlib
    import os
    import time
    
    import requests
    
    API = "https://api.specstream.ai/v1/research"
    ENDED = ("completed", "failed", "cancelled", "payment_required")
    WAIT_SECONDS = 420  # how long one call waits for the report before it returns the report ID
    
    
    def call(method, url, **kwargs):
        headers = {"Authorization": "Bearer " + os.environ["SPECSTREAM_API_KEY"]}
        headers.update(kwargs.pop("headers", {}))
        reply = requests.request(method, url, headers=headers, timeout=60, **kwargs)
        if reply.status_code >= 400:
            raise RuntimeError("SpecStream answered " + str(reply.status_code) + ": " + reply.text[:300])
        return reply.json()["data"]
    
    
    def shown(run):
        status = run.get("status")
        if status == "completed":
            report = run.get("report") or {}
            sources = [
                "[" + str(source["n"]) + "] " + source["title"] + " " + source["url"] + " (read " + source["read_at"] + ")"
                for source in report.get("sources") or []
                if source.get("cited")
            ]
            return str(report.get("content") or "") + "\n\nSources\n" + "\n".join(sources)
        if status == "payment_required":
            return (
                "The report is written, but the balance cannot pay for it. Add balance in the SpecStream "
                "workspace, then call read_research_report with report ID " + str(run.get("id")) + "."
            )
        if status in ENDED:
            return "The report did not finish and costs nothing: " + str(status)
        return (
            "The report is still in work (stage: " + str(run.get("stage")) + "). Call read_research_report "
            "with report ID " + str(run.get("id")) + " in a minute."
        )
    
    
    def research_report(question, depth="quick"):
        question = (question or "").strip()
        depth = (depth or "quick").strip().lower()
        if not question:
            return "Give the question the report answers."
        if depth not in ("quick", "standard", "deep"):
            return "Set depth to quick, standard or deep."
        # The same question at the same depth in the same hour gets the report it already started,
        # so a repeated tool call never buys the report twice.
        hour = time.strftime("%Y-%m-%dT%H", time.gmtime())
        once = "mistral-" + hashlib.sha256((question + "|" + depth + "|" + hour).encode()).hexdigest()[:32]
        try:
            run = call("POST", API, json={"input": question, "depth": depth}, headers={"Idempotency-Key": once})
            deadline = time.monotonic() + WAIT_SECONDS
            while run.get("status") not in ENDED and time.monotonic() < deadline:
                time.sleep(5)
                run = call("GET", API + "/" + run["id"])
        except RuntimeError as refused:
            return str(refused)
        return shown(run)
    
    
    def read_research_report(report_id):
        try:
            return shown(call("GET", API + "/" + (report_id or "").strip()))
        except RuntimeError as refused:
            return str(refused)
    ```

3.  3

    ## 3. Create the agent with the function tool

    Save this as `agent.py` and run it once. `client.beta.agents.create` takes the tools list: `{"type": "web_search"}` is Mistral's built in search, and each `{"type": "function", ...}` entry is a JSON schema for one of your functions. The description carries the three prices, and the instructions make the agent name the price and wait for a yes before it starts a report.

    The script prints the agent ID, which starts with `ag_`. Keep it for the next step.

    agent.py

    ```python
    import os
    
    from mistralai.client import Mistral
    
    client = Mistral(os.environ["MISTRAL_API_KEY"])
    
    research_agent = client.beta.agents.create(
        model="mistral-medium-latest",
        name="research-agent",
        description="Answers questions from the live web with a written report and its sources.",
        instructions=(
            "Use web_search for one fact or a quick answer. When the user asks for a written answer "
            "with sources, tell them the price of a report: $0.10 for quick, $0.30 for standard, "
            "$1.00 for deep. Use quick unless the user asks for more depth. Call research_report only "
            "after the user approves that price in their own message. If research_report returns a "
            "report ID in place of the report, say the report is in work and call read_research_report "
            "with that ID when the user asks for it. Show the report as the tool returns it, with its "
            "[1] markers and its Sources list."
        ),
        tools=[
            {"type": "web_search"},
            {
                "type": "function",
                "function": {
                    "name": "research_report",
                    "description": (
                        "Write a research report that answers one question from the live web, with "
                        "numbered sources. Costs $0.10 (quick), $0.30 (standard) or $1.00 (deep), so "
                        "call it only after the user approved the price. Returns the report text, or "
                        "a report ID when the report needs more time."
                    ),
                    "parameters": {
                        "type": "object",
                        "properties": {
                            "question": {
                                "type": "string",
                                "description": "The question the report answers, in the user's words, with the scope, the dates and the places that matter.",
                            },
                            "depth": {
                                "type": "string",
                                "enum": ["quick", "standard", "deep"],
                                "description": "Use quick unless the user asked for more depth.",
                            },
                        },
                        "required": ["question", "depth"],
                    },
                },
            },
            {
                "type": "function",
                "function": {
                    "name": "read_research_report",
                    "description": "Read a research report by its report ID: the finished text with its sources, or the stage it is in. Costs nothing.",
                    "parameters": {
                        "type": "object",
                        "properties": {
                            "report_id": {
                                "type": "string",
                                "description": "The report ID that research_report returned. It starts with rr_.",
                            },
                        },
                        "required": ["report_id"],
                    },
                },
            },
        ],
    )
    print(research_agent.id)
    ```

4.  4

    ## 4. Answer each function call

    Save this as `chat.py`. It starts a conversation with `client.beta.conversations.start` and continues it with `client.beta.conversations.append`. When an output has the type `function.call`, the script runs the function named in it and sends the result back as a `FunctionResultEntry` with the same `tool_call_id`. It answers every call in the reply, so two calls in one turn both get their result.

    Mistral runs `web_search` on its own servers. Only your two functions reach this loop.

    chat.py

    ```python
    import json
    import os
    import sys
    
    from mistralai.client import FunctionResultEntry, Mistral
    
    from specstream_research import read_research_report, research_report
    
    TOOLS = {"research_report": research_report, "read_research_report": read_research_report}
    
    client = Mistral(os.environ["MISTRAL_API_KEY"])
    agent_id = sys.argv[1]
    conversation_id = None
    
    while True:
        inputs = [{"role": "user", "content": input("You: ")}]
        while True:
            if conversation_id is None:
                response = client.beta.conversations.start(agent_id=agent_id, inputs=inputs)
                conversation_id = response.conversation_id
            else:
                response = client.beta.conversations.append(conversation_id=conversation_id, inputs=inputs)
            calls = [output for output in response.outputs if output.type == "function.call"]
            if not calls:
                break
            inputs = []
            for call in calls:
                tool = TOOLS.get(call.name)
                result = tool(**json.loads(call.arguments)) if tool else "No such tool: " + call.name
                inputs.append(FunctionResultEntry(tool_call_id=call.tool_call_id, result=result))
        print("Agent:", response.outputs[-1].content)
    ```

5.  5

    ## 5. Ask for a report

    Run the chat with the agent ID from `agent.py`. Ask your question. The agent names the price, and after your yes it calls `research_report`. The terminal waits about a minute for a quick report, then prints the report with its sources. SpecStream takes the $0.10 from your balance when the report is delivered.

    Terminal

    ```text
    python chat.py ag_YOUR_AGENT_ID
    
    You: Write a quick research report: which EU countries cap rent increases in 2026, and what is each cap? Cite the pages.
    Agent: A quick report costs $0.10. Shall I start it?
    You: Yes, go ahead.
    ```

## Beside Mistral's web\_search

The two tools do different jobs, and the agent's instructions tell the model which to pick. `web_search` answers one fact inside the turn, on Mistral's servers. `research_report` plans the question, searches the live web, reads the pages and writes an answer of several paragraphs, where each cited sentence carries a `[1]` marker and the Sources list gives that page's title, its URL and the time it was read in this run.

`web_search` works with the Conversations API and the Agents API, and Mistral's docs say it is not supported in Chat Completions. A function tool works in all three. For Chat Completions, declare the same two schemas in the `tools` field and answer each tool call with a `tool` message. The [Mistral API guide](https://specstream.ai/docs/integrations/mistral-api.md) has that loop for web search.

## On the Agents SDK

Mistral's docs now carry this notice: the Agents API v1 is about to be deprecated, and new agents are built with the Agents SDK (`mistralai-agents`, in public preview). The function from step 2 moves over as it is. Decorate a wrapper with `@agents.tool` and pass it in `tools=`. The SDK takes the tool's name from the function's name, its description from the docstring and its input schema from the typed parameters, so there is no JSON schema to write.

The wrappers are plain `def` functions, since they wait for the report. Mistral's docs say a blocking tool written as a plain `def` runs in its own thread. Save the file as `main.py` beside `specstream_research.py`, add the dependency with `uv add requests`, and run `uv run --env-file .env agents chat`. Put `SPECSTREAM_API_KEY` in the same `.env` as your Mistral key. To make the price approval a rule of the harness and not only an instruction, see Mistral's [Approvals (opens in a new tab)](https://docs.mistral.ai/studio/agents/learn/agent/approvals) page.

## What the agent gets back

The function returns the report as text, which is what a model reads best: the answer with `[1]` markers, then the Sources list. In your own code, `GET /v1/research/{id}` returns more. `report.citations` holds each cited sentence with its page, read time and passage, and `report.sources` lists every page read, cited or not.

Send `"output": {"type": "json", "schema": {...}}` to get JSON in the shape of your schema in place of text, `length` to choose `short`, `standard` or `long`, and `include_domains` or `exclude_domains` to choose the sites. The [research docs](https://specstream.ai/docs/research.md) list every field.

## How spending stays under control

-   One report has one fixed price for its depth. No report costs more than $1.00.
-   The user approves the price. The agent's instructions make it name the price and wait for a yes before `research_report`.
-   A repeated call buys nothing new. The function sends an `Idempotency-Key` built from the question, the depth and the hour, so the same question in the same hour returns the report it already started.
-   The charge comes at delivery. A report that fails or is cancelled costs nothing.
-   The SpecStream key stays in your process. The model sees the function's name, its schema and its result, never the key.
-   To keep deep reports out, remove `"deep"` from the `enum` in `agent.py` and from the depth check in `specstream_research.py`.

## If something doesn't work

| What you see | What to do |
| --- | --- |
| `The report is still in work` | The report needs more than the 7 minutes one call waits. Ask the agent for it again in a minute. It calls `read_research_report` with the report ID. |
| `SpecStream answered 401` | The key is unknown, expired or revoked. Create a new key in your SpecStream workspace and set `SPECSTREAM_API_KEY` again. |
| `SpecStream answered 402`, or `the balance cannot pay for it` | The balance does not hold the report's price. Add balance in your SpecStream workspace. A report that was already written is delivered on the next read. |
| `SpecStream answered 422` | A field is wrong, and the reply names it. The question holds up to 15,000 characters. |
| `SpecStream answered 429` | The account used its calls for the minute. Wait and ask again. Nothing is charged. |
| `KeyError: 'SPECSTREAM_API_KEY'` | The key is not set in the shell that runs `chat.py`. Export it there, or pass `--env-file .env` to `uv run` on the Agents SDK. |
| `ImportError` on `mistralai.client` | Your SDK is V1. Import `Mistral` and `FunctionResultEntry` from `mistralai`. |
| The agent starts a report without asking | The instructions were changed or left out. Create the agent again with the instructions from `agent.py`. |

## Cost

A report costs $0.10 at `quick`, $0.30 at `standard` and $1.00 at `deep`, taken from your SpecStream balance once, when the report is delivered. Reading a report costs nothing. Mistral bills the model's tokens as it bills any conversation, and the report text counts as input tokens when your code returns it. The rates are on [pricing](https://specstream.ai/pricing.md).

## Web search and page reading too

For one fact or the text of a page as a function tool, the [Mistral API guide](https://specstream.ai/docs/integrations/mistral-api.md) declares a search function on the same key. The [Search API](https://specstream.ai/docs/search-api.md) is $5 per 1,000 searches of up to 10 results and the [Extract API](https://specstream.ai/docs/extract-api.md) $0.80 per 1,000 pages. Mistral also registers MCP servers as Connectors: the SpecStream server is at `https://api.specstream.ai/mcp`, and the [MCP docs](https://specstream.ai/docs/mcp.md) list its tools, `research_price`, `start_research` and `get_research` among them. The [REST API reference](https://specstream.ai/docs/api.md) lists every call on this page.

## Remove

Delete the agent in Mistral, remove the three files, and revoke the key in your SpecStream workspace. Reports already delivered stay in your account. Questions: [support@specstream.ai](mailto:support@specstream.ai).

## More

[SpecStream for Mistral Agents API Research](https://specstream.ai/integrations/mistral-agents-research.md) covers what it does in Mistral Agents API Research on one page. Every other tool has its guide in [Connect your tools](https://specstream.ai/docs/integrations.md). You see the most a request can cost before it starts, and the matches are paid from your balance. [Billing](https://specstream.ai/docs/billing.md) has the details. For help, email [support@specstream.ai](mailto:support@specstream.ai).
