# Chat-driven media dashboard with Mistral

Build a conversational interface for media operations using Mistral agents and Ittybit

Most media dashboards force non-technical users through forms, dropdowns, and modal dialogs for every operation. A conversational interface flips this: a content manager types "resize all product photos to 800x800" and a Mistral agent translates that into Ittybit task batches behind the scenes. No UI to learn, no settings to remember.

## Install dependencies

<CodeGroup labels={["TypeScript", "Python"]}>
```bash
npm install @mistralai/mistralai
```

```bash
pip install mistralai requests
```

</CodeGroup>

## Define the tools

The agent needs two tools: `process_media` for creating tasks and `list_tasks` for checking on them. This lets users both dispatch work and ask about progress in the same conversation.

<CodeGroup labels={["TypeScript", "Python"]}>
```typescript
const tools = [
  {
    type: "function" as const,
    function: {
      name: "process_media",
      description:
        "Create a media processing task via Ittybit. Handles resizing, " +
        "format conversion, transcoding, thumbnails, and adaptive streams. " +
        "Call this once per file that needs processing.",
      parameters: {
        type: "object",
        properties: {
          input: {
            type: "string",
            description: "URL of the source media file",
          },
          kind: {
            type: "string",
            enum: ["video", "audio", "image", "adaptive_video"],
            description: "Type of processing task",
          },
          options: {
            type: "object",
            description:
              "Processing options: width, height, format, codec, quality, start, end",
          },
        },
        required: ["input", "kind"],
      },
    },
  },
  {
    type: "function" as const,
    function: {
      name: "list_tasks",
      description:
        "List recent media processing tasks. Use this to check status, " +
        "find completed outputs, or see what's currently running.",
      parameters: {
        type: "object",
        properties: {
          status: {
            type: "string",
            enum: ["queued", "processing", "succeeded", "failed"],
            description: "Filter tasks by status",
          },
          limit: {
            type: "number",
            description: "Number of tasks to return (default 10)",
          },
        },
      },
    },
  },
];
```

```python
tools = [
    {
        "type": "function",
        "function": {
            "name": "process_media",
            "description": (
                "Create a media processing task via Ittybit. Handles resizing, "
                "format conversion, transcoding, thumbnails, and adaptive streams. "
                "Call this once per file that needs processing."
            ),
            "parameters": {
                "type": "object",
                "properties": {
                    "input": {
                        "type": "string",
                        "description": "URL of the source media file",
                    },
                    "kind": {
                        "type": "string",
                        "enum": ["video", "audio", "image", "adaptive_video"],
                        "description": "Type of processing task",
                    },
                    "options": {
                        "type": "object",
                        "description": (
                            "Processing options: width, height, format, "
                            "codec, quality, start, end"
                        ),
                    },
                },
                "required": ["input", "kind"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "list_tasks",
            "description": (
                "List recent media processing tasks. Use this to check status, "
                "find completed outputs, or see what's currently running."
            ),
            "parameters": {
                "type": "object",
                "properties": {
                    "status": {
                        "type": "string",
                        "enum": ["queued", "processing", "succeeded", "failed"],
                        "description": "Filter tasks by status",
                    },
                    "limit": {
                        "type": "number",
                        "description": "Number of tasks to return (default 10)",
                    },
                },
            },
        },
    },
]
```

</CodeGroup>

## Implement the tool handlers

Each tool maps to an Ittybit API endpoint. `process_media` creates a task, `list_tasks` fetches recent tasks with optional filters.

<CodeGroup labels={["TypeScript", "Python"]}>
```typescript
const ITTYBIT_BASE = "https://api.ittybit.com";

const headers = {
Authorization: `Bearer ${process.env.ITTYBIT_API_KEY}`,
"Content-Type": "application/json",
};

async function processMedia(args: {
input: string;
kind: string;
options?: Record<string, unknown>;
}) {
const res = await fetch(`${ITTYBIT_BASE}/tasks`, {
method: "POST",
headers,
body: JSON.stringify({
input: args.input,
kind: args.kind,
options: args.options ?? {},
}),
});
return res.json();
}

async function listTasks(args: {
status?: string;
limit?: number;
}) {
const params = new URLSearchParams();
if (args.status) params.set("status", args.status);
if (args.limit) params.set("limit", String(args.limit));

const res = await fetch(
`${ITTYBIT_BASE}/tasks?${params.toString()}`,
{ headers },
);
return res.json();
}

const toolHandlers: Record<string, (args: any) => Promise<any>> = {
process_media: processMedia,
list_tasks: listTasks,
};

````

```python

ITTYBIT_BASE = "https://api.ittybit.com"
ITTYBIT_HEADERS = {
    "Authorization": f"Bearer {os.environ['ITTYBIT_API_KEY']}",
}

