mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-15 00:36:56 +08:00
Refine agent strategy routing and config handling
This commit is contained in:
@@ -1,210 +1,12 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"nofx/mcp"
|
||||
"nofx/store"
|
||||
)
|
||||
|
||||
type skillSemanticGateDecision struct {
|
||||
Decision string `json:"decision,omitempty"`
|
||||
Field string `json:"field,omitempty"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
}
|
||||
|
||||
func (a *Agent) evaluateHardSkillCandidate(ctx context.Context, storeUserID string, userID int64, lang string, session skillSession, skillName, action, text string) skillSemanticGateDecision {
|
||||
fallback := fallbackSkillSemanticGate(skillName, action, session, text)
|
||||
if a == nil || a.aiClient == nil {
|
||||
return fallback
|
||||
}
|
||||
|
||||
systemPrompt := `You are the second-stage semantic gate for one NOFXi hard skill.
|
||||
Return JSON only. No markdown.
|
||||
|
||||
Decide exactly one:
|
||||
- "execute": this request is concrete enough for this hard skill to handle now
|
||||
- "explain": the user is asking about visible UI fields, options, requirements, or how to fill them
|
||||
- "planner": this request is too open-ended, strategic, subjective, ambiguous, or not yet UI-ready for this hard skill
|
||||
|
||||
Rules:
|
||||
- Use "execute" when the request maps to the current skill's visible fields, existing entity options, or a normal multi-turn form flow.
|
||||
- Use "explain" when the user asks what fields/options exist, what a field means, or how to fill it.
|
||||
- Use "planner" when the user asks for outcome-seeking advice like "make money", "don't lose", "best", "optimize", or otherwise asks you to design the solution before UI-level parameters are clear.
|
||||
- Use "planner" when semantic readiness is not met: the route points to a skill/action, but the request is still missing core required fields and would otherwise mostly result in a mechanical missing-field error.
|
||||
- Be conservative. If this hard skill would mostly respond with a misleading missing-field error, choose "planner" instead.
|
||||
|
||||
Return JSON:
|
||||
{"decision":"execute|explain|planner","field":"","reason":""}`
|
||||
|
||||
userPrompt := fmt.Sprintf(
|
||||
"Language: %s\nSkill: %s\nAction: %s\nActive skill session JSON: %s\nSemantic readiness summary:\n%s\nVisible field summary:\n%s\nVisible option summary:\n%s\nDomain primer:\n%s\nUser message: %s",
|
||||
lang,
|
||||
skillName,
|
||||
action,
|
||||
mustMarshalJSON(session),
|
||||
a.skillSemanticReadinessSummary(lang, session, skillName, action, text),
|
||||
a.skillVisibleFieldSummary(storeUserID, lang, skillName, action),
|
||||
a.skillVisibleOptionSummary(storeUserID, lang, skillName, action),
|
||||
buildSkillDomainPrimer(lang, skillName),
|
||||
text,
|
||||
)
|
||||
|
||||
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 fallback
|
||||
}
|
||||
if parsed, ok := parseSkillSemanticGateDecision(raw); ok {
|
||||
if parsed.Field == "" {
|
||||
parsed.Field = detectSkillQuestionField(skillName, text, session)
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func parseSkillSemanticGateDecision(raw string) (skillSemanticGateDecision, bool) {
|
||||
raw = strings.TrimSpace(raw)
|
||||
raw = strings.TrimPrefix(raw, "```json")
|
||||
raw = strings.TrimPrefix(raw, "```")
|
||||
raw = strings.TrimSuffix(raw, "```")
|
||||
raw = strings.TrimSpace(raw)
|
||||
|
||||
var out skillSemanticGateDecision
|
||||
if err := json.Unmarshal([]byte(raw), &out); err != nil {
|
||||
start := strings.Index(raw, "{")
|
||||
end := strings.LastIndex(raw, "}")
|
||||
if start < 0 || end <= start || json.Unmarshal([]byte(raw[start:end+1]), &out) != nil {
|
||||
return skillSemanticGateDecision{}, false
|
||||
}
|
||||
}
|
||||
out.Decision = strings.TrimSpace(strings.ToLower(out.Decision))
|
||||
out.Field = strings.TrimSpace(out.Field)
|
||||
out.Reason = strings.TrimSpace(out.Reason)
|
||||
switch out.Decision {
|
||||
case "execute", "explain", "planner":
|
||||
return out, true
|
||||
default:
|
||||
return skillSemanticGateDecision{}, false
|
||||
}
|
||||
}
|
||||
|
||||
func fallbackSkillSemanticGate(skillName, action string, session skillSession, text string) skillSemanticGateDecision {
|
||||
if looksLikeExplanationQuestion(text) {
|
||||
return skillSemanticGateDecision{
|
||||
Decision: "explain",
|
||||
Field: "",
|
||||
}
|
||||
}
|
||||
if strings.TrimSpace(session.Name) == skillName && strings.TrimSpace(session.Action) != "" {
|
||||
return skillSemanticGateDecision{Decision: "execute"}
|
||||
}
|
||||
if isSimpleEntityMutationAction(action) && !hasExplicitSkillCueForSemanticGate(skillName, text) {
|
||||
return skillSemanticGateDecision{Decision: "planner", Reason: "requires_llm_intent_resolution"}
|
||||
}
|
||||
if missing := semanticReadinessMissingSlots(skillName, action, session, text); len(missing) > 0 {
|
||||
return skillSemanticGateDecision{Decision: "planner", Reason: "semantic_readiness_missing_core_fields"}
|
||||
}
|
||||
return skillSemanticGateDecision{Decision: "execute"}
|
||||
}
|
||||
|
||||
func hasExplicitSkillCueForSemanticGate(skillName, text string) bool {
|
||||
switch strings.TrimSpace(skillName) {
|
||||
case "trader_management":
|
||||
return hasExplicitManagementDomainCue(text, "trader") || hasExplicitCreateIntentForDomain(text, "trader")
|
||||
case "exchange_management":
|
||||
return hasExplicitManagementDomainCue(text, "exchange")
|
||||
case "model_management":
|
||||
return hasExplicitManagementDomainCue(text, "model")
|
||||
case "strategy_management":
|
||||
return hasExplicitManagementDomainCue(text, "strategy")
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func semanticReadinessMissingSlots(skillName, action string, session skillSession, text string) []string {
|
||||
if strings.TrimSpace(action) == "" {
|
||||
return nil
|
||||
}
|
||||
if strings.TrimSpace(session.Name) == skillName && strings.TrimSpace(session.Action) != "" {
|
||||
return nil
|
||||
}
|
||||
values := map[string]string{}
|
||||
missing := missingRequiredActionSlots(skillName, action, values)
|
||||
if action != "create" {
|
||||
return nil
|
||||
}
|
||||
if skillName == "strategy_management" {
|
||||
return nil
|
||||
}
|
||||
if skillName == "model_management" {
|
||||
coreMissing := missingCoreReadinessSlots([]string{"provider"}, values)
|
||||
if len(coreMissing) > 0 {
|
||||
return coreMissing
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if skillName == "exchange_management" {
|
||||
coreMissing := missingCoreReadinessSlots([]string{"exchange_type", "account_name", "api_key", "secret_key"}, values)
|
||||
if len(coreMissing) > 0 {
|
||||
return coreMissing
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if len(missing) == 0 || len(missing) == len(missingRequiredActionSlots(skillName, action, map[string]string{})) {
|
||||
if hasExplicitCreateIntentForDomain(text, "trader") || containsAny(strings.ToLower(text), []string{"创建", "新建", "create", "new"}) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if len(missing) >= 2 {
|
||||
return missing
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func missingCoreReadinessSlots(keys []string, values map[string]string) []string {
|
||||
missing := make([]string, 0, len(keys))
|
||||
for _, key := range keys {
|
||||
if strings.TrimSpace(values[key]) == "" {
|
||||
missing = append(missing, key)
|
||||
}
|
||||
}
|
||||
return missing
|
||||
}
|
||||
|
||||
func (a *Agent) skillSemanticReadinessSummary(lang string, session skillSession, skillName, action, text string) string {
|
||||
missing := semanticReadinessMissingSlots(skillName, action, session, text)
|
||||
if len(missing) == 0 {
|
||||
if lang == "zh" {
|
||||
return "当前语义已足够进入该 skill。"
|
||||
}
|
||||
return "Semantic readiness is sufficient for this skill."
|
||||
}
|
||||
display := make([]string, 0, len(missing))
|
||||
for _, slot := range missing {
|
||||
display = append(display, slotDisplayName(slot, lang))
|
||||
}
|
||||
if lang == "zh" {
|
||||
return "当前语义还缺核心字段:" + strings.Join(display, "、") + "。如果直接执行只会变成程序式缺字段提示,应优先走 planner/ask_user。"
|
||||
}
|
||||
return "Core semantic fields are still missing: " + strings.Join(display, ", ") + ". Prefer planner/ask_user before direct execution."
|
||||
}
|
||||
|
||||
func detectSkillQuestionField(skillName, text string, session skillSession) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (a *Agent) skillVisibleFieldSummary(storeUserID, lang, skillName, action string) string {
|
||||
fieldNames := make([]string, 0, 20)
|
||||
add := func(field string) {
|
||||
@@ -259,6 +61,42 @@ func (a *Agent) skillVisibleFieldSummary(storeUserID, lang, skillName, action st
|
||||
return prefix + ":" + strings.Join(fieldNames, "、")
|
||||
}
|
||||
|
||||
func (a *Agent) strategyTypeForTarget(storeUserID string, target *EntityReference) (string, bool) {
|
||||
if a == nil || a.store == nil || target == nil {
|
||||
return "", false
|
||||
}
|
||||
var strategy *store.Strategy
|
||||
var err error
|
||||
if id := strings.TrimSpace(target.ID); id != "" {
|
||||
strategy, err = a.store.Strategy().Get(storeUserID, id)
|
||||
} else if name := strings.TrimSpace(target.Name); name != "" {
|
||||
strategies, listErr := a.store.Strategy().List(storeUserID)
|
||||
if listErr != nil {
|
||||
return "", false
|
||||
}
|
||||
for _, item := range strategies {
|
||||
if item != nil && strings.EqualFold(strings.TrimSpace(item.Name), name) {
|
||||
strategy = item
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return "", false
|
||||
}
|
||||
if err != nil || strategy == nil {
|
||||
return "", false
|
||||
}
|
||||
cfg := store.GetDefaultStrategyConfig("zh")
|
||||
if strings.TrimSpace(strategy.Config) != "" {
|
||||
_ = json.Unmarshal([]byte(strategy.Config), &cfg)
|
||||
}
|
||||
strategyType := strings.TrimSpace(cfg.StrategyType)
|
||||
if strategyType == "" {
|
||||
strategyType = "ai_trading"
|
||||
}
|
||||
return strategyType, true
|
||||
}
|
||||
|
||||
func (a *Agent) skillVisibleOptionSummary(storeUserID, lang, skillName, action string) string {
|
||||
switch skillName {
|
||||
case "model_management":
|
||||
|
||||
Reference in New Issue
Block a user