Learn how to create your own LLM driver for LarAgent
LarAgent\Core\Contracts\LlmDriver
): Defines the contract all LLM drivers must implementLarAgent\Core\Abstractions\LlmDriver
): Provides common functionality for all driversOpenAiDriver
)<?php
namespace App\Drivers\YourProvider;
use LarAgent\Core\Abstractions\LlmDriver;
use LarAgent\Core\Contracts\LlmDriver as LlmDriverInterface;
use LarAgent\Core\Contracts\ToolCall as ToolCallInterface;
use LarAgent\Messages\AssistantMessage;
use LarAgent\Messages\ToolCallMessage;
use LarAgent\ToolCall;
class YourProviderDriver extends LlmDriver implements LlmDriverInterface
{
protected mixed $client;
public function __construct(array $settings = [])
{
parent::__construct($settings);
// Initialize your provider's client/SDK
// Example:
$this->client = $this->initializeClient($settings);
}
/**
* Initialize the client for your LLM provider
*/
protected function initializeClient(array $settings): mixed
{
// Example implementation:
$apiKey = $settings['api_key'] ?? null;
if (!$apiKey) {
return null;
}
// Return your initialized client
// This will depend on your provider's SDK
return new YourProviderClient($apiKey);
}
// Implement required methods...
}
/**
* Send a message to the LLM and receive a response.
*
* @param array $messages Array of messages to send
* @param array $options Configuration options
* @return AssistantMessage The response from the LLM
*
* @throws \Exception
*/
public function sendMessage(array $messages, array $options = []): AssistantMessage
{
if (empty($this->client)) {
throw new \Exception('API key is required to use the YourProvider driver.');
}
// Prepare the payload with common settings
$payload = $this->preparePayload($messages, $options);
// Make an API call to your provider
$response = $this->client->createCompletion($payload);
$this->lastResponse = $response;
// Handle the response based on your provider's response format
// For example, if your provider supports tool calls:
if ($this->isToolCallResponse($response)) {
// Extract tool calls from the response
$toolCalls = $this->extractToolCalls($response);
// Build tool calls message
$message = $this->toolCallsToMessage($toolCalls);
return new ToolCallMessage($toolCalls, $message, $this->getResponseMetadata($response));
}
// For regular text responses:
$content = $this->extractContent($response);
return new AssistantMessage($content, $this->getResponseMetadata($response));
}
/**
* Convert a tool result to a message format for the LLM
*
* @param ToolCallInterface $toolCall The tool call
* @param mixed $result The result from the tool
* @return array The formatted message
*/
public function toolResultToMessage(ToolCallInterface $toolCall, mixed $result): array
{
// Format depends on your provider's expected format
// Example for OpenAI-compatible format:
$content = json_decode($toolCall->getArguments(), true);
$content[$toolCall->getToolName()] = $result;
return [
'role' => 'tool',
'content' => json_encode($content),
'tool_call_id' => $toolCall->getId(),
];
}
/**
* Convert tool calls to a message format for the LLM
*
* @param array $toolCalls Array of tool calls
* @return array The formatted message
*/
public function toolCallsToMessage(array $toolCalls): array
{
$toolCallsArray = [];
foreach ($toolCalls as $tc) {
$toolCallsArray[] = $this->toolCallToContent($tc);
}
// Format depends on your provider's expected format
// Example for OpenAI-compatible format:
return [
'role' => 'assistant',
'tool_calls' => $toolCallsArray,
];
}
/**
* Format a tool call for your provider's API payload
*/
public function formatToolForPayload(ToolInterface $tool): array
{
// Override the default implementation if your provider has a different format
// Example for a provider with a different tool format:
return [
'name' => $tool->getName(),
'description' => $tool->getDescription(),
'parameters' => $tool->getProperties(),
'required_params' => $tool->getRequired(),
];
}
/**
* Send a message to the LLM and receive a streamed response.
*
* @param array $messages Array of messages to send
* @param array $options Configuration options
* @param callable|null $callback Optional callback function to process each chunk
* @return \Generator A generator that yields chunks of the response
*
* @throws \Exception
*/
public function sendMessageStreamed(array $messages, array $options = [], ?callable $callback = null): \Generator
{
if (empty($this->client)) {
throw new \Exception('API key is required to use the YourProvider driver.');
}
// Prepare the payload with common settings
$payload = $this->preparePayload($messages, $options);
// Add stream-specific options
$payload['stream'] = true;
// Create a streamed response
$stream = $this->client->createCompletionStream($payload);
// Initialize a streamed message
$streamedMessage = new StreamedAssistantMessage;
// Process the stream according to your provider's format
foreach ($stream as $chunk) {
$this->lastResponse = $chunk;
// Process the chunk and update the message
// This will depend on your provider's streaming format
$this->processStreamChunk($chunk, $streamedMessage);
// Execute callback if provided
if ($callback) {
$callback($streamedMessage);
}
// Yield the updated message
yield $streamedMessage;
}
}
/**
* Prepare the payload for API request with common settings
*/
protected function preparePayload(array $messages, array $options = []): array
{
// Add model if from provider data if not provided via options
if (empty($options['model'])) {
$options['model'] = $this->getSettings()['model'] ?? 'default-model';
}
$this->setConfig($options);
$payload = array_merge($this->getConfig(), [
'messages' => $this->formatMessages($messages),
]);
// Set the response format if structured output is enabled
if ($this->structuredOutputEnabled()) {
$payload['response_format'] = $this->formatResponseSchema($this->getResponseSchema());
}
// Add tools to payload if any are registered
if (!empty($this->tools)) {
$payload['tools'] = array_map(
fn($tool) => $this->formatToolForPayload($tool),
$this->getRegisteredTools()
);
}
return $payload;
}
/**
* Format messages for your provider's expected format
*/
protected function formatMessages(array $messages): array
{
// Transform LarAgent message format to your provider's format if needed
// Return the formatted messages
return $messages;
}
/**
* Format the response schema for your provider
*/
protected function formatResponseSchema(array $schema): array
{
// Transform the schema to your provider's expected format
return [
'type' => 'json_schema',
'schema' => $schema,
];
}
/**
* Check if a response contains tool calls
*/
protected function isToolCallResponse($response): bool
{
// Implement based on your provider's response format
// Example:
return isset($response->tool_calls) && !empty($response->tool_calls);
}
/**
* Extract tool calls from a response
*/
protected function extractToolCalls($response): array
{
// Implement based on your provider's response format
$toolCalls = [];
foreach ($response->tool_calls as $tc) {
$toolCalls[] = new ToolCall(
$tc->id ?? 'tool_call_' . uniqid(),
$tc->name ?? $tc->function->name ?? '',
$tc->arguments ?? $tc->function->arguments ?? '{}'
);
}
return $toolCalls;
}
/**
* Extract content from a response
*/
protected function extractContent($response): string
{
// Implement based on your provider's response format
// Example:
return $response->choices[0]->message->content ?? '';
}
/**
* Get metadata from a response
*/
protected function getResponseMetadata($response): array
{
// Extract usage information or other metadata
// Example:
return [
'usage' => [
'prompt_tokens' => $response->usage->prompt_tokens ?? 0,
'completion_tokens' => $response->usage->completion_tokens ?? 0,
'total_tokens' => $response->usage->total_tokens ?? 0,
],
];
}
/**
* Process a stream chunk
*/
protected function processStreamChunk($chunk, StreamedAssistantMessage $message): void
{
// Implement based on your provider's streaming format
// Example:
if (isset($chunk->content)) {
$message->appendContent($chunk->content);
}
if (isset($chunk->usage)) {
$message->setUsage([
'prompt_tokens' => $chunk->usage->prompt_tokens ?? 0,
'completion_tokens' => $chunk->usage->completion_tokens ?? 0,
'total_tokens' => $chunk->usage->total_tokens ?? 0,
]);
$message->setComplete(true);
}
}
<?php
namespace Tests\Drivers\YourProvider;
use LarAgent\Drivers\YourProvider\YourProviderDriver;
use LarAgent\Messages\AssistantMessage;
use LarAgent\Messages\ToolCallMessage;
use PHPUnit\Framework\TestCase;
class YourProviderDriverTest extends TestCase
{
protected YourProviderDriver $driver;
protected function setUp(): void
{
$this->driver = new YourProviderDriver([
'api_key' => 'test_key',
'model' => 'test_model',
]);
}
public function testSendMessage()
{
// Mock your provider's client response
$this->mockClientResponse();
$messages = [
['role' => 'system', 'content' => 'You are a helpful assistant.'],
['role' => 'user', 'content' => 'Hello!'],
];
$response = $this->driver->sendMessage($messages);
$this->assertInstanceOf(AssistantMessage::class, $response);
$this->assertEquals('Hello! How can I help you today?', $response->getContent());
}
public function testSendMessageWithToolCalls()
{
// Mock your provider's client response for tool calls
$this->mockClientToolCallResponse();
$messages = [
['role' => 'user', 'content' => 'What\'s the weather?'],
];
$response = $this->driver->sendMessage($messages);
$this->assertInstanceOf(ToolCallMessage::class, $response);
$this->assertEquals('get_weather', $response->getToolCalls()[0]->getToolName());
}
// Add more tests for other methods
}
// config/laragent.php
return [
// ...
'providers' => [
'your-provider' => [
'label' => 'your-provider-name',
'driver' => \App\Drivers\YourProvider\YourProviderDriver::class,
'api_key' => env('YOUR_PROVIDER_API_KEY'),
'model' => 'your-default-model',
// Other provider-specific settings
],
],
];
namespace App\AiAgents;
use LarAgent\Agent;
class YourAgent extends Agent
{
protected $driver = \App\Drivers\YourProvider\YourProviderDriver::class;
// ...
}
<?php
namespace LarAgent\Drivers\YourProvider;
use LarAgent\Core\Abstractions\LlmDriver;
use LarAgent\Core\Contracts\LlmDriver as LlmDriverInterface;
use LarAgent\Core\Contracts\Tool as ToolInterface;
use LarAgent\Core\Contracts\ToolCall as ToolCallInterface;
use LarAgent\Messages\AssistantMessage;
use LarAgent\Messages\StreamedAssistantMessage;
use LarAgent\Messages\ToolCallMessage;
use LarAgent\ToolCall;
use YourProvider\Client as YourProviderClient;
class YourProviderDriver extends LlmDriver implements LlmDriverInterface
{
protected mixed $client;
public function __construct(array $settings = [])
{
parent::__construct($settings);
$apiKey = $settings['api_key'] ?? null;
$this->client = $apiKey ? new YourProviderClient($apiKey) : null;
}
public function sendMessage(array $messages, array $options = []): AssistantMessage
{
if (empty($this->client)) {
throw new \Exception('API key is required to use the YourProvider driver.');
}
$payload = $this->preparePayload($messages, $options);
$response = $this->client->createCompletion($payload);
$this->lastResponse = $response;
if (isset($response->tool_calls) && !empty($response->tool_calls)) {
$toolCalls = [];
foreach ($response->tool_calls as $tc) {
$toolCalls[] = new ToolCall(
$tc->id ?? 'tool_call_' . uniqid(),
$tc->function->name ?? '',
$tc->function->arguments ?? '{}'
);
}
$message = $this->toolCallsToMessage($toolCalls);
return new ToolCallMessage($toolCalls, $message, ['usage' => $response->usage]);
}
$content = $response->choices[0]->message->content ?? '';
return new AssistantMessage($content, ['usage' => $response->usage]);
}
public function sendMessageStreamed(array $messages, array $options = [], ?callable $callback = null): \Generator
{
if (empty($this->client)) {
throw new \Exception('API key is required to use the YourProvider driver.');
}
$payload = $this->preparePayload($messages, $options);
$payload['stream'] = true;
$stream = $this->client->createCompletionStream($payload);
$streamedMessage = new StreamedAssistantMessage;
foreach ($stream as $chunk) {
$this->lastResponse = $chunk;
if (isset($chunk->content)) {
$streamedMessage->appendContent($chunk->content);
}
if (isset($chunk->usage)) {
$streamedMessage->setUsage([
'prompt_tokens' => $chunk->usage->prompt_tokens,
'completion_tokens' => $chunk->usage->completion_tokens,
'total_tokens' => $chunk->usage->total_tokens,
]);
$streamedMessage->setComplete(true);
}
if ($callback) {
$callback($streamedMessage);
}
yield $streamedMessage;
}
}
public function toolResultToMessage(ToolCallInterface $toolCall, mixed $result): array
{
$content = json_decode($toolCall->getArguments(), true);
$content[$toolCall->getToolName()] = $result;
return [
'role' => 'tool',
'content' => json_encode($content),
'tool_call_id' => $toolCall->getId(),
];
}
public function toolCallsToMessage(array $toolCalls): array
{
$toolCallsArray = [];
foreach ($toolCalls as $tc) {
$toolCallsArray[] = [
'id' => $tc->getId(),
'type' => 'function',
'function' => [
'name' => $tc->getToolName(),
'arguments' => $tc->getArguments(),
],
];
}
return [
'role' => 'assistant',
'tool_calls' => $toolCallsArray,
];
}
protected function preparePayload(array $messages, array $options = []): array
{
if (empty($options['model'])) {
$options['model'] = $this->getSettings()['model'] ?? 'default-model';
}
$this->setConfig($options);
$payload = array_merge($this->getConfig(), [
'messages' => $messages,
]);
if ($this->structuredOutputEnabled()) {
$payload['response_format'] = [
'type' => 'json_schema',
'schema' => $this->getResponseSchema(),
];
}
if (!empty($this->tools)) {
foreach ($this->getRegisteredTools() as $tool) {
$payload['tools'][] = $this->formatToolForPayload($tool);
}
}
return $payload;
}
}