main: 引入 AgentProvider 流式事件与 OpenAI 兼容适配
- 增加流式事件流支持,Provider 输出 `message.delta` 等事件 - 实现 OpenAI 兼容适配器,包括 RequestBuilder、ApiClient 等模块 - 更新 Agent Run 逻辑,支持流式增量写入与模型完成状态管理 - 扩展配置项 `agent.openai.*`,支持模型、密钥等配置 - 优化文档,完善流式事件与消息类型说明 - 增加单元测试,覆盖 Provider 和 OpenAI 适配相关逻辑 - 更新环境变量与配置示例,支持新功能
This commit is contained in:
102
app/Services/Agent/OpenAi/OpenAiChatCompletionsAdapter.php
Normal file
102
app/Services/Agent/OpenAi/OpenAiChatCompletionsAdapter.php
Normal file
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Agent\OpenAi;
|
||||
|
||||
use App\Services\Agent\AgentContext;
|
||||
use App\Services\Agent\AgentPlatformAdapterInterface;
|
||||
use App\Services\Agent\ProviderEvent;
|
||||
use App\Services\Agent\ProviderEventType;
|
||||
use App\Services\Agent\ProviderException;
|
||||
|
||||
class OpenAiChatCompletionsAdapter implements AgentPlatformAdapterInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ChatCompletionsRequestBuilder $requestBuilder,
|
||||
private readonly OpenAiApiClient $apiClient,
|
||||
private readonly OpenAiStreamParser $streamParser,
|
||||
private readonly OpenAiEventNormalizer $eventNormalizer,
|
||||
private ?int $retryTimes = null,
|
||||
private ?int $retryBackoffMs = null,
|
||||
) {
|
||||
$this->retryTimes = $this->retryTimes ?? (int) config('agent.provider.retry_times', 1);
|
||||
$this->retryBackoffMs = $this->retryBackoffMs ?? (int) config('agent.provider.retry_backoff_ms', 500);
|
||||
}
|
||||
|
||||
/**
|
||||
* Streams OpenAI-compatible chat completions and yields normalized events.
|
||||
*
|
||||
* @param array<string, mixed> $options
|
||||
* @return \Generator<int, ProviderEvent>
|
||||
*/
|
||||
public function stream(AgentContext $context, array $options = []): \Generator
|
||||
{
|
||||
$payload = $this->requestBuilder->build($context, $options);
|
||||
$attempts = $this->retryTimes + 1;
|
||||
$attempt = 1;
|
||||
$backoffMs = $this->retryBackoffMs;
|
||||
$hasYielded = false;
|
||||
$shouldStop = $options['should_stop'] ?? null;
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
$response = $this->apiClient->openStream($payload);
|
||||
$stream = $response->getBody();
|
||||
|
||||
try {
|
||||
foreach ($this->streamParser->parse($stream, is_callable($shouldStop) ? $shouldStop : null) as $chunk) {
|
||||
$events = $this->eventNormalizer->normalize($chunk);
|
||||
foreach ($events as $event) {
|
||||
$hasYielded = true;
|
||||
yield $event;
|
||||
if ($event->type === ProviderEventType::Done || $event->type === ProviderEventType::Error) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
$stream->close();
|
||||
}
|
||||
|
||||
if (! $hasYielded) {
|
||||
if (is_callable($shouldStop) && $shouldStop()) {
|
||||
return;
|
||||
}
|
||||
yield ProviderEvent::error('EMPTY_STREAM', 'Agent provider returned empty stream');
|
||||
}
|
||||
|
||||
return;
|
||||
} catch (ProviderException $exception) {
|
||||
if (! $hasYielded && is_callable($shouldStop) && $shouldStop()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! $hasYielded && $exception->retryable && $attempt < $attempts) {
|
||||
usleep($backoffMs * 1000);
|
||||
$attempt++;
|
||||
$backoffMs *= 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
yield ProviderEvent::error($exception->errorCode, $exception->getMessage(), [
|
||||
'retryable' => $exception->retryable,
|
||||
'http_status' => $exception->httpStatus,
|
||||
'raw_message' => $exception->rawMessage,
|
||||
]);
|
||||
|
||||
return;
|
||||
} catch (\Throwable $exception) {
|
||||
if (! $hasYielded && is_callable($shouldStop) && $shouldStop()) {
|
||||
return;
|
||||
}
|
||||
|
||||
yield ProviderEvent::error('UNKNOWN_ERROR', $exception->getMessage());
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function name(): string
|
||||
{
|
||||
return 'openai.chat.completions';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user