Files
smartmate/backend/agent2/node/taskquery_tool.go
Losita e06284d0b0 Version: 0.7.6.dev.260325
后端:
- ♻️ 将 `taskquery` 模块迁移至 `agent2`,并完成与 `agent2` 业务链路及整体结构的正式接入

前端:
- 🧱 已完成基础框架搭建,并完成了登录、注册、主页等页面并对接了对应接口;但整体功能实现仍在完善中
2026-03-25 00:49:16 +08:00

287 lines
8.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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")
}