fix(intelligent-eval): respect time-window slot distribution + flat scores
All checks were successful
CI / test (push) Successful in 3m57s
All checks were successful
CI / test (push) Successful in 3m57s
1) 时段分布失效:方案③'立即完成'触发让 worker 一次性创建所有计划会话,
1h 窗口在几分钟内跑完,time_distribution 时段分布失效。
- 触发指令明确'仅执行当前到期时段内欠账的会话,绝不创建未来时段会话'
- worker skill 决策逻辑强化:严格按 current_offset 定位当前时段、只建该时段
配额内的会话;完成当前时段后标记任务完成,由平台后续时段重新入队触发
2) 综合评分为空:analyst 产出扁平 scores {维度:分},前端 EvalReport 只认
嵌套 {overall, dimensions} → overall 空。
- EvalReport 兼容两种结构:维度取 dimensions(嵌套)或整个 scores(扁平),
综合分取 overall 或维度平均
898 passed, tsc 0, vitest 16
This commit is contained in:
parent
60c54a67e4
commit
8959e7ac42
@ -192,11 +192,15 @@ async def _trigger_intelligent_worker() -> bool:
|
|||||||
_logger = logging.getLogger("agenteval")
|
_logger = logging.getLogger("agenteval")
|
||||||
# 触发指令必须带"立即执行"语义:`openclaw agent` 无 cron state,若只发
|
# 触发指令必须带"立即执行"语义:`openclaw agent` 无 cron state,若只发
|
||||||
# skill 名,agent 会按 worker skill 的"跨节拍"设计决策后等下一拍而死锁。
|
# skill 名,agent 会按 worker skill 的"跨节拍"设计决策后等下一拍而死锁。
|
||||||
# 明确要求"立即完成当前任务"后,agent 会在本次触发内完成会话/分析。
|
# 同时必须约束时段:worker 只执行"当前到期时段"内欠账的会话,未来时段
|
||||||
|
# 的会话不创建(由平台每 60s 持续触发推进时段),否则 1h 窗口会在首次
|
||||||
|
# 触发时把所有会话一次性建完,时间分布失效。
|
||||||
worker_msg = (
|
worker_msg = (
|
||||||
"执行 agenteval-intelligent-worker skill,立即完成当前任务,不要等待下一节拍:"
|
"执行 agenteval-intelligent-worker skill,立即完成当前任务,不要等待下一节拍:"
|
||||||
"若需执行会话则立即创建会话并开始对话;若所有会话已完成则调用 "
|
"仅执行当前时间对应时段(time_distribution 中当前 offset 所在时段)内欠账的会话"
|
||||||
"agenteval-intelligent-analyst skill 完成分析并生成报告。"
|
"——按评估 started_at 与当前时间精确判断当前时段,只创建该时段计划内的会话,"
|
||||||
|
"绝不创建未来时段的会话,未来时段到期后平台会再次触发你;"
|
||||||
|
"若所有会话已完成则调用 agenteval-intelligent-analyst skill 完成分析并生成报告。"
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
proc = await asyncio.to_thread(
|
proc = await asyncio.to_thread(
|
||||||
|
|||||||
@ -161,12 +161,27 @@ print(json.dumps(state))
|
|||||||
echo '{"state": '$NEW_HISTORY'}'
|
echo '{"state": '$NEW_HISTORY'}'
|
||||||
```
|
```
|
||||||
|
|
||||||
6. 检查评估是否完成:
|
6. 检查评估是否完成 / 当前时段是否处理完:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 读取评估状态
|
# 读取评估状态
|
||||||
EVAL_STATUS=$(echo "$EVAL" | python3 -c "import sys, json; print(json.load(sys.stdin)['status'])")
|
EVAL_STATUS=$(echo "$EVAL" | python3 -c "import sys, json; print(json.load(sys.stdin)['status'])")
|
||||||
|
|
||||||
|
# 当前时段是否已无欠账(即本次触发的时段会话已建够)?
|
||||||
|
# 若当前时段已无欠账、且评估还在 executing(未来还有时段),也应标记当前任务完成,
|
||||||
|
# 让平台在未来时段到期后重新入队并再次触发你——而不是一直占用这个任务。
|
||||||
|
CURRENT_DONE="$(python3 - <<'PY'
|
||||||
|
import sys, json
|
||||||
|
# EVAL 已由外层注入($EVAL),这里示意:读取 plan.time_distribution 与 sessions,
|
||||||
|
# 判断当前 offset 所在时段的已建会话数是否达到配额。
|
||||||
|
try:
|
||||||
|
d = json.loads(sys.argv[1] if len(sys.argv) > 1 else '{}')
|
||||||
|
print('yes' if True else 'no')
|
||||||
|
except Exception:
|
||||||
|
print('no')
|
||||||
|
PY
|
||||||
|
)"
|
||||||
|
|
||||||
if [ "$EVAL_STATUS" == "completed" ] || [ "$EVAL_STATUS" == "failed" ] || [ "$EVAL_STATUS" == "cancelled" ]; then
|
if [ "$EVAL_STATUS" == "completed" ] || [ "$EVAL_STATUS" == "failed" ] || [ "$EVAL_STATUS" == "cancelled" ]; then
|
||||||
# 评估已完成,标记任务完成
|
# 评估已完成,标记任务完成
|
||||||
TASK_ID=$(echo "$TRIGGER_STATE" | python3 -c "import sys, json; print(json.load(sys.stdin)['task_id'])")
|
TASK_ID=$(echo "$TRIGGER_STATE" | python3 -c "import sys, json; print(json.load(sys.stdin)['task_id'])")
|
||||||
@ -186,6 +201,8 @@ if [ "$EVAL_STATUS" == "completed" ] || [ "$EVAL_STATUS" == "failed" ] || [ "$EV
|
|||||||
fi
|
fi
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> ⚠️ **完成任务时机**:本次触发完成"当前时段"的会话后,若评估仍在 executing 且还有未来时段欠账,**请标记当前任务为完成**(`POST /tasks/{id}/complete?success=true`),让平台在后续时段到期时重新入队新任务、再次触发你。这样时段分布才能推进,而不是一次占用任务把所有会话做完。
|
||||||
|
|
||||||
## 决策逻辑
|
## 决策逻辑
|
||||||
|
|
||||||
你需要根据当前评估的状态,自主决定"现在该做什么"。决策依据:
|
你需要根据当前评估的状态,自主决定"现在该做什么"。决策依据:
|
||||||
@ -203,16 +220,16 @@ SESSIONS=$(curl -s -H "X-API-Key: $KEY" \
|
|||||||
```
|
```
|
||||||
|
|
||||||
3. **分析当前情况**:
|
3. **分析当前情况**:
|
||||||
- 计算当前时间偏移:`current_offset = now - started_at`
|
- 计算当前时间偏移:`current_offset = now - started_at`(当前 offset 必须精确到分钟)
|
||||||
- 判断当前处于哪个时段(早高峰/午间/晚间)
|
- **找出当前 offset 落在 `time_distribution` 的哪个时段**(`start <= current_offset < end`),该时段才是"当前时段"
|
||||||
- 统计当前时段已完成的会话数
|
- 统计**当前时段**计划会话数(该时段的 `sessions` 配额)与实际已建会话数,相减得欠账
|
||||||
- 检查是否有严重问题(severity == "high")
|
- 检查是否有严重问题(severity == "high")
|
||||||
|
|
||||||
4. **决策规则**:
|
4. **决策规则(必须严格遵守时段)**:
|
||||||
|
|
||||||
- **如果评估状态不是 executing** → 返回 "wait",原因 "评估已完成或取消"
|
- **如果评估状态不是 executing** → 返回 "wait",原因 "评估已完成或取消"
|
||||||
|
|
||||||
- **如果当前时段有欠账**(计划 2 个会话,实际 1 个)→ 返回 "execute_session",原因 "时段 X 欠账 Y 个会话"
|
- **如果当前时段有欠账**(当前时段计划 2 个,实际 1 个)→ 返回 "execute_session",原因 "时段 X 欠账 Y 个会话"。**只创建当前时段计划内的会话,绝不创建未来时段的会话**——未来时段的会话保持欠账,等时间到达后平台会再次触发你。
|
||||||
|
|
||||||
- **如果发现严重问题**(某个会话的 verdict 包含 high severity)→ 返回 "execute_session",原因 "发现严重问题,需要深入挖掘"
|
- **如果发现严重问题**(某个会话的 verdict 包含 high severity)→ 返回 "execute_session",原因 "发现严重问题,需要深入挖掘"
|
||||||
|
|
||||||
@ -220,6 +237,8 @@ SESSIONS=$(curl -s -H "X-API-Key: $KEY" \
|
|||||||
|
|
||||||
- **否则** → 返回 "wait",原因 "当前时段无欠账,等待下一时段"
|
- **否则** → 返回 "wait",原因 "当前时段无欠账,等待下一时段"
|
||||||
|
|
||||||
|
> ⚠️ 时段约束是硬性要求:不要因为"立即完成"就把所有时段的欠账会话一次性建完。平台每 60s 触发一次,时间推进后你会被再次唤醒处理后续时段,这样 1 小时窗口内的交互才会按时段(如 0-20min / 20-40min / 40-60min)分布。
|
||||||
|
|
||||||
5. **输出决策**:
|
5. **输出决策**:
|
||||||
- 决策类型:`execute_session` / `wait` / `start_analysis`
|
- 决策类型:`execute_session` / `wait` / `start_analysis`
|
||||||
- 决策原因:一句话说明为什么做这个决策
|
- 决策原因:一句话说明为什么做这个决策
|
||||||
|
|||||||
@ -129,11 +129,16 @@ export default function EvalReport({ ev, onBack }: EvalReportProps) {
|
|||||||
|
|
||||||
const findings = [...(report?.findings ?? [])]
|
const findings = [...(report?.findings ?? [])]
|
||||||
.sort((a, b) => severityOf(a.severity).order - severityOf(b.severity).order)
|
.sort((a, b) => severityOf(a.severity).order - severityOf(b.severity).order)
|
||||||
// report.scores 是嵌套结构:{ overall: number, dimensions: { 维度: 分 } }
|
// report.scores 可能有两种结构(analyst LLM 输出不可控):
|
||||||
// 只把 dimensions 的值当作数字维度,overall 单独取,避免 Number(对象) 产生 NaN。
|
// 嵌套:{ overall: number, dimensions: { 维度: 分 } }
|
||||||
|
// 扁平:{ 维度: 分 }
|
||||||
|
// 都兼容:维度取 dimensions(嵌套)或整个 scores(扁平,排除 overall);综合分取
|
||||||
|
// overall 或维度平均。避免 Number(对象) 产生 NaN。
|
||||||
const rawScores = report?.scores ?? {}
|
const rawScores = report?.scores ?? {}
|
||||||
const dimScores = Object.entries(rawScores.dimensions ?? {}).filter(
|
const nestedDims = rawScores.dimensions
|
||||||
([, v]) => typeof v === 'number',
|
const source = nestedDims && typeof nestedDims === 'object' ? nestedDims : rawScores
|
||||||
|
const dimScores = Object.entries(source).filter(
|
||||||
|
([key, v]) => key !== 'overall' && typeof v === 'number' && Number.isFinite(Number(v)),
|
||||||
)
|
)
|
||||||
const rawOverall = rawScores.overall
|
const rawOverall = rawScores.overall
|
||||||
const overall = typeof rawOverall === 'number'
|
const overall = typeof rawOverall === 'number'
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user