Version: 0.7.6.dev.260325
后端: - ♻️ 将 `taskquery` 模块迁移至 `agent2`,并完成与 `agent2` 业务链路及整体结构的正式接入 前端: - 🧱 已完成基础框架搭建,并完成了登录、注册、主页等页面并对接了对应接口;但整体功能实现仍在完善中
This commit is contained in:
286
backend/agent2/node/taskquery_tool.go
Normal file
286
backend/agent2/node/taskquery_tool.go
Normal file
@@ -0,0 +1,286 @@
|
||||
package agentnode
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
agentmodel "github.com/LoveLosita/smartflow/backend/agent2/model"
|
||||
"github.com/cloudwego/eino/components/tool"
|
||||
toolutils "github.com/cloudwego/eino/components/tool/utils"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
)
|
||||
|
||||
const (
|
||||
ToolNameTaskQueryTasks = "query_tasks"
|
||||
ToolDescTaskQueryTasks = "按象限、关键词、截止时间筛选并排序任务,返回结构化任务列表"
|
||||
)
|
||||
|
||||
var taskQueryTimeLayouts = []string{
|
||||
time.RFC3339,
|
||||
"2006-01-02 15:04:05",
|
||||
"2006-01-02 15:04",
|
||||
"2006-01-02",
|
||||
}
|
||||
|
||||
// TaskQueryToolDeps 描述任务查询工具依赖的外部查询能力。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. QueryTasks 负责读取真实任务数据。
|
||||
// 2. 工具层只负责参数校验、归一化和结构化输出,不直接耦合 DAO。
|
||||
type TaskQueryToolDeps struct {
|
||||
QueryTasks func(ctx context.Context, req TaskQueryRequest) ([]TaskQueryTaskRecord, error)
|
||||
}
|
||||
|
||||
// Validate 负责校验任务查询工具依赖是否齐全。
|
||||
func (d TaskQueryToolDeps) Validate() error {
|
||||
if d.QueryTasks == nil {
|
||||
return errors.New("task query tool deps: QueryTasks is nil")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TaskQueryToolBundle 同时返回工具实例和工具元信息。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. Tools 给执行节点使用。
|
||||
// 2. ToolInfos 给模型注册 schema 使用。
|
||||
type TaskQueryToolBundle struct {
|
||||
Tools []tool.BaseTool
|
||||
ToolInfos []*schema.ToolInfo
|
||||
}
|
||||
|
||||
// TaskQueryRequest 是工具层传给业务层的内部查询请求。
|
||||
type TaskQueryRequest struct {
|
||||
UserID int
|
||||
Quadrant *int
|
||||
SortBy string
|
||||
Order string
|
||||
Limit int
|
||||
IncludeCompleted bool
|
||||
Keyword string
|
||||
DeadlineBefore *time.Time
|
||||
DeadlineAfter *time.Time
|
||||
}
|
||||
|
||||
// TaskQueryTaskRecord 是业务层返回给工具层的任务记录。
|
||||
type TaskQueryTaskRecord struct {
|
||||
ID int
|
||||
Title string
|
||||
PriorityGroup int
|
||||
IsCompleted bool
|
||||
DeadlineAt *time.Time
|
||||
UrgencyThresholdAt *time.Time
|
||||
}
|
||||
|
||||
// TaskQueryToolInput 是暴露给大模型的工具入参。
|
||||
type TaskQueryToolInput struct {
|
||||
Quadrant *int `json:"quadrant,omitempty" jsonschema:"description=可选象限(1~4)"`
|
||||
SortBy string `json:"sort_by,omitempty" jsonschema:"description=排序字段(deadline|priority|id)"`
|
||||
Order string `json:"order,omitempty" jsonschema:"description=排序方向(asc|desc)"`
|
||||
Limit int `json:"limit,omitempty" jsonschema:"description=返回条数,默认5,上限20"`
|
||||
IncludeCompleted *bool `json:"include_completed,omitempty" jsonschema:"description=是否包含已完成任务,默认false"`
|
||||
Keyword string `json:"keyword,omitempty" jsonschema:"description=可选标题关键词,模糊匹配"`
|
||||
DeadlineBefore string `json:"deadline_before,omitempty" jsonschema:"description=可选截止时间上界,支持RFC3339或yyyy-MM-dd HH:mm"`
|
||||
DeadlineAfter string `json:"deadline_after,omitempty" jsonschema:"description=可选截止时间下界,支持RFC3339或yyyy-MM-dd HH:mm"`
|
||||
}
|
||||
|
||||
// TaskQueryToolOutput 是返回给模型的结构化结果。
|
||||
type TaskQueryToolOutput struct {
|
||||
Total int `json:"total"`
|
||||
Items []agentmodel.TaskQueryItem `json:"items"`
|
||||
}
|
||||
|
||||
// BuildTaskQueryToolBundle 负责构建任务查询工具包。
|
||||
//
|
||||
// 步骤说明:
|
||||
// 1. 先校验依赖是否完整,避免生成一个运行时必定失败的工具。
|
||||
// 2. 再把输入归一化成内部请求,调用业务查询函数拿到真实数据。
|
||||
// 3. 最后把业务记录转换成统一的轻量任务视图,供模型和反思节点复用。
|
||||
func BuildTaskQueryToolBundle(ctx context.Context, deps TaskQueryToolDeps) (*TaskQueryToolBundle, error) {
|
||||
if err := deps.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
queryTool, err := toolutils.InferTool(
|
||||
ToolNameTaskQueryTasks,
|
||||
ToolDescTaskQueryTasks,
|
||||
func(ctx context.Context, input *TaskQueryToolInput) (*TaskQueryToolOutput, error) {
|
||||
req, err := normalizeTaskQueryToolInput(input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
records, err := deps.QueryTasks(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
items := make([]agentmodel.TaskQueryItem, 0, len(records))
|
||||
for _, record := range records {
|
||||
items = append(items, agentmodel.TaskQueryItem{
|
||||
ID: record.ID,
|
||||
Title: record.Title,
|
||||
PriorityGroup: record.PriorityGroup,
|
||||
PriorityLabel: agentmodel.PriorityLabelCN(record.PriorityGroup),
|
||||
IsCompleted: record.IsCompleted,
|
||||
DeadlineAt: formatTaskQueryTime(record.DeadlineAt),
|
||||
UrgencyThresholdAt: formatTaskQueryTime(record.UrgencyThresholdAt),
|
||||
})
|
||||
}
|
||||
|
||||
return &TaskQueryToolOutput{
|
||||
Total: len(items),
|
||||
Items: items,
|
||||
}, nil
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("构建任务查询工具失败: %w", err)
|
||||
}
|
||||
|
||||
tools := []tool.BaseTool{queryTool}
|
||||
infos, err := collectToolInfos(ctx, tools)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &TaskQueryToolBundle{
|
||||
Tools: tools,
|
||||
ToolInfos: infos,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetTaskQueryInvokableToolByName 按工具名提取可执行工具。
|
||||
func GetTaskQueryInvokableToolByName(bundle *TaskQueryToolBundle, name string) (tool.InvokableTool, error) {
|
||||
if bundle == nil {
|
||||
return nil, errors.New("task query tool bundle is nil")
|
||||
}
|
||||
return getInvokableToolByName(bundle.Tools, bundle.ToolInfos, name)
|
||||
}
|
||||
|
||||
// normalizeTaskQueryToolInput 负责参数默认值回填与合法性校验。
|
||||
//
|
||||
// 步骤说明:
|
||||
// 1. 先准备默认值,保证空参数也能执行一次合理查询。
|
||||
// 2. 再校验象限、排序、条数和时间区间,阻止非法参数下沉到业务层。
|
||||
// 3. 若上下界冲突,则直接返回错误,避免查出必为空的结果。
|
||||
func normalizeTaskQueryToolInput(input *TaskQueryToolInput) (TaskQueryRequest, error) {
|
||||
req := TaskQueryRequest{
|
||||
SortBy: "deadline",
|
||||
Order: "asc",
|
||||
Limit: agentmodel.DefaultTaskQueryLimit,
|
||||
IncludeCompleted: false,
|
||||
}
|
||||
if input == nil {
|
||||
return req, nil
|
||||
}
|
||||
|
||||
if input.Quadrant != nil {
|
||||
if *input.Quadrant < 1 || *input.Quadrant > 4 {
|
||||
return TaskQueryRequest{}, fmt.Errorf("quadrant=%d 非法,必须在 1~4", *input.Quadrant)
|
||||
}
|
||||
quadrant := *input.Quadrant
|
||||
req.Quadrant = &quadrant
|
||||
}
|
||||
|
||||
if sortBy := strings.ToLower(strings.TrimSpace(input.SortBy)); sortBy != "" {
|
||||
req.SortBy = sortBy
|
||||
}
|
||||
switch req.SortBy {
|
||||
case "deadline", "priority", "id":
|
||||
default:
|
||||
return TaskQueryRequest{}, fmt.Errorf("sort_by=%s 非法,仅支持 deadline|priority|id", req.SortBy)
|
||||
}
|
||||
|
||||
if order := strings.ToLower(strings.TrimSpace(input.Order)); order != "" {
|
||||
req.Order = order
|
||||
}
|
||||
switch req.Order {
|
||||
case "asc", "desc":
|
||||
default:
|
||||
return TaskQueryRequest{}, fmt.Errorf("order=%s 非法,仅支持 asc|desc", req.Order)
|
||||
}
|
||||
|
||||
if input.Limit > 0 {
|
||||
req.Limit = input.Limit
|
||||
}
|
||||
if req.Limit > agentmodel.MaxTaskQueryLimit {
|
||||
req.Limit = agentmodel.MaxTaskQueryLimit
|
||||
}
|
||||
if req.Limit <= 0 {
|
||||
req.Limit = agentmodel.DefaultTaskQueryLimit
|
||||
}
|
||||
|
||||
if input.IncludeCompleted != nil {
|
||||
req.IncludeCompleted = *input.IncludeCompleted
|
||||
}
|
||||
req.Keyword = strings.TrimSpace(input.Keyword)
|
||||
|
||||
before, err := parseTaskQueryBoundaryTime(input.DeadlineBefore, true)
|
||||
if err != nil {
|
||||
return TaskQueryRequest{}, err
|
||||
}
|
||||
after, err := parseTaskQueryBoundaryTime(input.DeadlineAfter, false)
|
||||
if err != nil {
|
||||
return TaskQueryRequest{}, err
|
||||
}
|
||||
req.DeadlineBefore = before
|
||||
req.DeadlineAfter = after
|
||||
if req.DeadlineBefore != nil && req.DeadlineAfter != nil && req.DeadlineAfter.After(*req.DeadlineBefore) {
|
||||
return TaskQueryRequest{}, errors.New("deadline_after 不能晚于 deadline_before")
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// parseTaskQueryBoundaryTime 解析截止时间上下界。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. isUpper=true 时,纯日期补到当天 23:59:59。
|
||||
// 2. isUpper=false 时,纯日期补到当天 00:00:00。
|
||||
// 3. 不支持的格式直接返回错误,由调用方决定是否回退。
|
||||
func parseTaskQueryBoundaryTime(raw string, isUpper bool) (*time.Time, error) {
|
||||
text := strings.TrimSpace(raw)
|
||||
if text == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
loc := time.Local
|
||||
for _, layout := range taskQueryTimeLayouts {
|
||||
var (
|
||||
parsed time.Time
|
||||
err error
|
||||
)
|
||||
if layout == time.RFC3339 {
|
||||
parsed, err = time.Parse(layout, text)
|
||||
if err == nil {
|
||||
parsed = parsed.In(loc)
|
||||
}
|
||||
} else {
|
||||
parsed, err = time.ParseInLocation(layout, text, loc)
|
||||
}
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if layout == "2006-01-02" {
|
||||
if isUpper {
|
||||
parsed = time.Date(parsed.Year(), parsed.Month(), parsed.Day(), 23, 59, 59, 0, loc)
|
||||
} else {
|
||||
parsed = time.Date(parsed.Year(), parsed.Month(), parsed.Day(), 0, 0, 0, 0, loc)
|
||||
}
|
||||
}
|
||||
return &parsed, nil
|
||||
}
|
||||
return nil, fmt.Errorf("时间格式不支持: %s", text)
|
||||
}
|
||||
|
||||
// formatTaskQueryTime 负责把内部时间格式化为给模型展示的分钟级文本。
|
||||
func formatTaskQueryTime(value *time.Time) string {
|
||||
if value == nil {
|
||||
return ""
|
||||
}
|
||||
return value.In(time.Local).Format("2006-01-02 15:04")
|
||||
}
|
||||
Reference in New Issue
Block a user