Skip to content

Issue with mcp tool streaming #74

@nicobleiler

Description

@nicobleiler

There is an issue where a second message added to the conversation immediately ends without any output text.
Adding another message then causes Not the same number of function calls and responses error.

With this Testcase I can reproduce it consistently.

<?php

namespace Tests\Feature\Mcp;

use Partitech\PhpMistral\Clients\Mistral\MistralAgent;
use Partitech\PhpMistral\Clients\Mistral\MistralAgentClient;
use Partitech\PhpMistral\Clients\Mistral\MistralConversation;
use Partitech\PhpMistral\Clients\Mistral\MistralConversationClient;
use Partitech\PhpMistral\Mcp\McpConfig;
use Tests\TestCase;

class MCPToolStreamingTest extends TestCase
{
    protected string $apiKey;

    protected string $model;

    protected function setUp(): void
    {
        parent::setUp();
        $this->apiKey = getenv('MISTRAL_API_KEY');
        $this->model = 'mistral-small-latest';
    }

    public function test_php_mistral_streaming_call_tool_multiple(): void
    {
        if (empty($this->apiKey)) {
            $this->markTestSkipped('MISTRAL_API_KEY not set.');
        }
        if (! shell_exec('which docker')) {
            $this->markTestSkipped('Docker not available.');
        }
        srand(1337); // Make the test deterministic
        $apiKey = $this->apiKey;
        $model = $this->model;
        $numbers1 = [];
        $numbers2 = [];

        $numbers1[0] = rand(1, 100);
        $numbers1[1] = rand(1, 100);
        $numbers2[0] = rand(1, 100);
        $numbers2[1] = rand(1, 100);
        $expected = ($numbers1[0] + $numbers1[1]) + ($numbers2[0] + $numbers2[1]);
        $configArray = [
            'mcp' => [
                'servers' => [
                    'test' => [
                        'command' => 'docker',
                        'args' => [
                            'run',
                            '-i',
                            '--rm',
                            'mcp/everything',
                        ],
                    ],
                ],
            ],
        ];

        $mcpConfig = new McpConfig(
            $configArray
        );

        $conversation = (new MistralConversation)
            ->setModel($model)
            ->setName('Test')
            ->setDescription('Conversation used for testing')
            ->setTools($mcpConfig);

        $conversationClient = new MistralConversationClient($apiKey);

        $userMessage = "Use the tool 'add' to add the numbers ({$numbers1[0]} and {$numbers1[1]} as Number1) and ({$numbers2[0]} and {$numbers2[1]} as Number2), finally use the tool 'add' to add Number1 and Number2 with eachother and only return the resulting number with nothing else. If you encounter any issues explain them instead of returning the number.";
        $messages = $conversationClient
            ->getMessages()
            ->addAssistantMessage('Do as the user requests')
            ->addUserMessage($userMessage);

        /** @var Response $chunk */
        $text = null;
        $done = false;
        foreach ($conversationClient->conversation(
            conversation: $conversation,
            messages    : $messages,
            store       : true,
            stream      : true
        ) as $chunk) {

            if ($chunk->getType() !== 'conversation.response.done') {
                $text .= $chunk->getChunk();
            }

            if ($chunk->getType() === 'conversation.response.done') {
                $conversation->setId($chunk->getId());
                $done = true;
            }
        }
        $this->assertTrue($done);
        $this->assertNotEmpty($text);
        $this->assertEquals($expected, (int) $text);

        $conversationClient = new MistralConversationClient($apiKey);
        $conversation = $conversationClient->getConversation($conversation->getId());

        $userMessage = "Use the tool 'add' to add the numbers ({$numbers1[0]} and {$numbers1[1]} as Number1) and ({$numbers2[0]} and {$numbers2[1]} as Number2), finally use the tool 'add' to add Number1 and Number2 with eachother and only return the resulting number with nothing else. If you encounter any issues explain them instead of returning the number.";
        $messages = $conversationClient
            ->getMessages()
            ->addAssistantMessage('Do as the user requests')
            ->addUserMessage($userMessage);

        /** @var Response $chunk */
        $text = null;
        $done = false;
        foreach ($conversationClient->appendConversation(
            conversation: $conversation,
            messages    : $messages,
            store       : true,
            stream      : true
        ) as $chunk) {

            if ($chunk->getType() !== 'conversation.response.done') {
                $text .= $chunk->getChunk();
            }

            if ($chunk->getType() === 'conversation.response.done') {
                $conversation->setId($chunk->getId());
                $done = true;
            }
        }
        $this->assertTrue($done);
        // Already fails here as the stream immediately ends
        // Commented out to highlight the "Not the same number of function calls and responses" issue
        // $this->assertNotEmpty($text);
        // $this->assertEquals($expected, (int) $text);

        $conversationClient = new MistralConversationClient($apiKey);
        $conversation = $conversationClient->getConversation($conversation->getId());

        $userMessage = 'Ping!';
        $messages = $conversationClient
            ->getMessages()
            ->addUserMessage($userMessage);

        /** @var Response $chunk */
        $text = null;
        $done = false;
        foreach ($conversationClient->appendConversation(
            conversation: $conversation,
            messages    : $messages,
            store       : true,
            stream      : true
        ) as $chunk) {

            if ($chunk->getType() !== 'conversation.response.done') {
                $text .= $chunk->getChunk();
            }

            if ($chunk->getType() === 'conversation.response.done') {
                $conversation->setId($chunk->getId());
                $done = true;
            }
        }
        $this->assertTrue($done);
        $this->assertNotEmpty($text);
        $this->assertStringContainsString('Pong', (string) $text);
    }
}

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions