后端: 1.收口阶段 6 agent 结构迁移,将 newAgent 内核与 agentsvc 编排层迁入 services/agent - 切换 Agent 启动装配与 HTTP handler 直连 agent sv,移除旧 service agent bridge - 补齐 Agent 对 memory、task、task-class、schedule 的 RPC 适配与契约字段 - 扩展 schedule、task、task-class RPC/contract 支撑 Agent 查询、写入与 provider 切流 - 更新迁移文档、README 与相关注释,明确 agent 当前切流点和剩余 memory 迁移面
327 lines
10 KiB
Go
327 lines
10 KiB
Go
package schedule_analysis
|
||
|
||
import (
|
||
"fmt"
|
||
"strings"
|
||
)
|
||
|
||
// BuildAnalyzeRhythmView 把 analyze_rhythm 的原始 JSON observation 转成诊断卡片。
|
||
//
|
||
// 步骤化说明:
|
||
// 1. 只读取现有 metrics / issues / next_actions,不改变 observation JSON;
|
||
// 2. collapsed 聚焦节律结论和关键指标,expanded 展开问题日、问题清单和建议动作;
|
||
// 3. detail / hard_categories 等参数只在父包参数区回显,不在这里声明它们已影响算法。
|
||
func BuildAnalyzeRhythmView(input AnalyzeRhythmViewInput) AnalysisResultView {
|
||
payload, ok := parseObservationJSON(input.Observation)
|
||
if !ok || !isSuccessPayload(payload) {
|
||
return BuildFailureView(BuildFailureViewInput{
|
||
ToolName: "analyze_rhythm",
|
||
Observation: input.Observation,
|
||
ArgFields: input.ArgFields,
|
||
})
|
||
}
|
||
|
||
metricsMap := readMap(payload, "metrics")
|
||
overview := readMap(metricsMap, "overview")
|
||
days := readList(metricsMap, "days")
|
||
issues := readList(payload, "issues")
|
||
actions := readList(payload, "next_actions")
|
||
|
||
actionItems := buildRhythmActionItems(actions)
|
||
problemDayItems := buildRhythmProblemDayItems(days)
|
||
issueItems := buildIssueItems(issues)
|
||
|
||
sections := []map[string]any{
|
||
BuildKVSection("节律概览", buildRhythmOverviewFields(overview)),
|
||
}
|
||
if len(problemDayItems) > 0 {
|
||
sections = append(sections, BuildItemsSection("问题日", problemDayItems))
|
||
} else {
|
||
sections = append(sections, BuildCalloutSection("问题日", "当前没有命中的高风险问题日。", "info", nil))
|
||
}
|
||
if len(issueItems) > 0 {
|
||
sections = append(sections, BuildItemsSection("问题清单", issueItems))
|
||
}
|
||
if len(actionItems) > 0 {
|
||
sections = append(sections, BuildItemsSection("建议动作", actionItems))
|
||
} else {
|
||
sections = append(sections, BuildCalloutSection("建议动作", "当前节律诊断没有返回候选动作。", "info", nil))
|
||
}
|
||
appendSectionIfPresent(§ions, BuildArgsSection("分析参数", input.ArgFields))
|
||
|
||
return BuildResultView(BuildResultViewInput{
|
||
Status: StatusDone,
|
||
Title: buildRhythmTitle(issues),
|
||
Subtitle: buildRhythmSubtitle(issues, overview),
|
||
Metrics: buildRhythmMetrics(overview),
|
||
Items: actionItems,
|
||
Sections: sections,
|
||
Observation: input.Observation,
|
||
MachinePayload: payload,
|
||
})
|
||
}
|
||
|
||
func buildRhythmTitle(issues []any) string {
|
||
severity := highestSeverity(issues)
|
||
switch severity {
|
||
case "critical":
|
||
return "学习节律分析:存在高风险"
|
||
case "warning":
|
||
return "学习节律分析:有待微调"
|
||
default:
|
||
return "学习节律分析:整体平稳"
|
||
}
|
||
}
|
||
|
||
func buildRhythmSubtitle(issues []any, overview map[string]any) string {
|
||
if issue := firstHighPriorityIssue(issues); issue != nil {
|
||
return describeIssue(issue)
|
||
}
|
||
maxDay := readInt(overview, "max_switch_day")
|
||
maxSwitch := readInt(overview, "max_switch_count")
|
||
if maxDay > 0 {
|
||
return fmt.Sprintf("最大切换出现在第 %d 天,共 %d 次。", maxDay, maxSwitch)
|
||
}
|
||
return "当前学习节律没有明显异常信号。"
|
||
}
|
||
|
||
func buildRhythmMetrics(overview map[string]any) []MetricField {
|
||
return []MetricField{
|
||
BuildMetric("平均切换", fmt.Sprintf("%.1f 次/天", readFloat(overview, "avg_switches_per_day"))),
|
||
BuildMetric("最大切换", fmt.Sprintf("%d 次", readInt(overview, "max_switch_count"))),
|
||
BuildMetric("高认知相邻", fmt.Sprintf("%d 天", readInt(overview, "heavy_adjacent_days"))),
|
||
BuildMetric("块平衡", fmt.Sprintf("%d", readInt(overview, "block_balance"))),
|
||
BuildMetric("同类型占比", formatPercent(readFloat(overview, "same_type_transition_ratio"))),
|
||
}
|
||
}
|
||
|
||
func buildRhythmOverviewFields(overview map[string]any) []KVField {
|
||
return []KVField{
|
||
BuildKVField("平均每日切换", fmt.Sprintf("%.1f 次", readFloat(overview, "avg_switches_per_day"))),
|
||
BuildKVField("最大切换日", formatDayIndex(readInt(overview, "max_switch_day"))),
|
||
BuildKVField("最大切换次数", fmt.Sprintf("%d 次", readInt(overview, "max_switch_count"))),
|
||
BuildKVField("平均块长度", fmt.Sprintf("%.1f 节", readFloat(overview, "avg_block_size"))),
|
||
BuildKVField("最长同科连续", fmt.Sprintf("%d 节", readInt(overview, "longest_same_subject_run"))),
|
||
BuildKVField("高认知相邻", fmt.Sprintf("%d 天", readInt(overview, "heavy_adjacent_days"))),
|
||
BuildKVField("高强度连续过长", fmt.Sprintf("%d 天", readInt(overview, "long_high_intensity_days"))),
|
||
BuildKVField("偏碎天数", fmt.Sprintf("%d 天", readInt(overview, "fragmented_count"))),
|
||
BuildKVField("偏压缩天数", fmt.Sprintf("%d 天", readInt(overview, "compressed_run_count"))),
|
||
BuildKVField("同类型切换占比", formatPercent(readFloat(overview, "same_type_transition_ratio"))),
|
||
}
|
||
}
|
||
|
||
func buildRhythmProblemDayItems(days []any) []ItemView {
|
||
if len(days) == 0 {
|
||
return make([]ItemView, 0)
|
||
}
|
||
items := make([]ItemView, 0)
|
||
for _, raw := range days {
|
||
day, ok := raw.(map[string]any)
|
||
if !ok || !isProblemRhythmDay(day) {
|
||
continue
|
||
}
|
||
dayIndex := readInt(day, "day_index")
|
||
switchCount := readInt(day, "switch_count")
|
||
fragmentation := readFloat(day, "fragmentation")
|
||
maxBlock := readInt(day, "max_block")
|
||
heavyAdjacent, _ := readBool(day, "heavy_adjacent")
|
||
tags := []string{}
|
||
if switchCount >= 3 || fragmentation >= 0.55 {
|
||
tags = append(tags, "偏碎")
|
||
}
|
||
if heavyAdjacent {
|
||
tags = append(tags, "高认知相邻")
|
||
}
|
||
if maxBlock >= 5 {
|
||
tags = append(tags, "连续块偏长")
|
||
}
|
||
detailLines := []string{
|
||
fmt.Sprintf("切换次数:%d 次", switchCount),
|
||
"碎片化程度:" + formatFloat(fragmentation),
|
||
fmt.Sprintf("最长连续块:%d 节", maxBlock),
|
||
"科目序列:" + formatSequence(readList(day, "sequence")),
|
||
}
|
||
items = append(items, BuildItem(
|
||
formatDayIndex(dayIndex),
|
||
fmt.Sprintf("切换 %d 次,最长连续 %d 节", switchCount, maxBlock),
|
||
tags,
|
||
detailLines,
|
||
day,
|
||
))
|
||
}
|
||
return items
|
||
}
|
||
|
||
func buildRhythmActionItems(actions []any) []ItemView {
|
||
if len(actions) == 0 {
|
||
return make([]ItemView, 0)
|
||
}
|
||
items := make([]ItemView, 0, len(actions))
|
||
for _, raw := range actions {
|
||
action, ok := raw.(map[string]any)
|
||
if !ok {
|
||
continue
|
||
}
|
||
scope := readMap(action, "candidate_scope")
|
||
title := formatIntentCN(readString(action, "intent_code"))
|
||
if title == "" {
|
||
title = fallbackLabel(readString(action, "action_id"), "建议动作")
|
||
}
|
||
reads := formatStringList(readList(action, "required_reads"))
|
||
writes := formatStringList(readList(action, "candidate_write_tools"))
|
||
tags := []string{
|
||
fmt.Sprintf("优先级 %d", readInt(action, "priority")),
|
||
fallbackLabel(writes, "无写工具"),
|
||
}
|
||
detailLines := []string{
|
||
"需要读取:" + fallbackLabel(reads, "无"),
|
||
"候选写工具:" + fallbackLabel(writes, "无"),
|
||
"作用范围:" + formatCandidateScope(scope),
|
||
"成功标准:" + compactJSON(action["success_criteria"]),
|
||
}
|
||
items = append(items, BuildItem(
|
||
title,
|
||
fmt.Sprintf("先读 %s,再考虑 %s", fallbackLabel(reads, "相关事实"), fallbackLabel(writes, "局部调整")),
|
||
tags,
|
||
detailLines,
|
||
action,
|
||
))
|
||
}
|
||
return items
|
||
}
|
||
|
||
func highestSeverity(issues []any) string {
|
||
best := "info"
|
||
bestRank := severityRank(best)
|
||
for _, raw := range issues {
|
||
issue, ok := raw.(map[string]any)
|
||
if !ok {
|
||
continue
|
||
}
|
||
severity := readString(issue, "severity")
|
||
if rank := severityRank(severity); rank < bestRank {
|
||
best = severity
|
||
bestRank = rank
|
||
}
|
||
}
|
||
return strings.ToLower(strings.TrimSpace(best))
|
||
}
|
||
|
||
func firstHighPriorityIssue(issues []any) map[string]any {
|
||
var best map[string]any
|
||
bestRank := 99
|
||
for _, raw := range issues {
|
||
issue, ok := raw.(map[string]any)
|
||
if !ok {
|
||
continue
|
||
}
|
||
rank := severityRank(readString(issue, "severity"))
|
||
if best == nil || rank < bestRank {
|
||
best = issue
|
||
bestRank = rank
|
||
}
|
||
}
|
||
return best
|
||
}
|
||
|
||
func isProblemRhythmDay(day map[string]any) bool {
|
||
heavyAdjacent, _ := readBool(day, "heavy_adjacent")
|
||
return readInt(day, "switch_count") >= 3 ||
|
||
readFloat(day, "fragmentation") >= 0.55 ||
|
||
readInt(day, "max_block") >= 5 ||
|
||
heavyAdjacent
|
||
}
|
||
|
||
func formatDayIndex(day int) string {
|
||
if day <= 0 {
|
||
return "未知日期"
|
||
}
|
||
return fmt.Sprintf("第 %d 天", day)
|
||
}
|
||
|
||
func formatSequence(rows []any) string {
|
||
if len(rows) == 0 {
|
||
return "无"
|
||
}
|
||
parts := make([]string, 0, len(rows))
|
||
for _, raw := range rows {
|
||
text := strings.TrimSpace(fmt.Sprintf("%v", raw))
|
||
if text == "" || text == "<nil>" {
|
||
continue
|
||
}
|
||
parts = append(parts, text)
|
||
}
|
||
if len(parts) == 0 {
|
||
return "无"
|
||
}
|
||
return strings.Join(parts, " -> ")
|
||
}
|
||
|
||
func formatStringList(rows []any) string {
|
||
if len(rows) == 0 {
|
||
return ""
|
||
}
|
||
parts := make([]string, 0, len(rows))
|
||
for _, raw := range rows {
|
||
text := strings.TrimSpace(fmt.Sprintf("%v", raw))
|
||
if text == "" || text == "<nil>" {
|
||
continue
|
||
}
|
||
parts = append(parts, text)
|
||
}
|
||
return strings.Join(parts, "、")
|
||
}
|
||
|
||
func formatCandidateScope(scope map[string]any) string {
|
||
if len(scope) == 0 {
|
||
return "未返回"
|
||
}
|
||
parts := make([]string, 0, 3)
|
||
if days := formatNumberList(readList(scope, "day_range"), "第 %d 天"); days != "" {
|
||
parts = append(parts, "日期:"+days)
|
||
}
|
||
if categories := formatStringList(readList(scope, "categories")); categories != "" {
|
||
parts = append(parts, "类别:"+categories)
|
||
}
|
||
if pool := readString(scope, "task_pool"); pool != "" {
|
||
parts = append(parts, "任务池:"+pool)
|
||
}
|
||
if len(parts) == 0 {
|
||
return compactJSON(scope)
|
||
}
|
||
return strings.Join(parts, ";")
|
||
}
|
||
|
||
func formatNumberList(rows []any, pattern string) string {
|
||
if len(rows) == 0 {
|
||
return ""
|
||
}
|
||
parts := make([]string, 0, len(rows))
|
||
for _, raw := range rows {
|
||
number := 0
|
||
switch typed := raw.(type) {
|
||
case float64:
|
||
number = int(typed)
|
||
case int:
|
||
number = typed
|
||
default:
|
||
continue
|
||
}
|
||
parts = append(parts, fmt.Sprintf(pattern, number))
|
||
}
|
||
return strings.Join(parts, "、")
|
||
}
|
||
|
||
func formatIntentCN(intent string) string {
|
||
switch strings.TrimSpace(intent) {
|
||
case "reduce_switch":
|
||
return "减少同日切换"
|
||
case "smooth_rhythm":
|
||
return "平滑高认知相邻"
|
||
case "prefer_swap":
|
||
return "优先寻找交换机会"
|
||
default:
|
||
return strings.TrimSpace(intent)
|
||
}
|
||
}
|