Conversation with an AI Model

A conversation is a sequence of messages that the library keeps for you. You will learn how to hold a multi-turn dialogue, give the model a role through a system instruction, read more than the text out of a response, and assemble the history by hand when you need to resume an earlier conversation.

The Model Remembers Nothing

This is the first thing that surprises almost everyone: a language model has no memory. Every API call stands alone and the model knows nothing about the previous question. The illusion of a conversation comes from sending the entire history again with every request.

That is exactly what the conversation object is for. It keeps the history for you:

$chat = $client->createChat('gpt-5.6-luna');

$chat->sendMessage('What is the capital of France?');
$response = $chat->sendMessage('And what is a famous landmark there?');

echo $response->getText();

The second question does not mention Paris at all, yet the model answers correctly, because the first exchange traveled along with it. Ask through two separate calls and the second answer is nonsense.

There is one consequence worth planning for: a long conversation is expensive. Every turn grows the input, and input is billed. So it is sometimes worth trimming the history or starting over. And when you send the same question over hundreds of inputs and are in no hurry, batch processing comes out cheaper:

$chat->clearMessages();

The System Instruction

The system instruction tells the model what role to play and which rules to follow. It applies to the whole conversation and models weigh it more heavily than an ordinary message, so this is where instructions belong that must still hold ten turns later.

$chat->setSystemInstruction('You are an experienced PHP developer. Answer briefly and use Nette in examples.');

A good system instruction is specific. Instead of “be brief” write “answer in at most three sentences”; instead of “be accurate” write “when you are not sure, say so instead of guessing”. The model has no way of knowing what you picture behind a vague instruction.

The system instruction is sent with every request, so you pay for it every time. When it is long and the conversation has many turns, reach for the provider's cache; how much came from it is reported by cacheReadTokens in the usage.

History Assembled by Hand

Sometimes you need to hand the model a conversation that never happened that way. Typically when you restore a conversation stored in a database, or when you want to show a few examples of correct answers, a technique known as few-shot prompting.

use AIAccess\Chat\Role;

$chat = $client->createChat('gpt-5.6-luna');
$chat->addMessage('What is the capital of France?', Role::User);
$chat->addMessage('Paris.', Role::Model);
$chat->addMessage('And what is a famous landmark there?', Role::User);

$response = $chat->sendMessage(); // no argument: continue from history

addMessage() only appends a message to the history and sends nothing. sendMessage() without an argument then sends the conversation as it stands.

There are three roles. Role::User is the user, Role::Model is the model, and Role::Tool carries the results of tool calls. The library uses its own naming here: Gemini calls the same role model while the others call it assistant, and the library translates it to whichever name the provider expects.

You can ask for the whole history back at any time:

foreach ($chat->getMessages() as $message) {
	echo $message->getRole()->value, ': ', $message->getText(), "\n";
}

What Is in the Response

sendMessage() does not return a string but an object, because the text alone is only part of what happened.

$response = $chat->sendMessage('Write me a short story about PHP.');

echo $response->getText();

An empty text is not necessarily an error. The model may have declined to answer, it may have hit a limit before writing the first word, or it may have asked for a tool instead of answering. Which of those happened is revealed by the finish reason.

Why the Model Stopped Writing

An answer does not always end because the model had said everything. Sometimes a token limit stops it, sometimes a safety filter, and sometimes it is waiting for you to supply something. You need to tell these apart, because each one calls for a different reaction. That is what getFinishReason() is for, returning one of the values of the FinishReason enum:

Value What happened What to do about it
Complete The model said all it wanted and stopped on its own. Nothing, this is the good case.
TokenLimit The answer is cut off mid-way, the token allowance ran out. Raise the limit in options, or ask for a shorter answer.
ContentFiltered The model refused to answer. The text will be empty. Rephrase the question; on OpenAI getRefusal() gives the reason.
ToolCall The model asked for a tool to be called. Call it and send the result back.
Cancelled You interrupted the stream yourself. You have only part of the answer, which is fine.
Unknown The provider sent a reason outside this scale. The original value is in getRawFinishReason().

In code it looks like this:

use AIAccess\Chat\FinishReason;

if ($response->getFinishReason() === FinishReason::TokenLimit) {
	echo 'The answer is cut off, the model hit the limit.';
}

Reasoning models may also return their chain of thought. It is never part of getText(), because it does not belong in your application's output, but you can read it:

if ($reasoning = $response->getReasoning()) {
	echo "The model reasoned like this:\n", $reasoning;
}

And when the abstraction is not enough, getRawResponse() hands you the provider's complete decoded answer exactly as it arrived. The unified interface is a convenience, never a cage.

Where to Go Next