Version: 0.9.75.dev.260505

后端:
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 迁移面
This commit is contained in:
Losita
2026-05-05 16:00:57 +08:00
parent e1819c5653
commit d7184b776b
174 changed files with 2189 additions and 1236 deletions

View File

@@ -0,0 +1,326 @@
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(&sections, 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)
}
}