Version: 0.9.52.dev.260428
后端: 1. 工具结果结构化切流继续推进:schedule 读工具改为“父包 adapter + 子包 view builder”,`queue_pop_head` / `queue_skip_head` 脱离 legacy wrapper,`analyze_health` / `analyze_rhythm` 补齐 `schedule.analysis_result` 诊断卡片。 2. 非 schedule 工具补齐专属结果协议:`web_search` / `web_fetch`、`upsert_task_class`、`context_tools_add` / `context_tools_remove` 全部接入结构化 `ResultView`,注册表继续去 legacy wrapper,同时保持原始 `ObservationText` 供模型链路复用。 3. 工具展示细节继续收口:参数本地化补齐 `domain` / `packs` / `mode` / `all`,deliver 阶段补发段落分隔,避免 execute 与总结正文黏连。 前端: 4. `ToolCardRenderer` 升级为多协议通用渲染器,补齐 read / analysis / web / taskclass / context 卡片渲染、参数折叠区、未知协议兜底与操作明细展示。 5. `AssistantPanel` 修正 `tool_result` 结果回填与卡片布局宽度问题,并新增结构化卡片 fixture / mock 调试入口,便于整体验收。 仓库: 6. 更新工具结果结构化交接文档,补记第四批切流范围、当前切流点与后续收尾建议。
This commit is contained in:
326
backend/newAgent/tools/schedule_analysis/rhythm.go
Normal file
326
backend/newAgent/tools/schedule_analysis/rhythm.go
Normal 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(§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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user