main: 增强工具调用与消息流程
- 支持 tool.call 和 tool.result 消息类型处理 - 引入 Tool 调度与执行逻辑,支持超时与结果截断 - 增加 ToolRegistry 和 ToolExecutor 管理工具定义与执行 - 更新上下文构建与消息映射逻辑,适配工具闭环处理 - 扩展配置与环境变量,支持 Tool 调用相关选项 - 增强单元测试覆盖工具调用与执行情景 - 更新文档和 OpenAPI,新增工具相关说明与模型定义
This commit is contained in:
@@ -5,7 +5,7 @@ namespace App\Services\Agent;
|
||||
final class AgentContext
|
||||
{
|
||||
/**
|
||||
* @param array<int, array{message_id: string, role: string, type: string, content: ?string, seq: int}> $messages
|
||||
* @param array<int, array{message_id: string, role: string, type: string, content: ?string, payload: ?array, seq: int}> $messages
|
||||
*/
|
||||
public function __construct(
|
||||
public string $runId,
|
||||
|
||||
@@ -4,19 +4,23 @@ namespace App\Services\Agent\OpenAi;
|
||||
|
||||
use App\Models\Message;
|
||||
use App\Services\Agent\AgentContext;
|
||||
use App\Services\Tool\ToolRegistry;
|
||||
|
||||
class ChatCompletionsRequestBuilder
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ToolRegistry $toolRegistry,
|
||||
private ?string $model = null,
|
||||
private ?float $temperature = null,
|
||||
private ?float $topP = null,
|
||||
private ?bool $includeUsage = null,
|
||||
private ?string $toolChoice = null,
|
||||
) {
|
||||
$this->model = $this->model ?? (string) config('agent.openai.model', 'gpt-4o-mini');
|
||||
$this->temperature = $this->temperature ?? (float) config('agent.openai.temperature', 0.7);
|
||||
$this->topP = $this->topP ?? (float) config('agent.openai.top_p', 1.0);
|
||||
$this->includeUsage = $this->includeUsage ?? (bool) config('agent.openai.include_usage', false);
|
||||
$this->toolChoice = $this->toolChoice ?? (string) config('agent.tools.tool_choice', 'auto');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -63,11 +67,18 @@ class ChatCompletionsRequestBuilder
|
||||
$payload['response_format'] = $options['response_format'];
|
||||
}
|
||||
|
||||
$toolsSpec = $this->toolRegistry->openAiToolsSpec();
|
||||
|
||||
if (! empty($toolsSpec)) {
|
||||
$payload['tools'] = $toolsSpec;
|
||||
$payload['tool_choice'] = $options['tool_choice'] ?? $this->toolChoice ?? 'auto';
|
||||
}
|
||||
|
||||
return $payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array{role: string, content: string}>
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
private function buildMessages(AgentContext $context): array
|
||||
{
|
||||
@@ -83,15 +94,35 @@ class ChatCompletionsRequestBuilder
|
||||
foreach ($context->messages as $message) {
|
||||
$role = $this->mapRole((string) ($message['role'] ?? ''));
|
||||
$content = $message['content'] ?? null;
|
||||
$type = (string) ($message['type'] ?? '');
|
||||
$payload = $message['payload'] ?? null;
|
||||
|
||||
if (! $role || ! is_string($content) || $content === '') {
|
||||
$content = null;
|
||||
}
|
||||
|
||||
if ($type === 'tool.call' && is_array($payload)) {
|
||||
$toolCall = $this->normalizeToolCallPayload($payload, $content);
|
||||
if ($toolCall) {
|
||||
$messages[] = $toolCall;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
$messages[] = [
|
||||
'role' => $role,
|
||||
'content' => $content,
|
||||
];
|
||||
if ($type === 'tool.result' && is_array($payload)) {
|
||||
$toolResult = $this->normalizeToolResultPayload($payload, $content);
|
||||
if ($toolResult) {
|
||||
$messages[] = $toolResult;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($content !== null) {
|
||||
$messages[] = [
|
||||
'role' => $role,
|
||||
'content' => $content,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $messages;
|
||||
@@ -106,4 +137,63 @@ class ChatCompletionsRequestBuilder
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $payload
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
private function normalizeToolCallPayload(array $payload, ?string $content): ?array
|
||||
{
|
||||
$toolCallId = $payload['tool_call_id'] ?? null;
|
||||
$name = $payload['name'] ?? null;
|
||||
$arguments = $payload['arguments'] ?? null;
|
||||
|
||||
if (! is_string($toolCallId) || ! is_string($name) || $toolCallId === '' || $name === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$argumentsString = is_string($arguments) ? $arguments : json_encode($arguments, JSON_UNESCAPED_UNICODE);
|
||||
|
||||
return [
|
||||
'role' => 'assistant',
|
||||
'content' => $content,
|
||||
'tool_calls' => [
|
||||
[
|
||||
'id' => $toolCallId,
|
||||
'type' => 'function',
|
||||
'function' => [
|
||||
'name' => $name,
|
||||
'arguments' => $argumentsString ?? '',
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $payload
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
private function normalizeToolResultPayload(array $payload, ?string $content): ?array
|
||||
{
|
||||
$toolCallId = $payload['tool_call_id'] ?? null;
|
||||
$name = $payload['name'] ?? null;
|
||||
|
||||
if (! is_string($toolCallId) || $toolCallId === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$resultContent = $content ?? ($payload['output'] ?? null);
|
||||
|
||||
if (! is_string($resultContent)) {
|
||||
$resultContent = json_encode($payload['output'] ?? $payload, JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
|
||||
return [
|
||||
'role' => 'tool',
|
||||
'tool_call_id' => $toolCallId,
|
||||
'name' => is_string($name) ? $name : null,
|
||||
'content' => $resultContent,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,25 @@ class OpenAiEventNormalizer
|
||||
if (is_string($content) && $content !== '') {
|
||||
$events[] = ProviderEvent::messageDelta($content);
|
||||
}
|
||||
|
||||
if (isset($delta['tool_calls']) && is_array($delta['tool_calls'])) {
|
||||
$toolCalls = [];
|
||||
foreach ($delta['tool_calls'] as $toolCall) {
|
||||
if (! is_array($toolCall)) {
|
||||
continue;
|
||||
}
|
||||
$toolCalls[] = [
|
||||
'id' => $toolCall['id'] ?? null,
|
||||
'name' => $toolCall['function']['name'] ?? null,
|
||||
'arguments' => $toolCall['function']['arguments'] ?? '',
|
||||
'index' => $toolCall['index'] ?? null,
|
||||
];
|
||||
}
|
||||
|
||||
if (! empty($toolCalls)) {
|
||||
$events[] = ProviderEvent::toolDelta(['tool_calls' => $toolCalls]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (is_array($firstChoice) && array_key_exists('finish_reason', $firstChoice) && $firstChoice['finish_reason'] !== null) {
|
||||
|
||||
Reference in New Issue
Block a user