feat: 昔涟工具扩展 — OpenAI Function Calling 集成 (网络搜索/网页抓取/IoT设备查询)
This commit is contained in:
@@ -55,11 +55,28 @@ type openAIRequest struct {
|
||||
Temperature float64 `json:"temperature"`
|
||||
MaxTokens int `json:"max_tokens,omitempty"`
|
||||
Stream bool `json:"stream"`
|
||||
Tools []OpenAITool `json:"tools,omitempty"`
|
||||
ToolChoice string `json:"tool_choice,omitempty"` // "auto", "none", or specific tool
|
||||
}
|
||||
|
||||
type openAIMessage struct {
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content"`
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
ToolCalls []openAIToolCall `json:"tool_calls,omitempty"`
|
||||
ToolCallID string `json:"tool_call_id,omitempty"`
|
||||
}
|
||||
|
||||
// openAIToolCall OpenAI工具调用
|
||||
type openAIToolCall struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Function openAIToolCallFunction `json:"function"`
|
||||
}
|
||||
|
||||
type openAIToolCallFunction struct {
|
||||
Name string `json:"name"`
|
||||
Arguments string `json:"arguments"` // JSON string
|
||||
}
|
||||
|
||||
// openAIResponse OpenAI响应结构
|
||||
@@ -74,6 +91,7 @@ type openAIResponse struct {
|
||||
type openAIChoice struct {
|
||||
Index int `json:"index"`
|
||||
Message openAIMessage `json:"message"`
|
||||
Delta openAIMessage `json:"delta,omitempty"`
|
||||
FinishReason string `json:"finish_reason"`
|
||||
}
|
||||
|
||||
@@ -91,12 +109,17 @@ type openAIError struct {
|
||||
|
||||
// Chat 同步对话
|
||||
func (p *OpenAIProvider) Chat(ctx context.Context, messages []model.LLMMessage) (*model.LLMResponse, error) {
|
||||
resp, err := p.doChat(ctx, messages, p.config.Model, false)
|
||||
return p.ChatWithTools(ctx, messages, nil)
|
||||
}
|
||||
|
||||
// ChatWithTools 同步对话(支持工具调用)
|
||||
func (p *OpenAIProvider) ChatWithTools(ctx context.Context, messages []model.LLMMessage, tools []OpenAITool) (*model.LLMResponse, error) {
|
||||
resp, err := p.doChat(ctx, messages, p.config.Model, false, tools)
|
||||
if err != nil {
|
||||
// 尝试fallback模型
|
||||
if p.config.FallbackModel != "" && p.config.FallbackModel != p.config.Model {
|
||||
log.Printf("[LLM] 主模型 %s 调用失败,降级到 %s: %v", p.config.Model, p.config.FallbackModel, err)
|
||||
return p.doChat(ctx, messages, p.config.FallbackModel, false)
|
||||
return p.doChat(ctx, messages, p.config.FallbackModel, false, tools)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
@@ -105,17 +128,22 @@ func (p *OpenAIProvider) Chat(ctx context.Context, messages []model.LLMMessage)
|
||||
|
||||
// ChatStream 流式对话
|
||||
func (p *OpenAIProvider) ChatStream(ctx context.Context, messages []model.LLMMessage) (<-chan StreamChunk, error) {
|
||||
return p.ChatStreamWithTools(ctx, messages, nil)
|
||||
}
|
||||
|
||||
// ChatStreamWithTools 流式对话(支持工具调用)
|
||||
func (p *OpenAIProvider) ChatStreamWithTools(ctx context.Context, messages []model.LLMMessage, tools []OpenAITool) (<-chan StreamChunk, error) {
|
||||
ch := make(chan StreamChunk, 100)
|
||||
|
||||
go func() {
|
||||
defer close(ch)
|
||||
|
||||
resp, err := p.doChatStream(ctx, messages, p.config.Model)
|
||||
resp, err := p.doChatStream(ctx, messages, p.config.Model, tools)
|
||||
if err != nil {
|
||||
// Fallback
|
||||
if p.config.FallbackModel != "" {
|
||||
log.Printf("[LLM] 流式调用主模型失败,降级: %v", err)
|
||||
resp, err = p.doChatStream(ctx, messages, p.config.FallbackModel)
|
||||
resp, err = p.doChatStream(ctx, messages, p.config.FallbackModel, tools)
|
||||
}
|
||||
if err != nil {
|
||||
ch <- StreamChunk{Error: err, Done: true}
|
||||
@@ -193,14 +221,31 @@ type openAIStreamChoice struct {
|
||||
}
|
||||
|
||||
// doChat 执行同步对话请求
|
||||
func (p *OpenAIProvider) doChat(ctx context.Context, messages []model.LLMMessage, modelName string, stream bool) (*model.LLMResponse, error) {
|
||||
func (p *OpenAIProvider) doChat(ctx context.Context, messages []model.LLMMessage, modelName string, stream bool, tools []OpenAITool) (*model.LLMResponse, error) {
|
||||
// 转换消息格式
|
||||
oaiMessages := make([]openAIMessage, len(messages))
|
||||
for i, msg := range messages {
|
||||
oaiMessages[i] = openAIMessage{
|
||||
Role: string(msg.Role),
|
||||
Content: msg.Content,
|
||||
oaiMsg := openAIMessage{
|
||||
Role: string(msg.Role),
|
||||
Content: msg.Content,
|
||||
Name: msg.Name,
|
||||
ToolCallID: msg.ToolCallID,
|
||||
}
|
||||
// 转换工具调用
|
||||
if len(msg.ToolCalls) > 0 {
|
||||
oaiMsg.ToolCalls = make([]openAIToolCall, len(msg.ToolCalls))
|
||||
for j, tc := range msg.ToolCalls {
|
||||
oaiMsg.ToolCalls[j] = openAIToolCall{
|
||||
ID: tc.ID,
|
||||
Type: "function",
|
||||
Function: openAIToolCallFunction{
|
||||
Name: tc.Name,
|
||||
Arguments: tc.Arguments,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
oaiMessages[i] = oaiMsg
|
||||
}
|
||||
|
||||
reqBody := openAIRequest{
|
||||
@@ -208,6 +253,10 @@ func (p *OpenAIProvider) doChat(ctx context.Context, messages []model.LLMMessage
|
||||
Messages: oaiMessages,
|
||||
Temperature: 0.8,
|
||||
Stream: stream,
|
||||
Tools: tools,
|
||||
}
|
||||
if len(tools) > 0 {
|
||||
reqBody.ToolChoice = "auto"
|
||||
}
|
||||
|
||||
jsonBody, err := json.Marshal(reqBody)
|
||||
@@ -251,25 +300,56 @@ func (p *OpenAIProvider) doChat(ctx context.Context, messages []model.LLMMessage
|
||||
return nil, fmt.Errorf("API返回空choices")
|
||||
}
|
||||
|
||||
return &model.LLMResponse{
|
||||
Content: oaiResp.Choices[0].Message.Content,
|
||||
FinishReason: oaiResp.Choices[0].FinishReason,
|
||||
// 检查是否有工具调用
|
||||
choice := oaiResp.Choices[0]
|
||||
llmResp := &model.LLMResponse{
|
||||
Content: choice.Message.Content,
|
||||
FinishReason: choice.FinishReason,
|
||||
Usage: model.Usage{
|
||||
PromptTokens: oaiResp.Usage.PromptTokens,
|
||||
CompletionTokens: oaiResp.Usage.CompletionTokens,
|
||||
TotalTokens: oaiResp.Usage.TotalTokens,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
if len(choice.Message.ToolCalls) > 0 {
|
||||
llmResp.ToolCalls = make([]model.ToolCall, 0, len(choice.Message.ToolCalls))
|
||||
for _, tc := range choice.Message.ToolCalls {
|
||||
llmResp.ToolCalls = append(llmResp.ToolCalls, model.ToolCall{
|
||||
ID: tc.ID,
|
||||
Name: tc.Function.Name,
|
||||
Arguments: tc.Function.Arguments,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return llmResp, nil
|
||||
}
|
||||
|
||||
// doChatStream 执行流式对话请求(返回原始HTTP响应)
|
||||
func (p *OpenAIProvider) doChatStream(ctx context.Context, messages []model.LLMMessage, modelName string) (*http.Response, error) {
|
||||
func (p *OpenAIProvider) doChatStream(ctx context.Context, messages []model.LLMMessage, modelName string, tools []OpenAITool) (*http.Response, error) {
|
||||
oaiMessages := make([]openAIMessage, len(messages))
|
||||
for i, msg := range messages {
|
||||
oaiMessages[i] = openAIMessage{
|
||||
Role: string(msg.Role),
|
||||
Content: msg.Content,
|
||||
oaiMsg := openAIMessage{
|
||||
Role: string(msg.Role),
|
||||
Content: msg.Content,
|
||||
Name: msg.Name,
|
||||
ToolCallID: msg.ToolCallID,
|
||||
}
|
||||
if len(msg.ToolCalls) > 0 {
|
||||
oaiMsg.ToolCalls = make([]openAIToolCall, len(msg.ToolCalls))
|
||||
for j, tc := range msg.ToolCalls {
|
||||
oaiMsg.ToolCalls[j] = openAIToolCall{
|
||||
ID: tc.ID,
|
||||
Type: "function",
|
||||
Function: openAIToolCallFunction{
|
||||
Name: tc.Name,
|
||||
Arguments: tc.Arguments,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
oaiMessages[i] = oaiMsg
|
||||
}
|
||||
|
||||
reqBody := openAIRequest{
|
||||
@@ -277,6 +357,10 @@ func (p *OpenAIProvider) doChatStream(ctx context.Context, messages []model.LLMM
|
||||
Messages: oaiMessages,
|
||||
Temperature: 0.8,
|
||||
Stream: true,
|
||||
Tools: tools,
|
||||
}
|
||||
if len(tools) > 0 {
|
||||
reqBody.ToolChoice = "auto"
|
||||
}
|
||||
|
||||
jsonBody, err := json.Marshal(reqBody)
|
||||
|
||||
Reference in New Issue
Block a user