2026-07-25 12:04:06 +08:00
|
|
|
package ai
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"fmt"
|
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
|
|
openai "github.com/openai/openai-go/v3"
|
|
|
|
|
"github.com/openai/openai-go/v3/shared"
|
|
|
|
|
|
2026-08-21 00:41:07 +08:00
|
|
|
"code.tczkiot.com/wlw/ai-agent/internal/models"
|
2026-07-25 12:04:06 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type ToolDefinition struct {
|
|
|
|
|
Name string
|
|
|
|
|
Description string
|
|
|
|
|
Parameters map[string]any
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type ToolCall struct {
|
|
|
|
|
ID string
|
|
|
|
|
Name string
|
|
|
|
|
Arguments string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type ToolCallExecutor func(context.Context, ToolCall) (string, error)
|
|
|
|
|
|
|
|
|
|
type ToolLoopResult struct {
|
|
|
|
|
ChatCompletionResult
|
|
|
|
|
ToolCalls []ToolCall
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ChatWithTools executes a bounded OpenAI-compatible function-calling loop.
|
|
|
|
|
// Tool execution stays in the caller so business operations remain behind the
|
|
|
|
|
// application Tool Registry and Service layer.
|
|
|
|
|
func (s *llm) ChatWithTools(ctx context.Context, config models.AIConfig, systemPrompt, userPrompt string, definitions []ToolDefinition, maxSteps int, execute ToolCallExecutor) (*ToolLoopResult, error) {
|
2026-08-28 22:23:13 +08:00
|
|
|
ctx = ensurePlatformAIRequestScope(ctx)
|
2026-07-25 12:04:06 +08:00
|
|
|
if len(definitions) == 0 || execute == nil {
|
|
|
|
|
result, err := s.ChatWithConfig(ctx, config, systemPrompt, userPrompt)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
return &ToolLoopResult{ChatCompletionResult: *result}, nil
|
|
|
|
|
}
|
|
|
|
|
if maxSteps <= 0 {
|
|
|
|
|
maxSteps = 5
|
|
|
|
|
}
|
|
|
|
|
messages := []openai.ChatCompletionMessageParamUnion{}
|
|
|
|
|
if strings.TrimSpace(systemPrompt) != "" {
|
|
|
|
|
messages = append(messages, openai.ChatCompletionMessageParamUnion{OfSystem: &openai.ChatCompletionSystemMessageParam{Content: openai.ChatCompletionSystemMessageParamContentUnion{OfString: openai.String(systemPrompt)}}})
|
|
|
|
|
}
|
|
|
|
|
messages = append(messages, openai.ChatCompletionMessageParamUnion{OfUser: &openai.ChatCompletionUserMessageParam{Content: openai.ChatCompletionUserMessageParamContentUnion{OfString: openai.String(userPrompt)}}})
|
|
|
|
|
tools := make([]openai.ChatCompletionToolUnionParam, 0, len(definitions))
|
|
|
|
|
for _, definition := range definitions {
|
|
|
|
|
tools = append(tools, openai.ChatCompletionToolUnionParam{OfFunction: &openai.ChatCompletionFunctionToolParam{Function: shared.FunctionDefinitionParam{Name: definition.Name, Description: openai.String(definition.Description), Parameters: shared.FunctionParameters(definition.Parameters)}}})
|
|
|
|
|
}
|
|
|
|
|
client := newOpenAIClient(config)
|
|
|
|
|
allCalls := make([]ToolCall, 0)
|
|
|
|
|
for step := 0; step < maxSteps; step++ {
|
|
|
|
|
params := openai.ChatCompletionNewParams{Messages: messages, Model: shared.ChatModel(config.ModelName), Tools: tools}
|
|
|
|
|
if config.MaxOutputTokens > 0 {
|
|
|
|
|
params.MaxCompletionTokens = openai.Int(int64(config.MaxOutputTokens))
|
|
|
|
|
}
|
|
|
|
|
applyProviderSpecificChatParams(¶ms, config)
|
2026-08-28 22:23:13 +08:00
|
|
|
response, err := client.Chat.Completions.New(ctx, params, platformRequestOptions(ctx, config, "chat.tool-loop")...)
|
2026-07-25 12:04:06 +08:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("tool loop chat completion failed: %w", err)
|
|
|
|
|
}
|
|
|
|
|
if len(response.Choices) == 0 {
|
|
|
|
|
return nil, fmt.Errorf("tool loop returned no choices")
|
|
|
|
|
}
|
|
|
|
|
message := response.Choices[0].Message
|
|
|
|
|
if len(message.ToolCalls) == 0 {
|
|
|
|
|
return &ToolLoopResult{ChatCompletionResult: ChatCompletionResult{Content: strings.TrimSpace(message.Content), ModelName: config.ModelName, PromptTokens: int(response.Usage.PromptTokens), CompletionTokens: int(response.Usage.CompletionTokens)}, ToolCalls: allCalls}, nil
|
|
|
|
|
}
|
|
|
|
|
messages = append(messages, message.ToParam())
|
|
|
|
|
for _, rawCall := range message.ToolCalls {
|
|
|
|
|
call := rawCall.AsFunction()
|
|
|
|
|
if call.ID == "" || call.Function.Name == "" {
|
|
|
|
|
return nil, fmt.Errorf("tool loop received unsupported tool call")
|
|
|
|
|
}
|
|
|
|
|
toolCall := ToolCall{ID: call.ID, Name: call.Function.Name, Arguments: call.Function.Arguments}
|
|
|
|
|
allCalls = append(allCalls, toolCall)
|
|
|
|
|
output, callErr := execute(ctx, toolCall)
|
|
|
|
|
if callErr != nil {
|
|
|
|
|
output = "tool execution failed: " + callErr.Error()
|
|
|
|
|
}
|
|
|
|
|
messages = append(messages, openai.ChatCompletionMessageParamUnion{OfTool: &openai.ChatCompletionToolMessageParam{ToolCallID: call.ID, Content: openai.ChatCompletionToolMessageParamContentUnion{OfString: openai.String(output)}}})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil, fmt.Errorf("tool loop exceeded maximum steps")
|
|
|
|
|
}
|