Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion packages/anthropic-adapter/phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,11 @@ parameters:
-
message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertInstanceOf\(\) with ''ModelflowAi\\\\Chat\\\\Response\\\\AIChatResponse'' and ModelflowAi\\Chat\\Response\\AIChatResponse will always evaluate to true\.$#'
identifier: method.alreadyNarrowedType
count: 4
count: 8
path: tests/Unit/Chat/AnthropicChatAdapterTest.php

-
message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertSame\(\) with ModelflowAi\\Chat\\ToolInfo\\ToolTypeEnum\:\:FUNCTION and ModelflowAi\\Chat\\ToolInfo\\ToolTypeEnum will always evaluate to true\.$#'
identifier: method.alreadyNarrowedType
count: 1
path: tests/Unit/Chat/AnthropicChatAdapterTest.php
61 changes: 57 additions & 4 deletions packages/anthropic-adapter/src/Chat/AnthropicChatAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,18 @@
use ModelflowAi\Chat\Request\Message\AIChatMessageRoleEnum;
use ModelflowAi\Chat\Request\Message\ImageBase64Part;
use ModelflowAi\Chat\Request\Message\TextPart;
use ModelflowAi\Chat\Request\Message\ToolCallPart;
use ModelflowAi\Chat\Request\Message\ToolCallsPart;
use ModelflowAi\Chat\Request\ResponseFormat\JsonSchemaResponseFormat;
use ModelflowAi\Chat\Request\ResponseFormat\ResponseFormatInterface;
use ModelflowAi\Chat\Request\ResponseFormat\SupportsResponseFormatInterface;
use ModelflowAi\Chat\Response\AIChatResponse;
use ModelflowAi\Chat\Response\AIChatResponseMessage;
use ModelflowAi\Chat\Response\AIChatResponseStream;
use ModelflowAi\Chat\Response\AIChatToolCall;
use ModelflowAi\Chat\Response\StreamingUsageTracker;
use ModelflowAi\Chat\Response\Usage;
use ModelflowAi\Chat\ToolInfo\ToolTypeEnum;

/**
* @phpstan-import-type Parameters from MessagesInterface
Expand All @@ -41,6 +45,7 @@
AIChatMessageRoleEnum::SYSTEM,
AIChatMessageRoleEnum::ASSISTANT,
AIChatMessageRoleEnum::USER,
AIChatMessageRoleEnum::TOOL,
];

/**
Expand Down Expand Up @@ -87,15 +92,27 @@ public function handleRequest(AIChatRequest $request): AIChatResponse
$parameters['temperature'] = $temperature;
}

if ($request->hasTools()) {
$parameters['tools'] = ToolFormatter::formatTools($request->getToolInfos());
}

$messages = [];
/** @var AIChatMessage $aiMessage */
foreach ($request->getMessages() as $aiMessage) {
if (!\in_array($aiMessage->role, self::EXPECTED_ROLES, true)) {
throw new \Exception('Not supported message role.');
}

// Anthropic has no dedicated "tool" role; tool results are sent as a user message
// containing tool_result content blocks.
$role = match ($aiMessage->role) {
AIChatMessageRoleEnum::USER, AIChatMessageRoleEnum::TOOL => 'user',
AIChatMessageRoleEnum::ASSISTANT => 'assistant',
AIChatMessageRoleEnum::SYSTEM => 'system',
};

$message = [
'role' => $aiMessage->role->value,
'role' => $role,
'content' => [],
];

Expand All @@ -114,6 +131,26 @@ public function handleRequest(AIChatRequest $request): AIChatResponse
'data' => $part->content,
],
];
} elseif ($part instanceof ToolCallsPart) {
foreach ($part->toolCalls as $toolCall) {
$message['content'][] = [
'type' => 'tool_use',
'id' => $toolCall->id,
'name' => $toolCall->name,
'input' => $toolCall->arguments,
];
}
} elseif ($part instanceof ToolCallPart) {
$message['content'][] = [
'type' => 'tool_result',
'tool_use_id' => $part->toolCallId,
'content' => [
[
'type' => 'text',
'text' => $part->content,
],
],
];
} else {
throw new \Exception('Not supported message part type.');
}
Expand Down Expand Up @@ -149,6 +186,7 @@ public function handleRequest(AIChatRequest $request): AIChatResponse
];
}