def process_media(args: dict) -> dict:
    res = requests.post(
        f"{ITTYBIT_BASE}/tasks",
        headers=ITTYBIT_HEADERS,
        json={
            "input": args["input"],
            "kind": args["kind"],
            "options": args.get("options", {}),
        },
    )
    return res.json()

def list_tasks(args: dict) -> dict:
    params = {}
    if "status" in args:
        params["status"] = args["status"]
    if "limit" in args:
        params["limit"] = args["limit"]

    res = requests.get(
        f"{ITTYBIT_BASE}/tasks",
        headers=ITTYBIT_HEADERS,
        params=params,
    )
    return res.json()

tool_handlers = {
    "process_media": process_media,
    "list_tasks": list_tasks,
}
````

</CodeGroup>

## Build the agent loop

The core of the dashboard: a multi-turn conversation loop that handles tool calls from Mistral, executes them, feeds results back, and lets the model continue until it has a final answer for the user.

<CodeGroup labels={["TypeScript", "Python"]}>
```typescript

const mistral = new Mistral({
apiKey: process.env.MISTRAL_API_KEY,
});

const SYSTEM_PROMPT = `You are a media operations assistant for a content team.
You help non-technical users manage their media files through conversation.

When a user asks you to process files, use the process_media tool.
When they ask about task status or recent activity, use list_tasks.
For batch operations across multiple files, call process_media once per file.

Always confirm what you did: how many tasks were created, what kind, and
what options were applied. If something fails, explain why in plain language.`;

type Message = {
role: string;
content?: string;
name?: string;
toolCalls?: any[];
toolCallId?: string;
};

async function chat(
messages: Message[],
): Promise<{ reply: string; tasks: any[] }> {
const response = await mistral.chat.complete({
model: "mistral-large-latest",
messages,
tools,
});

const choice = response.choices?.[0];
if (!choice) {
return { reply: "No response from model.", tasks: [] };
}

// If no tool calls, return the text response
if (
choice.finishReason !== "tool_calls" ||
!choice.message.toolCalls?.length
) {
return {
reply: choice.message.content as string,
tasks: [],
};
}

// Execute each tool call
const allTasks: any[] = [];
messages.push(choice.message as any);

for (const toolCall of choice.message.toolCalls) {
const handler = toolHandlers[toolCall.function.name];
const args = JSON.parse(toolCall.function.arguments);
const result = await handler(args);

    if (toolCall.function.name === "process_media") {
      allTasks.push(result);
    }

    messages.push({
      role: "tool",
      name: toolCall.function.name,
      content: JSON.stringify(result),
      toolCallId: toolCall.id,
    });

}

// Let the model continue -- it may make more tool calls
// or produce a final summary
const next = await chat(messages);
return {
reply: next.reply,
tasks: [...allTasks, ...next.tasks],
};
}

async function runAgent(userMessage: string) {
const messages: Message[] = [
{ role: "system", content: SYSTEM_PROMPT },
{ role: "user", content: userMessage },
];

return chat(messages);
}

````

```python

from mistralai import Mistral

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

SYSTEM_PROMPT = (
    "You are a media operations assistant for a content team. "
    "You help non-technical users manage their media files through conversation.\n\n"
    "When a user asks you to process files, use the process_media tool. "
    "When they ask about task status or recent activity, use list_tasks. "
    "For batch operations across multiple files, call process_media once per file.\n\n"
    "Always confirm what you did: how many tasks were created, what kind, and "
    "what options were applied. If something fails, explain why in plain language."
)

def chat(messages: list) -> dict:
    response = client.chat.complete(
        model="mistral-large-latest",
        messages=messages,
        tools=tools,
    )

    choice = response.choices[0]

    if (
        choice.finish_reason != "tool_calls"
        or not choice.message.tool_calls
    ):
        return {"reply": choice.message.content, "tasks": []}

    all_tasks = []
    messages.append(choice.message)

    for tool_call in choice.message.tool_calls:
        handler = tool_handlers[tool_call.function.name]
        args = json.loads(tool_call.function.arguments)
        result = handler(args)

        if tool_call.function.name == "process_media":
            all_tasks.append(result)

        messages.append(
            {
                "role": "tool",
                "name": tool_call.function.name,
                "content": json.dumps(result),
                "tool_call_id": tool_call.id,
            }
        )

    # Let the model continue -- it may make more tool calls
    # or produce a final summary
    next_result = chat(messages)
    return {
        "reply": next_result["reply"],
        "tasks": all_tasks + next_result["tasks"],
    }

def run_agent(user_message: str) -> dict:
    messages = [
        {"role": "system", "content": SYSTEM_PROMPT},
        {"role": "user", "content": user_message},
    ]
    return chat(messages)
````

</CodeGroup>

## Multi-turn conversation

A real dashboard session looks like this. The user starts with a batch resize, checks status, then kicks off another operation -- all in natural language.

