Files
nofx/agent/skill_registry.go
2026-05-02 22:55:10 +08:00

722 lines
25 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package agent
import (
"embed"
"encoding/json"
"fmt"
"sort"
"strings"
"sync"
)
//go:embed skills/*.json
var embeddedSkillDefinitions embed.FS
type SkillDefinition struct {
Name string `json:"name"`
Kind string `json:"kind"`
Domain string `json:"domain"`
Description string `json:"description"`
Intents []string `json:"intents,omitempty"`
Capabilities []string `json:"capabilities,omitempty"`
DynamicRules []string `json:"dynamic_rules,omitempty"`
Actions map[string]SkillActionDefinition `json:"actions,omitempty"`
ToolMapping map[string]string `json:"tool_mapping,omitempty"`
FieldConstraints map[string]SkillFieldConstraint `json:"field_constraints,omitempty"`
ValidationRules []string `json:"validation_rules,omitempty"`
PerExchangeRequiredFields map[string][]string `json:"per_exchange_required_fields,omitempty"`
}
type SkillFieldConstraint struct {
Type string `json:"type,omitempty"`
Required bool `json:"required,omitempty"`
Values []string `json:"values,omitempty"`
Aliases map[string]string `json:"aliases,omitempty"`
Description string `json:"description,omitempty"`
RequiredFor []string `json:"required_for,omitempty"`
Default any `json:"default,omitempty"`
Min *float64 `json:"min,omitempty"`
Max *float64 `json:"max,omitempty"`
MaxLength int `json:"max_length,omitempty"`
MustBeHTTPS bool `json:"must_be_https,omitempty"`
Pattern string `json:"pattern,omitempty"`
}
type SkillActionDefinition struct {
Description string `json:"description,omitempty"`
RequiredSlots []string `json:"required_slots,omitempty"`
OptionalSlots []string `json:"optional_slots,omitempty"`
NeedsConfirmation bool `json:"needs_confirmation,omitempty"`
Goal string `json:"goal,omitempty"`
DynamicRules []string `json:"dynamic_rules,omitempty"`
SuccessOutput string `json:"success_output,omitempty"`
FailureOutput string `json:"failure_output,omitempty"`
}
var skillRegistry = mustLoadSkillRegistry()
var skillContextCache sync.Map
func mustLoadSkillRegistry() map[string]SkillDefinition {
registry, err := loadSkillRegistry()
if err != nil {
panic(err)
}
return registry
}
func loadSkillRegistry() (map[string]SkillDefinition, error) {
entries, err := embeddedSkillDefinitions.ReadDir("skills")
if err != nil {
return nil, err
}
registry := make(map[string]SkillDefinition, len(entries))
for _, entry := range entries {
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".json") {
continue
}
raw, err := embeddedSkillDefinitions.ReadFile("skills/" + entry.Name())
if err != nil {
return nil, err
}
var def SkillDefinition
if err := json.Unmarshal(raw, &def); err != nil {
return nil, fmt.Errorf("parse skill definition %s: %w", entry.Name(), err)
}
def = normalizeSkillDefinition(def)
if def.Name == "" {
return nil, fmt.Errorf("skill definition %s has empty name", entry.Name())
}
registry[def.Name] = def
}
return registry, nil
}
func normalizeSkillDefinition(def SkillDefinition) SkillDefinition {
def.Name = strings.TrimSpace(def.Name)
def.Kind = strings.TrimSpace(def.Kind)
def.Domain = strings.TrimSpace(def.Domain)
def.Description = strings.TrimSpace(def.Description)
def.Intents = cleanStringList(def.Intents)
def.Capabilities = cleanStringList(def.Capabilities)
def.DynamicRules = cleanStringList(def.DynamicRules)
if len(def.Actions) > 0 {
normalized := make(map[string]SkillActionDefinition, len(def.Actions))
for key, action := range def.Actions {
key = strings.TrimSpace(key)
if key == "" {
continue
}
action.Description = strings.TrimSpace(action.Description)
action.RequiredSlots = cleanStringList(action.RequiredSlots)
action.OptionalSlots = cleanStringList(action.OptionalSlots)
action.Goal = strings.TrimSpace(action.Goal)
action.DynamicRules = cleanStringList(action.DynamicRules)
action.SuccessOutput = strings.TrimSpace(action.SuccessOutput)
action.FailureOutput = strings.TrimSpace(action.FailureOutput)
normalized[key] = action
}
def.Actions = normalized
}
if len(def.ToolMapping) > 0 {
normalized := make(map[string]string, len(def.ToolMapping))
for key, value := range def.ToolMapping {
key = strings.TrimSpace(key)
value = strings.TrimSpace(value)
if key == "" || value == "" {
continue
}
normalized[key] = value
}
def.ToolMapping = normalized
}
if len(def.FieldConstraints) > 0 {
normalized := make(map[string]SkillFieldConstraint, len(def.FieldConstraints))
for key, constraint := range def.FieldConstraints {
key = strings.TrimSpace(key)
if key == "" {
continue
}
constraint.Type = strings.TrimSpace(constraint.Type)
constraint.Values = cleanStringList(constraint.Values)
constraint.RequiredFor = cleanStringList(constraint.RequiredFor)
constraint.Description = strings.TrimSpace(constraint.Description)
if len(constraint.Aliases) > 0 {
aliases := make(map[string]string, len(constraint.Aliases))
for alias, value := range constraint.Aliases {
alias = strings.TrimSpace(alias)
value = strings.TrimSpace(value)
if alias == "" || value == "" {
continue
}
aliases[alias] = value
}
constraint.Aliases = aliases
}
normalized[key] = constraint
}
def.FieldConstraints = normalized
}
def.ValidationRules = cleanStringList(def.ValidationRules)
if len(def.PerExchangeRequiredFields) > 0 {
normalized := make(map[string][]string, len(def.PerExchangeRequiredFields))
for key, fields := range def.PerExchangeRequiredFields {
key = strings.TrimSpace(key)
if key == "" {
continue
}
normalized[key] = cleanStringList(fields)
}
def.PerExchangeRequiredFields = normalized
}
return def
}
func getSkillDefinition(name string) (SkillDefinition, bool) {
def, ok := skillRegistry[strings.TrimSpace(name)]
return def, ok
}
func listSkillNames() []string {
names := make([]string, 0, len(skillRegistry))
for name := range skillRegistry {
names = append(names, name)
}
sort.Strings(names)
return names
}
func buildSkillRoutingSummary(lang string, skillNames []string) string {
lines := make([]string, 0, len(skillNames))
for _, name := range skillNames {
def, ok := getSkillDefinition(name)
if !ok {
continue
}
parts := []string{strings.TrimSpace(def.Description)}
if len(def.DynamicRules) > 0 {
parts = append(parts, strings.Join(def.DynamicRules, " "))
}
switch name {
case "trader_management":
if lang == "zh" {
parts = append(parts, "这个 skill 负责交易员本体和绑定关系;交易员编辑默认只换绑定,不改策略、模型、交易所的内部配置。")
} else {
parts = append(parts, "This skill owns the trader itself and its bindings; trader edits should switch bindings, not mutate the internals of the strategy, model, or exchange.")
}
case "strategy_management":
if lang == "zh" {
parts = append(parts, "策略模板创建后应出现在策略列表/策略页。用户没问运行时,不要主动延伸到交易员绑定。")
} else {
parts = append(parts, "After creation, strategy templates should appear in the strategy list/page. Do not proactively bring up trader binding unless the user asks to run it.")
}
}
lines = append(lines, fmt.Sprintf("- %s: %s", name, strings.Join(cleanStringList(parts), " ")))
}
return strings.Join(lines, "\n")
}
func buildSkillDefinitionSummary(lang string, skillNames []string) string {
lines := make([]string, 0, len(skillNames))
for _, name := range skillNames {
def, ok := getSkillDefinition(name)
if !ok {
continue
}
parts := []string{strings.TrimSpace(def.Description)}
if len(def.Capabilities) > 0 {
if lang == "zh" {
parts = append(parts, "能力: "+strings.Join(def.Capabilities, ""))
} else {
parts = append(parts, "capabilities: "+strings.Join(def.Capabilities, "; "))
}
}
if len(def.DynamicRules) > 0 {
if lang == "zh" {
parts = append(parts, "规则: "+strings.Join(def.DynamicRules, ""))
} else {
parts = append(parts, "rules: "+strings.Join(def.DynamicRules, "; "))
}
}
if action, ok := def.Actions["create"]; ok && len(action.RequiredSlots) > 0 {
if lang == "zh" {
parts = append(parts, "创建必填: "+formatRequiredSlotList(lang, action.RequiredSlots))
} else {
parts = append(parts, "create requires: "+formatRequiredSlotList(lang, action.RequiredSlots))
}
}
switch name {
case "trader_management":
if lang == "zh" {
parts = append(parts, "这个 skill 负责交易员本体和绑定关系;交易员编辑默认只换绑定,不改策略、模型、交易所的内部配置。")
} else {
parts = append(parts, "This skill owns the trader itself and its bindings; trader edits should switch bindings, not mutate the internals of the strategy, model, or exchange.")
}
case "strategy_management":
if lang == "zh" {
parts = append(parts, "策略模板创建后应出现在策略列表/策略页。用户没问运行时,不要主动延伸到交易员绑定。")
} else {
parts = append(parts, "After creation, strategy templates should appear in the strategy list/page. Do not proactively bring up trader binding unless the user asks to run it.")
}
}
lines = append(lines, fmt.Sprintf("- %s: %s", name, strings.Join(cleanStringList(parts), " ")))
}
return strings.Join(lines, "\n")
}
func defaultManagementSkillNames() []string {
return []string{
"trader_management",
"exchange_management",
"model_management",
"strategy_management",
}
}
func buildSkillDependencySummary(lang string, session skillSession) string {
if strings.TrimSpace(session.Name) == "" {
return ""
}
switch session.Name {
case "trader_management":
if session.Action == "create" {
if lang == "zh" {
return "trader_management:create 必须收齐 4 个核心槽位:交易员名称、交易所、模型、策略。后 3 个依赖项都允许两种补法:直接选用户已有可用资源,或在当前主流程里立即新建/启用后再回流继续创建交易员。若用户是在启用、修复或新建这些依赖资源,这仍然是在继续创建交易员主流程,不是新开平级任务。"
}
return "trader_management:create requires 4 core slots: trader name, exchange, model, and strategy. The last 3 dependencies can be satisfied in two ways: choose an existing usable resource, or create/enable one inline and then resume trader creation. If the user is enabling, fixing, or creating one of those dependencies, that is still continuation of the trader creation flow, not a new peer task."
}
if lang == "zh" {
return "当当前对象是交易员时,换绑模型、交易所、策略都属于 trader_management 的继续操作;但如果用户要改这些对象的内部配置,应切到对应 management skill。"
}
return "When the current object is a trader, rebinding its model, exchange, or strategy remains inside trader_management; but if the user wants to change the internals of those resources, switch to the corresponding management skill."
default:
return ""
}
}
func buildSkillActionContractSummary(lang string, session skillSession) string {
if strings.TrimSpace(session.Name) == "" || strings.TrimSpace(session.Action) == "" {
return ""
}
def, ok := getSkillDefinition(session.Name)
if !ok {
return ""
}
action, ok := def.Actions[session.Action]
if !ok {
return ""
}
required := defaultIfEmpty(formatRequiredSlotList(lang, action.RequiredSlots), "无")
goal := strings.TrimSpace(action.Goal)
if goal == "" {
goal = strings.TrimSpace(action.Description)
}
lines := []string{
fmt.Sprintf("### Active Skill Contract: %s:%s", session.Name, session.Action),
}
if lang == "zh" {
lines = append(lines, "- 目标:"+defaultIfEmpty(goal, "按该动作的业务规则完成当前请求。"))
lines = append(lines, "- 必填输入:"+required)
if len(action.DynamicRules) > 0 {
lines = append(lines, "- 动态逻辑规则:")
for i, rule := range action.DynamicRules {
lines = append(lines, fmt.Sprintf(" %d. %s", i+1, rule))
}
}
if action.SuccessOutput != "" || action.FailureOutput != "" {
lines = append(lines, "- 预期输出:"+strings.TrimSpace(strings.Join(cleanStringList([]string{
ifThenElse(action.SuccessOutput != "", "成功:"+action.SuccessOutput, ""),
ifThenElse(action.FailureOutput != "", "失败:"+action.FailureOutput, ""),
}), "")))
}
} else {
lines = append(lines, "- Goal: "+defaultIfEmpty(goal, "Complete the current request under this action's business rules."))
lines = append(lines, "- Required input: "+required)
if len(action.DynamicRules) > 0 {
lines = append(lines, "- Dynamic rules:")
for i, rule := range action.DynamicRules {
lines = append(lines, fmt.Sprintf(" %d. %s", i+1, rule))
}
}
if action.SuccessOutput != "" || action.FailureOutput != "" {
lines = append(lines, "- Expected output: "+strings.TrimSpace(strings.Join(cleanStringList([]string{
ifThenElse(action.SuccessOutput != "", "success: "+action.SuccessOutput, ""),
ifThenElse(action.FailureOutput != "", "failure: "+action.FailureOutput, ""),
}), "; ")))
}
}
return strings.Join(lines, "\n")
}
func ifThenElse[T any](cond bool, a, b T) T {
if cond {
return a
}
return b
}
func buildSkillForbiddenSummary(lang string, skillNames []string) string {
lines := make([]string, 0, len(skillNames))
for _, name := range skillNames {
switch name {
case "trader_management":
if lang == "zh" {
lines = append(lines, "- trader_management 不能直接设计赚钱/不亏钱方案;那类目标应交给 planner。")
lines = append(lines, "- trader_management 不能让用户手动设置、充值或修改交易员余额;交易员初始余额应由系统自动读取绑定交易所净值。")
} else {
lines = append(lines, "- trader_management must not invent a profit-seeking plan; those requests belong to the planner.")
lines = append(lines, "- trader_management must not let the user set, top up, or manually edit trader balance; trader initial balance should be auto-read from the bound exchange equity.")
}
case "exchange_management":
if lang == "zh" {
lines = append(lines, "- exchange_management 只负责保存和修改交易所配置,不负责行情查询、交易执行或诊断 API 报错。")
} else {
lines = append(lines, "- exchange_management only saves and updates exchange configs; it does not do market reads, trading, or API diagnosis.")
}
case "model_management":
if lang == "zh" {
lines = append(lines, "- model_management 只负责保存和修改模型配置,不负责测试连接、诊断上游错误或生成策略方案。")
} else {
lines = append(lines, "- model_management only saves and updates model configs; it does not test connectivity, diagnose upstream failures, or design strategies.")
}
case "strategy_management":
if lang == "zh" {
lines = append(lines, "- strategy_management 只负责模板管理;策略模板不能直接启动运行,运行态属于 trader。")
} else {
lines = append(lines, "- strategy_management only manages templates; strategy templates do not run directly and runtime belongs to traders.")
}
}
}
return strings.Join(lines, "\n")
}
func buildManagementSkillContext(lang string, session *skillSession) string {
key := fmt.Sprintf("full|%s|", lang)
if session != nil {
key = fmt.Sprintf("full|%s|%s|%s", lang, strings.TrimSpace(session.Name), strings.TrimSpace(session.Action))
}
return cachedSkillContext(key, func() string {
parts := make([]string, 0, 3)
if summary := buildSkillDefinitionSummary(lang, defaultManagementSkillNames()); summary != "" {
parts = append(parts, "Management skill summary:\n"+summary)
}
if forbidden := buildSkillForbiddenSummary(lang, defaultManagementSkillNames()); forbidden != "" {
parts = append(parts, "Management skill negative constraints:\n"+forbidden)
}
if session != nil {
if dependency := buildSkillDependencySummary(lang, *session); dependency != "" {
parts = append(parts, "Active skill dependency summary:\n"+dependency)
}
if contract := buildSkillActionContractSummary(lang, *session); contract != "" {
parts = append(parts, contract)
}
}
return strings.Join(parts, "\n\n")
})
}
func buildManagementSkillRoutingContext(lang string) string {
return buildManagementSkillRoutingContextWithSession(lang, nil)
}
func buildSkillActionRoutingSummary(lang string, session skillSession) string {
if strings.TrimSpace(session.Name) == "" || strings.TrimSpace(session.Action) == "" {
return ""
}
def, ok := getSkillDefinition(session.Name)
if !ok {
return ""
}
action, ok := def.Actions[session.Action]
if !ok {
return ""
}
lines := []string{
fmt.Sprintf("### Active skill routing hints: %s:%s", session.Name, session.Action),
}
if goal := strings.TrimSpace(action.Goal); goal != "" {
if lang == "zh" {
lines = append(lines, "- 当前动作目标:"+goal)
} else {
lines = append(lines, "- Current action goal: "+goal)
}
}
if dependency := buildSkillDependencySummary(lang, session); dependency != "" {
if lang == "zh" {
lines = append(lines, "- 当前 flow 依赖提示:"+dependency)
} else {
lines = append(lines, "- Flow dependency hint: "+dependency)
}
}
if len(action.DynamicRules) > 0 {
if lang == "zh" {
lines = append(lines, "- 当前动作动态规则:")
} else {
lines = append(lines, "- Current action dynamic rules:")
}
for i, rule := range action.DynamicRules {
lines = append(lines, fmt.Sprintf(" %d. %s", i+1, rule))
}
}
return strings.Join(lines, "\n")
}
func buildManagementSkillRoutingContextWithSession(lang string, session *skillSession) string {
key := fmt.Sprintf("routing|%s|", lang)
if session != nil {
key = fmt.Sprintf("routing|%s|%s|%s", lang, strings.TrimSpace(session.Name), strings.TrimSpace(session.Action))
}
return cachedSkillContext(key, func() string {
parts := make([]string, 0, 1)
if summary := buildSkillRoutingSummary(lang, defaultManagementSkillNames()); summary != "" {
parts = append(parts, "Management skill summary:\n"+summary)
}
if session != nil {
if summary := buildSkillActionRoutingSummary(lang, *session); summary != "" {
parts = append(parts, summary)
}
}
return strings.Join(parts, "\n\n")
})
}
func buildCurrentSkillExecutionContext(lang string, session skillSession) string {
key := fmt.Sprintf("current|%s|%s|%s", lang, strings.TrimSpace(session.Name), strings.TrimSpace(session.Action))
return cachedSkillContext(key, func() string {
parts := make([]string, 0, 3)
if dependency := buildSkillDependencySummary(lang, session); dependency != "" {
parts = append(parts, "Active skill dependency summary:\n"+dependency)
}
if contract := buildSkillActionContractSummary(lang, session); contract != "" {
parts = append(parts, contract)
}
if knowledge := buildSkillFieldKnowledgeSummary(lang, session); knowledge != "" {
parts = append(parts, knowledge)
}
return strings.Join(parts, "\n\n")
})
}
func buildSkillFieldKnowledgeSummary(lang string, session skillSession) string {
def, ok := getSkillDefinition(session.Name)
if !ok {
return ""
}
action, hasAction := def.Actions[session.Action]
relevant := orderedSkillFieldKeys(def, action, hasAction)
lines := make([]string, 0, len(relevant)+6)
title := "### Active Field Knowledge"
if lang == "zh" {
title = "### 当前字段知识"
}
lines = append(lines, title)
for _, field := range relevant {
constraint, ok := def.FieldConstraints[field]
if !ok {
continue
}
lines = append(lines, formatFieldKnowledgeLine(lang, field, constraint))
}
if len(def.PerExchangeRequiredFields) > 0 {
if lang == "zh" {
lines = append(lines, "- 按交易所类型的必填字段:")
} else {
lines = append(lines, "- Required fields by exchange type:")
}
keys := make([]string, 0, len(def.PerExchangeRequiredFields))
for key := range def.PerExchangeRequiredFields {
keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
fields := make([]string, 0, len(def.PerExchangeRequiredFields[key]))
for _, field := range def.PerExchangeRequiredFields[key] {
fields = append(fields, fieldKnowledgeDisplayName(field, lang))
}
lines = append(lines, fmt.Sprintf(" - %s: %s", key, strings.Join(fields, "、")))
}
}
if len(def.ValidationRules) > 0 {
if lang == "zh" {
lines = append(lines, "- 关键校验规则:")
} else {
lines = append(lines, "- Key validation rules:")
}
for i, rule := range def.ValidationRules {
lines = append(lines, fmt.Sprintf(" %d. %s", i+1, rule))
}
}
if len(lines) == 1 {
return ""
}
return strings.Join(lines, "\n")
}
func orderedSkillFieldKeys(def SkillDefinition, action SkillActionDefinition, hasAction bool) []string {
keys := make([]string, 0, len(def.FieldConstraints))
seen := map[string]struct{}{}
add := func(field string) {
field = strings.TrimSpace(field)
if field == "" {
return
}
if _, ok := def.FieldConstraints[field]; !ok {
return
}
if _, ok := seen[field]; ok {
return
}
seen[field] = struct{}{}
keys = append(keys, field)
}
if hasAction {
for _, field := range action.RequiredSlots {
add(field)
}
for _, field := range action.OptionalSlots {
add(field)
}
}
if len(keys) == 0 {
for field := range def.FieldConstraints {
add(field)
}
}
return keys
}
func formatFieldKnowledgeLine(lang, field string, constraint SkillFieldConstraint) string {
parts := make([]string, 0, 8)
if constraint.Description != "" {
parts = append(parts, constraint.Description)
}
if constraint.Type != "" {
if lang == "zh" {
parts = append(parts, "类型="+constraint.Type)
} else {
parts = append(parts, "type="+constraint.Type)
}
}
if constraint.Required {
if lang == "zh" {
parts = append(parts, "当前全局必填")
} else {
parts = append(parts, "globally required")
}
}
if len(constraint.Values) > 0 {
label := "可选值="
if lang != "zh" {
label = "values="
}
parts = append(parts, label+strings.Join(constraint.Values, "/"))
}
if len(constraint.RequiredFor) > 0 {
label := "仅这些类型必填="
if lang != "zh" {
label = "required_for="
}
parts = append(parts, label+strings.Join(constraint.RequiredFor, "/"))
}
if len(constraint.Aliases) > 0 {
aliasPairs := make([]string, 0, len(constraint.Aliases))
keys := make([]string, 0, len(constraint.Aliases))
for alias := range constraint.Aliases {
keys = append(keys, alias)
}
sort.Strings(keys)
for _, alias := range keys {
aliasPairs = append(aliasPairs, alias+"->"+constraint.Aliases[alias])
}
label := "别名="
if lang != "zh" {
label = "aliases="
}
parts = append(parts, label+strings.Join(aliasPairs, ", "))
}
if constraint.MustBeHTTPS {
if lang == "zh" {
parts = append(parts, "必须是 HTTPS")
} else {
parts = append(parts, "must be HTTPS")
}
}
if constraint.Min != nil || constraint.Max != nil {
rangeText := ""
switch {
case constraint.Min != nil && constraint.Max != nil:
rangeText = fmt.Sprintf("%.0f~%.0f", *constraint.Min, *constraint.Max)
case constraint.Min != nil:
rangeText = fmt.Sprintf(">=%.0f", *constraint.Min)
case constraint.Max != nil:
rangeText = fmt.Sprintf("<=%.0f", *constraint.Max)
}
if rangeText != "" {
label := "范围="
if lang != "zh" {
label = "range="
}
parts = append(parts, label+rangeText)
}
}
return fmt.Sprintf("- %s: %s", fieldKnowledgeDisplayName(field, lang), strings.Join(cleanStringList(parts), ""))
}
func fieldKnowledgeDisplayName(field, lang string) string {
if lang == "zh" {
switch field {
case "exchange_type":
return "交易所类型"
case "account_name":
return "账户名"
case "provider":
return "模型提供商"
case "custom_model_name":
return "模型名称"
case "custom_api_url":
return "接口地址"
}
}
return displayCatalogFieldName(field, lang)
}
func formatRequiredSlotList(lang string, slots []string) string {
display := make([]string, 0, len(slots))
for _, slot := range cleanStringList(slots) {
display = append(display, slotDisplayName(slot, lang))
}
return strings.Join(display, "、")
}
func missingRequiredActionSlots(skillName, action string, values map[string]string) []string {
runtime, ok := getSkillActionRuntime(skillName, action)
if !ok {
return nil
}
missing := make([]string, 0, len(runtime.Action.RequiredSlots))
for _, slot := range runtime.Action.RequiredSlots {
if strings.TrimSpace(values[slot]) == "" {
missing = append(missing, slot)
}
}
return missing
}
func cachedSkillContext(key string, build func() string) string {
if cached, ok := skillContextCache.Load(key); ok {
if s, ok := cached.(string); ok {
return s
}
}
value := build()
skillContextCache.Store(key, value)
return value
}