mirror of
https://github.com/laoxong/nofx.git
synced 2026-06-04 09:58:22 +08:00
3ca95b294d
* feat: integrate NOFXi agent into dev * Enhance NOFXi agent workflow and diagnostics
181 lines
5.6 KiB
Go
181 lines
5.6 KiB
Go
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)
|
|
}
|