```
feat(engine): 优化智能体循环中的助手消息处理逻辑 - 在没有工具调用时才添加助手消息到上下文 - 确保工具调用响应正确添加到消息上下文中 - 修复了消息构建的条件逻辑 fix(cron): 改进定时任务调度的时间解析功能 - 添加正则表达式导入用于时间显示解析 - 实现从显示文本中提取毫秒间隔的功能 - 增强整数转换的安全性,避免类型错误 - 优化定时任务配置的解析逻辑 feat(outlook): 增强Outlook集成的功能和稳定性 - 将默认超时时间从10秒增加到180秒 - 为状态检查函数添加可选的验证参数 - 串行执行邮件概览获取操作而非并行 - 改进连接状态验证逻辑 feat(channel): 添加设备名称作为会话标识的选项 - 为终端WebSocket适配器添加新的配置选项 - 实现基于设备名称生成会话对等ID的功能 - 记录原始对等ID和设备名称的元数据 - 支持从设备名称创建会话对等ID feat(skills): 完善技能学习评估系统和进度跟踪 - 在应用启动时自动调度待评估的技能草稿 - 为技能评估工作创建独立的循环工厂 - 实现异步技能评估任务的取消和清理机制 - 添加技能评估进度报告和状态跟踪功能 - 扩展会话列表API以包含更多详细信息 - 防止对不存在的会话进行操作 - 优化技能草稿提交和评估的业务逻辑 perf(skills): 提升技能评估的并发性能 - 实现并行技能案例评估以提高效率 - 添加最大并行案例数的环境变量控制 - 实现实时评估进度更新和回调机制 - 优化评估过程中的资源管理和同步 refactor(services): 创建隔离的智能体循环实例 - 添加创建独立智能体循环的工厂方法 - 确保新循环继承运行时服务配置 - 支持技能评估等需要隔离环境的场景 ```
This commit is contained in:
@ -264,6 +264,25 @@ async def _app_lifespan(
|
||||
)
|
||||
app.state.channel_runtime = channel_runtime
|
||||
await channel_runtime.start()
|
||||
for candidate in loaded.skill_learning_pipeline.list_candidates(status="review_pending"): # type: ignore[union-attr]
|
||||
skill_name = candidate.draft_skill_name
|
||||
draft_id = candidate.draft_id
|
||||
if not skill_name or not draft_id:
|
||||
continue
|
||||
if loaded.skill_learning_pipeline.get_eval_report(skill_name, draft_id) is not None: # type: ignore[union-attr]
|
||||
continue
|
||||
draft = loaded.skill_learning_pipeline.get_draft(skill_name, draft_id) # type: ignore[union-attr]
|
||||
if draft.status != "in_review":
|
||||
continue
|
||||
_schedule_skill_draft_eval(
|
||||
app,
|
||||
agent_service=attached_service,
|
||||
loop=attached_service.create_loop(),
|
||||
loaded=loaded,
|
||||
candidate_id=candidate.candidate_id,
|
||||
skill_name=skill_name,
|
||||
draft_id=draft_id,
|
||||
)
|
||||
except BaseException:
|
||||
if owns_service and started:
|
||||
with suppress(BaseException):
|
||||
@ -280,7 +299,10 @@ async def _app_lifespan(
|
||||
worker = SkillLearningWorker(
|
||||
pipeline=loaded.skill_learning_pipeline, # type: ignore[arg-type]
|
||||
provider_bundle_factory=lambda: attached_service._make_provider_bundle_for_task(loaded, {}), # noqa: SLF001
|
||||
replay_runner_factory=lambda: ReplayRunner(agent_loop=attached_service.create_loop()),
|
||||
replay_runner_factory=lambda: ReplayRunner(
|
||||
agent_loop=attached_service.create_loop(),
|
||||
isolated_loop_factory=attached_service.create_isolated_loop,
|
||||
),
|
||||
config=worker_config,
|
||||
)
|
||||
worker_task = asyncio.create_task(worker.run_forever())
|
||||
@ -289,6 +311,13 @@ async def _app_lifespan(
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
skill_eval_tasks = getattr(app.state, "skill_eval_tasks", {})
|
||||
for task in list(skill_eval_tasks.values()):
|
||||
task.cancel()
|
||||
for task in list(skill_eval_tasks.values()):
|
||||
with suppress(BaseException):
|
||||
await task
|
||||
skill_eval_tasks.clear()
|
||||
runtime = getattr(app.state, "channel_runtime", None)
|
||||
if isinstance(runtime, ChannelRuntime):
|
||||
with suppress(BaseException):
|
||||
@ -587,6 +616,7 @@ def create_app(
|
||||
)
|
||||
app.state.auth_tokens = {}
|
||||
app.state.handoff_codes = {}
|
||||
app.state.skill_eval_tasks = {}
|
||||
app.state.auth_file = Path(os.getenv("BEAVER_AUTH_FILE") or "")
|
||||
max_file_size = 50 * 1024 * 1024
|
||||
max_user_file_upload_size = _int_env("BEAVER_USER_FILES_MAX_UPLOAD_BYTES", 5 * 1024 * 1024 * 1024)
|
||||
@ -1250,7 +1280,7 @@ def create_app(
|
||||
session_manager = loaded.session_manager
|
||||
rows = session_manager.list_sessions_rich(
|
||||
limit=100,
|
||||
exclude_sources=["subagent", "notification"],
|
||||
exclude_sources=["subagent", "notification", "skill_replay_eval"],
|
||||
exclude_end_reasons=["archived", "deleted"],
|
||||
) # type: ignore[union-attr]
|
||||
return [
|
||||
@ -1259,6 +1289,9 @@ def create_app(
|
||||
"created_at": _iso_from_timestamp(row.get("started_at")),
|
||||
"updated_at": _iso_from_timestamp(row.get("last_active")),
|
||||
"path": str(row.get("id")),
|
||||
"source": row.get("source"),
|
||||
"title": row.get("title"),
|
||||
"preview": row.get("preview"),
|
||||
}
|
||||
for row in rows
|
||||
]
|
||||
@ -1337,7 +1370,9 @@ def create_app(
|
||||
async def get_session(session_id: str, request: Request) -> dict[str, Any]:
|
||||
loaded = get_agent_service(request).create_loop().boot()
|
||||
session_manager = loaded.session_manager
|
||||
session = session_manager.get_or_create(session_id, source="web") # type: ignore[union-attr]
|
||||
session = session_manager.get_session(session_id) # type: ignore[union-attr]
|
||||
if session is None:
|
||||
raise HTTPException(status_code=404, detail="Session not found")
|
||||
return _session_detail(session_manager, session_id, session) # type: ignore[arg-type]
|
||||
|
||||
@app.delete("/api/sessions/{session_id:path}")
|
||||
@ -2216,21 +2251,33 @@ def create_app(
|
||||
try:
|
||||
safety = loaded.skill_learning_pipeline.check_safety(skill_name, draft_id) # type: ignore[union-attr]
|
||||
if safety.passed and safety.risk_level != "critical":
|
||||
loaded.skill_learning_pipeline.submit_review( # type: ignore[union-attr]
|
||||
skill_name,
|
||||
draft_id,
|
||||
requested_by=str((payload or {}).get("requested_by") or "web"),
|
||||
notes=str((payload or {}).get("notes") or ""),
|
||||
)
|
||||
candidate_id = _skill_learning_candidate_id_for_draft(loaded, skill_name, draft_id)
|
||||
if candidate_id is not None:
|
||||
provider_bundle = agent_service._make_provider_bundle_for_task(loaded, {}) # noqa: SLF001
|
||||
await loaded.skill_learning_pipeline.evaluate_draft( # type: ignore[union-attr]
|
||||
candidate_id,
|
||||
draft = loaded.skill_learning_pipeline.get_draft(skill_name, draft_id) # type: ignore[union-attr]
|
||||
if draft.status == "draft":
|
||||
loaded.skill_learning_pipeline.submit_review( # type: ignore[union-attr]
|
||||
skill_name,
|
||||
draft_id,
|
||||
provider_bundle=provider_bundle,
|
||||
replay_runner=ReplayRunner(agent_loop=loop),
|
||||
requested_by=str((payload or {}).get("requested_by") or "web"),
|
||||
notes=str((payload or {}).get("notes") or ""),
|
||||
)
|
||||
elif draft.status not in {"in_review", "approved"}:
|
||||
raise ValueError("Draft cannot be submitted from its current status")
|
||||
candidate_id = _skill_learning_candidate_id_for_draft(loaded, skill_name, draft_id)
|
||||
eval_report = loaded.skill_learning_pipeline.get_eval_report(skill_name, draft_id) # type: ignore[union-attr]
|
||||
if candidate_id is not None and eval_report is None:
|
||||
loaded.skill_learning_store.transition_learning_candidate( # type: ignore[union-attr]
|
||||
candidate_id,
|
||||
"review_pending",
|
||||
event_type="eval_queued",
|
||||
last_error=None,
|
||||
)
|
||||
_schedule_skill_draft_eval(
|
||||
app,
|
||||
agent_service=agent_service,
|
||||
loop=loop,
|
||||
loaded=loaded,
|
||||
candidate_id=candidate_id,
|
||||
skill_name=skill_name,
|
||||
draft_id=draft_id,
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise _skill_draft_http_error(exc) from exc
|
||||
@ -3810,14 +3857,88 @@ def _skill_learning_candidate_task_text(loaded: Any, candidate: Any) -> str:
|
||||
return str(evidence.get("task_text") or "").strip()
|
||||
|
||||
|
||||
def _schedule_skill_draft_eval(
|
||||
app: FastAPI,
|
||||
*,
|
||||
agent_service: AgentService,
|
||||
loop: Any,
|
||||
loaded: Any,
|
||||
candidate_id: str,
|
||||
skill_name: str,
|
||||
draft_id: str,
|
||||
) -> None:
|
||||
key = f"{skill_name}:{draft_id}"
|
||||
tasks: dict[str, asyncio.Task[None]] = app.state.skill_eval_tasks
|
||||
current = tasks.get(key)
|
||||
if current is not None and not current.done():
|
||||
return
|
||||
|
||||
loaded.skill_learning_pipeline.mark_eval_progress( # type: ignore[union-attr]
|
||||
candidate_id,
|
||||
{
|
||||
"phase": "preparing",
|
||||
"completed_arms": 0,
|
||||
"total_arms": 20,
|
||||
"completed_cases": 0,
|
||||
"total_cases": 10,
|
||||
},
|
||||
)
|
||||
|
||||
async def run_eval() -> None:
|
||||
try:
|
||||
provider_bundle = agent_service._make_provider_bundle_for_task(loaded, {}) # noqa: SLF001
|
||||
await loaded.skill_learning_pipeline.evaluate_draft( # type: ignore[union-attr]
|
||||
candidate_id,
|
||||
skill_name,
|
||||
draft_id,
|
||||
provider_bundle=provider_bundle,
|
||||
replay_runner=ReplayRunner(
|
||||
agent_loop=loop,
|
||||
isolated_loop_factory=agent_service.create_isolated_loop,
|
||||
),
|
||||
progress_callback=lambda progress: loaded.skill_learning_pipeline.mark_eval_progress( # type: ignore[union-attr]
|
||||
candidate_id,
|
||||
progress,
|
||||
),
|
||||
)
|
||||
except asyncio.CancelledError:
|
||||
raise
|
||||
except Exception as exc:
|
||||
loaded.skill_learning_pipeline.mark_eval_failed(candidate_id, str(exc)) # type: ignore[union-attr]
|
||||
|
||||
task = asyncio.create_task(run_eval())
|
||||
tasks[key] = task
|
||||
|
||||
def remove_completed(completed: asyncio.Task[None]) -> None:
|
||||
if tasks.get(key) is completed:
|
||||
tasks.pop(key, None)
|
||||
|
||||
task.add_done_callback(remove_completed)
|
||||
|
||||
|
||||
def _skill_draft_payload(loaded: Any, skill_name: str, draft_id: str, *, include_reviews: bool = False) -> dict[str, Any]:
|
||||
draft = loaded.skill_learning_pipeline.get_draft(skill_name, draft_id) # type: ignore[union-attr]
|
||||
safety = loaded.skill_learning_pipeline.get_safety_report(skill_name, draft_id) # type: ignore[union-attr]
|
||||
eval_report = loaded.skill_learning_pipeline.get_eval_report(skill_name, draft_id) # type: ignore[union-attr]
|
||||
candidate_id = _skill_learning_candidate_id_for_draft(loaded, skill_name, draft_id)
|
||||
candidate = loaded.skill_learning_pipeline.get_candidate(candidate_id) if candidate_id is not None else None # type: ignore[union-attr]
|
||||
if eval_report is not None:
|
||||
eval_status = eval_report.status
|
||||
elif candidate is None:
|
||||
eval_status = "not_applicable"
|
||||
elif candidate.status == "eval_failed":
|
||||
eval_status = "failed"
|
||||
elif draft.status in {"in_review", "approved"}:
|
||||
eval_status = "pending"
|
||||
else:
|
||||
eval_status = "not_started"
|
||||
payload = {
|
||||
**draft.to_dict(),
|
||||
"safety_report": safety.to_dict() if safety is not None else None,
|
||||
"eval_report": eval_report.to_dict() if eval_report is not None else None,
|
||||
"eval_status": eval_status,
|
||||
"eval_error": candidate.last_error if candidate is not None and candidate.status == "eval_failed" else None,
|
||||
"eval_progress": dict(candidate.eval_progress) if candidate is not None else None,
|
||||
"target_version": _skill_draft_target_version(loaded, draft.skill_name, draft.proposal_kind),
|
||||
"base_skill": _skill_draft_base_skill_payload(loaded, draft),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user