🧭 How toProject ManagementIntermediate✨ AI-assisted

How to Automate Status Reports With Mistral's API

WittyTech Daily··4 min read
#mistral#function-calling#jira#json-mode

Every Friday, someone on your team closes their laptop an hour late because the status report still needs writing. The tickets got closed, the blockers got resolved, but none of it turns itself into a paragraph a stakeholder wants to read. The Mistral API can do that translation for you, and if you're already pulling data from Jira, the rest of the pipeline is a couple hundred lines of Python.

This walkthrough builds a script that pulls a week of tickets from Jira, uses Mistral's function calling to pull structured signal out of each one, then uses Mistral's JSON mode to assemble a clean stakeholder update. It runs entirely on La Plateforme, Mistral's hosted API, so there's no infrastructure to stand up.

What you'll need

  • A Mistral API key from La Plateforme (console.mistral.ai)
  • A Jira Cloud instance with an API token
  • Python 3.10+

1. Install the SDK and set your keys

pip install mistralai requests
export MISTRAL_API_KEY="<your-mistral-api-key>"
export JIRA_API_TOKEN="<your-jira-api-token>"

Grab the Mistral key from La Plateforme under API Keys, and the Jira token from your Atlassian account's security settings.

2. Pull a week of tickets from Jira

import os, requests
from datetime import date, timedelta

JIRA_BASE = "https://<your-domain>.atlassian.net"
JIRA_EMAIL = "you@company.com"

since = (date.today() - timedelta(days=7)).isoformat()
jql = f'project = "PROJ" AND updated >= "{since}" ORDER BY updated DESC'

resp = requests.get(
    f"{JIRA_BASE}/rest/api/3/search",
    params={"jql": jql, "fields": "summary,status,assignee,priority,comment"},
    auth=(JIRA_EMAIL, os.environ["JIRA_API_TOKEN"]),
)
tickets = resp.json()["issues"]

This grabs everything touched in the last week for one project. Swap the JQL if you track multiple projects, or scope it to a sprint instead of a rolling window.

3. Extract structured ticket data with Mistral function calling

Raw ticket JSON is noisy: nested fields, null assignees, long comment threads. Instead of prompting Mistral to "summarize this," hand it a tool with the exact shape you want and force it to call that tool. This is what Mistral function calling is actually good at: not writing the report, but guaranteeing every ticket comes out in the same shape.

from mistralai import Mistral
import json

client = Mistral(api_key=os.environ["MISTRAL_API_KEY"])

extract_tool = {
    "type": "function",
    "function": {
        "name": "record_ticket_update",
        "description": "Record a structured summary of a Jira ticket for a status report",
        "parameters": {
            "type": "object",
            "properties": {
                "key": {"type": "string"},
                "status_category": {"type": "string", "enum": ["done", "in_progress", "blocked"]},
                "one_line_summary": {"type": "string"},
                "is_risk": {"type": "boolean"},
            },
            "required": ["key", "status_category", "one_line_summary", "is_risk"],
        },
    },
}

def extract_ticket(ticket):
    prompt = f"Ticket {ticket['key']}: {ticket['fields']['summary']}, status: {ticket['fields']['status']['name']}"
    response = client.chat.complete(
        model="mistral-large-latest",
        messages=[{"role": "user", "content": prompt}],
        tools=[extract_tool],
        tool_choice="any",
    )
    call = response.choices[0].message.tool_calls[0]
    return json.loads(call.function.arguments)

updates = [extract_ticket(t) for t in tickets]

tool_choice="any" forces Mistral to call record_ticket_update instead of replying in prose, so function.arguments is always valid JSON matching your schema. See Mistral's function calling docs for the full parameter reference.

4. Assemble the report with JSON mode

Feed the structured ticket updates back in and ask for a report shaped by a schema, using Mistral JSON mode.

report_prompt = f"""
Write a weekly status report from this data: {json.dumps(updates)}
Return a JSON object with keys: headline (string), completed (list of strings),
in_progress (list of strings), risks (list of strings), narrative (string, 3-4 sentences).
"""

report = client.chat.complete(
    model="mistral-large-latest",
    messages=[{"role": "user", "content": report_prompt}],
    response_format={"type": "json_object"},
)
report_data = json.loads(report.choices[0].message.content)

JSON mode only guarantees valid JSON, it doesn't enforce your exact keys unless you spell them out in the prompt, which is why the instructions above list every field by name. For a stricter contract once your report format has nested fields, swap in response_format={"type": "json_schema", "json_schema": {...}}, which constrains decoding to match a schema exactly.

5. Render and send it

message = f"""*Weekly Status: {report_data['headline']}*

*Completed:* {', '.join(report_data['completed'])}
*In progress:* {', '.join(report_data['in_progress'])}
*Risks:* {', '.join(report_data['risks']) or 'None flagged'}

{report_data['narrative']}
"""
print(message)  # or POST to a Slack webhook / send via email from here

Post that string to a Slack webhook or drop it into an email template, and the report is done before the coffee's cold.

Gotchas

  • JSON mode won't invent your schema for you. Name every key you want in the prompt, or the model picks its own.
  • tool_choice="any" forces a tool call every time, which is what you want for extraction but wrong for open-ended chat turns.
  • Ticket descriptions and comment threads can be long. Truncate or pre-summarize fields before sending them to keep cost and latency down.
  • Pin a dated model name instead of mistral-large-latest once this runs unattended, so a routine model update doesn't silently change your report's format.
  • Jira's /rest/api/3/search paginates by default. Add startAt handling if a project has a busy week and you're pulling more than 50 issues.

Once this is wired up, the only manual work left is deciding who reads the report. That's a better use of a Friday afternoon than reformatting ticket titles into sentences, and it's one less place where a production AI rollout depends on someone remembering to do a chore.

← More in Project Management