<CodeGroup labels={["TypeScript", "Python"]}>
```typescript
// Turn 1: Batch resize product photos
const turn1 = await runAgent(
  "Resize all these product photos to 800x800:\n" +
  "- https://cdn.example.com/products/jacket-front.jpg\n" +
  "- https://cdn.example.com/products/jacket-back.jpg\n" +
  "- https://cdn.example.com/products/jacket-detail.jpg"
);
console.log(turn1.reply);
// "I've created 3 image tasks to resize your product photos to 800x800.
//  Each one will be processed as a square crop. Task IDs: task_a1, task_a2, task_a3."
console.log(`Tasks created: ${turn1.tasks.length}`);

// Turn 2: Check on progress
const turn2 = await runAgent(
"How are those resize tasks doing?"
);
console.log(turn2.reply);
// "All 3 tasks have succeeded. Your resized images are ready."

// Turn 3: Process a video
const turn3 = await runAgent(
"Now take this demo video and make a 720p MP4 and a thumbnail: " +
"https://cdn.example.com/videos/product-demo.mov"
);
console.log(turn3.reply);
// "Done — I created 2 tasks:
// 1. Video transcode to 720p MP4 (task_b1)
// 2. Thumbnail extraction as JPG (task_b2)"
console.log(`Tasks created: ${turn3.tasks.length}`);

````

```python
# Turn 1: Batch resize product photos
turn1 = run_agent(
    "Resize all these product photos to 800x800:\n"
    "- https://cdn.example.com/products/jacket-front.jpg\n"
    "- https://cdn.example.com/products/jacket-back.jpg\n"
    "- https://cdn.example.com/products/jacket-detail.jpg"
)
print(turn1["reply"])
# "I've created 3 image tasks to resize your product photos to 800x800.
#  Each one will be processed as a square crop. Task IDs: task_a1, task_a2, task_a3."
print(f"Tasks created: {len(turn1['tasks'])}")

# Turn 2: Check on progress
turn2 = run_agent(
    "How are those resize tasks doing?"
)
print(turn2["reply"])
# "All 3 tasks have succeeded. Your resized images are ready."

# Turn 3: Process a video
turn3 = run_agent(
    "Now take this demo video and make a 720p MP4 and a thumbnail: "
    "https://cdn.example.com/videos/product-demo.mov"
)
print(turn3["reply"])
# "Done — I created 2 tasks:
#  1. Video transcode to 720p MP4 (task_b1)
#  2. Thumbnail extraction as JPG (task_b2)"
print(f"Tasks created: {len(turn3['tasks'])}")
````

</CodeGroup>

For the batch resize, Mistral issues three parallel `process_media` calls in a single response -- one per image URL. For the video request, it recognizes two distinct operations (transcode and thumbnail) and calls the tool twice with different `kind` values.

## Persistent conversation

The examples above start fresh each turn. To maintain context across a session -- so the agent remembers file URLs and task IDs from earlier messages -- accumulate messages in a shared array.

<CodeGroup labels={["TypeScript", "Python"]}>
```typescript
class DashboardSession {
  private messages: Message[] = [
    { role: "system", content: SYSTEM_PROMPT },
  ];

async send(userMessage: string) {
this.messages.push({ role: "user", content: userMessage });

    const result = await chat(this.messages);

    this.messages.push({
      role: "assistant",
      content: result.reply,
    });

    return result;

}
}

// Usage
const session = new DashboardSession();

await session.send(
"Resize https://cdn.example.com/hero.jpg to 1200x630"
);

// The agent remembers the previous file
await session.send(
"Also make a 400x400 square version of that same image"
);

// Ask about everything so far
await session.send("Show me all my recent tasks");

````

```python
class DashboardSession:
    def __init__(self):
        self.messages = [
            {"role": "system", "content": SYSTEM_PROMPT},
        ]

    def send(self, user_message: str) -> dict:
        self.messages.append(
            {"role": "user", "content": user_message}
        )

        result = chat(self.messages)

        self.messages.append(
            {"role": "assistant", "content": result["reply"]}
        )

        return result

# Usage
session = DashboardSession()

session.send(
    "Resize https://cdn.example.com/hero.jpg to 1200x630"
)

# The agent remembers the previous file
session.send(
    "Also make a 400x400 square version of that same image"
)

# Ask about everything so far
session.send("Show me all my recent tasks")
````

</CodeGroup>

## See also

- [Media processing agent with Mistral](/guides/media-processing-agent-with-mistral) -- single-tool agent for simpler use cases
- [Responsive image sets](/guides/generate-responsive-image-sizes) -- generate multiple sizes from one source
- [Build a user upload pipeline](/guides/build-a-user-upload-pipeline) -- end-to-end upload and processing flow
- [Mistral function calling docs](https://docs.mistral.ai/capabilities/function_calling/)