From 8dd60b087799868cfecac6b4ed422c4d1b754d6e Mon Sep 17 00:00:00 2001 From: fuzhongyun <15339891972@163.com> Date: Thu, 11 Sep 2025 11:32:25 +0800 Subject: [PATCH] =?UTF-8?q?=E5=BE=85=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config.yaml | 20 +++- internal/config/config.go | 15 ++- internal/constants/caller.go | 12 +++ internal/constants/knowledge.go | 22 ++++ internal/handlers/chat.go | 82 ++++----------- internal/handlers/router.go | 5 +- internal/services/router.go | 150 +++++++++++++++++++++++----- internal/tools/manager.go | 26 ++++- internal/tools/weather.go | 8 +- internal/tools/zltx_order_detail.go | 105 +++++++++++++++++++ pkg/types/types.go | 24 +++-- 11 files changed, 355 insertions(+), 114 deletions(-) create mode 100644 internal/constants/caller.go create mode 100644 internal/constants/knowledge.go create mode 100644 internal/tools/zltx_order_detail.go diff --git a/config.yaml b/config.yaml index 4b58254..87aecbd 100644 --- a/config.yaml +++ b/config.yaml @@ -1,5 +1,5 @@ server: - port: 8080 + port: 8090 host: "0.0.0.0" ollama: @@ -7,12 +7,28 @@ ollama: model: "qwen3:8b" timeout: 30s +# 模型参数 +modelParam: + temperature: 0.7 + max_tokens: 2000 + tools: weather: enabled: true - mock_data: true calculator: enabled: true + zltxOrderDetail: # 直连天下订单详情 + enabled: true + base_url: https://gateway.dev.cdlsxd.cn + biz_system: "zltx" + zltxOrderLog: # 直连天下订单日志 + enabled: true + base_url: https://gateway.dev.cdlsxd.cn + biz_system: "zltx" + knowledge: # 知识库 + enabled: true + base_url: http://117.175.169.61:8080 + api_key: sk-EfnUANKMj3DUOiEPJZ5xS8SGMsbO6be_qYAg9uZ8T3zyoFM- logging: level: "info" diff --git a/internal/config/config.go b/internal/config/config.go index aa0f0c1..178649c 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -30,14 +30,19 @@ type OllamaConfig struct { // ToolsConfig 工具配置 type ToolsConfig struct { - Weather ToolConfig `mapstructure:"weather"` - Calculator ToolConfig `mapstructure:"calculator"` + Weather ToolConfig `mapstructure:"weather"` + Calculator ToolConfig `mapstructure:"calculator"` + ZltxOrderDetail ToolConfig `mapstructure:"zltxOrderDetail"` + ZltxOrderLog ToolConfig `mapstructure:"zltxOrderLog"` + Knowledge ToolConfig `mapstructure:"knowledge"` } // ToolConfig 单个工具配置 type ToolConfig struct { - Enabled bool `mapstructure:"enabled"` - MockData bool `mapstructure:"mock_data"` + Enabled bool `mapstructure:"enabled"` + BaseURL string `mapstructure:"base_url"` + APIKey string `mapstructure:"api_key"` + BizSystem string `mapstructure:"biz_system"` } // LoggingConfig 日志配置 @@ -72,4 +77,4 @@ func LoadConfig(configPath string) (*Config, error) { } return &config, nil -} \ No newline at end of file +} diff --git a/internal/constants/caller.go b/internal/constants/caller.go new file mode 100644 index 0000000..75a7349 --- /dev/null +++ b/internal/constants/caller.go @@ -0,0 +1,12 @@ +package constants + +type Caller string + +const ( + CallerZltx Caller = "zltx" // 直连天下 + CallerHyt Caller = "hyt" // 货易通 +) + +func (c Caller) String() string { + return string(c) +} diff --git a/internal/constants/knowledge.go b/internal/constants/knowledge.go new file mode 100644 index 0000000..b8b7861 --- /dev/null +++ b/internal/constants/knowledge.go @@ -0,0 +1,22 @@ +package constants + +type KnowledgeId string + +const ( + KnowledgeIdZltx KnowledgeId = "kb-00000001" +) + +func (k KnowledgeId) String() string { + return string(k) +} + +var CallerKnowledgeIdMap = map[Caller]KnowledgeId{ + CallerZltx: KnowledgeIdZltx, +} + +func GetKnowledgeId(caller Caller) KnowledgeId { + if _, ok := CallerKnowledgeIdMap[caller]; !ok { + return "" + } + return CallerKnowledgeIdMap[caller] +} diff --git a/internal/handlers/chat.go b/internal/handlers/chat.go index fb5d2e1..6961c9e 100644 --- a/internal/handlers/chat.go +++ b/internal/handlers/chat.go @@ -21,14 +21,17 @@ func NewChatHandler(routerService types.RouterService) *ChatHandler { // ChatRequest HTTP聊天请求 type ChatRequest struct { - Message string `json:"message" binding:"required" example:"北京今天天气怎么样?"` + UserInput string `json:"user_input" binding:"required" example:"考勤规则"` + Caller string `json:"caller" binding:"required" example:"zltx"` + SessionID string `json:"session_id" example:"default"` } // ChatResponse HTTP聊天响应 type ChatResponse struct { - Message string `json:"message" example:"北京今天天气晴朗,温度15.3°C"` - ToolCalls []ToolCallResponse `json:"tool_calls,omitempty"` - Finished bool `json:"finished" example:"true"` + Status string `json:"status" example:"success"` // 处理状态 + Message string `json:"message" example:""` // 响应消息 + Data any `json:"data,omitempty"` // 响应数据 + TaskCode string `json:"task_code,omitempty"` // 任务代码 } // ToolCallResponse 工具调用响应 @@ -45,46 +48,31 @@ type FunctionCallResponse struct { Arguments interface{} `json:"arguments"` } -// ErrorResponse 错误响应 -type ErrorResponse struct { - Error string `json:"error" example:"Invalid request"` - Code int `json:"code" example:"400"` - Message string `json:"message" example:"请求参数错误"` -} - -// Chat 处理聊天请求 -// @Summary 智能聊天 -// @Description 发送消息给AI助手,支持工具调用 -// @Tags chat -// @Accept json -// @Produce json -// @Param request body ChatRequest true "聊天请求" -// @Success 200 {object} ChatResponse "聊天响应" -// @Failure 400 {object} ErrorResponse "请求错误" -// @Failure 500 {object} ErrorResponse "服务器错误" -// @Router /api/v1/chat [post] func (h *ChatHandler) Chat(c *gin.Context) { var req ChatRequest if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, ErrorResponse{ - Error: "Invalid request", - Code: http.StatusBadRequest, - Message: err.Error(), + c.JSON(http.StatusBadRequest, ChatResponse{ + Status: "error", + Message: "请求参数错误", }) return } // 转换为服务层请求 serviceReq := &types.ChatRequest{ - Message: req.Message, + UserInput: req.UserInput, + Caller: req.Caller, + SessionID: req.SessionID, + ChatRequestMeta: types.ChatRequestMeta{ + Authorization: c.GetHeader("Authorization"), + }, } // 调用路由服务 response, err := h.routerService.Route(c.Request.Context(), serviceReq) if err != nil { - c.JSON(http.StatusInternalServerError, ErrorResponse{ - Error: "Service error", - Code: http.StatusInternalServerError, + c.JSON(http.StatusInternalServerError, ChatResponse{ + Status: "error", Message: err.Error(), }) return @@ -93,38 +81,10 @@ func (h *ChatHandler) Chat(c *gin.Context) { // 转换响应格式 httpResponse := &ChatResponse{ Message: response.Message, - Finished: response.Finished, - } - - // 转换工具调用 - if len(response.ToolCalls) > 0 { - httpResponse.ToolCalls = make([]ToolCallResponse, len(response.ToolCalls)) - for i, toolCall := range response.ToolCalls { - httpResponse.ToolCalls[i] = ToolCallResponse{ - ID: toolCall.ID, - Type: toolCall.Type, - Function: FunctionCallResponse{ - Name: toolCall.Function.Name, - Arguments: toolCall.Function.Arguments, - }, - Result: toolCall.Result, - } - } + Status: response.Status, + Data: response.Data, + TaskCode: response.TaskCode, } c.JSON(http.StatusOK, httpResponse) } - -// Health 健康检查 -// @Summary 健康检查 -// @Description 检查服务是否正常运行 -// @Tags system -// @Produce json -// @Success 200 {object} map[string]string -// @Router /health [get] -func (h *ChatHandler) Health(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{ - "status": "ok", - "service": "ai-scheduler", - }) -} diff --git a/internal/handlers/router.go b/internal/handlers/router.go index 38997ee..6b55820 100644 --- a/internal/handlers/router.go +++ b/internal/handlers/router.go @@ -27,9 +27,6 @@ func SetupRoutes(routerService types.RouterService) *gin.Engine { // 创建处理器 chatHandler := NewChatHandler(routerService) - // 健康检查 - r.GET("/health", chatHandler.Health) - // API路由组 v1 := r.Group("/api/v1") { @@ -40,4 +37,4 @@ func SetupRoutes(routerService types.RouterService) *gin.Engine { r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) return r -} \ No newline at end of file +} diff --git a/internal/services/router.go b/internal/services/router.go index 002ea26..d25bd77 100644 --- a/internal/services/router.go +++ b/internal/services/router.go @@ -1,12 +1,14 @@ package services import ( + "ai_scheduler/internal/constants" "ai_scheduler/internal/tools" "ai_scheduler/pkg/types" "context" "encoding/json" "fmt" "log" + "strings" ) // RouterService 智能路由服务 @@ -27,20 +29,48 @@ func NewRouterService(aiClient types.AIClient, toolManager *tools.Manager) *Rout func (r *RouterService) Route(ctx context.Context, req *types.ChatRequest) (*types.ChatResponse, error) { // 构建消息 messages := []types.Message{ + // { + // Role: "system", + // Content: r.buildSystemPrompt(), + // }, { - Role: "system", - Content: r.buildSystemPrompt(), + Role: "assistant", + Content: r.buildIntentPrompt(req.UserInput), }, { Role: "user", - Content: req.Message, + Content: req.UserInput, }, } - // 获取工具定义 - toolDefinitions := r.toolManager.GetToolDefinitions() + // 第1次调用AI,获取用户意图 + intentResponse, err := r.aiClient.Chat(ctx, messages, nil) + if err != nil { + return nil, fmt.Errorf("AI响应失败: %w", err) + } - // 第一次调用AI,获取是否需要使用工具 + // 从AI响应中提取意图 + intent := r.extractIntent(intentResponse) + if intent == "" { + return nil, fmt.Errorf("未识别到用户意图") + } + + switch intent { + case "order_diagnosis": + // 订单诊断意图 + return r.handleOrderDiagnosis(ctx, req, messages) + case "knowledge_qa": + // 知识问答意图 + return r.handleKnowledgeQA(ctx, req, messages) + default: + // 未知意图 + return nil, fmt.Errorf("意图识别失败,请明确您的需求呢,我可以为您") + } + + // 获取工具定义 + toolDefinitions := r.toolManager.GetToolDefinitions(constants.Caller(req.Caller)) + + // 第2次调用AI,获取是否需要使用工具 response, err := r.aiClient.Chat(ctx, messages, toolDefinitions) if err != nil { return nil, fmt.Errorf("failed to chat with AI: %w", err) @@ -81,31 +111,99 @@ func (r *RouterService) Route(ctx context.Context, req *types.ChatRequest) (*typ // 合并工具调用信息到最终响应 finalResponse.ToolCalls = toolResults - log.Printf("Router processed request: %s, used %d tools", req.Message, len(toolResults)) + log.Printf("Router processed request: %s, used %d tools", req.UserInput, len(toolResults)) return finalResponse, nil } // buildSystemPrompt 构建系统提示词 func (r *RouterService) buildSystemPrompt() string { - prompt := `你是一个智能助手,可以帮助用户解决各种问题。你有以下工具可以使用: - -` - - // 添加工具描述 - tools := r.toolManager.GetAllTools() - for _, tool := range tools { - prompt += fmt.Sprintf("- %s: %s\n", tool.Name(), tool.Description()) - } - - prompt += ` -请根据用户的问题,判断是否需要使用工具。如果需要,请调用相应的工具获取信息,然后基于工具返回的结果给出完整的回答。 - -注意事项: -1. 只有在确实需要获取实时信息或进行计算时才使用工具 -2. 如果用户只是普通聊天,不需要使用工具 -3. 使用工具后,请基于工具返回的结果给出自然、友好的回复 -4. 如果工具执行出错,请告知用户并提供替代建议` + prompt := `你是一个智能路由系统,你的任务是根据用户输入判断用户的意图,并且执行对应的任务。` return prompt -} \ No newline at end of file +} + +// buildIntentPrompt 构建意图识别提示词 +func (r *RouterService) buildIntentPrompt(userInput string) string { + prompt := `请分析以下用户输入,判断用户的意图类型。 + +用户输入:{user_input} + +意图类型说明: +1. order_diagnosis - 订单诊断:用户想要查询、诊断或了解订单相关信息 +2. knowledge_qa - 知识问答:用户想要进行一般性问答或获取知识信息 + +- 当用户意图不够清晰且不匹配 knowledge_qa 以外意图时,使用knowledge_qa +- 当用户意图非常不清晰时使用 unknown + +请只返回以下格式的JSON: +{ + "intent": "order_diagnosis" | "knowledge_qa" | "unknown", + "confidence": 0.0-1.0, + "reasoning": "判断理由" +} +` + + prompt = strings.ReplaceAll(prompt, "{user_input}", userInput) + + return prompt +} + +// extractIntent 从AI响应中提取意图 +func (r *RouterService) extractIntent(response *types.ChatResponse) string { + if response == nil || response.Message == "" { + return "" + } + + // 尝试解析JSON + var intent struct { + Intent string `json:"intent"` + Confidence string `json:"confidence"` + Reasoning string `json:"reasoning"` + } + err := json.Unmarshal([]byte(response.Message), &intent) + if err != nil { + log.Printf("Failed to parse intent JSON: %v", err) + return "" + } + + return intent.Intent +} + +// handleOrderDiagnosis 处理订单诊断意图 +func (r *RouterService) handleOrderDiagnosis(ctx context.Context, req *types.ChatRequest, messages []types.Message) (*types.ChatResponse, error) { + // 调用订单详情工具 + orderDetailTool, ok := r.toolManager.GetTool("zltxOrderDetail") + if orderDetailTool == nil || !ok { + return nil, fmt.Errorf("order detail tool not found") + } + orderDetailTool.Execute(ctx, json.RawMessage{}) + + // 获取相关工具定义 + toolDefinitions := r.toolManager.GetToolDefinitions(constants.Caller(req.Caller)) + + // 调用AI,获取是否需要使用工具 + response, err := r.aiClient.Chat(ctx, messages, toolDefinitions) + if err != nil { + return nil, fmt.Errorf("failed to chat with AI: %w", err) + } + + // 如果没有工具调用,直接返回 + if len(response.ToolCalls) == 0 { + return response, nil + } + + // 执行工具调用 + toolResults, err := r.toolManager.ExecuteToolCalls(ctx, response.ToolCalls) + if err != nil { + return nil, fmt.Errorf("failed to execute tools: %w", err) + } + + return nil, nil +} + +// handleKnowledgeQA 处理知识问答意图 +func (r *RouterService) handleKnowledgeQA(ctx context.Context, req *types.ChatRequest, messages []types.Message) (*types.ChatResponse, error) { + + return nil, nil +} diff --git a/internal/tools/manager.go b/internal/tools/manager.go index aa28461..dbd4f46 100644 --- a/internal/tools/manager.go +++ b/internal/tools/manager.go @@ -2,6 +2,7 @@ package tools import ( "ai_scheduler/internal/config" + "ai_scheduler/internal/constants" "ai_scheduler/pkg/types" "context" "encoding/json" @@ -21,7 +22,7 @@ func NewManager(config *config.ToolsConfig) *Manager { // 注册天气工具 if config.Weather.Enabled { - weatherTool := NewWeatherTool(config.Weather.MockData) + weatherTool := NewWeatherTool() m.tools[weatherTool.Name()] = weatherTool } @@ -31,6 +32,24 @@ func NewManager(config *config.ToolsConfig) *Manager { m.tools[calcTool.Name()] = calcTool } + // 注册知识库工具 + // if config.Knowledge.Enabled { + // knowledgeTool := NewKnowledgeTool() + // m.tools[knowledgeTool.Name()] = knowledgeTool + // } + + // 注册直连天下订单详情工具 + if config.ZltxOrderDetail.Enabled { + zltxOrderDetailTool := NewZltxOrderDetailTool(config.ZltxOrderDetail) + m.tools[zltxOrderDetailTool.Name()] = zltxOrderDetailTool + } + + // 注册直连天下订单日志工具 + // if config.ZltxOrderLog.Enabled { + // zltxOrderLogTool := NewZltxOrderLogTool(config.ZltxOrderLog) + // m.tools[zltxOrderLogTool.Name()] = zltxOrderLogTool + // } + return m } @@ -50,11 +69,12 @@ func (m *Manager) GetAllTools() []types.Tool { } // GetToolDefinitions 获取所有工具定义 -func (m *Manager) GetToolDefinitions() []types.ToolDefinition { +func (m *Manager) GetToolDefinitions(caller constants.Caller) []types.ToolDefinition { definitions := make([]types.ToolDefinition, 0, len(m.tools)) for _, tool := range m.tools { definitions = append(definitions, tool.Definition()) } + return definitions } @@ -98,4 +118,4 @@ func (m *Manager) ExecuteToolCalls(ctx context.Context, toolCalls []types.ToolCa } return results, nil -} \ No newline at end of file +} diff --git a/internal/tools/weather.go b/internal/tools/weather.go index 3de5aff..9d662b7 100644 --- a/internal/tools/weather.go +++ b/internal/tools/weather.go @@ -15,10 +15,8 @@ type WeatherTool struct { } // NewWeatherTool 创建天气工具 -func NewWeatherTool(mockData bool) *WeatherTool { - return &WeatherTool{ - mockData: mockData, - } +func NewWeatherTool() *WeatherTool { + return &WeatherTool{} } // Name 返回工具名称 @@ -137,4 +135,4 @@ func (w *WeatherTool) getMockWeather(city, unit string) *WeatherResponse { WindSpeed: float64(rand.Intn(20)) + 1.0, Timestamp: time.Now().Format("2006-01-02 15:04:05"), } -} \ No newline at end of file +} diff --git a/internal/tools/zltx_order_detail.go b/internal/tools/zltx_order_detail.go new file mode 100644 index 0000000..934ae38 --- /dev/null +++ b/internal/tools/zltx_order_detail.go @@ -0,0 +1,105 @@ +package tools + +import ( + "ai_scheduler/internal/config" + "ai_scheduler/pkg/types" + "context" + "encoding/json" + "fmt" + "net/http" +) + +// ZltxOrderDetailTool 直连天下订单详情工具 +type ZltxOrderDetailTool struct { + config config.ToolConfig +} + +// NewZltxOrderDetailTool 创建直连天下订单详情工具 +func NewZltxOrderDetailTool(config config.ToolConfig) *ZltxOrderDetailTool { + return &ZltxOrderDetailTool{config: config} +} + +// Name 返回工具名称 +func (w *ZltxOrderDetailTool) Name() string { + return "zltxOrderDetail" +} + +// Description 返回工具描述 +func (w *ZltxOrderDetailTool) Description() string { + return "获取直连天下订单详情" +} + +// Definition 返回工具定义 +func (w *ZltxOrderDetailTool) Definition() types.ToolDefinition { + return types.ToolDefinition{ + Type: "function", + Function: types.FunctionDef{ + Name: w.Name(), + Description: w.Description(), + Parameters: map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "number": map[string]interface{}{ + "type": "string", + "description": "订单编号/流水号", + }, + }, + "required": []string{"number"}, + }, + }, + } +} + +// ZltxOrderDetailRequest 直连天下订单详情请求参数 +type ZltxOrderDetailRequest struct { + Number string `json:"number"` +} + +// ZltxOrderDetailResponse 直连天下订单详情响应 +type ZltxOrderDetailResponse struct { + Code int `json:"code"` + Error string `json:"error"` + Data ZltxOrderDetailData `json:"data"` +} + +// ZltxOrderDetailData 直连天下订单详情数据 +type ZltxOrderDetailData struct { + Direct map[string]any `json:"direct"` + Order map[string]any `json:"order"` +} + +// Execute 执行直连天下订单详情查询 +func (w *ZltxOrderDetailTool) Execute(ctx context.Context, args json.RawMessage) (interface{}, error) { + var req ZltxOrderDetailRequest + if err := json.Unmarshal(args, &req); err != nil { + return nil, fmt.Errorf("invalid zltxOrderDetail request: %w", err) + } + + if req.Number == "" { + return nil, fmt.Errorf("number is required") + } + + // 这里可以集成真实的直连天下订单详情API + return w.getZltxOrderDetail(ctx, req.Number), nil +} + +// getMockZltxOrderDetail 获取模拟直连天下订单详情数据 +func (w *ZltxOrderDetailTool) getZltxOrderDetail(ctx context.Context, number string) *ZltxOrderDetailResponse { + url := fmt.Sprintf("%s/admin/direct/ai/%s", w.config.BaseURL, number) + authorization := fmt.Sprintf("Bearer %s", w.config.APIKey) + + // 发送http请求 + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return &ZltxOrderDetailResponse{} + } + req.Header.Set("Authorization", authorization) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return &ZltxOrderDetailResponse{} + } + defer resp.Body.Close() + + return &ZltxOrderDetailResponse{} +} diff --git a/pkg/types/types.go b/pkg/types/types.go index 899e09a..fe5321c 100644 --- a/pkg/types/types.go +++ b/pkg/types/types.go @@ -7,15 +7,23 @@ import ( // ChatRequest 聊天请求 type ChatRequest struct { - Message string `json:"message" binding:"required"` - Model string `json:"model,omitempty"` + UserInput string `json:"user_input" binding:"required"` + Caller string `json:"caller" binding:"required"` + SessionID string `json:"session_id"` + ChatRequestMeta ChatRequestMeta `json:"meta,omitempty"` +} + +// ChatRequestMeta 聊天请求元数据 +type ChatRequestMeta struct { + Authorization string `json:"authorization"` } // ChatResponse 聊天响应 type ChatResponse struct { - Message string `json:"message"` - ToolCalls []ToolCall `json:"tool_calls,omitempty"` - Finished bool `json:"finished"` + Status string `json:"status"` + Message string `json:"message"` + Data any `json:"data,omitempty"` + TaskCode string `json:"task_code,omitempty"` } // ToolCall 工具调用 @@ -34,8 +42,8 @@ type FunctionCall struct { // ToolDefinition 工具定义 type ToolDefinition struct { - Type string `json:"type"` - Function FunctionDef `json:"function"` + Type string `json:"type"` + Function FunctionDef `json:"function"` } // FunctionDef 函数定义 @@ -67,4 +75,4 @@ type Message struct { // RouterService 路由服务接口 type RouterService interface { Route(ctx context.Context, req *ChatRequest) (*ChatResponse, error) -} \ No newline at end of file +}