main: 引入 AgentProvider 流式事件与 OpenAI 兼容适配

- 增加流式事件流支持,Provider 输出 `message.delta` 等事件
- 实现 OpenAI 兼容适配器,包括 RequestBuilder、ApiClient 等模块
- 更新 Agent Run 逻辑,支持流式增量写入与模型完成状态管理
- 扩展配置项 `agent.openai.*`,支持模型、密钥等配置
- 优化文档,完善流式事件与消息类型说明
- 增加单元测试,覆盖 Provider 和 OpenAI 适配相关逻辑
- 更新环境变量与配置示例,支持新功能
This commit is contained in:
2025-12-19 02:35:37 +08:00
parent 56523c1f0a
commit 8c4ad80dab
27 changed files with 1006 additions and 166 deletions

View File

@@ -4,8 +4,9 @@ namespace Tests\Feature;
use App\Jobs\AgentRunJob;
use App\Models\Message;
use App\Services\Agent\AgentContext;
use App\Services\Agent\AgentProviderInterface;
use App\Services\Agent\ProviderException;
use App\Services\Agent\ProviderEvent;
use App\Services\CancelChecker;
use App\Services\ChatService;
use App\Services\RunDispatcher;
@@ -204,14 +205,17 @@ class AgentRunTest extends TestCase
$this->assertTrue($messages->contains(fn ($m) => $m->type === 'run.status' && ($m->payload['status'] ?? null) === 'CANCELED'));
}
public function test_provider_exception_writes_error_and_failed_status(): void
public function test_provider_error_event_writes_error_and_failed_status(): void
{
Queue::fake();
$this->app->bind(AgentProviderInterface::class, function () {
return new class implements AgentProviderInterface {
public function generate(array $context, array $options = []): string
public function stream(AgentContext $context, array $options = []): \Generator
{
throw new ProviderException('HTTP_ERROR', 'provider failed', true, 500, 'boom');
yield ProviderEvent::error('HTTP_ERROR', 'provider failed', [
'retryable' => true,
'http_status' => 500,
]);
}
};
});
@@ -229,16 +233,11 @@ class AgentRunTest extends TestCase
$runId = $dispatcher->dispatchForPrompt($session->session_id, $prompt->message_id);
try {
(new AgentRunJob($session->session_id, $runId))->handle(
app(RunLoop::class),
app(OutputSink::class),
app(CancelChecker::class)
);
$this->fail('Expected provider exception');
} catch (ProviderException $exception) {
$this->assertSame('HTTP_ERROR', $exception->errorCode);
}
(new AgentRunJob($session->session_id, $runId))->handle(
app(RunLoop::class),
app(OutputSink::class),
app(CancelChecker::class)
);
$messages = Message::query()
->where('session_id', $session->session_id)