- 增加流式事件流支持,Provider 输出 `message.delta` 等事件 - 实现 OpenAI 兼容适配器,包括 RequestBuilder、ApiClient 等模块 - 更新 Agent Run 逻辑,支持流式增量写入与模型完成状态管理 - 扩展配置项 `agent.openai.*`,支持模型、密钥等配置 - 优化文档,完善流式事件与消息类型说明 - 增加单元测试,覆盖 Provider 和 OpenAI 适配相关逻辑 - 更新环境变量与配置示例,支持新功能
115 lines
3.8 KiB
PHP
115 lines
3.8 KiB
PHP
<?php
|
|
|
|
namespace Tests\Unit;
|
|
|
|
use App\Models\Message;
|
|
use App\Services\Agent\AgentContext;
|
|
use App\Services\Agent\OpenAi\ChatCompletionsRequestBuilder;
|
|
use App\Services\Agent\OpenAi\OpenAiEventNormalizer;
|
|
use App\Services\Agent\OpenAi\OpenAiStreamParser;
|
|
use App\Services\Agent\ProviderEventType;
|
|
use GuzzleHttp\Psr7\Utils;
|
|
use Tests\TestCase;
|
|
|
|
class OpenAiAdapterTest extends TestCase
|
|
{
|
|
public function test_request_builder_maps_context_to_openai_payload(): void
|
|
{
|
|
config()->set('agent.openai.model', 'test-model');
|
|
config()->set('agent.openai.temperature', 0.2);
|
|
config()->set('agent.openai.top_p', 0.9);
|
|
config()->set('agent.openai.include_usage', true);
|
|
|
|
$context = new AgentContext('run-1', 'session-1', 'system prompt', [
|
|
[
|
|
'message_id' => 'm1',
|
|
'role' => Message::ROLE_USER,
|
|
'type' => 'user.prompt',
|
|
'content' => 'hello',
|
|
'seq' => 1,
|
|
],
|
|
[
|
|
'message_id' => 'm2',
|
|
'role' => Message::ROLE_AGENT,
|
|
'type' => 'agent.message',
|
|
'content' => 'hi',
|
|
'seq' => 2,
|
|
],
|
|
]);
|
|
|
|
$payload = (new ChatCompletionsRequestBuilder())->build($context);
|
|
|
|
$this->assertSame('test-model', $payload['model']);
|
|
$this->assertTrue($payload['stream']);
|
|
$this->assertSame(0.2, $payload['temperature']);
|
|
$this->assertSame(0.9, $payload['top_p']);
|
|
$this->assertSame(['include_usage' => true], $payload['stream_options']);
|
|
$this->assertSame([
|
|
['role' => 'system', 'content' => 'system prompt'],
|
|
['role' => 'user', 'content' => 'hello'],
|
|
['role' => 'assistant', 'content' => 'hi'],
|
|
], $payload['messages']);
|
|
}
|
|
|
|
public function test_event_normalizer_maps_delta_and_done(): void
|
|
{
|
|
$normalizer = new OpenAiEventNormalizer();
|
|
|
|
$delta = json_encode([
|
|
'choices' => [
|
|
[
|
|
'delta' => ['content' => 'Hi'],
|
|
'finish_reason' => null,
|
|
],
|
|
],
|
|
]);
|
|
|
|
$events = $normalizer->normalize($delta);
|
|
$this->assertCount(1, $events);
|
|
$this->assertSame(ProviderEventType::MessageDelta, $events[0]->type);
|
|
$this->assertSame('Hi', $events[0]->payload['text']);
|
|
|
|
$done = json_encode([
|
|
'choices' => [
|
|
[
|
|
'delta' => [],
|
|
'finish_reason' => 'stop',
|
|
],
|
|
],
|
|
]);
|
|
|
|
$events = $normalizer->normalize($done);
|
|
$this->assertCount(1, $events);
|
|
$this->assertSame(ProviderEventType::Done, $events[0]->type);
|
|
$this->assertSame('stop', $events[0]->payload['reason']);
|
|
}
|
|
|
|
public function test_event_normalizer_handles_invalid_json(): void
|
|
{
|
|
$normalizer = new OpenAiEventNormalizer();
|
|
$events = $normalizer->normalize('{invalid');
|
|
|
|
$this->assertCount(1, $events);
|
|
$this->assertSame(ProviderEventType::Error, $events[0]->type);
|
|
$this->assertSame('INVALID_JSON', $events[0]->payload['code']);
|
|
}
|
|
|
|
public function test_event_normalizer_handles_done_marker(): void
|
|
{
|
|
$normalizer = new OpenAiEventNormalizer();
|
|
$events = $normalizer->normalize('[DONE]');
|
|
|
|
$this->assertCount(1, $events);
|
|
$this->assertSame(ProviderEventType::Done, $events[0]->type);
|
|
}
|
|
|
|
public function test_stream_parser_splits_sse_events(): void
|
|
{
|
|
$stream = Utils::streamFor("data: {\"id\":1}\n\ndata: [DONE]\n\n");
|
|
$parser = new OpenAiStreamParser(5);
|
|
$chunks = iterator_to_array($parser->parse($stream));
|
|
|
|
$this->assertSame(['{"id":1}', '[DONE]'], $chunks);
|
|
}
|
|
}
|