mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-12 23:36:55 +08:00
Enhance NOFXi agent workflow and diagnostics
This commit is contained in:
180
agent/skill_outcome.go
Normal file
180
agent/skill_outcome.go
Normal file
@@ -0,0 +1,180 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"nofx/mcp"
|
||||
)
|
||||
|
||||
const (
|
||||
skillOutcomeSuccess = "success"
|
||||
skillOutcomeNeedMoreInfo = "need_more_info"
|
||||
skillOutcomeRecoverableError = "recoverable_error"
|
||||
skillOutcomeFatalError = "fatal_error"
|
||||
skillOutcomeNotHandled = "not_handled"
|
||||
)
|
||||
|
||||
type skillOutcome struct {
|
||||
Skill string `json:"skill"`
|
||||
Action string `json:"action"`
|
||||
Status string `json:"status"`
|
||||
GoalAchieved bool `json:"goal_achieved"`
|
||||
UserMessage string `json:"user_message,omitempty"`
|
||||
ErrorCode string `json:"error_code,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Data map[string]any `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
type taskReviewDecision struct {
|
||||
Route string `json:"route"`
|
||||
Answer string `json:"answer,omitempty"`
|
||||
}
|
||||
|
||||
func normalizeAtomicSkillAction(skill, action string) string {
|
||||
action = strings.TrimSpace(strings.ToLower(action))
|
||||
switch skill {
|
||||
case "trader_management":
|
||||
switch action {
|
||||
case "query", "query_list":
|
||||
return "query_list"
|
||||
case "query_running":
|
||||
return "query_running"
|
||||
case "query_detail":
|
||||
return "query_detail"
|
||||
case "update":
|
||||
return "update_name"
|
||||
case "update_name", "update_bindings":
|
||||
return action
|
||||
}
|
||||
case "exchange_management":
|
||||
switch action {
|
||||
case "query", "query_list":
|
||||
return "query_list"
|
||||
case "query_detail":
|
||||
return "query_detail"
|
||||
case "update":
|
||||
return "update_name"
|
||||
case "update_name", "update_status":
|
||||
return action
|
||||
}
|
||||
case "model_management":
|
||||
switch action {
|
||||
case "query", "query_list":
|
||||
return "query_list"
|
||||
case "query_detail":
|
||||
return "query_detail"
|
||||
case "update":
|
||||
return "update_name"
|
||||
case "update_name", "update_endpoint", "update_status":
|
||||
return action
|
||||
}
|
||||
case "strategy_management":
|
||||
switch action {
|
||||
case "query", "query_list":
|
||||
return "query_list"
|
||||
case "query_detail":
|
||||
return "query_detail"
|
||||
case "update":
|
||||
return "update_name"
|
||||
case "update_name", "update_config", "update_prompt":
|
||||
return action
|
||||
}
|
||||
}
|
||||
return action
|
||||
}
|
||||
|
||||
func inferSkillOutcome(skill, action, answer string, activeSession skillSession, data map[string]any) skillOutcome {
|
||||
outcome := skillOutcome{
|
||||
Skill: skill,
|
||||
Action: action,
|
||||
Status: skillOutcomeSuccess,
|
||||
UserMessage: strings.TrimSpace(answer),
|
||||
Data: data,
|
||||
}
|
||||
if activeSession.Name != "" {
|
||||
outcome.Status = skillOutcomeNeedMoreInfo
|
||||
outcome.GoalAchieved = false
|
||||
return outcome
|
||||
}
|
||||
|
||||
lower := strings.ToLower(strings.TrimSpace(answer))
|
||||
switch {
|
||||
case lower == "":
|
||||
outcome.Status = skillOutcomeNotHandled
|
||||
case strings.Contains(lower, "失败") || strings.Contains(lower, "failed") || strings.Contains(lower, "error"):
|
||||
outcome.Status = skillOutcomeRecoverableError
|
||||
outcome.Error = strings.TrimSpace(answer)
|
||||
default:
|
||||
outcome.GoalAchieved = true
|
||||
}
|
||||
return outcome
|
||||
}
|
||||
|
||||
func parseTaskReviewDecision(raw string) (taskReviewDecision, error) {
|
||||
raw = strings.TrimSpace(raw)
|
||||
raw = strings.TrimPrefix(raw, "```json")
|
||||
raw = strings.TrimPrefix(raw, "```")
|
||||
raw = strings.TrimSuffix(raw, "```")
|
||||
raw = strings.TrimSpace(raw)
|
||||
|
||||
var decision taskReviewDecision
|
||||
if err := json.Unmarshal([]byte(raw), &decision); err == nil {
|
||||
decision.Route = strings.TrimSpace(strings.ToLower(decision.Route))
|
||||
decision.Answer = strings.TrimSpace(decision.Answer)
|
||||
return decision, nil
|
||||
}
|
||||
start := strings.Index(raw, "{")
|
||||
end := strings.LastIndex(raw, "}")
|
||||
if start >= 0 && end > start {
|
||||
if err := json.Unmarshal([]byte(raw[start:end+1]), &decision); err == nil {
|
||||
decision.Route = strings.TrimSpace(strings.ToLower(decision.Route))
|
||||
decision.Answer = strings.TrimSpace(decision.Answer)
|
||||
return decision, nil
|
||||
}
|
||||
}
|
||||
return taskReviewDecision{}, fmt.Errorf("invalid task review json")
|
||||
}
|
||||
|
||||
func (a *Agent) reviewTaskCompletion(ctx context.Context, userID int64, lang, text string, outcome skillOutcome) (taskReviewDecision, error) {
|
||||
if a.aiClient == nil {
|
||||
if outcome.Status == skillOutcomeRecoverableError || outcome.Status == skillOutcomeFatalError || outcome.Status == skillOutcomeNotHandled {
|
||||
return taskReviewDecision{Route: "replan"}, nil
|
||||
}
|
||||
return taskReviewDecision{Route: "complete", Answer: outcome.UserMessage}, nil
|
||||
}
|
||||
|
||||
recentConversationCtx := a.buildRecentConversationContext(userID, text)
|
||||
outcomeJSON, _ := json.Marshal(outcome)
|
||||
systemPrompt := `You are the task-level Plan-Execute-Review supervisor for NOFXi.
|
||||
You are reviewing the JSON result returned by one structured skill execution.
|
||||
Return JSON only. Do not return markdown.
|
||||
|
||||
Rules:
|
||||
- Decide whether the OVERALL user task is finished, not whether the skill itself ran successfully.
|
||||
- Use route "complete" only when the user's task is now complete or the best next message is a final user-facing reply.
|
||||
- Use route "replan" when the user's task is not complete yet and the planner should continue from the new skill outcome.
|
||||
- Prefer route "replan" for recoverable errors, unmet goals, missing prerequisites, or cases where another skill/tool sequence may help.
|
||||
- If you choose "complete", produce the final user-facing answer in the user's language.
|
||||
|
||||
Return JSON with this exact shape:
|
||||
{"route":"complete|replan","answer":""}`
|
||||
userPrompt := fmt.Sprintf("Language: %s\nUser message: %s\n\nRecent conversation:\n%s\n\nSkill outcome JSON:\n%s", lang, text, recentConversationCtx, string(outcomeJSON))
|
||||
|
||||
stageCtx, cancel := withPlannerStageTimeout(ctx, directReplyTimeout)
|
||||
defer cancel()
|
||||
|
||||
raw, err := a.aiClient.CallWithRequest(&mcp.Request{
|
||||
Messages: []mcp.Message{
|
||||
mcp.NewSystemMessage(systemPrompt),
|
||||
mcp.NewUserMessage(userPrompt),
|
||||
},
|
||||
Ctx: stageCtx,
|
||||
})
|
||||
if err != nil {
|
||||
return taskReviewDecision{}, err
|
||||
}
|
||||
return parseTaskReviewDecision(raw)
|
||||
}
|
||||
Reference in New Issue
Block a user