feat: Enhance AI Agent and Channel Management
- Updated labels in the AI Agents dashboard for clarity, changing "流程状态" to "Playbook 状态" and "未发布流程" to "未发布 Playbook". - Introduced AI Agent rollout percentage management in channel editing, allowing users to set and rollback rollout percentages. - Added new API endpoints for rolling back AI Agent rollout and fetching agent run metrics. - Implemented new UI components for displaying agent run details, including status, duration, and input/output tokens. - Enhanced type definitions for AdminChannel and AIAgent to include rollout percentages and runtime modes. - Updated navigation to include a section for agent runs. - Added new translations for agent run features in both English and Chinese.
This commit is contained in:
@@ -0,0 +1,514 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"agent-desk/internal/models"
|
||||
"agent-desk/internal/pkg/dto"
|
||||
"agent-desk/internal/pkg/dto/request"
|
||||
"agent-desk/internal/pkg/enums"
|
||||
"agent-desk/internal/pkg/errorsx"
|
||||
"agent-desk/internal/pkg/httpx/params"
|
||||
"agent-desk/internal/pkg/utils"
|
||||
"agent-desk/internal/repositories"
|
||||
|
||||
"github.com/mlogclub/simple/sqls"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var AgentRunService = newAgentRunService()
|
||||
|
||||
func newAgentRunService() *agentRunService {
|
||||
return &agentRunService{}
|
||||
}
|
||||
|
||||
type agentRunService struct{}
|
||||
|
||||
type AgentRunMetrics struct {
|
||||
TotalRuns int `json:"totalRuns"`
|
||||
CompletedRuns int `json:"completedRuns"`
|
||||
FailedRuns int `json:"failedRuns"`
|
||||
InterruptedRuns int `json:"interruptedRuns"`
|
||||
CompletionRate float64 `json:"completionRate"`
|
||||
ToolCalls int `json:"toolCalls"`
|
||||
ToolSuccessRate float64 `json:"toolSuccessRate"`
|
||||
AverageSteps float64 `json:"averageSteps"`
|
||||
AverageDurationMS int64 `json:"averageDurationMs"`
|
||||
P95DurationMS int64 `json:"p95DurationMs"`
|
||||
PromptTokens int64 `json:"promptTokens"`
|
||||
CompletionTokens int64 `json:"completionTokens"`
|
||||
HandoffRate float64 `json:"handoffRate"`
|
||||
KnowledgeFallbackRate float64 `json:"knowledgeFallbackRate"`
|
||||
ResumedInterrupts int `json:"resumedInterrupts"`
|
||||
ResolvedInterrupts int `json:"resolvedInterrupts"`
|
||||
InterruptRecoveryRate float64 `json:"interruptRecoveryRate"`
|
||||
ReviewedRuns int `json:"reviewedRuns"`
|
||||
ResolvedRuns int `json:"resolvedRuns"`
|
||||
ResolutionRate float64 `json:"resolutionRate"`
|
||||
UnsupportedEvidenceRuns int `json:"unsupportedEvidenceRuns"`
|
||||
UnsupportedEvidenceRate float64 `json:"unsupportedEvidenceRate"`
|
||||
}
|
||||
|
||||
type AgentRunEngineComparison struct {
|
||||
EngineCode string `json:"engineCode"`
|
||||
Metrics AgentRunMetrics `json:"metrics"`
|
||||
}
|
||||
|
||||
const maxAgentAuditPreviewChars = 4000
|
||||
|
||||
var agentAuditSecretPattern = regexp.MustCompile(`(?i)(?:"|')?(api[_-]?key|authorization|password|secret|token|cookie)(?:"|')?\s*([:=])\s*(?:"[^"]*"|'[^']*'|[^\s,;}]+)`)
|
||||
|
||||
func (s *agentRunService) Get(id int64) *models.AgentRun {
|
||||
if id <= 0 {
|
||||
return nil
|
||||
}
|
||||
return repositories.AgentRunRepository.Get(sqls.DB(), id)
|
||||
}
|
||||
|
||||
func (s *agentRunService) FindPageByParams(queryParams *params.QueryParams) (list []models.AgentRun, paging *sqls.Paging) {
|
||||
return repositories.AgentRunRepository.FindPageByParams(sqls.DB(), queryParams)
|
||||
}
|
||||
|
||||
func (s *agentRunService) GetDetail(id int64) (*models.AgentRun, []models.AgentStep, []models.AgentToolCall) {
|
||||
run := s.Get(id)
|
||||
if run == nil {
|
||||
return nil, nil, nil
|
||||
}
|
||||
return run,
|
||||
repositories.AgentStepRepository.FindByAgentRunID(sqls.DB(), id),
|
||||
repositories.AgentToolCallRepository.FindByAgentRunID(sqls.DB(), id)
|
||||
}
|
||||
|
||||
func (s *agentRunService) GetLatestStepID(agentRunID int64) int64 {
|
||||
step := repositories.AgentStepRepository.LastByAgentRunID(sqls.DB(), agentRunID)
|
||||
if step == nil {
|
||||
return 0
|
||||
}
|
||||
return step.ID
|
||||
}
|
||||
|
||||
func (s *agentRunService) GetQualityFeedback(agentRunID int64) *models.AgentRunQualityFeedback {
|
||||
return repositories.AgentRunQualityFeedbackRepository.GetByAgentRunID(sqls.DB(), agentRunID)
|
||||
}
|
||||
|
||||
func (s *agentRunService) SaveQualityFeedback(req request.SaveAgentRunQualityFeedbackRequest, operator *dto.AuthPrincipal) error {
|
||||
if operator == nil {
|
||||
return errorsx.UnauthorizedI18n("error.auth.expired")
|
||||
}
|
||||
if req.AgentRunID <= 0 {
|
||||
return errorsx.InvalidParam("agent run id is required")
|
||||
}
|
||||
if !slices.Contains(enums.AgentRunResolutionStatusValues, req.ResolutionStatus) || !slices.Contains(enums.AgentRunEvidenceStatusValues, req.EvidenceStatus) {
|
||||
return errorsx.InvalidParam("invalid agent run quality feedback status")
|
||||
}
|
||||
comment := strings.TrimSpace(req.Comment)
|
||||
if len([]rune(comment)) > 2000 {
|
||||
return errorsx.InvalidParam("agent run quality feedback comment is too long")
|
||||
}
|
||||
return sqls.WithTransaction(func(ctx *sqls.TxContext) error {
|
||||
if repositories.AgentRunRepository.Get(ctx.Tx, req.AgentRunID) == nil {
|
||||
return errorsx.InvalidParam("agent run does not exist")
|
||||
}
|
||||
current := repositories.AgentRunQualityFeedbackRepository.GetByAgentRunID(ctx.Tx, req.AgentRunID)
|
||||
if current == nil {
|
||||
return repositories.AgentRunQualityFeedbackRepository.Create(ctx.Tx, &models.AgentRunQualityFeedback{
|
||||
AgentRunID: req.AgentRunID, ResolutionStatus: req.ResolutionStatus, EvidenceStatus: req.EvidenceStatus, Comment: comment,
|
||||
AuditFields: utils.BuildAuditFields(operator),
|
||||
})
|
||||
}
|
||||
return repositories.AgentRunQualityFeedbackRepository.Updates(ctx.Tx, current.ID, map[string]any{
|
||||
"resolution_status": req.ResolutionStatus,
|
||||
"evidence_status": req.EvidenceStatus,
|
||||
"comment": comment,
|
||||
"update_user_id": operator.UserID,
|
||||
"update_user_name": operator.Username,
|
||||
"updated_at": time.Now(),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// GetMetrics aggregates normalized audit records in Go so SQLite and MySQL
|
||||
// use identical percentile and rate semantics.
|
||||
func (s *agentRunService) GetMetrics(aiAgentID int64) AgentRunMetrics {
|
||||
runs := repositories.AgentRunRepository.FindRecent(sqls.DB(), aiAgentID, 5000)
|
||||
metrics := s.aggregateMetrics(sqls.DB(), runs)
|
||||
if len(runs) == 0 {
|
||||
return metrics
|
||||
}
|
||||
conversationCount := repositories.ConversationRepository.CountByAIAgentID(sqls.DB(), aiAgentID)
|
||||
if conversationCount > 0 {
|
||||
metrics.HandoffRate = float64(repositories.ConversationRepository.CountHandoffByAIAgentID(sqls.DB(), aiAgentID)) / float64(conversationCount)
|
||||
}
|
||||
return metrics
|
||||
}
|
||||
|
||||
// GetEngineComparisons keeps Workflow, Autonomous, and Hybrid reports based on
|
||||
// the same normalized audit and reviewed-quality records. Conversation-level
|
||||
// handoff is deliberately excluded because it cannot be attributed to one
|
||||
// Engine after a mode change.
|
||||
func (s *agentRunService) GetEngineComparisons(aiAgentID int64) []AgentRunEngineComparison {
|
||||
runs := repositories.AgentRunRepository.FindRecent(sqls.DB(), aiAgentID, 5000)
|
||||
groups := make(map[string][]models.AgentRun)
|
||||
for _, run := range runs {
|
||||
engineCode := strings.TrimSpace(run.EngineCode)
|
||||
if engineCode == "" {
|
||||
engineCode = "unknown"
|
||||
}
|
||||
groups[engineCode] = append(groups[engineCode], run)
|
||||
}
|
||||
engineCodes := make([]string, 0, len(groups))
|
||||
for engineCode := range groups {
|
||||
engineCodes = append(engineCodes, engineCode)
|
||||
}
|
||||
sort.Strings(engineCodes)
|
||||
ret := make([]AgentRunEngineComparison, 0, len(engineCodes))
|
||||
for _, engineCode := range engineCodes {
|
||||
ret = append(ret, AgentRunEngineComparison{EngineCode: engineCode, Metrics: s.aggregateMetrics(sqls.DB(), groups[engineCode])})
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (s *agentRunService) aggregateMetrics(db *gorm.DB, runs []models.AgentRun) AgentRunMetrics {
|
||||
metrics := AgentRunMetrics{TotalRuns: len(runs)}
|
||||
if len(runs) == 0 {
|
||||
return metrics
|
||||
}
|
||||
runIDs := make([]int64, 0, len(runs))
|
||||
durations := make([]int64, 0, len(runs))
|
||||
var durationTotal int64
|
||||
for _, run := range runs {
|
||||
runIDs = append(runIDs, run.ID)
|
||||
switch run.Status {
|
||||
case "completed":
|
||||
metrics.CompletedRuns++
|
||||
case "failed":
|
||||
metrics.FailedRuns++
|
||||
case "interrupted":
|
||||
metrics.InterruptedRuns++
|
||||
}
|
||||
metrics.PromptTokens += int64(run.PromptTokens)
|
||||
metrics.CompletionTokens += int64(run.CompletionTokens)
|
||||
if run.EndedAt != nil {
|
||||
duration := run.EndedAt.Sub(run.StartedAt).Milliseconds()
|
||||
if duration < 0 {
|
||||
duration = 0
|
||||
}
|
||||
durations = append(durations, duration)
|
||||
durationTotal += duration
|
||||
}
|
||||
}
|
||||
metrics.CompletionRate = float64(metrics.CompletedRuns) / float64(metrics.TotalRuns)
|
||||
if len(durations) > 0 {
|
||||
metrics.AverageDurationMS = durationTotal / int64(len(durations))
|
||||
sort.Slice(durations, func(i, j int) bool { return durations[i] < durations[j] })
|
||||
index := (len(durations)*95+99)/100 - 1
|
||||
metrics.P95DurationMS = durations[index]
|
||||
}
|
||||
steps := repositories.AgentStepRepository.FindByAgentRunIDs(db, runIDs)
|
||||
metrics.AverageSteps = float64(len(steps)) / float64(metrics.TotalRuns)
|
||||
fallbackRunIDs := make(map[int64]struct{})
|
||||
for _, step := range steps {
|
||||
if step.StepType == "policy" && step.StepCode == "knowledge_evidence" {
|
||||
fallbackRunIDs[step.AgentRunID] = struct{}{}
|
||||
}
|
||||
}
|
||||
metrics.KnowledgeFallbackRate = float64(len(fallbackRunIDs)) / float64(metrics.TotalRuns)
|
||||
toolCalls := repositories.AgentToolCallRepository.FindByAgentRunIDs(db, runIDs)
|
||||
metrics.ToolCalls = len(toolCalls)
|
||||
if len(toolCalls) > 0 {
|
||||
completed := 0
|
||||
for _, call := range toolCalls {
|
||||
if call.Status == "completed" {
|
||||
completed++
|
||||
}
|
||||
}
|
||||
metrics.ToolSuccessRate = float64(completed) / float64(len(toolCalls))
|
||||
}
|
||||
interrupts := repositories.ConversationInterruptRepository.FindByAgentRunIDs(db, runIDs)
|
||||
for _, interrupt := range interrupts {
|
||||
if interrupt.ResumeCount <= 0 {
|
||||
continue
|
||||
}
|
||||
metrics.ResumedInterrupts++
|
||||
if interrupt.Status == "resolved" {
|
||||
metrics.ResolvedInterrupts++
|
||||
}
|
||||
}
|
||||
if metrics.ResumedInterrupts > 0 {
|
||||
metrics.InterruptRecoveryRate = float64(metrics.ResolvedInterrupts) / float64(metrics.ResumedInterrupts)
|
||||
}
|
||||
feedbacks := repositories.AgentRunQualityFeedbackRepository.FindByAgentRunIDs(db, runIDs)
|
||||
metrics.ReviewedRuns = len(feedbacks)
|
||||
for _, feedback := range feedbacks {
|
||||
if feedback.ResolutionStatus == enums.AgentRunResolutionStatusResolved {
|
||||
metrics.ResolvedRuns++
|
||||
}
|
||||
if feedback.EvidenceStatus == enums.AgentRunEvidenceStatusUnsupported {
|
||||
metrics.UnsupportedEvidenceRuns++
|
||||
}
|
||||
}
|
||||
if metrics.ReviewedRuns > 0 {
|
||||
metrics.ResolutionRate = float64(metrics.ResolvedRuns) / float64(metrics.ReviewedRuns)
|
||||
metrics.UnsupportedEvidenceRate = float64(metrics.UnsupportedEvidenceRuns) / float64(metrics.ReviewedRuns)
|
||||
}
|
||||
return metrics
|
||||
}
|
||||
|
||||
type WorkflowAgentRunInput struct {
|
||||
WorkflowRunID int64
|
||||
WorkflowVersionID int64
|
||||
ConversationID int64
|
||||
AIAgentID int64
|
||||
SourceMessageID int64
|
||||
Status string
|
||||
PromptTokens int
|
||||
CompletionTokens int
|
||||
StartedAt time.Time
|
||||
EndedAt *time.Time
|
||||
ErrorMessage string
|
||||
TraceData string
|
||||
StepInputPreview string
|
||||
StepOutputPreview string
|
||||
}
|
||||
|
||||
type EngineAgentRunInput struct {
|
||||
ConversationID int64
|
||||
AIAgentID int64
|
||||
AgentRevisionID int64
|
||||
SourceMessageID int64
|
||||
EngineCode string
|
||||
Status string
|
||||
PromptTokens int
|
||||
CompletionTokens int
|
||||
StartedAt time.Time
|
||||
EndedAt *time.Time
|
||||
ErrorMessage string
|
||||
TraceData string
|
||||
StepType string
|
||||
StepCode string
|
||||
StepInputPreview string
|
||||
StepOutputPreview string
|
||||
AdditionalSteps []EngineStepInput
|
||||
ToolCalls []EngineToolCallInput
|
||||
}
|
||||
|
||||
type EngineStepInput struct {
|
||||
StepType string
|
||||
StepCode string
|
||||
WorkflowRunID int64
|
||||
Status string
|
||||
InputPreview string
|
||||
OutputPreview string
|
||||
ErrorMessage string
|
||||
}
|
||||
|
||||
type EngineToolCallInput struct {
|
||||
ToolCode string
|
||||
RiskLevel string
|
||||
RequireConfirm bool
|
||||
Status string
|
||||
ArgumentsPreview string
|
||||
ResultPreview string
|
||||
ErrorMessage string
|
||||
DurationMS int
|
||||
}
|
||||
|
||||
// RecordHybridPlaybookResume closes or re-interrupts the Hybrid AgentRun that
|
||||
// originally selected a Playbook. The detailed WorkflowRun remains separately
|
||||
// auditable; this step preserves the parent AgentRun -> AgentStep -> WorkflowRun
|
||||
// relationship across a human confirmation pause.
|
||||
func (s *agentRunService) RecordHybridPlaybookResume(db *gorm.DB, agentRunID, workflowRunID int64, status, replyText string) error {
|
||||
if agentRunID <= 0 {
|
||||
return nil
|
||||
}
|
||||
run := repositories.AgentRunRepository.Get(db, agentRunID)
|
||||
if run == nil || run.EngineCode != "hybrid" {
|
||||
return nil
|
||||
}
|
||||
status = strings.TrimSpace(status)
|
||||
if status == "" {
|
||||
status = "completed"
|
||||
}
|
||||
now := time.Now()
|
||||
durationMS := int(now.Sub(run.StartedAt).Milliseconds())
|
||||
if durationMS < 0 {
|
||||
durationMS = 0
|
||||
}
|
||||
if err := repositories.AgentRunRepository.Updates(db, run.ID, map[string]any{
|
||||
"status": status,
|
||||
"ended_at": &now,
|
||||
"error_message": "",
|
||||
"updated_at": now,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
return repositories.AgentStepRepository.Create(db, &models.AgentStep{
|
||||
AgentRunID: run.ID, WorkflowRunID: workflowRunID,
|
||||
StepType: "playbook", StepCode: "playbook_resume", Status: status,
|
||||
InputPreview: "human confirmation resume",
|
||||
OutputPreview: sanitizeAgentAuditPreview(replyText),
|
||||
StartedAt: now, EndedAt: &now, DurationMS: durationMS, CreatedAt: now,
|
||||
})
|
||||
}
|
||||
|
||||
// RecordEngineRun writes a non-workflow Engine audit run and its normalized
|
||||
// root step in one transaction owned by the caller.
|
||||
func (s *agentRunService) RecordEngineRun(db *gorm.DB, input EngineAgentRunInput) (int64, error) {
|
||||
now := time.Now()
|
||||
startedAt := input.StartedAt
|
||||
if startedAt.IsZero() {
|
||||
startedAt = now
|
||||
}
|
||||
status := strings.TrimSpace(input.Status)
|
||||
if status == "" {
|
||||
status = "completed"
|
||||
}
|
||||
run := &models.AgentRun{
|
||||
ConversationID: input.ConversationID, AIAgentID: input.AIAgentID, AgentRevisionID: input.AgentRevisionID,
|
||||
SourceMessageID: input.SourceMessageID, EngineCode: strings.TrimSpace(input.EngineCode), Status: status,
|
||||
PromptTokens: input.PromptTokens, CompletionTokens: input.CompletionTokens, StartedAt: startedAt, EndedAt: input.EndedAt,
|
||||
ErrorMessage: sanitizeAgentAuditPreview(input.ErrorMessage), TraceData: sanitizeAgentAuditPreview(input.TraceData), CreatedAt: now, UpdatedAt: now,
|
||||
}
|
||||
if err := repositories.AgentRunRepository.Create(db, run); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
durationMS := 0
|
||||
if input.EndedAt != nil {
|
||||
durationMS = int(input.EndedAt.Sub(startedAt).Milliseconds())
|
||||
if durationMS < 0 {
|
||||
durationMS = 0
|
||||
}
|
||||
}
|
||||
step := &models.AgentStep{
|
||||
AgentRunID: run.ID, StepType: strings.TrimSpace(input.StepType), StepCode: strings.TrimSpace(input.StepCode), Status: status,
|
||||
InputPreview: sanitizeAgentAuditPreview(input.StepInputPreview), OutputPreview: sanitizeAgentAuditPreview(input.StepOutputPreview), ErrorMessage: sanitizeAgentAuditPreview(input.ErrorMessage),
|
||||
StartedAt: startedAt, EndedAt: input.EndedAt, DurationMS: durationMS, CreatedAt: now,
|
||||
}
|
||||
if err := repositories.AgentStepRepository.Create(db, step); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
for _, extra := range input.AdditionalSteps {
|
||||
extraStep := &models.AgentStep{
|
||||
AgentRunID: run.ID, WorkflowRunID: extra.WorkflowRunID, StepType: strings.TrimSpace(extra.StepType), StepCode: strings.TrimSpace(extra.StepCode),
|
||||
Status: firstNonEmptyString(extra.Status, status), InputPreview: sanitizeAgentAuditPreview(extra.InputPreview), OutputPreview: sanitizeAgentAuditPreview(extra.OutputPreview),
|
||||
ErrorMessage: sanitizeAgentAuditPreview(extra.ErrorMessage), StartedAt: startedAt, EndedAt: input.EndedAt, DurationMS: durationMS, CreatedAt: now,
|
||||
}
|
||||
if err := repositories.AgentStepRepository.Create(db, extraStep); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
for _, call := range input.ToolCalls {
|
||||
toolCall := &models.AgentToolCall{
|
||||
AgentRunID: run.ID, AgentStepID: step.ID, ToolCode: strings.TrimSpace(call.ToolCode), RiskLevel: strings.TrimSpace(call.RiskLevel),
|
||||
RequireConfirm: call.RequireConfirm, Status: firstNonEmptyString(call.Status, status), ArgumentsPreview: sanitizeAgentAuditPreview(call.ArgumentsPreview),
|
||||
ResultPreview: sanitizeAgentAuditPreview(call.ResultPreview), ErrorMessage: sanitizeAgentAuditPreview(call.ErrorMessage), DurationMS: call.DurationMS, CreatedAt: now,
|
||||
}
|
||||
if err := repositories.AgentToolCallRepository.Create(db, toolCall); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
return run.ID, nil
|
||||
}
|
||||
|
||||
func sanitizeAgentAuditPreview(value string) string {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return ""
|
||||
}
|
||||
value = agentAuditSecretPattern.ReplaceAllString(value, "$1$2***")
|
||||
runes := []rune(value)
|
||||
if len(runes) <= maxAgentAuditPreviewChars {
|
||||
return value
|
||||
}
|
||||
return strings.TrimSpace(string(runes[:maxAgentAuditPreviewChars])) + "\n[preview truncated]"
|
||||
}
|
||||
|
||||
func firstNonEmptyString(items ...string) string {
|
||||
for _, item := range items {
|
||||
if value := strings.TrimSpace(item); value != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// RecordWorkflowRun writes the Engine-independent audit record inside the
|
||||
// caller's transaction. Workflow-specific tables remain the detailed source
|
||||
// for node-level diagnosis while AgentRun becomes the cross-engine summary.
|
||||
func (s *agentRunService) RecordWorkflowRun(db *gorm.DB, input WorkflowAgentRunInput) (int64, error) {
|
||||
now := time.Now()
|
||||
status := strings.TrimSpace(input.Status)
|
||||
if status == "" {
|
||||
status = "completed"
|
||||
}
|
||||
startedAt := input.StartedAt
|
||||
if startedAt.IsZero() {
|
||||
startedAt = now
|
||||
}
|
||||
run := repositories.AgentRunRepository.TakeByWorkflowRunID(db, input.WorkflowRunID)
|
||||
agentRevisionID := int64(0)
|
||||
if revision := repositories.AgentRevisionRepository.TakeByAgentIDAndWorkflowVersionID(db, input.AIAgentID, input.WorkflowVersionID); revision != nil {
|
||||
agentRevisionID = revision.ID
|
||||
}
|
||||
if run == nil {
|
||||
run = &models.AgentRun{
|
||||
ConversationID: input.ConversationID,
|
||||
AIAgentID: input.AIAgentID,
|
||||
AgentRevisionID: agentRevisionID,
|
||||
SourceMessageID: input.SourceMessageID,
|
||||
WorkflowRunID: input.WorkflowRunID,
|
||||
EngineCode: "workflow",
|
||||
Status: status,
|
||||
PromptTokens: input.PromptTokens,
|
||||
CompletionTokens: input.CompletionTokens,
|
||||
StartedAt: startedAt,
|
||||
EndedAt: input.EndedAt,
|
||||
ErrorMessage: sanitizeAgentAuditPreview(input.ErrorMessage),
|
||||
TraceData: sanitizeAgentAuditPreview(input.TraceData),
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
if err := repositories.AgentRunRepository.Create(db, run); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
} else if err := repositories.AgentRunRepository.Updates(db, run.ID, map[string]any{
|
||||
"agent_revision_id": agentRevisionID,
|
||||
"status": status,
|
||||
"prompt_tokens": input.PromptTokens,
|
||||
"completion_tokens": input.CompletionTokens,
|
||||
"ended_at": input.EndedAt,
|
||||
"error_message": sanitizeAgentAuditPreview(input.ErrorMessage),
|
||||
"trace_data": sanitizeAgentAuditPreview(input.TraceData),
|
||||
"updated_at": now,
|
||||
}); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
durationMS := 0
|
||||
if input.EndedAt != nil {
|
||||
durationMS = int(input.EndedAt.Sub(startedAt).Milliseconds())
|
||||
if durationMS < 0 {
|
||||
durationMS = 0
|
||||
}
|
||||
}
|
||||
step := &models.AgentStep{
|
||||
AgentRunID: run.ID,
|
||||
StepType: "workflow",
|
||||
StepCode: "workflow",
|
||||
Status: status,
|
||||
InputPreview: sanitizeAgentAuditPreview(input.StepInputPreview),
|
||||
OutputPreview: sanitizeAgentAuditPreview(input.StepOutputPreview),
|
||||
ErrorMessage: sanitizeAgentAuditPreview(input.ErrorMessage),
|
||||
StartedAt: startedAt,
|
||||
EndedAt: input.EndedAt,
|
||||
DurationMS: durationMS,
|
||||
CreatedAt: now,
|
||||
}
|
||||
if err := repositories.AgentStepRepository.Create(db, step); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return run.ID, nil
|
||||
}
|
||||
Reference in New Issue
Block a user