/** @var Parameters $parameters */
if ($request instanceof AIChatStreamedRequest) {
return $this->createStreamed($request, $parameters);
}
Expand All @@ -163,7 +201,22 @@ private function create(AIChatRequest $request, array $parameters): AIChatRespon
{
$result = $this->client->messages()->create($parameters);

$content = $result->content[0]->text ?? '';
$content = '';
$toolCalls = [];

foreach ($result->content as $contentBlock) {
if ('text' === $contentBlock->type) {
$content .= $contentBlock->text ?? '';
} elseif ('tool_use' === $contentBlock->type && null !== $contentBlock->toolUse) {
$toolCalls[] = new AIChatToolCall(
ToolTypeEnum::FUNCTION,
$contentBlock->toolUse->id,
$contentBlock->toolUse->name,
$contentBlock->toolUse->input,
);
}
}

if ('json' === $request->getFormat() && \str_ends_with($content, '}')) {
$content = '{' . $content;
}
Expand All @@ -173,6 +226,7 @@ private function create(AIChatRequest $request, array $parameters): AIChatRespon
new AIChatResponseMessage(
AIChatMessageRoleEnum::from($result->role),
$content,
[] !== $toolCalls ? $toolCalls : null,
),
new Usage(
$result->usage->promptTokens,
Expand Down Expand Up @@ -243,8 +297,7 @@ private function createStreamedMessages(\Iterator $responses, string $prefix, St

public function supports(object $request): bool
{
return $request instanceof AIChatRequest
&& !$request->hasTools();
return $request instanceof AIChatRequest;
}

public function supportsResponseFormat(ResponseFormatInterface $responseFormat): bool
Expand Down
146 changes: 146 additions & 0 deletions packages/anthropic-adapter/src/Chat/ToolFormatter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
<?php

declare(strict_types=1);

/*
* This file is part of the Modelflow AI package.
*
* (c) Johannes Wachter <johannes@sulu.io>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace ModelflowAi\AnthropicAdapter\Chat;

use ModelflowAi\Chat\ToolInfo\Parameter;
use ModelflowAi\Chat\ToolInfo\ToolInfo;

final class ToolFormatter
{
/**
* @return array{
* name: string,
* description: string,
* input_schema: array{
* type: string,
* properties: array<string, array<string, mixed>>,
* required?: string[],
* },
* }
*/
public static function formatTool(ToolInfo $tool): array
{
$parameters = [];
foreach ($tool->parameters as $parameter) {
$param = self::formatParameter($parameter);
$parameters[$parameter->name] = $param;
}

$requiredParameters = [];
foreach ($tool->requiredParameters as $requiredParameter) {
$requiredParameters[] = $requiredParameter->name;
}

return [
'name' => $tool->name,
'description' => $tool->description,
'input_schema' => [
'type' => 'object',
'properties' => $parameters,
'required' => $requiredParameters,
],
];
}

/**
* @param ToolInfo[] $tools
*
* @return array<array{
* name: string,
* description: string,
* input_schema: array{
* type: string,
* properties: array<string, array<string, mixed>>,
* required?: string[],
* },
* }>
*/
public static function formatTools(array $tools): array
{
return \array_map(
self::formatTool(...),
$tools,
);
}

/**
* @return array<string, mixed>
*/
private static function formatParameter(Parameter $parameter): array
{
$type = $parameter->nullable ? [$parameter->type, 'null'] : $parameter->type;

$param = [
'type' => $type,
'description' => $parameter->description,
];

if ('array' === $parameter->type) {
if (null === $parameter->itemsOrProperties) {
throw new \Exception('Array type parameter must have items description. Define a type or use the Parameter class for object.');
}

if (\is_string($parameter->itemsOrProperties)) {
$param['items'] = [
'type' => $parameter->itemsOrProperties,
];
} else {
$properties = [];
/** @var Parameter $property */
foreach ($parameter->itemsOrProperties as $property) {
$properties[$property->name] = self::formatParameter($property);
}

$items = [
'type' => 'object',
'properties' => $properties,
];

if ([] !== $parameter->required) {
$items['required'] = $parameter->required;
}

$param['items'] = $items;
}
}

if ('object' === $parameter->type) {
if (!\is_array($parameter->itemsOrProperties)) {
throw new \Exception('Object type parameter must have properties description. You need to pass an array of Parameter.');
}

$properties = [];
/** @var Parameter $item */
foreach ($parameter->itemsOrProperties as $item) {
$properties[$item->name] = self::formatParameter($item);
}

$param['properties'] = $properties;

if ([] !== $parameter->required) {
$param['required'] = $parameter->required;
}
}

if ([] !== $parameter->enum) {
$param['enum'] = $parameter->enum;
}

if (null !== $parameter->format) {
$param['format'] = $parameter->format;
}

return $param;
}
}
Loading
Loading