Skip to content

Commit b6d9873

Browse files
committed
wip
1 parent 84084fe commit b6d9873

File tree

5 files changed

+187
-111
lines changed

5 files changed

+187
-111
lines changed

src/LLM/AbstractLLM.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -434,7 +434,7 @@ protected function applyFormatInstructionsIfApplicable(
434434
MessageCollection $messages,
435435
): MessageCollection {
436436
if ($this->shouldApplyFormatInstructions && $formatInstructions = $this->outputParser?->formatInstructions()) {
437-
$messages = static::applyFormatInstructions($messages, $formatInstructions);
437+
return static::applyFormatInstructions($messages, $formatInstructions);
438438
}
439439

440440
return $messages;
@@ -443,6 +443,8 @@ protected function applyFormatInstructionsIfApplicable(
443443
/**
444444
* Resolve the messages to be used for the LLM.
445445
*
446+
* @param \Cortex\LLM\Data\Messages\MessageCollection|\Cortex\LLM\Contracts\Message|non-empty-array<int, \Cortex\LLM\Contracts\Message|\Cortex\LLM\Data\Messages\MessagePlaceholder>|string $messages
447+
*
446448
* @throws \Cortex\Exceptions\LLMException If no messages are provided
447449
*
448450
* @return \Cortex\LLM\Data\Messages\MessageCollection<int, \Cortex\LLM\Contracts\Message>

src/LLM/Drivers/AnthropicChat.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
use Throwable;
99
use JsonException;
1010
use DateTimeImmutable;
11-
use Cortex\Support\Utils;
1211
use Cortex\LLM\Data\Usage;
1312
use Cortex\LLM\AbstractLLM;
1413
use Illuminate\Support\Arr;

src/LLM/Drivers/OpenAIResponses.php

Lines changed: 119 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,9 @@
44

55
namespace Cortex\LLM\Drivers;
66

7-
use Generator;
7+
use Exception;
88
use Throwable;
99
use DateTimeImmutable;
10-
use Cortex\Support\Utils;
1110
use Cortex\LLM\Data\Usage;
1211
use Cortex\LLM\AbstractLLM;
1312
use Illuminate\Support\Arr;
@@ -20,7 +19,6 @@
2019
use Cortex\Events\ChatModelStart;
2120
use Cortex\LLM\Contracts\Message;
2221
use Cortex\LLM\Data\FunctionCall;
23-
use Cortex\Events\ChatModelStream;
2422
use Cortex\LLM\Enums\FinishReason;
2523
use Cortex\Exceptions\LLMException;
2624
use Cortex\LLM\Data\ChatGeneration;
@@ -30,12 +28,9 @@
3028
use Cortex\LLM\Data\ResponseMetadata;
3129
use OpenAI\Contracts\ResponseContract;
3230
use Cortex\LLM\Data\ToolCallCollection;
33-
use Cortex\LLM\Data\ChatGenerationChunk;
3431
use Cortex\ModelInfo\Enums\ModelFeature;
3532
use Cortex\LLM\Data\Messages\ToolMessage;
3633
use Cortex\ModelInfo\Enums\ModelProvider;
37-
use Cortex\OutputParsers\JsonOutputParser;
38-
use Cortex\Exceptions\OutputParserException;
3934
use Cortex\LLM\Data\Messages\AssistantMessage;
4035
use OpenAI\Responses\Responses\CreateResponse;
4136
use Cortex\LLM\Data\Messages\MessageCollection;
@@ -44,15 +39,10 @@
4439
use Cortex\LLM\Data\Messages\Content\AudioContent;
4540
use Cortex\LLM\Data\Messages\Content\ImageContent;
4641
use OpenAI\Responses\Responses\CreateResponseUsage;
47-
use OpenAI\Responses\Responses\CreateResponseChoice;
4842
use OpenAI\Responses\Responses\Output\OutputMessage;
4943
use Cortex\LLM\Data\Messages\Content\ReasoningContent;
50-
use OpenAI\Responses\Responses\CreateResponseToolCall;
5144
use OpenAI\Responses\Responses\Output\OutputReasoning;
52-
use OpenAI\Responses\Responses\Output\OutputComputerToolCall;
5345
use OpenAI\Responses\Responses\Output\OutputFunctionToolCall;
54-
use OpenAI\Responses\Responses\Output\OutputWebSearchToolCall;
55-
use OpenAI\Responses\Responses\Output\OutputFileSearchToolCall;
5646
use OpenAI\Responses\Responses\Output\OutputMessageContentRefusal;
5747
use OpenAI\Responses\Responses\Output\OutputMessageContentOutputText;
5848
use OpenAI\Testing\Responses\Fixtures\Responses\CreateResponseFixture;
@@ -101,24 +91,24 @@ protected function mapResponse(CreateResponse $response): ChatResult
10191
$finishReason = static::mapFinishReason($response->status);
10292

10393
/** @var \OpenAI\Responses\Responses\Output\OutputMessage $outputMessage */
104-
$outputMessage = $output->filter(fn(ResponseContract $item) => $item instanceof OutputMessage)->first();
94+
$outputMessage = $output->filter(fn(ResponseContract $item): bool => $item instanceof OutputMessage)->first();
10595

10696
$outputMessageContent = collect($outputMessage->content);
10797

108-
if ($outputMessageContent->contains(fn(ResponseContract $item) => $item instanceof OutputMessageContentRefusal)) {
98+
if ($outputMessageContent->contains(fn(ResponseContract $item): bool => $item instanceof OutputMessageContentRefusal)) {
10999
throw new LLMException('LLM refusal: ' . $outputMessageContent->first()->refusal);
110100
}
111101

112102
/** @var \OpenAI\Responses\Responses\Output\OutputMessageContentOutputText $textContent */
113103
$textContent = $outputMessageContent
114-
->filter(fn(ResponseContract $item) => $item instanceof OutputMessageContentOutputText)
104+
->filter(fn(ResponseContract $item): bool => $item instanceof OutputMessageContentOutputText)
115105
->first();
116106

117107
// TODO: Ignore other provider specific tool calls
118108
// and only support function tool calls for now
119109
$toolCalls = $output
120-
->filter(fn(ResponseContract $item) => $item instanceof OutputFunctionToolCall)
121-
->map(fn(OutputFunctionToolCall $toolCall) => new ToolCall(
110+
->filter(fn(ResponseContract $item): bool => $item instanceof OutputFunctionToolCall)
111+
->map(fn(OutputFunctionToolCall $toolCall): ToolCall => new ToolCall(
122112
$toolCall->id,
123113
new FunctionCall(
124114
$toolCall->name,
@@ -128,16 +118,15 @@ protected function mapResponse(CreateResponse $response): ChatResult
128118
->all();
129119

130120
$reasoningContent = $output
131-
->filter(fn(ResponseContract $item) => $item instanceof OutputReasoning)
132-
->map(fn(OutputReasoning $reasoning) => new ReasoningContent(
121+
->filter(fn(ResponseContract $item): bool => $item instanceof OutputReasoning)
122+
->map(fn(OutputReasoning $reasoning): ReasoningContent => new ReasoningContent(
133123
$reasoning->id,
134-
$reasoning->summary[0]->text,
124+
Arr::first($reasoning->summary)->text ?? '',
135125
))
136126
->all();
137127

138128
$generation = new ChatGeneration(
139129
message: new AssistantMessage(
140-
id: $outputMessage->id,
141130
content: [
142131
...$reasoningContent,
143132
new TextContent($textContent->text),
@@ -150,6 +139,7 @@ protected function mapResponse(CreateResponse $response): ChatResult
150139
finishReason: $finishReason,
151140
usage: $usage,
152141
),
142+
id: $outputMessage->id,
153143
),
154144
index: 0,
155145
createdAt: DateTimeImmutable::createFromFormat('U', (string) $response->createdAt),
@@ -164,7 +154,7 @@ protected function mapResponse(CreateResponse $response): ChatResult
164154
$result = new ChatResult(
165155
[$generation],
166156
$usage,
167-
$rawResponse,
157+
$rawResponse, // @phpstan-ignore argument.type
168158
);
169159

170160
$this->dispatchEvent(new ChatModelEnd($result));
@@ -175,14 +165,13 @@ protected function mapResponse(CreateResponse $response): ChatResult
175165
/**
176166
* Map a streaming response to a ChatStreamResult.
177167
*
178-
* @param StreamResponse<\OpenAI\Responses\Chat\CreateStreamedResponse> $response
168+
* @param StreamResponse<\OpenAI\Responses\Responses\CreateStreamedResponse> $response
179169
*
180-
* @return ChatStreamResult<ChatGenerationChunk>
170+
* @return \Cortex\LLM\Data\ChatStreamResult<\Cortex\LLM\Data\ChatGenerationChunk>
181171
*/
182172
protected function mapStreamResponse(StreamResponse $response): ChatStreamResult
183173
{
184-
throw new \Exception('Not implemented');
185-
174+
throw new Exception('Not implemented');
186175
// return new ChatStreamResult(function () use ($response): Generator {
187176
// $contentSoFar = '';
188177
// $toolCallsSoFar = [];
@@ -301,80 +290,124 @@ protected function mapUsage(CreateResponseUsage $usage): Usage
301290
protected function mapMessagesForInput(MessageCollection $messages): array
302291
{
303292
return $messages
304-
->map(function (Message $message) {
305-
// if ($message instanceof ToolMessage) {
306-
// return [
307-
// 'tool_call_id' => $message->id,
308-
// 'role' => $message->role->value,
309-
// 'content' => $message->content,
310-
// ];
311-
// }
293+
->map(function (Message $message): array {
294+
// Handle ToolMessage specifically for Responses API
295+
if ($message instanceof ToolMessage) {
296+
return [
297+
'role' => $message->role->value,
298+
'content' => $this->mapContentForResponsesAPI($message->content()),
299+
'tool_call_id' => $message->id,
300+
];
301+
}
312302

303+
// Handle AssistantMessage with tool calls
313304
if ($message instanceof AssistantMessage && $message->toolCalls?->isNotEmpty()) {
314-
$formattedMessage = $message->toArray();
315-
316-
// Ensure the function arguments are encoded as a string
317-
foreach ($message->toolCalls as $index => $toolCall) {
318-
Arr::set(
319-
$formattedMessage,
320-
'tool_calls.' . $index . '.function.arguments',
321-
json_encode($toolCall->function->arguments),
322-
);
323-
}
305+
$baseMessage = [
306+
'role' => $message->role->value,
307+
];
324308

325-
return $formattedMessage;
326-
}
309+
// Add content if present
310+
if ($message->content() !== null) {
311+
$baseMessage['content'] = $this->mapContentForResponsesAPI($message->content());
312+
}
327313

328-
$formattedMessage = $message->toArray();
329-
330-
if (isset($formattedMessage['content']) && is_array($formattedMessage['content'])) {
331-
$formattedMessage['content'] = array_map(function (mixed $content) {
332-
if ($content instanceof ImageContent) {
333-
$this->supportsFeatureOrFail(ModelFeature::Vision);
334-
335-
return [
336-
'type' => 'image_url',
337-
'image_url' => [
338-
'url' => $content->url,
339-
],
340-
];
341-
}
342-
343-
if ($content instanceof AudioContent) {
344-
$this->supportsFeatureOrFail(ModelFeature::AudioInput);
345-
346-
return [
347-
'type' => 'input_audio',
348-
'input_audio' => [
349-
'data' => $content->base64Data,
350-
'format' => $content->format,
351-
],
352-
];
353-
}
354-
355-
return match (true) {
356-
$content instanceof TextContent => [
357-
'type' => 'text',
358-
'text' => $content->text,
359-
],
360-
$content instanceof FileContent => [
361-
'type' => 'file',
362-
'file' => [
363-
'filename' => $content->fileName,
364-
'file_data' => $content->toDataUrl()->toString(),
365-
],
314+
// Add tool calls
315+
$baseMessage['tool_calls'] = $message->toolCalls->map(function (ToolCall $toolCall): array {
316+
return [
317+
'id' => $toolCall->id,
318+
'type' => 'function',
319+
'function' => [
320+
'name' => $toolCall->function->name,
321+
'arguments' => json_encode($toolCall->function->arguments),
366322
],
367-
default => $content,
368-
};
369-
}, $formattedMessage['content']);
323+
];
324+
})->toArray();
325+
326+
return $baseMessage;
370327
}
371328

329+
// Handle all other message types
330+
$formattedMessage = [
331+
'role' => $message->role()->value,
332+
'content' => $this->mapContentForResponsesAPI($message->content()),
333+
];
334+
372335
return $formattedMessage;
373336
})
374337
->values()
375338
->toArray();
376339
}
377340

341+
/**
342+
* Map content to the OpenAI Responses API format.
343+
*
344+
* @param string|array<int, \Cortex\LLM\Contracts\Content>|null $content
345+
*
346+
* @return array<int, array<string, mixed>>
347+
*/
348+
protected function mapContentForResponsesAPI(string|array|null $content): array
349+
{
350+
if ($content === null) {
351+
return [];
352+
}
353+
354+
if (is_string($content)) {
355+
return [
356+
[
357+
'type' => 'input_text',
358+
'text' => $content,
359+
],
360+
];
361+
}
362+
363+
return array_map(function (mixed $item): array {
364+
if ($item instanceof ImageContent) {
365+
$this->supportsFeatureOrFail(ModelFeature::Vision);
366+
367+
return [
368+
'type' => 'input_image',
369+
'image_url' => $item->url,
370+
'detail' => 'auto', // Default detail level
371+
];
372+
}
373+
374+
if ($item instanceof AudioContent) {
375+
$this->supportsFeatureOrFail(ModelFeature::AudioInput);
376+
377+
return [
378+
'type' => 'input_audio',
379+
'data' => $item->base64Data,
380+
'format' => $item->format,
381+
];
382+
}
383+
384+
if ($item instanceof FileContent) {
385+
return [
386+
'type' => 'input_file',
387+
'file_id' => $item->fileName, // Assuming file_id should be the fileName
388+
];
389+
}
390+
391+
if ($item instanceof TextContent) {
392+
return [
393+
'type' => 'input_text',
394+
'text' => $item->text ?? '',
395+
];
396+
}
397+
398+
// Handle ReasoningContent and ToolContent
399+
if ($item instanceof ReasoningContent) {
400+
return [
401+
'type' => 'input_text',
402+
'text' => $item->reasoning,
403+
];
404+
}
405+
406+
// Fallback for unknown content types
407+
return [];
408+
}, $content);
409+
}
410+
378411
protected static function mapFinishReason(?string $finishReason): ?FinishReason
379412
{
380413
if ($finishReason === null) {

0 commit comments

Comments
 (0)