yao/agent/robot/api/results.go
Max 2f040196ec Add Results and Activities Functionality to ExecutionStore
- Implemented `ListResults` and `CountResults` methods in `ExecutionStore` to retrieve and count completed executions with delivery content, supporting filtering by member ID, team ID, trigger type, and keyword.
- Developed `ListActivities` method to derive activities from execution status changes, allowing retrieval of recent activities for a team.
- Created corresponding unit tests in `execution_test.go` to validate the functionality of results and activities listing, ensuring accurate filtering and pagination.
- Updated OpenAPI routes and types to include new endpoints for listing results and activities, enhancing the API's capabilities for managing execution data.
2026-01-24 17:57:58 +08:00

194 lines
5.2 KiB
Go

package api
import (
"context"
"fmt"
"time"
"github.com/yaoapp/yao/agent/robot/store"
"github.com/yaoapp/yao/agent/robot/types"
)
// ==================== Result Types ====================
// ResultQuery - query parameters for listing results
type ResultQuery struct {
TriggerType types.TriggerType `json:"trigger_type,omitempty"` // clock | human | event
Keyword string `json:"keyword,omitempty"` // Search in name/summary
Page int `json:"page,omitempty"`
PageSize int `json:"pagesize,omitempty"`
}
// ResultItem - result list item (subset of execution)
type ResultItem struct {
ID string `json:"id"`
MemberID string `json:"member_id"`
TriggerType types.TriggerType `json:"trigger_type"`
Status types.ExecStatus `json:"status"`
Name string `json:"name"`
Summary string `json:"summary"`
StartTime time.Time `json:"start_time"`
EndTime *time.Time `json:"end_time,omitempty"`
HasAttachments bool `json:"has_attachments"`
}
// ResultDetail - full result with delivery content
type ResultDetail struct {
ID string `json:"id"`
MemberID string `json:"member_id"`
TriggerType types.TriggerType `json:"trigger_type"`
Status types.ExecStatus `json:"status"`
Name string `json:"name"`
Delivery *types.DeliveryResult `json:"delivery,omitempty"`
StartTime time.Time `json:"start_time"`
EndTime *time.Time `json:"end_time,omitempty"`
}
// ResultListResponse - paginated response
type ResultListResponse struct {
Data []*ResultItem `json:"data"`
Total int `json:"total"`
Page int `json:"page"`
PageSize int `json:"pagesize"`
}
// ==================== Result API Functions ====================
// ListResults returns completed executions with delivery content for a robot
func ListResults(ctx *types.Context, memberID string, query *ResultQuery) (*ResultListResponse, error) {
if memberID == "" {
return nil, fmt.Errorf("member_id is required")
}
if query == nil {
query = &ResultQuery{}
}
query.applyDefaults()
// Build store options
opts := &store.ResultListOptions{
MemberID: memberID,
Limit: query.PageSize,
Offset: (query.Page - 1) * query.PageSize,
}
if query.TriggerType != "" {
opts.TriggerType = query.TriggerType
}
if query.Keyword != "" {
opts.Keyword = query.Keyword
}
// Query from store
result, err := getExecutionStore().ListResults(context.Background(), opts)
if err != nil {
return nil, fmt.Errorf("failed to list results: %w", err)
}
// Transform to ResultItem slice
items := make([]*ResultItem, 0, len(result.Data))
for _, record := range result.Data {
item := recordToResultItem(record)
if item != nil {
items = append(items, item)
}
}
return &ResultListResponse{
Data: items,
Total: result.Total,
Page: result.Page,
PageSize: result.PageSize,
}, nil
}
// GetResult returns a single result by execution ID
func GetResult(ctx *types.Context, execID string) (*ResultDetail, error) {
if execID == "" {
return nil, fmt.Errorf("execution_id is required")
}
// Get from store
record, err := getExecutionStore().Get(context.Background(), execID)
if err != nil {
return nil, fmt.Errorf("failed to get result: %w", err)
}
if record == nil {
return nil, fmt.Errorf("result not found: %s", execID)
}
// Verify it has delivery content
if record.Delivery == nil || record.Delivery.Content == nil {
return nil, fmt.Errorf("result not found: %s (no delivery content)", execID)
}
return recordToResultDetail(record), nil
}
// ==================== Helper Functions ====================
// applyDefaults applies default values to ResultQuery
func (q *ResultQuery) applyDefaults() {
if q.Page <= 0 {
q.Page = 1
}
if q.PageSize <= 0 {
q.PageSize = 20
}
if q.PageSize > 100 {
q.PageSize = 100
}
}
// recordToResultItem converts ExecutionRecord to ResultItem
func recordToResultItem(record *store.ExecutionRecord) *ResultItem {
if record == nil {
return nil
}
item := &ResultItem{
ID: record.ExecutionID,
MemberID: record.MemberID,
TriggerType: record.TriggerType,
Status: record.Status,
Name: record.Name,
}
// Set times
if record.StartTime != nil {
item.StartTime = *record.StartTime
}
item.EndTime = record.EndTime
// Extract summary and attachments from delivery
if record.Delivery != nil && record.Delivery.Content != nil {
item.Summary = record.Delivery.Content.Summary
item.HasAttachments = len(record.Delivery.Content.Attachments) > 0
}
return item
}
// recordToResultDetail converts ExecutionRecord to ResultDetail
func recordToResultDetail(record *store.ExecutionRecord) *ResultDetail {
if record == nil {
return nil
}
detail := &ResultDetail{
ID: record.ExecutionID,
MemberID: record.MemberID,
TriggerType: record.TriggerType,
Status: record.Status,
Name: record.Name,
Delivery: record.Delivery,
}
// Set times
if record.StartTime != nil {
detail.StartTime = *record.StartTime
}
detail.EndTime = record.EndTime
return detail
}