feat(services): enhance services with rollback and observability
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -3,9 +3,13 @@ Skill Service - 技能管理服务层
|
||||
负责技能的创建、查询、更新、删除等操作
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from typing import Optional
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, and_, or_
|
||||
from app.agents.schemas.learning import SkillCandidate
|
||||
from app.agents.skills.models import SkillLifecycleDecision
|
||||
from app.models.skill import Skill
|
||||
from app.models.user import User
|
||||
|
||||
@@ -28,6 +32,10 @@ class SkillService:
|
||||
visibility=data.get("visibility", "private"),
|
||||
team_id=data.get("team_id"),
|
||||
is_active=data.get("is_active", True),
|
||||
status=data.get("status", "active"),
|
||||
scope=data.get("scope", []),
|
||||
effectiveness=data.get("effectiveness", 0.0),
|
||||
review_after=data.get("review_after"),
|
||||
)
|
||||
self.db.add(skill)
|
||||
await self.db.commit()
|
||||
@@ -41,6 +49,17 @@ class SkillService:
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def get_by_name_for_user(self, user_id: str, name: str) -> Optional[Skill]:
|
||||
access_scope = or_(
|
||||
Skill.owner_id == user_id,
|
||||
Skill.visibility == "market",
|
||||
Skill.team_id == user_id,
|
||||
)
|
||||
result = await self.db.execute(
|
||||
select(Skill).where(and_(Skill.name == name, access_scope))
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def list_for_user(
|
||||
self,
|
||||
user_id: str,
|
||||
@@ -56,7 +75,7 @@ class SkillService:
|
||||
Skill.team_id == user_id,
|
||||
)
|
||||
|
||||
filters = [access_scope, Skill.is_active == True]
|
||||
filters = [access_scope, Skill.is_active == True, Skill.status != "retired"]
|
||||
|
||||
if agent_type:
|
||||
filters.append(Skill.agent_type == agent_type)
|
||||
@@ -83,7 +102,7 @@ class SkillService:
|
||||
update_fields = [
|
||||
"name", "description", "instructions", "agent_type",
|
||||
"tools", "required_context", "output_format", "visibility",
|
||||
"team_id", "is_active"
|
||||
"team_id", "is_active", "status", "scope", "effectiveness", "review_after"
|
||||
]
|
||||
|
||||
for field in update_fields:
|
||||
@@ -117,6 +136,7 @@ class SkillService:
|
||||
and_(
|
||||
Skill.agent_type == agent_type,
|
||||
Skill.is_active == True,
|
||||
Skill.status == "active",
|
||||
or_(
|
||||
Skill.visibility == "market",
|
||||
Skill.visibility == "private"
|
||||
@@ -125,3 +145,234 @@ class SkillService:
|
||||
)
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def list_runtime_candidates(
|
||||
self,
|
||||
user_id: str,
|
||||
*,
|
||||
agent_type: Optional[str] = None,
|
||||
include_shadow: bool = True,
|
||||
include_learned: bool = True,
|
||||
) -> list[Skill]:
|
||||
allowed_statuses = ["active", "shadow"] if include_shadow else ["active"]
|
||||
access_scope = or_(
|
||||
Skill.owner_id == user_id,
|
||||
Skill.visibility == "market",
|
||||
Skill.team_id == user_id,
|
||||
)
|
||||
|
||||
filters = [
|
||||
access_scope,
|
||||
Skill.is_active == True,
|
||||
Skill.status.in_(allowed_statuses),
|
||||
]
|
||||
if not include_learned:
|
||||
filters.append(Skill.is_builtin == True)
|
||||
if agent_type:
|
||||
filters.append(Skill.agent_type == agent_type)
|
||||
|
||||
result = await self.db.execute(select(Skill).where(and_(*filters)))
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def upsert_learned_candidate(
|
||||
self,
|
||||
*,
|
||||
user_id: str,
|
||||
candidate: SkillCandidate,
|
||||
primary_agent: str | None,
|
||||
evidence_refs: list[dict] | None = None,
|
||||
) -> SkillLifecycleDecision:
|
||||
source_hash = self._build_candidate_source_hash(candidate)
|
||||
skill = await self.get_by_name_for_user(user_id, candidate.name)
|
||||
if skill is None:
|
||||
review_after = datetime.now(UTC) + timedelta(days=7)
|
||||
skill = Skill(
|
||||
owner_id=user_id,
|
||||
name=candidate.name,
|
||||
description=candidate.summary,
|
||||
instructions=candidate.summary,
|
||||
agent_type=primary_agent or "master",
|
||||
tools=[],
|
||||
required_context=[],
|
||||
output_format=None,
|
||||
visibility="private",
|
||||
is_active=True,
|
||||
status="candidate",
|
||||
scope=[primary_agent or "master", "learned", candidate.candidate_type],
|
||||
effectiveness=candidate.confidence,
|
||||
review_after=review_after,
|
||||
candidate_count=1,
|
||||
candidate_source_hashes=[source_hash],
|
||||
)
|
||||
self.db.add(skill)
|
||||
await self.db.commit()
|
||||
await self.db.refresh(skill)
|
||||
return SkillLifecycleDecision(
|
||||
skill_name=skill.name,
|
||||
action="created_candidate",
|
||||
previous_status=None,
|
||||
new_status="candidate",
|
||||
reason="First learned candidate created from retrospective evidence.",
|
||||
evidence_refs=evidence_refs or [],
|
||||
confidence=candidate.confidence,
|
||||
review_after=review_after,
|
||||
)
|
||||
|
||||
previous_status = skill.status
|
||||
known_hashes = list(skill.candidate_source_hashes or [])
|
||||
is_duplicate_candidate = source_hash in known_hashes
|
||||
if not is_duplicate_candidate:
|
||||
skill.candidate_count = int(skill.candidate_count or 0) + 1
|
||||
known_hashes.append(source_hash)
|
||||
skill.candidate_source_hashes = known_hashes
|
||||
current_effectiveness = float(skill.effectiveness or 0.0)
|
||||
skill.effectiveness = round(max(current_effectiveness, float(candidate.confidence or 0.0)), 3)
|
||||
skill.review_after = datetime.now(UTC) + timedelta(days=7)
|
||||
if primary_agent and primary_agent not in (skill.scope or []):
|
||||
skill.scope = [*(skill.scope or []), primary_agent]
|
||||
|
||||
action = "no_change"
|
||||
reason = "Candidate evidence refreshed."
|
||||
if is_duplicate_candidate:
|
||||
reason = "Duplicate candidate evidence ignored for promotion counting."
|
||||
if (
|
||||
not is_duplicate_candidate
|
||||
and skill.status == "candidate"
|
||||
and skill.candidate_count >= 2
|
||||
and skill.effectiveness >= 0.6
|
||||
):
|
||||
skill.status = "shadow"
|
||||
action = "promoted_to_shadow"
|
||||
reason = "Repeated candidate evidence promoted the learned skill to shadow."
|
||||
|
||||
await self.db.commit()
|
||||
await self.db.refresh(skill)
|
||||
return SkillLifecycleDecision(
|
||||
skill_name=skill.name,
|
||||
action=action,
|
||||
previous_status=previous_status,
|
||||
new_status=skill.status,
|
||||
reason=reason,
|
||||
evidence_refs=evidence_refs or [],
|
||||
confidence=skill.effectiveness,
|
||||
review_after=skill.review_after,
|
||||
)
|
||||
|
||||
async def record_activation_feedback(
|
||||
self,
|
||||
*,
|
||||
user_id: str,
|
||||
skill_name: str,
|
||||
outcome_score: float,
|
||||
evidence_refs: list[dict] | None = None,
|
||||
) -> SkillLifecycleDecision | None:
|
||||
skill = await self.get_by_name_for_user(user_id, skill_name)
|
||||
if skill is None or skill.status not in {"shadow", "active", "deprecated"}:
|
||||
return None
|
||||
|
||||
previous_status = skill.status
|
||||
previous_activation_count = int(skill.activation_count or 0)
|
||||
skill.activation_count = previous_activation_count + 1
|
||||
skill.last_activated_at = datetime.now(UTC)
|
||||
|
||||
previous_effectiveness = float(skill.effectiveness or 0.0)
|
||||
if previous_activation_count <= 0:
|
||||
skill.effectiveness = round(outcome_score, 3)
|
||||
else:
|
||||
skill.effectiveness = round(
|
||||
((previous_effectiveness * previous_activation_count) + outcome_score)
|
||||
/ skill.activation_count,
|
||||
3,
|
||||
)
|
||||
|
||||
action = "feedback_recorded"
|
||||
reason = "Activation outcome recorded."
|
||||
if skill.status == "shadow" and skill.activation_count >= 2 and skill.effectiveness >= 0.7:
|
||||
skill.status = "active"
|
||||
action = "promoted_to_active"
|
||||
reason = "Shadow skill proved effective enough to become active."
|
||||
elif skill.status == "active" and skill.activation_count >= 3 and skill.effectiveness < 0.35:
|
||||
skill.status = "deprecated"
|
||||
action = "degraded_to_deprecated"
|
||||
reason = "Active skill underperformed repeatedly and was deprecated."
|
||||
elif skill.status == "deprecated" and skill.activation_count >= 4 and skill.effectiveness < 0.2:
|
||||
skill.status = "retired"
|
||||
action = "retired"
|
||||
reason = "Deprecated skill stayed ineffective and was retired."
|
||||
elif skill.status == "deprecated" and skill.effectiveness >= 0.65 and outcome_score >= 0.8:
|
||||
skill.status = "active"
|
||||
action = "reactivated"
|
||||
reason = "Deprecated skill recovered with strong positive feedback."
|
||||
|
||||
skill.review_after = datetime.now(UTC) + timedelta(days=7)
|
||||
await self.db.commit()
|
||||
await self.db.refresh(skill)
|
||||
return SkillLifecycleDecision(
|
||||
skill_name=skill.name,
|
||||
action=action,
|
||||
previous_status=previous_status,
|
||||
new_status=skill.status,
|
||||
reason=reason,
|
||||
evidence_refs=evidence_refs or [],
|
||||
confidence=skill.effectiveness,
|
||||
review_after=skill.review_after,
|
||||
)
|
||||
|
||||
async def run_decay_review(
|
||||
self,
|
||||
*,
|
||||
user_id: str,
|
||||
as_of: datetime | None = None,
|
||||
) -> list[SkillLifecycleDecision]:
|
||||
review_time = as_of or datetime.now(UTC)
|
||||
result = await self.db.execute(
|
||||
select(Skill).where(
|
||||
and_(
|
||||
Skill.owner_id == user_id,
|
||||
Skill.is_active == True,
|
||||
Skill.status.in_(["shadow", "active", "deprecated"]),
|
||||
Skill.review_after.is_not(None),
|
||||
Skill.review_after <= review_time,
|
||||
)
|
||||
)
|
||||
)
|
||||
skills = list(result.scalars().all())
|
||||
decisions: list[SkillLifecycleDecision] = []
|
||||
for skill in skills:
|
||||
previous_status = skill.status
|
||||
action = "no_change"
|
||||
reason = "Review completed without status change."
|
||||
|
||||
if skill.status == "shadow" and float(skill.effectiveness or 0.0) < 0.45:
|
||||
skill.status = "deprecated"
|
||||
action = "degraded_to_deprecated"
|
||||
reason = "Shadow skill review found low effectiveness."
|
||||
elif skill.status == "deprecated" and float(skill.effectiveness or 0.0) < 0.2:
|
||||
skill.status = "retired"
|
||||
action = "retired"
|
||||
reason = "Deprecated skill remained weak through review."
|
||||
|
||||
skill.review_after = review_time + timedelta(days=7)
|
||||
decisions.append(
|
||||
SkillLifecycleDecision(
|
||||
skill_name=skill.name,
|
||||
action=action,
|
||||
previous_status=previous_status,
|
||||
new_status=skill.status,
|
||||
reason=reason,
|
||||
confidence=skill.effectiveness,
|
||||
review_after=skill.review_after,
|
||||
)
|
||||
)
|
||||
|
||||
await self.db.commit()
|
||||
return decisions
|
||||
|
||||
@staticmethod
|
||||
def _build_candidate_source_hash(candidate: SkillCandidate) -> str:
|
||||
raw = (
|
||||
f"{candidate.name}|{candidate.summary}|"
|
||||
f"{','.join(candidate.source_pattern_ids)}|"
|
||||
f"{len(candidate.evidence_refs)}"
|
||||
).encode("utf-8")
|
||||
return hashlib.sha1(raw).hexdigest()
|
||||
|
||||
Reference in New Issue
Block a user